Gif upload v1: sprites, costumes

This commit is contained in:
Paul Kaplan committed 2019-02-08 13:18:57 -05:00
1 parent da65b41366
commit 88651e6e48
6 files changed
+101 -12

No files matched your search

+1
View File
@@ -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
+10 -6
View File
@@ -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
+19 -4
View File
@@ -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);
+69
View File
@@ -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);
};