2018-05-08 14:25:32 -04:00
|
|
|
const TextEncoder = require('text-encoding').TextEncoder;
|
2017-05-24 18:30:29 -04:00
|
|
|
const StringUtil = require('../util/string-util');
|
2017-04-17 15:10:04 -04:00
|
|
|
const log = require('../util/log');
|
2018-05-08 12:03:34 -04:00
|
|
|
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
|
2017-03-27 15:04:44 -04:00
|
|
|
|
|
|
|
/**
|
2017-09-11 09:42:16 -04:00
|
|
|
* Initialize a costume from an asset asynchronously.
|
2017-03-27 15:04:44 -04:00
|
|
|
* Do not call this unless there is a renderer attached.
|
|
|
|
* @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.
|
2017-09-11 09:42:16 -04:00
|
|
|
* @param {!Asset} costumeAsset - the asset of the costume loaded from storage.
|
2017-03-27 15:04:44 -04:00
|
|
|
* @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.
|
|
|
|
*/
|
2017-09-11 09:42:16 -04:00
|
|
|
const loadCostumeFromAsset = function (costume, costumeAsset, runtime) {
|
|
|
|
costume.assetId = costumeAsset.assetId;
|
2018-04-20 00:04:08 -04:00
|
|
|
const renderer = runtime.renderer;
|
|
|
|
if (!renderer) {
|
2017-09-11 09:42:16 -04:00
|
|
|
log.error('No rendering module present; cannot load costume: ', costume.name);
|
|
|
|
return costume;
|
2017-03-27 15:04:44 -04:00
|
|
|
}
|
2017-04-20 18:30:38 -04:00
|
|
|
const AssetType = runtime.storage.AssetType;
|
2018-04-18 14:21:29 -04:00
|
|
|
let rotationCenter;
|
|
|
|
if (costume.rotationCenterX && costume.rotationCenterY && costume.bitmapResolution) {
|
|
|
|
rotationCenter = [
|
|
|
|
costume.rotationCenterX / costume.bitmapResolution,
|
|
|
|
costume.rotationCenterY / costume.bitmapResolution
|
|
|
|
];
|
|
|
|
}
|
2017-09-07 11:51:21 -04:00
|
|
|
if (costumeAsset.assetType === AssetType.ImageVector) {
|
2018-05-08 12:03:34 -04:00
|
|
|
let svgString = costumeAsset.decodeText();
|
2018-05-08 18:30:52 -04:00
|
|
|
// SVG Renderer load fixes "quirks" associated with Scratch 2 projects
|
|
|
|
// Currently costume.version only exists for scratch 2 costumes
|
2018-05-08 12:03:34 -04:00
|
|
|
if (costume.version && costume.version === 2) {
|
|
|
|
const svgRenderer = new SvgRenderer();
|
|
|
|
svgRenderer.loadString(svgString);
|
|
|
|
svgString = svgRenderer.toString();
|
|
|
|
delete costume.version;
|
|
|
|
|
|
|
|
// Put back into storage
|
|
|
|
const storage = runtime.storage;
|
|
|
|
costume.assetId = storage.builtinHelper.cache(
|
|
|
|
storage.AssetType.ImageVector,
|
|
|
|
storage.DataFormat.SVG,
|
|
|
|
(new TextEncoder()).encode(svgString)
|
|
|
|
);
|
|
|
|
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
2018-05-08 18:30:52 -04:00
|
|
|
} else {
|
|
|
|
delete costume.version;
|
2018-05-08 12:03:34 -04:00
|
|
|
}
|
2018-05-08 18:30:52 -04:00
|
|
|
// createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's
|
|
|
|
// undefined here
|
2018-05-08 12:03:34 -04:00
|
|
|
costume.skinId = renderer.createSVGSkin(svgString, rotationCenter);
|
2018-04-20 00:04:08 -04:00
|
|
|
costume.size = renderer.getSkinSize(costume.skinId);
|
|
|
|
// Now we should have a rotationCenter even if we didn't before
|
|
|
|
if (!rotationCenter) {
|
|
|
|
rotationCenter = renderer.getSkinRotationCenter(costume.skinId);
|
|
|
|
costume.rotationCenterX = rotationCenter[0];
|
|
|
|
costume.rotationCenterY = rotationCenter[1];
|
|
|
|
}
|
|
|
|
|
2017-09-07 11:51:21 -04:00
|
|
|
return costume;
|
2017-04-03 15:58:13 -04:00
|
|
|
}
|
2018-05-08 18:30:52 -04:00
|
|
|
// Other image types do not need a compatibility step
|
|
|
|
delete costume.version;
|
2017-03-27 15:04:44 -04:00
|
|
|
|
2017-09-07 11:51:21 -04:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const imageElement = new Image();
|
|
|
|
const onError = function () {
|
2017-09-11 15:15:30 -04:00
|
|
|
// eslint-disable-next-line no-use-before-define
|
2017-09-07 11:51:21 -04:00
|
|
|
removeEventListeners();
|
|
|
|
reject();
|
|
|
|
};
|
|
|
|
const onLoad = function () {
|
2017-09-11 15:15:30 -04:00
|
|
|
// eslint-disable-next-line no-use-before-define
|
2017-09-07 11:51:21 -04:00
|
|
|
removeEventListeners();
|
|
|
|
resolve(imageElement);
|
|
|
|
};
|
|
|
|
const removeEventListeners = function () {
|
|
|
|
imageElement.removeEventListener('error', onError);
|
|
|
|
imageElement.removeEventListener('load', onLoad);
|
|
|
|
};
|
|
|
|
imageElement.addEventListener('error', onError);
|
|
|
|
imageElement.addEventListener('load', onLoad);
|
|
|
|
imageElement.src = costumeAsset.encodeDataURI();
|
|
|
|
}).then(imageElement => {
|
2018-04-18 14:21:29 -04:00
|
|
|
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
2018-04-20 00:04:08 -04:00
|
|
|
costume.skinId = renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter);
|
|
|
|
costume.size = renderer.getSkinSize(costume.skinId);
|
|
|
|
|
|
|
|
if (!rotationCenter) {
|
|
|
|
rotationCenter = renderer.getSkinRotationCenter(costume.skinId);
|
2018-04-22 21:22:03 -04:00
|
|
|
costume.rotationCenterX = rotationCenter[0] * 2;
|
|
|
|
costume.rotationCenterY = rotationCenter[1] * 2;
|
2018-04-20 00:04:08 -04:00
|
|
|
}
|
2017-09-07 11:51:21 -04:00
|
|
|
return costume;
|
|
|
|
});
|
2017-03-27 15:04:44 -04:00
|
|
|
};
|
|
|
|
|
2017-09-11 09:42:16 -04:00
|
|
|
/**
|
|
|
|
* Load a costume's asset into memory asynchronously.
|
|
|
|
* Do not call this unless there is a renderer attached.
|
|
|
|
* @param {string} md5ext - the MD5 and extension 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 loadCostume = function (md5ext, costume, runtime) {
|
|
|
|
if (!runtime.storage) {
|
|
|
|
log.error('No storage module present; cannot load costume asset: ', md5ext);
|
|
|
|
return Promise.resolve(costume);
|
|
|
|
}
|
|
|
|
|
|
|
|
const AssetType = runtime.storage.AssetType;
|
|
|
|
const idParts = StringUtil.splitFirst(md5ext, '.');
|
|
|
|
const md5 = idParts[0];
|
|
|
|
const ext = idParts[1].toLowerCase();
|
|
|
|
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
2017-09-07 11:51:21 -04:00
|
|
|
|
2017-09-11 09:42:16 -04:00
|
|
|
return runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
|
|
|
|
costume.dataFormat = ext;
|
|
|
|
return loadCostumeFromAsset(costume, costumeAsset, runtime);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
loadCostume,
|
|
|
|
loadCostumeFromAsset
|
|
|
|
};
|