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-07-10 10:21:21 -04:00
|
|
|
const loadVector_ = function (costume, costumeAsset, runtime, rotationCenter, optVersion) {
|
|
|
|
let svgString = costumeAsset.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;
|
2018-10-18 07:39:28 -04:00
|
|
|
costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true);
|
|
|
|
costume.assetId = costume.asset.assetId;
|
2018-07-10 10:21:21 -04:00
|
|
|
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
2017-03-27 15:04:44 -04:00
|
|
|
}
|
2018-07-10 10:21:21 -04:00
|
|
|
// 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];
|
2018-07-12 17:25:53 -04:00
|
|
|
costume.bitmapResolution = 1;
|
2018-04-18 14:21:29 -04:00
|
|
|
}
|
2018-04-20 00:04:08 -04:00
|
|
|
|
2018-10-03 14:58:36 -04:00
|
|
|
return Promise.resolve(costume);
|
2018-07-10 10:21:21 -04:00
|
|
|
};
|
2017-03-27 15:04:44 -04:00
|
|
|
|
2018-07-10 10:21:21 -04:00
|
|
|
const loadBitmap_ = function (costume, costumeAsset, runtime, rotationCenter) {
|
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();
|
2018-07-12 17:18:38 -04:00
|
|
|
reject('Image load failed');
|
2017-09-07 11:51:21 -04:00
|
|
|
};
|
|
|
|
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);
|
2018-07-12 14:33:38 -04:00
|
|
|
const src = costumeAsset.encodeDataURI();
|
2018-07-10 10:21:21 -04:00
|
|
|
if (costume.bitmapResolution === 1 && !runtime.v2BitmapAdapter) {
|
|
|
|
log.error('No V2 bitmap adapter present; bitmaps may not render correctly.');
|
|
|
|
} else if (costume.bitmapResolution === 1) {
|
2018-07-12 14:33:38 -04:00
|
|
|
runtime.v2BitmapAdapter.convertResolution1Bitmap(src, (error, dataURI) => {
|
|
|
|
if (error) {
|
|
|
|
log.error(error);
|
|
|
|
} else if (dataURI) {
|
2018-07-10 10:21:21 -04:00
|
|
|
// Put back into storage
|
|
|
|
const storage = runtime.storage;
|
2018-10-18 07:39:28 -04:00
|
|
|
costume.asset = storage.createAsset(
|
2018-07-10 10:21:21 -04:00
|
|
|
storage.AssetType.ImageBitmap,
|
|
|
|
storage.DataFormat.PNG,
|
2018-10-18 07:39:28 -04:00
|
|
|
runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
|
|
|
|
null,
|
|
|
|
true // generate md5
|
2018-07-10 10:21:21 -04:00
|
|
|
);
|
2018-10-18 07:39:28 -04:00
|
|
|
costume.assetId = costume.asset.assetId;
|
2018-07-10 10:21:21 -04:00
|
|
|
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
|
|
|
}
|
2018-07-12 14:33:38 -04:00
|
|
|
// Regardless of if conversion succeeds, convert it to bitmap resolution 2,
|
|
|
|
// since all code from here on will assume that.
|
|
|
|
if (rotationCenter) {
|
|
|
|
rotationCenter[0] = rotationCenter[0] * 2;
|
|
|
|
rotationCenter[1] = rotationCenter[1] * 2;
|
|
|
|
costume.rotationCenterX = rotationCenter[0];
|
|
|
|
costume.rotationCenterY = rotationCenter[1];
|
|
|
|
}
|
|
|
|
costume.bitmapResolution = 2;
|
|
|
|
// Use original src if conversion fails.
|
|
|
|
// The image will appear half-sized.
|
|
|
|
imageElement.src = dataURI ? dataURI : src;
|
2018-07-10 10:21:21 -04:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
imageElement.src = src;
|
|
|
|
}
|
2017-09-07 11:51:21 -04:00
|
|
|
}).then(imageElement => {
|
2018-04-18 14:21:29 -04:00
|
|
|
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
2018-07-10 10:21:21 -04:00
|
|
|
costume.skinId = runtime.renderer.createBitmapSkin(imageElement, 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
|
2018-04-20 00:04:08 -04:00
|
|
|
|
|
|
|
if (!rotationCenter) {
|
2018-07-10 10:21:21 -04:00
|
|
|
rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
|
2018-07-10 11:47:11 -04:00
|
|
|
// Actual rotation center, since all bitmaps are resolution 2
|
2018-04-22 21:22:03 -04:00
|
|
|
costume.rotationCenterX = rotationCenter[0] * 2;
|
|
|
|
costume.rotationCenterY = rotationCenter[1] * 2;
|
2018-07-10 11:47:11 -04:00
|
|
|
costume.bitmapResolution = 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
|
|
|
};
|
|
|
|
|
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.
|
|
|
|
* @param {!Asset} costumeAsset - 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.
|
|
|
|
*/
|
|
|
|
const loadCostumeFromAsset = function (costume, costumeAsset, runtime, optVersion) {
|
|
|
|
costume.assetId = costumeAsset.assetId;
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
if (costumeAsset.assetType === AssetType.ImageVector) {
|
|
|
|
return loadVector_(costume, costumeAsset, runtime, rotationCenter, optVersion);
|
|
|
|
}
|
|
|
|
return loadBitmap_(costume, costumeAsset, runtime, rotationCenter, optVersion);
|
|
|
|
};
|
|
|
|
|
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.
|
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-05 15:50:28 -05:00
|
|
|
let costumePromise;
|
2018-11-06 10:36:52 -05:00
|
|
|
let textLayerPromise;
|
2018-11-05 15:50:28 -05:00
|
|
|
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.
|
|
|
|
|
2018-11-05 15:50:28 -05:00
|
|
|
// 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);
|
|
|
|
}
|
2017-09-11 09:42:16 -04:00
|
|
|
}
|
|
|
|
|
2018-11-06 10:36:52 -05:00
|
|
|
return Promise.all(costumePromise, textLayerPromise).then(assetArray => {
|
|
|
|
costume.asset = assetArray[0];
|
|
|
|
if (assetArray[1]) {
|
|
|
|
costume.textLayerAsset = assetArray[1];
|
|
|
|
}
|
2018-05-09 15:48:49 -04:00
|
|
|
return loadCostumeFromAsset(costume, costumeAsset, runtime, optVersion);
|
2018-07-12 17:18:38 -04:00
|
|
|
})
|
|
|
|
.catch(e => {
|
|
|
|
log.error(e);
|
|
|
|
});
|
2017-09-11 09:42:16 -04:00
|
|
|
};
|
|
|
|
|
2018-01-23 10:35:46 -05:00
|
|
|
/**
|
|
|
|
* 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.
|
2018-01-23 11:07:03 -05:00
|
|
|
* @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
|
2018-01-23 10:35:46 -05:00
|
|
|
*/
|
2018-01-23 15:10:39 -05:00
|
|
|
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.
|
2018-01-23 10:35:46 -05:00
|
|
|
|
|
|
|
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?
|
2017-04-17 15:10:04 -04:00
|
|
|
const rotationCenter = [
|
2017-03-27 15:04:44 -04:00
|
|
|
costume.rotationCenterX / costume.bitmapResolution,
|
|
|
|
costume.rotationCenterY / costume.bitmapResolution
|
|
|
|
];
|
|
|
|
|
2018-01-23 15:10:39 -05:00
|
|
|
// @todo what should the assetId be? Probably unset, since we'll be doing image processing (which will produce
|
|
|
|
// a completely new image)?
|
2018-01-23 10:35:46 -05:00
|
|
|
// @todo what about the dataFormat? This depends on how the image processing is implemented.
|
2017-04-03 15:58:13 -04:00
|
|
|
|
2018-01-23 10:35:46 -05:00
|
|
|
return Promise.all([
|
|
|
|
runtime.storage.load(assetType, baseMD5, baseExt),
|
|
|
|
runtime.storage.load(assetType, textMD5, textExt)
|
2018-01-23 15:10:39 -05:00
|
|
|
])
|
|
|
|
.then(costumeAssets => (
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
const baseImageElement = new Image();
|
|
|
|
const textImageElement = new Image();
|
2018-01-23 10:35:46 -05:00
|
|
|
|
2018-01-23 15:10:39 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|
2018-01-23 17:34:28 -05:00
|
|
|
|
2018-01-23 15:10:39 -05:00
|
|
|
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);
|
|
|
|
|
2018-01-23 17:34:28 -05:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
canvas.toBlob(blob => {
|
|
|
|
const reader = new FileReader();
|
2018-01-23 17:36:33 -05:00
|
|
|
const onError = function () {
|
2018-01-23 17:34:28 -05:00
|
|
|
// 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)
|
|
|
|
);
|
|
|
|
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');
|
|
|
|
});
|
2018-01-23 15:10:39 -05:00
|
|
|
});
|
2017-03-27 15:04:44 -04:00
|
|
|
};
|
|
|
|
|
2017-09-11 09:42:16 -04:00
|
|
|
module.exports = {
|
|
|
|
loadCostume,
|
2018-01-23 10:35:46 -05:00
|
|
|
loadCostumeFromAsset,
|
|
|
|
loadOldTextCostume
|
2017-09-11 09:42:16 -04:00
|
|
|
};
|