mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
Merge pull request #1970 from mzgoddard/image-bitmap
Use Promise.all, createImageBitmap, and one canvas in `load-costume.js`
This commit is contained in:
commit
a791a1e64e
1 changed files with 141 additions and 97 deletions
|
@ -32,6 +32,53 @@ const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
.then(() => {
|
||||||
|
this.pool.length = 0;
|
||||||
|
this.clearSoon = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise to fetch a bitmap from storage and return it as a canvas
|
* Return a promise to fetch a bitmap from storage and return it as 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 bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
|
||||||
|
@ -54,64 +101,53 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
return Promise.reject('No V2 Bitmap adapter present.');
|
return Promise.reject('No V2 Bitmap adapter present.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const baseImageElement = new Image();
|
const image = new Image();
|
||||||
let textImageElement;
|
image.onload = function () {
|
||||||
|
resolve(image);
|
||||||
// We need to wait for 2 images total to load. loadedOne will be true when one
|
image.onload = null;
|
||||||
// is done, and we are just waiting for one more.
|
image.onerror = null;
|
||||||
let loadedOne = false;
|
};
|
||||||
|
image.onerror = function () {
|
||||||
const onError = function () {
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
removeEventListeners();
|
|
||||||
reject('Costume load failed. Asset could not be read.');
|
reject('Costume load failed. Asset could not be read.');
|
||||||
|
image.onload = null;
|
||||||
|
image.onerror = null;
|
||||||
};
|
};
|
||||||
const onLoad = function () {
|
image.src = asset.encodeDataURI();
|
||||||
if (loadedOne) {
|
});
|
||||||
// eslint-disable-next-line no-use-before-define
|
}))
|
||||||
removeEventListeners();
|
.then(([baseImageElement, textImageElement]) => {
|
||||||
resolve([baseImageElement, textImageElement]);
|
const mergeCanvas = canvasPool.create();
|
||||||
} else {
|
|
||||||
loadedOne = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEventListeners = function () {
|
|
||||||
baseImageElement.removeEventListener('error', onError);
|
|
||||||
baseImageElement.removeEventListener('load', onLoad);
|
|
||||||
if (textImageElement) {
|
|
||||||
textImageElement.removeEventListener('error', onError);
|
|
||||||
textImageElement.removeEventListener('load', onLoad);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
baseImageElement.addEventListener('load', onLoad);
|
|
||||||
baseImageElement.addEventListener('error', onError);
|
|
||||||
if (costume.textLayerAsset) {
|
|
||||||
textImageElement = new Image();
|
|
||||||
textImageElement.addEventListener('load', onLoad);
|
|
||||||
textImageElement.addEventListener('error', onError);
|
|
||||||
textImageElement.src = costume.textLayerAsset.encodeDataURI();
|
|
||||||
} else {
|
|
||||||
loadedOne = true;
|
|
||||||
}
|
|
||||||
baseImageElement.src = costume.asset.encodeDataURI();
|
|
||||||
}).then(imageElements => {
|
|
||||||
const [baseImageElement, textImageElement] = imageElements;
|
|
||||||
|
|
||||||
let canvas = document.createElement('canvas');
|
|
||||||
const scale = costume.bitmapResolution === 1 ? 2 : 1;
|
const scale = costume.bitmapResolution === 1 ? 2 : 1;
|
||||||
canvas.width = baseImageElement.width;
|
mergeCanvas.width = baseImageElement.width;
|
||||||
canvas.height = baseImageElement.height;
|
mergeCanvas.height = baseImageElement.height;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = mergeCanvas.getContext('2d');
|
||||||
ctx.drawImage(baseImageElement, 0, 0);
|
ctx.drawImage(baseImageElement, 0, 0);
|
||||||
if (textImageElement) {
|
if (textImageElement) {
|
||||||
ctx.drawImage(textImageElement, 0, 0);
|
ctx.drawImage(textImageElement, 0, 0);
|
||||||
}
|
}
|
||||||
|
// 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.
|
||||||
|
let canvas = mergeCanvas;
|
||||||
if (scale !== 1) {
|
if (scale !== 1) {
|
||||||
canvas = runtime.v2BitmapAdapter.resize(canvas, canvas.width * scale, canvas.height * scale);
|
canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// By scaling, we've converted it to bitmap resolution 2
|
// By scaling, we've converted it to bitmap resolution 2
|
||||||
|
@ -128,8 +164,9 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
delete costume.textLayerAsset;
|
delete costume.textLayerAsset;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canvas: canvas,
|
canvas,
|
||||||
rotationCenter: rotationCenter,
|
mergeCanvas,
|
||||||
|
rotationCenter,
|
||||||
// True if the asset matches the base layer; false if it required adjustment
|
// True if the asset matches the base layer; false if it required adjustment
|
||||||
assetMatchesBase: scale === 1 && !textImageElement
|
assetMatchesBase: scale === 1 && !textImageElement
|
||||||
};
|
};
|
||||||
|
@ -141,12 +178,17 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBitmap_ = function (costume, runtime, rotationCenter) {
|
const loadBitmap_ = function (costume, runtime, _rotationCenter) {
|
||||||
return fetchBitmapCanvas_(costume, runtime, rotationCenter).then(fetched => new Promise(resolve => {
|
return fetchBitmapCanvas_(costume, runtime, _rotationCenter)
|
||||||
rotationCenter = fetched.rotationCenter;
|
.then(fetched => {
|
||||||
|
|
||||||
const updateCostumeAsset = function (dataURI) {
|
const updateCostumeAsset = function (dataURI) {
|
||||||
if (!runtime.v2BitmapAdapter) {
|
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.');
|
return Promise.reject('No V2 Bitmap adapter present.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,11 +208,13 @@ const loadBitmap_ = function (costume, runtime, rotationCenter) {
|
||||||
if (!fetched.assetMatchesBase) {
|
if (!fetched.assetMatchesBase) {
|
||||||
updateCostumeAsset(fetched.canvas.toDataURL());
|
updateCostumeAsset(fetched.canvas.toDataURL());
|
||||||
}
|
}
|
||||||
resolve(fetched.canvas);
|
|
||||||
}))
|
return fetched;
|
||||||
.then(canvas => {
|
})
|
||||||
|
.then(({canvas, mergeCanvas, rotationCenter}) => {
|
||||||
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
||||||
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
|
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
|
||||||
|
canvasPool.release(mergeCanvas);
|
||||||
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
|
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
|
||||||
costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2
|
costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue