Gif upload v1: sprites, costumes
This commit is contained in:
1 parent
da65b41366
commit
88651e6e48
6 files changed
+101
-12
No files matched your search
@@ -76,6 +76,7 @@
|
||||
"lodash.throttle": "4.0.1",
|
||||
"minilog": "3.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"omggif": "1.0.9",
|
||||
"papaparse": "4.6.2",
|
||||
"postcss-import": "^12.0.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
|
||||
@@ -120,7 +120,7 @@ const SpriteSelectorComponent = function (props) {
|
||||
title: intl.formatMessage(messages.addSpriteFromFile),
|
||||
img: fileUploadIcon,
|
||||
onClick: onFileUploadClick,
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2, .sprite3',
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2, .sprite3, .gif',
|
||||
fileChange: onSpriteUpload,
|
||||
fileInput: spriteFileInput,
|
||||
fileMultiple: true
|
||||
|
||||
@@ -102,7 +102,7 @@ const StageSelector = props => {
|
||||
title: intl.formatMessage(messages.addBackdropFromFile),
|
||||
img: fileUploadIcon,
|
||||
onClick: onBackdropFileUploadClick,
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg', // Bitmap coming soon
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg, .gif',
|
||||
fileChange: onBackdropFileUpload,
|
||||
fileInput: fileInputRef,
|
||||
fileMultiple: true
|
||||
|
||||
@@ -158,11 +158,15 @@ class CostumeTab extends React.Component {
|
||||
downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob);
|
||||
}
|
||||
handleNewCostume (costume, fromCostumeLibrary) {
|
||||
if (fromCostumeLibrary) {
|
||||
this.props.vm.addCostumeFromLibrary(costume.md5, costume);
|
||||
} else {
|
||||
this.props.vm.addCostume(costume.md5, costume);
|
||||
}
|
||||
const costumes = Array.isArray(costume) ? costume : [costume];
|
||||
|
||||
costumes.forEach(c => {
|
||||
if (fromCostumeLibrary) {
|
||||
this.props.vm.addCostumeFromLibrary(c.md5, c);
|
||||
} else {
|
||||
this.props.vm.addCostume(c.md5, c);
|
||||
}
|
||||
});
|
||||
}
|
||||
handleNewBlankCostume () {
|
||||
const name = this.props.vm.editingTarget.isStage ?
|
||||
@@ -291,7 +295,7 @@ class CostumeTab extends React.Component {
|
||||
title: intl.formatMessage(addFileMessage),
|
||||
img: fileUploadIcon,
|
||||
onClick: this.handleFileUploadClick,
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg',
|
||||
fileAccept: '.svg, .png, .jpg, .jpeg, .gif',
|
||||
fileChange: this.handleCostumeUpload,
|
||||
fileInput: this.setFileInput,
|
||||
fileMultiple: true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {BitmapAdapter} from 'scratch-svg-renderer';
|
||||
import log from './log.js';
|
||||
import randomizeSpritePosition from './randomize-sprite-position.js';
|
||||
import gifDecoder from './gif-decoder';
|
||||
|
||||
/**
|
||||
* Extract the file name given a string of the form fileName + ext
|
||||
@@ -114,6 +115,19 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
|
||||
assetType = storage.AssetType.ImageBitmap;
|
||||
break;
|
||||
}
|
||||
case 'image/gif': {
|
||||
let costumes = [];
|
||||
const onFrame = (frameNumber, dataUrl) => {
|
||||
costumeUpload(dataUrl, 'image/png', `${costumeName}-${frameNumber}`, storage, costumes_ => {
|
||||
costumes = costumes.concat(costumes_);
|
||||
});
|
||||
};
|
||||
const onDone = () => {
|
||||
handleCostume(costumes);
|
||||
};
|
||||
gifDecoder(fileData, {onFrame, onDone});
|
||||
return; // Abandon this load, do not try to load gif itself
|
||||
}
|
||||
default:
|
||||
log.warn(`Encountered unexpected file type: ${fileType}`);
|
||||
return;
|
||||
@@ -128,7 +142,7 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
|
||||
costumeFormat,
|
||||
dataBuffer
|
||||
);
|
||||
handleCostume(vmCostume);
|
||||
handleCostume([vmCostume]);
|
||||
};
|
||||
|
||||
if (costumeFormat === storage.DataFormat.SVG) {
|
||||
@@ -197,9 +211,10 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp
|
||||
}
|
||||
case 'image/svg+xml':
|
||||
case 'image/png':
|
||||
case 'image/jpeg': {
|
||||
case 'image/jpeg':
|
||||
case 'image/gif': {
|
||||
// Make a sprite from an image by making it a costume first
|
||||
costumeUpload(fileData, fileType, `${spriteName}-${costumeName}`, storage, (vmCostume => {
|
||||
costumeUpload(fileData, fileType, `${spriteName}-${costumeName}`, storage, (vmCostumes => {
|
||||
const newSprite = {
|
||||
name: spriteName,
|
||||
isStage: false,
|
||||
@@ -213,7 +228,7 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp
|
||||
currentCostume: 0,
|
||||
blocks: {},
|
||||
variables: {},
|
||||
costumes: [vmCostume],
|
||||
costumes: vmCostumes,
|
||||
sounds: [] // TODO are all of these necessary?
|
||||
};
|
||||
randomizeSpritePosition(newSprite);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {GifReader} from 'omggif';
|
||||
|
||||
export default (arrayBuffer, {onFrame, onDone}) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const gifReader = new GifReader(new Uint8Array(arrayBuffer));
|
||||
canvas.width = gifReader.width;
|
||||
canvas.height = gifReader.height;
|
||||
|
||||
let imageData = ctx.createImageData(canvas.width, canvas.height);
|
||||
let previousData = ctx.createImageData(canvas.width, canvas.height);
|
||||
|
||||
const loadFrame = i => {
|
||||
const framePixels = [];
|
||||
gifReader.decodeAndBlitFrameRGBA(i, framePixels);
|
||||
const {x, y, width, height, disposal} = gifReader.frameInfo(i);
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let column = 0; column < width; column++) {
|
||||
const indexOffset = 4 * (x + (y * canvas.width));
|
||||
const j = indexOffset + (4 * (column + (row * canvas.width)));
|
||||
if (framePixels[j + 3]) {
|
||||
imageData.data[j + 0] = framePixels[j + 0];
|
||||
imageData.data[j + 1] = framePixels[j + 1];
|
||||
imageData.data[j + 2] = framePixels[j + 2];
|
||||
imageData.data[j + 3] = framePixels[j + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const dataUrl = canvas.toDataURL();
|
||||
|
||||
switch (disposal) {
|
||||
case 0:
|
||||
case 1: // Both are "keep previous frame"
|
||||
previousData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
break;
|
||||
case 2: // "Return to background", blank out the current frame
|
||||
ctx.clearRect(x, y, width, height);
|
||||
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
break;
|
||||
case 3: // "Restore to previous", copy previous data to current
|
||||
imageData = ctx.createImageData(canvas.width, canvas.height);
|
||||
imageData.data.set(previousData.data);
|
||||
break;
|
||||
default:
|
||||
// Not defined, treat like do-not-dispose
|
||||
previousData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
break;
|
||||
|
||||
}
|
||||
onFrame(i, dataUrl);
|
||||
|
||||
if (i < gifReader.numFrames() - 1) {
|
||||
setTimeout(() => {
|
||||
loadFrame(i + 1);
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
onDone();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadFrame(0);
|
||||
};
|
||||
Reference in new issue
Block a user