Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Scratch 1.4 costumes with text (that are imported from 2.0) #915

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/import/.load-costume.js.swp
Binary file not shown.
135 changes: 134 additions & 1 deletion src/import/load-costume.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,140 @@ const loadCostume = function (md5ext, costume, runtime) {
});
};

/**
* Load an "old text" costume's asset into memory asynchronously.
* "Old text" costumes are ones who have a text part from Scratch 1.4.
* See the issue LLK/scratch-vm#672 for more information.
* Do not call this unless there is a renderer attached.
* @param {string} baseMD5ext - the MD5 and extension of the base layer of the costume to be loaded.
* @param {string} textMD5ext - the MD5 and extension of the text layer of the costume to be loaded.
* @param {!object} costume - the Scratch costume object.
* @property {int} skinId - the ID of the costume's render skin, once installed.
* @property {number} rotationCenterX - the X component of the costume's origin.
* @property {number} rotationCenterY - the Y component of the costume's origin.
* @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
* @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
*/
const loadOldTextCostume = function (baseMD5ext, textMD5ext, costume, runtime) {
// @todo should [bitmapResolution] (in the documentation comment) not be optional? After all, the resulting image
// is always a bitmap.

if (!runtime.storage) {
log.error('No storage module present; cannot load costume asset: ', baseMD5ext, textMD5ext);
return Promise.resolve(costume);
}

const [baseMD5, baseExt] = StringUtil.splitFirst(baseMD5ext, '.');
const [textMD5, textExt] = StringUtil.splitFirst(textMD5ext, '.');

if (baseExt === 'svg' || textExt === 'svg') {
log.error('Old text costumes should never be SVGs');
return Promise.resolve(costume);
}

const assetType = runtime.storage.AssetType.ImageBitmap;

// @todo should this be in a separate function, which could also be used by loadCostume?
const rotationCenter = [
costume.rotationCenterX / costume.bitmapResolution,
costume.rotationCenterY / costume.bitmapResolution
];

// @todo what should the assetId be? Probably unset, since we'll be doing image processing (which will produce
// a completely new image)?
// @todo what about the dataFormat? This depends on how the image processing is implemented.

return Promise.all([
runtime.storage.load(assetType, baseMD5, baseExt),
runtime.storage.load(assetType, textMD5, textExt)
])
.then(costumeAssets => (
new Promise((resolve, reject) => {
const baseImageElement = new Image();
const textImageElement = new Image();

let loadedOne = false;

const onError = function () {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
reject();
};
const onLoad = function () {
if (loadedOne) {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
resolve([baseImageElement, textImageElement]);
} else {
loadedOne = true;
}
};

const removeEventListeners = function () {
baseImageElement.removeEventListener('error', onError);
textImageElement.removeEventListener('error', onError);
baseImageElement.removeEventListener('load', onLoad);
textImageElement.removeEventListener('load', onLoad);
};

baseImageElement.addEventListener('error', onError);
textImageElement.addEventListener('error', onError);
baseImageElement.addEventListener('load', onLoad);
textImageElement.addEventListener('load', onLoad);

const [baseAsset, textAsset] = costumeAssets;

baseImageElement.src = baseAsset.encodeDataURI();
textImageElement.src = textAsset.encodeDataURI();
})
))
.then(imageElements => {
const [baseImageElement, textImageElement] = imageElements;

const canvas = document.createElement('canvas');
canvas.width = baseImageElement.width;
canvas.height = baseImageElement.height;

const ctx = canvas.getContext('2d');
ctx.drawImage(baseImageElement, 0, 0);
ctx.drawImage(textImageElement, 0, 0);

return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
const reader = new FileReader();
const onError = function () {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
reject();
};
const onLoad = function () {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
costume.assetId = runtime.storage.builtinHelper.cache(
assetType,
runtime.storage.DataFormat.PNG,
new Buffer(reader.result)

This comment was marked as abuse.

This comment was marked as abuse.

);
costume.skinId = runtime.renderer.createBitmapSkin(
canvas, costume.bitmapResolution, rotationCenter
);
resolve(costume);
};
const removeEventListeners = function () {
reader.removeEventListener('error', onError);
reader.removeEventListener('load', onLoad);
};
reader.addEventListener('error', onError);
reader.addEventListener('load', onLoad);
reader.readAsArrayBuffer(blob);
}, 'image/png');
});
});
};

module.exports = {
loadCostume,
loadCostumeFromAsset
loadCostumeFromAsset,
loadOldTextCostume
};
Binary file added src/serialization/.sb2.js.swp
Binary file not shown.
11 changes: 9 additions & 2 deletions src/serialization/sb2.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const uid = require('../util/uid');
const specMap = require('./sb2_specmap');
const Variable = require('../engine/variable');

const {loadCostume} = require('../import/load-costume.js');
const {loadCostume, loadOldTextCostume} = require('../import/load-costume.js');
const {loadSound} = require('../import/load-sound.js');

/**
Expand Down Expand Up @@ -226,7 +226,14 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) {
rotationCenterY: costumeSource.rotationCenterY,
skinId: null
};
costumePromises.push(loadCostume(costumeSource.baseLayerMD5, costume, runtime));

if ('textLayerMD5' in costumeSource) {
costumePromises.push(
loadOldTextCostume(costumeSource.baseLayerMD5, costumeSource.textLayerMD5, costume, runtime)
);
} else {
costumePromises.push(loadCostume(costumeSource.baseLayerMD5, costume, runtime));
}
}
}
// Sounds from JSON
Expand Down