scratch-vm/src/import/load-costume.js

269 lines
12 KiB
JavaScript
Raw Normal View History

const StringUtil = require('../util/string-util');
2017-04-17 15:10:04 -04:00
const log = require('../util/log');
2018-11-06 16:44:17 -05:00
const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
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;
}
return Promise.resolve(costume);
};
2018-11-06 16:44:17 -05:00
/**
* Fetch bitmap from storage and return a canvas.
* 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.
* @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
*/
const fetchBitmapCanvas_ = function (costume, rotationCenter) {
if (!costume || !costume.asset) {
return Promise.reject('Costume load failed. Assets were missing.');
}
2017-09-07 11:51:21 -04:00
return new Promise((resolve, reject) => {
2018-11-06 16:44:17 -05:00
const baseImageElement = new Image();
const textImageElement = new Image();
let loadedOne = false;
2017-09-07 11:51:21 -04:00
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();
2018-11-06 16:44:17 -05:00
reject('Costume load failed. Asset could not be read.');
2017-09-07 11:51:21 -04:00
};
const onLoad = function () {
2018-11-06 16:44:17 -05:00
if (loadedOne) {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
resolve([baseImageElement, textImageElement]);
} else {
loadedOne = true;
}
2017-09-07 11:51:21 -04:00
};
2018-11-06 16:44:17 -05:00
2017-09-07 11:51:21 -04:00
const removeEventListeners = function () {
2018-11-06 16:44:17 -05:00
baseImageElement.removeEventListener('error', onError);
textImageElement.removeEventListener('error', onError);
baseImageElement.removeEventListener('load', onLoad);
textImageElement.removeEventListener('load', onLoad);
2017-09-07 11:51:21 -04:00
};
2018-11-06 16:44:17 -05:00
baseImageElement.addEventListener('error', onError);
textImageElement.addEventListener('error', onError);
baseImageElement.addEventListener('load', onLoad);
textImageElement.addEventListener('load', onLoad);
if (costume.textLayerAsset) {
textImageElement.src = costume.textLayerAsset.encodeDataURI();
} else {
2018-11-06 16:44:17 -05:00
loadedOne = true;
}
baseImageElement.src = costume.asset.encodeDataURI();
})
.then(imageElements => {
const [baseImageElement, textImageElement] = imageElements;
const canvas = document.createElement('canvas');
canvas.getContext('2d').imageSmoothingEnabled = false;
const scale = costume.bitmapResolution === 1 ? 2 : 1;
canvas.width = baseImageElement.width * scale;
canvas.height = baseImageElement.height * scale;
const ctx = canvas.getContext('2d');
ctx.drawImage(baseImageElement, 0, 0, canvas.width, canvas.height);
if (textImageElement.src) {
ctx.drawImage(textImageElement, 0, 0, canvas.width, canvas.height);
}
2018-11-06 16:44:17 -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;
return {
canvas: canvas,
rotationCenter: rotationCenter,
// True if the asset matches the base layer; false if it required adjustment
assetMatchesBase: scale !== 1 || textImageElement.src
};
})
.catch(e => Promise.reject(e))
.finally(() => {
// Clean up the costume object
delete costume.textLayerMD5;
delete costume.textLayerAsset;
});
};
const loadBitmap_ = function (costume, runtime, rotationCenter) {
return fetchBitmapCanvas_(costume, rotationCenter).then(fetched => new Promise(resolve => {
rotationCenter = fetched.rotationCenter;
const saveAssetToStorage = function (dataURI) {
const storage = runtime.storage;
costume.asset = storage.createAsset(
storage.AssetType.ImageBitmap,
storage.DataFormat.PNG,
runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
null,
true // generate md5
);
costume.assetId = costume.asset.assetId;
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
};
if (!fetched.assetMatchesBase) {
saveAssetToStorage(fetched.canvas.toDataURL());
}
resolve(fetched.canvas);
}))
.catch(e => Promise.reject(e))
.then(canvas => {
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
2018-11-06 16:44:17 -05:00
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
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);
2018-07-10 11:47:11 -04:00
// Actual rotation center, since all bitmaps are resolution 2
costume.rotationCenterX = rotationCenter[0] * 2;
costume.rotationCenterY = rotationCenter[1] * 2;
2018-07-10 11:47:11 -04:00
costume.bitmapResolution = 2;
}
2017-09-07 11:51:21 -04:00
return costume;
});
};
/**
* 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-06 16:44:17 -05:00
* @param {!Asset} costume.asset - the asset of the costume loaded from storage.
* @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;
const renderer = runtime.renderer;
if (!renderer) {
log.error('No rendering module present; cannot load costume: ', costume.name);
return Promise.resolve(costume);
}
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-11-06 16:44:17 -05:00
if (costume.asset.assetType === AssetType.ImageVector) {
return loadVector_(costume, runtime, rotationCenter, optVersion);
}
2018-11-06 16:44:17 -05:00
return loadBitmap_(costume, runtime, rotationCenter, optVersion);
};
/**
* 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.
* @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.
*/
const loadCostume = function (md5ext, costume, runtime, optVersion) {
let costumePromise;
2018-11-06 10:36:52 -05:00
let textLayerPromise;
if (costume.asset) {
2018-11-06 10:36:52 -05:00
// TODO if text asset exists, merge the 2 assets, put it back in storage, clean up text layer
// data from costume and return the costume with merged asset here.
// Costume comes with asset. It could be coming from camera, image upload, drag and drop, or sb file
costumePromise = Promise.resolve(costume.asset);
} else {
// 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);
}
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;
costume.dataFormat = ext;
costumePromise = runtime.storage.load(assetType, md5, ext);
if (!costumePromise) {
log.error(`Couldn't fetch costume asset: ${md5ext}`);
return;
}
2018-11-06 10:36:52 -05:00
if (costume.textLayerMD5) {
textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png');
} else {
textLayerPromise = Promise.resolve(null);
}
}
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-07-12 17:18:38 -04:00
})
.catch(e => {
log.error(e);
});
};
module.exports = {
loadCostume,
2018-11-06 16:44:17 -05:00
loadCostumeFromAsset
};