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');
|
2017-03-27 15:04:44 -04:00
|
|
|
|
2018-11-06 16:44:17 -05:00
|
|
|
const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
|
2018-12-10 14:39:17 -05:00
|
|
|
return new Promise(resolve => {
|
|
|
|
let svgString = costume.asset.decodeText();
|
|
|
|
// SVG Renderer load fixes "quirks" associated with Scratch 2 projects
|
|
|
|
if (optVersion && optVersion === 2 && !runtime.v2SvgAdapter) {
|
|
|
|
log.error('No V2 SVG adapter present; SVGs may not render correctly.');
|
|
|
|
} else if (optVersion && optVersion === 2 && runtime.v2SvgAdapter) {
|
|
|
|
runtime.v2SvgAdapter.loadString(svgString, true /* fromVersion2 */);
|
|
|
|
svgString = runtime.v2SvgAdapter.toString();
|
|
|
|
// Put back into storage
|
|
|
|
const storage = runtime.storage;
|
|
|
|
costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true);
|
|
|
|
costume.assetId = costume.asset.assetId;
|
|
|
|
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
|
|
|
}
|
|
|
|
// createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's
|
|
|
|
// undefined here
|
|
|
|
costume.skinId = runtime.renderer.createSVGSkin(svgString, rotationCenter);
|
|
|
|
costume.size = runtime.renderer.getSkinSize(costume.skinId);
|
|
|
|
// Now we should have a rotationCenter even if we didn't before
|
|
|
|
if (!rotationCenter) {
|
|
|
|
rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
|
|
|
|
costume.rotationCenterX = rotationCenter[0];
|
|
|
|
costume.rotationCenterY = rotationCenter[1];
|
|
|
|
costume.bitmapResolution = 1;
|
|
|
|
}
|
2018-04-20 00:04:08 -04:00
|
|
|
|
2018-12-10 14:39:17 -05:00
|
|
|
resolve(costume);
|
|
|
|
});
|
2018-07-10 10:21:21 -04:00
|
|
|
};
|
2017-03-27 15:04:44 -04:00
|
|
|
|
2019-02-04 13:38:46 -05:00
|
|
|
const canvasPool = (function () {
|
|
|
|
/**
|
|
|
|
* A pool of canvas objects that can be reused to reduce memory
|
|
|
|
* allocations. And time spent in those allocations and the later garbage
|
|
|
|
* collection.
|
|
|
|
*/
|
|
|
|
class CanvasPool {
|
|
|
|
constructor () {
|
|
|
|
this.pool = [];
|
|
|
|
this.clearSoon = null;
|
|
|
|
}
|
2019-02-04 12:43:31 -05:00
|
|
|
|
2019-02-04 13:38:46 -05:00
|
|
|
/**
|
|
|
|
* After a short wait period clear the pool to let the VM collect
|
|
|
|
* garbage.
|
|
|
|
*/
|
|
|
|
clear () {
|
|
|
|
if (!this.clearSoon) {
|
|
|
|
this.clearSoon = new Promise(resolve => setTimeout(resolve, 1000))
|
2019-02-04 12:43:31 -05:00
|
|
|
.then(() => {
|
2019-02-04 13:38:46 -05:00
|
|
|
this.pool.length = 0;
|
|
|
|
this.clearSoon = null;
|
2019-02-04 12:43:31 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2019-02-04 13:38:46 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a canvas. Create the canvas if the pool is empty.
|
|
|
|
* @returns {HTMLCanvasElement} A canvas element.
|
|
|
|
*/
|
|
|
|
create () {
|
|
|
|
return this.pool.pop() || document.createElement('canvas');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Release the canvas to be reused.
|
|
|
|
* @param {HTMLCanvasElement} canvas A canvas element.
|
|
|
|
*/
|
|
|
|
release (canvas) {
|
|
|
|
this.clear();
|
|
|
|
this.pool.push(canvas);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new CanvasPool();
|
2019-02-04 12:43:31 -05:00
|
|
|
}());
|
|
|
|
|
2018-11-06 16:44:17 -05:00
|
|
|
/**
|
2018-11-06 17:51:39 -05:00
|
|
|
* Return a promise to fetch a bitmap from storage and return it as a canvas
|
2018-11-06 16:44:17 -05:00
|
|
|
* If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
|
|
|
|
* If the costume has a text layer asset, which is a text part from Scratch 1.4, then this function
|
|
|
|
* will merge the two image assets. See the issue LLK/scratch-vm#672 for more information.
|
|
|
|
* @param {!object} costume - the Scratch costume object.
|
2018-11-06 17:22:25 -05:00
|
|
|
* @param {!Runtime} runtime - Scratch runtime, used to access the v2BitmapAdapter
|
2018-11-06 16:44:17 -05:00
|
|
|
* @param {?object} rotationCenter - optionally passed in coordinates for the center of rotation for the image. If
|
|
|
|
* none is given, the rotation center of the costume will be set to the middle of the costume later on.
|
|
|
|
* @property {number} costume.bitmapResolution - the resolution scale for a bitmap costume.
|
|
|
|
* @returns {?Promise} - a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase},
|
|
|
|
* or reject on error.
|
|
|
|
* assetMatchesBase is true if the asset matches the base layer; false if it required adjustment
|
|
|
|
*/
|
2018-11-06 17:22:25 -05:00
|
|
|
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
2018-11-06 16:44:17 -05:00
|
|
|
if (!costume || !costume.asset) {
|
|
|
|
return Promise.reject('Costume load failed. Assets were missing.');
|
|
|
|
}
|
2018-11-20 15:08:25 -05:00
|
|
|
if (!runtime.v2BitmapAdapter) {
|
|
|
|
return Promise.reject('No V2 Bitmap adapter present.');
|
|
|
|
}
|
|
|
|
|
2019-02-04 12:45:07 -05:00
|
|
|
return Promise.all([costume.asset, costume.textLayerAsset].map(asset => {
|
|
|
|
if (!asset) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof createImageBitmap !== 'undefined') {
|
|
|
|
return createImageBitmap(
|
|
|
|
new Blob([asset.data], {type: asset.assetType.contentType})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const image = new Image();
|
|
|
|
image.onload = function () {
|
|
|
|
resolve(image);
|
|
|
|
image.onload = null;
|
|
|
|
image.onerror = null;
|
|
|
|
};
|
|
|
|
image.onerror = function () {
|
|
|
|
reject('Costume load failed. Asset could not be read.');
|
|
|
|
image.onload = null;
|
|
|
|
image.onerror = null;
|
|
|
|
};
|
|
|
|
image.src = asset.encodeDataURI();
|
|
|
|
});
|
|
|
|
}))
|
|
|
|
.then(([baseImageElement, textImageElement]) => {
|
2019-02-04 13:38:46 -05:00
|
|
|
const mergeCanvas = canvasPool.create();
|
2018-11-06 16:44:17 -05:00
|
|
|
|
2019-02-04 12:45:07 -05:00
|
|
|
const scale = costume.bitmapResolution === 1 ? 2 : 1;
|
|
|
|
mergeCanvas.width = baseImageElement.width;
|
|
|
|
mergeCanvas.height = baseImageElement.height;
|
|
|
|
|
|
|
|
const ctx = mergeCanvas.getContext('2d');
|
|
|
|
ctx.drawImage(baseImageElement, 0, 0);
|
2018-11-13 15:31:00 -05:00
|
|
|
if (textImageElement) {
|
2019-02-04 12:45:07 -05:00
|
|
|
ctx.drawImage(textImageElement, 0, 0);
|
|
|
|
}
|
2019-02-04 13:38:46 -05:00
|
|
|
// Track the canvas we merged the bitmaps onto separately from the
|
|
|
|
// canvas that we receive from resize if scale is not 1. We know
|
|
|
|
// resize treats mergeCanvas as read only data. We don't know when
|
|
|
|
// resize may use or modify the canvas. So we'll only release the
|
|
|
|
// mergeCanvas back into the canvas pool. Reusing the canvas from
|
|
|
|
// resize may cause errors.
|
2019-02-04 12:45:07 -05:00
|
|
|
let canvas = mergeCanvas;
|
|
|
|
if (scale !== 1) {
|
|
|
|
canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale);
|
2018-11-13 15:31:00 -05:00
|
|
|
}
|
2018-11-06 16:44:17 -05:00
|
|
|
|
2019-02-04 12:45:07 -05:00
|
|
|
// By scaling, we've converted it to bitmap resolution 2
|
|
|
|
if (rotationCenter) {
|
|
|
|
rotationCenter[0] = rotationCenter[0] * scale;
|
|
|
|
rotationCenter[1] = rotationCenter[1] * scale;
|
|
|
|
costume.rotationCenterX = rotationCenter[0];
|
|
|
|
costume.rotationCenterY = rotationCenter[1];
|
|
|
|
}
|
|
|
|
costume.bitmapResolution = 2;
|
|
|
|
|
|
|
|
// Clean up the costume object
|
|
|
|
delete costume.textLayerMD5;
|
|
|
|
delete costume.textLayerAsset;
|
|
|
|
|
|
|
|
return {
|
|
|
|
canvas,
|
|
|
|
mergeCanvas,
|
|
|
|
rotationCenter,
|
|
|
|
// True if the asset matches the base layer; false if it required adjustment
|
|
|
|
assetMatchesBase: scale === 1 && !textImageElement
|
|
|
|
};
|
|
|
|
})
|
2019-08-23 17:28:13 -04:00
|
|
|
.catch(error => {
|
|
|
|
log.warn(e.name, ': ',e.message);
|
2018-12-03 16:47:46 -05:00
|
|
|
// Clean up the text layer properties if it fails to load
|
2018-11-06 16:54:41 -05:00
|
|
|
delete costume.textLayerMD5;
|
|
|
|
delete costume.textLayerAsset;
|
|
|
|
});
|
2018-11-06 16:44:17 -05:00
|
|
|
};
|
|
|
|
|
2019-02-04 12:45:07 -05:00
|
|
|
const loadBitmap_ = function (costume, runtime, _rotationCenter) {
|
|
|
|
return fetchBitmapCanvas_(costume, runtime, _rotationCenter)
|
|
|
|
.then(fetched => {
|
|
|
|
const updateCostumeAsset = function (dataURI) {
|
|
|
|
if (!runtime.v2BitmapAdapter) {
|
|
|
|
// TODO: This might be a bad practice since the returned
|
|
|
|
// promise isn't acted on. If this is something we should be
|
|
|
|
// creating a rejected promise for we should also catch it
|
|
|
|
// somewhere and act on that error (like logging).
|
|
|
|
//
|
|
|
|
// Return a rejection to stop executing updateCostumeAsset.
|
|
|
|
return Promise.reject('No V2 Bitmap adapter present.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const storage = runtime.storage;
|
|
|
|
costume.asset = storage.createAsset(
|
|
|
|
storage.AssetType.ImageBitmap,
|
|
|
|
storage.DataFormat.PNG,
|
|
|
|
runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
|
|
|
|
null,
|
|
|
|
true // generate md5
|
|
|
|
);
|
|
|
|
costume.dataFormat = storage.DataFormat.PNG;
|
|
|
|
costume.assetId = costume.asset.assetId;
|
|
|
|
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!fetched.assetMatchesBase) {
|
|
|
|
updateCostumeAsset(fetched.canvas.toDataURL());
|
2018-11-06 17:22:25 -05:00
|
|
|
}
|
|
|
|
|
2019-02-04 12:45:07 -05:00
|
|
|
return fetched;
|
|
|
|
})
|
|
|
|
.then(({canvas, mergeCanvas, rotationCenter}) => {
|
2018-11-06 16:54:41 -05:00
|
|
|
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
|
|
|
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
|
2019-02-04 13:38:46 -05:00
|
|
|
canvasPool.release(mergeCanvas);
|
2018-11-06 16:54:41 -05:00
|
|
|
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
|
|
|
|
costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2
|
|
|
|
|
|
|
|
if (!rotationCenter) {
|
|
|
|
rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
|
|
|
|
// Actual rotation center, since all bitmaps are resolution 2
|
|
|
|
costume.rotationCenterX = rotationCenter[0] * 2;
|
|
|
|
costume.rotationCenterY = rotationCenter[1] * 2;
|
|
|
|
costume.bitmapResolution = 2;
|
|
|
|
}
|
|
|
|
return costume;
|
|
|
|
});
|
2017-03-27 15:04:44 -04:00
|
|
|
};
|
|
|
|
|
2018-07-10 10:21:21 -04:00
|
|
|
/**
|
|
|
|
* Initialize a costume from an asset asynchronously.
|
|
|
|
* 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.
|
2018-11-13 15:31:00 -05:00
|
|
|
* @property {!Asset} costume.asset - the asset of the costume loaded from storage.
|
2018-07-10 10:21:21 -04:00
|
|
|
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
|
|
|
* @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
|
|
|
|
* to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
|
|
|
|
* @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
|
|
|
|
*/
|
2018-11-06 16:44:17 -05:00
|
|
|
const loadCostumeFromAsset = function (costume, runtime, optVersion) {
|
|
|
|
costume.assetId = costume.asset.assetId;
|
2018-07-10 10:21:21 -04:00
|
|
|
const renderer = runtime.renderer;
|
|
|
|
if (!renderer) {
|
|
|
|
log.error('No rendering module present; cannot load costume: ', costume.name);
|
2018-10-03 14:58:36 -04:00
|
|
|
return Promise.resolve(costume);
|
2018-07-10 10:21:21 -04:00
|
|
|
}
|
|
|
|
const AssetType = runtime.storage.AssetType;
|
|
|
|
let rotationCenter;
|
|
|
|
// Use provided rotation center and resolution if they are defined. Bitmap resolution
|
|
|
|
// should only ever be 1 or 2.
|
|
|
|
if (typeof costume.rotationCenterX === 'number' && !isNaN(costume.rotationCenterX) &&
|
|
|
|
typeof costume.rotationCenterY === 'number' && !isNaN(costume.rotationCenterY)) {
|
|
|
|
rotationCenter = [costume.rotationCenterX, costume.rotationCenterY];
|
|
|
|
}
|
2018-12-07 21:46:28 -05:00
|
|
|
if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
|
2018-12-10 14:39:17 -05:00
|
|
|
return loadVector_(costume, runtime, rotationCenter, optVersion)
|
|
|
|
.catch(() => {
|
|
|
|
// Use default asset if original fails to load
|
|
|
|
costume.assetId = runtime.storage.defaultAssetId.ImageVector;
|
|
|
|
costume.asset = runtime.storage.get(costume.assetId);
|
2018-12-20 19:29:37 -05:00
|
|
|
costume.md5 = `${costume.assetId}.${AssetType.ImageVector.runtimeFormat}`;
|
2018-12-10 14:39:17 -05:00
|
|
|
return loadVector_(costume, runtime);
|
|
|
|
});
|
2018-07-10 10:21:21 -04:00
|
|
|
}
|
2018-11-06 16:44:17 -05:00
|
|
|
return loadBitmap_(costume, runtime, rotationCenter, optVersion);
|
2018-07-10 10:21:21 -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.
|
2018-11-06 17:39:26 -05:00
|
|
|
* @param {!string} md5ext - the MD5 and extension of the costume to be loaded.
|
2017-09-11 09:42:16 -04:00
|
|
|
* @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.
|
2018-05-09 15:48:49 -04:00
|
|
|
* @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
|
|
|
|
* to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
|
2017-09-11 09:42:16 -04:00
|
|
|
* @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
|
|
|
|
*/
|
2018-05-09 15:48:49 -04:00
|
|
|
const loadCostume = function (md5ext, costume, runtime, optVersion) {
|
2018-11-06 17:39:26 -05:00
|
|
|
const idParts = StringUtil.splitFirst(md5ext, '.');
|
|
|
|
const md5 = idParts[0];
|
|
|
|
const ext = idParts[1].toLowerCase();
|
|
|
|
costume.dataFormat = ext;
|
|
|
|
|
2018-11-05 15:50:28 -05:00
|
|
|
if (costume.asset) {
|
2018-11-06 17:34:07 -05:00
|
|
|
// Costume comes with asset. It could be coming from camera, image upload, drag and drop, or file
|
2018-11-06 17:39:26 -05:00
|
|
|
return loadCostumeFromAsset(costume, runtime, optVersion);
|
|
|
|
}
|
2018-11-05 15:50:28 -05:00
|
|
|
|
2018-11-06 17:39:26 -05:00
|
|
|
// Need to load the costume from storage. The server should have a reference to this md5.
|
|
|
|
if (!runtime.storage) {
|
|
|
|
log.error('No storage module present; cannot load costume asset: ', md5ext);
|
|
|
|
return Promise.resolve(costume);
|
|
|
|
}
|
2018-11-06 10:36:52 -05:00
|
|
|
|
2018-12-10 14:39:17 -05:00
|
|
|
if (!runtime.storage.defaultAssetId) {
|
|
|
|
log.error(`No default assets found`);
|
|
|
|
return Promise.resolve(costume);
|
|
|
|
}
|
|
|
|
|
2018-11-06 17:39:26 -05:00
|
|
|
const AssetType = runtime.storage.AssetType;
|
|
|
|
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
|
|
|
|
|
|
|
const costumePromise = runtime.storage.load(assetType, md5, ext);
|
|
|
|
if (!costumePromise) {
|
|
|
|
log.error(`Couldn't fetch costume asset: ${md5ext}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let textLayerPromise;
|
|
|
|
if (costume.textLayerMD5) {
|
|
|
|
textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png');
|
|
|
|
} else {
|
|
|
|
textLayerPromise = Promise.resolve(null);
|
2017-09-11 09:42:16 -04:00
|
|
|
}
|
|
|
|
|
2018-11-06 16:44:17 -05:00
|
|
|
return Promise.all([costumePromise, textLayerPromise]).then(assetArray => {
|
2018-11-06 10:36:52 -05:00
|
|
|
costume.asset = assetArray[0];
|
|
|
|
if (assetArray[1]) {
|
|
|
|
costume.textLayerAsset = assetArray[1];
|
|
|
|
}
|
2018-11-06 16:44:17 -05:00
|
|
|
return loadCostumeFromAsset(costume, runtime, optVersion);
|
2018-12-26 14:15:57 -05:00
|
|
|
});
|
2017-09-11 09:42:16 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
loadCostume,
|
2018-11-06 16:44:17 -05:00
|
|
|
loadCostumeFromAsset
|
2017-09-11 09:42:16 -04:00
|
|
|
};
|