Skip to content

Commit

Permalink
Merge pull request #1315 from fsih/loadCostume
Browse files Browse the repository at this point in the history
Don't depend on this until scratchfoundation/scratch-gui#2575 is in

Break out loadVector and loadBitmap from loadCostumeAsset, and handle bitmap resolution 1 assets in loadBitmap
  • Loading branch information
fsih committed Jul 12, 2018
1 parent e5999f8 commit 546cf38
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 65 deletions.
9 changes: 9 additions & 0 deletions packages/scratch-vm/src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,15 @@ class Runtime extends EventEmitter {
this.v2SvgAdapter = svgAdapter;
}

/**
* Set the bitmap adapter for the VM/runtime, which converts scratch 2
* bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2)
* @param {!function} bitmapAdapter The adapter to attach
*/
attachV2BitmapAdapter (bitmapAdapter) {
this.v2BitmapAdapter = bitmapAdapter;
}

/**
* Attach the storage module
* @param {!ScratchStorage} storage The storage module to attach
Expand Down
174 changes: 109 additions & 65 deletions packages/scratch-vm/src/import/load-costume.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,46 @@
const StringUtil = require('../util/string-util');
const log = require('../util/log');

/**
* 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);
return costume;
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;
costumeAsset.encodeTextData(svgString, storage.DataFormat.SVG);
costume.assetId = storage.builtinHelper.cache(
storage.AssetType.ImageVector,
storage.DataFormat.SVG,
costumeAsset.data
);
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
}
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) &&
costume.bitmapResolution) {
rotationCenter = [
costume.rotationCenterX / costume.bitmapResolution,
costume.rotationCenterY / costume.bitmapResolution
];
// 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;
}
if (costumeAsset.assetType === AssetType.ImageVector) {
let svgString = costumeAsset.decodeText();
// SVG Renderer load fixes "quirks" associated with Scratch 2 projects
if (optVersion && optVersion === 2 && runtime.v2SvgAdapter) {
runtime.v2SvgAdapter.loadString(svgString, true /* fromVersion2 */);
svgString = runtime.v2SvgAdapter.toString();
// Put back into storage
const storage = runtime.storage;
costumeAsset.encodeTextData(svgString, storage.DataFormat.SVG);
costume.assetId = storage.builtinHelper.cache(
storage.AssetType.ImageVector,
storage.DataFormat.SVG,
costumeAsset.data
);
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 = renderer.createSVGSkin(svgString, rotationCenter);
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];
}

return costume;
}
return costume;
};

const loadBitmap_ = function (costume, costumeAsset, runtime, rotationCenter) {
return new Promise((resolve, reject) => {
const imageElement = new Image();
const onError = function () {
// eslint-disable-next-line no-use-before-define
removeEventListeners();
reject();
reject('Image load failed');
};
const onLoad = function () {
// eslint-disable-next-line no-use-before-define
Expand All @@ -82,21 +53,91 @@ const loadCostumeFromAsset = function (costume, costumeAsset, runtime, optVersio
};
imageElement.addEventListener('error', onError);
imageElement.addEventListener('load', onLoad);
imageElement.src = costumeAsset.encodeDataURI();
const src = costumeAsset.encodeDataURI();
if (costume.bitmapResolution === 1 && !runtime.v2BitmapAdapter) {
log.error('No V2 bitmap adapter present; bitmaps may not render correctly.');
} else if (costume.bitmapResolution === 1) {
runtime.v2BitmapAdapter.convertResolution1Bitmap(src, (error, dataURI) => {
if (error) {
log.error(error);
} else if (dataURI) {
// Put back into storage
const storage = runtime.storage;
costume.assetId = storage.builtinHelper.cache(
storage.AssetType.ImageBitmap,
storage.DataFormat.PNG,
runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI)
);
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
}
// 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;
});
} else {
imageElement.src = src;
}
}).then(imageElement => {
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
costume.skinId = renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter);
costume.size = renderer.getSkinSize(costume.skinId);
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

if (!rotationCenter) {
rotationCenter = renderer.getSkinRotationCenter(costume.skinId);
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;
});
};

/**
* 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);
return 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];
}
if (costumeAsset.assetType === AssetType.ImageVector) {
return loadVector_(costume, costumeAsset, runtime, rotationCenter, optVersion);
}
return loadBitmap_(costume, costumeAsset, runtime, rotationCenter, optVersion);
};

/**
* Load a costume's asset into memory asynchronously.
* Do not call this unless there is a renderer attached.
Expand Down Expand Up @@ -126,7 +167,10 @@ const loadCostume = function (md5ext, costume, runtime, optVersion) {
return runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
costume.dataFormat = ext;
return loadCostumeFromAsset(costume, costumeAsset, runtime, optVersion);
});
})
.catch(e => {
log.error(e);
});
};

module.exports = {
Expand Down
9 changes: 9 additions & 0 deletions packages/scratch-vm/src/virtual-machine.js
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,15 @@ class VirtualMachine extends EventEmitter {
this.runtime.attachV2SVGAdapter(svgAdapter);
}

/**
* Set the bitmap adapter for the VM/runtime, which converts scratch 2
* bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2)
* @param {!function} bitmapAdapter The adapter to attach
*/
attachV2BitmapAdapter (bitmapAdapter) {
this.runtime.attachV2BitmapAdapter(bitmapAdapter);
}

/**
* Set the storage module for the VM/runtime
* @param {!ScratchStorage} storage The storage module to attach
Expand Down

0 comments on commit 546cf38

Please sign in to comment.