From f1e70c430f1f445972f997d9511d176bd592c30f Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 24 Apr 2018 17:34:59 -0400 Subject: [PATCH 1/8] updateBitmap --- src/virtual-machine.js | 49 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 8d0445598..202481a36 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -2,6 +2,7 @@ const TextEncoder = require('text-encoding').TextEncoder; const EventEmitter = require('events'); const JSZip = require('jszip'); +const Buffer = require('buffer').Buffer; const centralDispatch = require('./dispatch/central-dispatch'); const ExtensionManager = require('./extension-support/extension-manager'); const log = require('./util/log'); @@ -543,17 +544,59 @@ class VirtualMachine extends EventEmitter { /** * Get an SVG string from storage. * @param {int} costumeIndex - the index of the costume to be got. - * @return {string} the costume's SVG string, or null if it's not an SVG costume. + * @return {string|HTMLImageElement} the costume's SVG string if it's SVG, + * an HTMLImageElement if it's a PNG, or null if it couldn't be found or decoded. */ getCostume (costumeIndex) { const id = this.editingTarget.getCostumes()[costumeIndex].assetId; - if (id && this.runtime && this.runtime.storage && - this.runtime.storage.get(id).dataFormat === 'svg') { + if (!id || !this.runtime || !this.runtime.storage) return null; + const format = this.runtime.storage.get(id).dataFormat; + if (format === this.runtime.storage.DataFormat.SVG) { return this.runtime.storage.get(id).decodeText(); + } else if (format === this.runtime.storage.DataFormat.PNG) { + const data = this.runtime.storage.get(id).encodeDataURI('image/png'); + const image = new Image(); + image.src = data; + return image; } + log.error(`Unhandled format: ${this.runtime.storage.get(id).dataFormat}`); return null; } + /** + * Update a costume with the given bitmap + * @param {int} costumeIndex - the index of the costume to be updated. + * @param {HTMLCanvasElement} bitmap - new bitmap for the renderer. + * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner + * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner + */ + updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY) { + const costume = this.editingTarget.getCostumes()[costumeIndex]; + if (costume && this.runtime && this.runtime.renderer) { + costume.rotationCenterX = rotationCenterX; + costume.rotationCenterY = rotationCenterY; + this.runtime.renderer.updateBitmapSkin(costume.skinId, bitmap, [rotationCenterX, rotationCenterY]); + } + + bitmap.toBlob(blob => { + const reader = new FileReader(); + reader.addEventListener('loadend', () => { + const storage = this.runtime.storage; + costume.assetId = storage.builtinHelper.cache( + storage.AssetType.ImageBitmap, + storage.DataFormat.PNG, + Buffer.from(reader.result) + ); + // @todo is there a better way to make sure all info is up to date on the costume? + costume.dataFormat = storage.DataFormat.PNG; + costume.bitmapResolution = 2; + this.emitTargetsUpdate(); + }); + reader.readAsArrayBuffer(blob); + }); + + } + /** * Update a costume with the given SVG * @param {int} costumeIndex - the index of the costume to be updated. From c73b06a2540fd52fca7f3dd6cb866d584b976e59 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 26 Apr 2018 12:14:19 -0400 Subject: [PATCH 2/8] add polyfill --- package.json | 1 + src/virtual-machine.js | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 73dde72c0..6fad0df04 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "babel-eslint": "^7.1.1", "babel-loader": "^7.0.0", "babel-preset-es2015": "^6.24.1", + "blueimp-canvas-to-blob": "3.14.0", "buffer-loader": "0.0.1", "copy-webpack-plugin": "4.2.1", "decode-html": "2.0.0", diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 202481a36..e06bb16dd 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -18,6 +18,7 @@ const Variable = require('./engine/variable'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); +require('blueimp-canvas-to-blob'); const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; From 3a3d807cba453dbbed0ce8d9965d7bf25d8750ab Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 26 Apr 2018 13:09:54 -0400 Subject: [PATCH 3/8] switch polyfill to one that doesn't break tests --- package.json | 2 +- src/virtual-machine.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6fad0df04..b39779db8 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "babel-eslint": "^7.1.1", "babel-loader": "^7.0.0", "babel-preset-es2015": "^6.24.1", - "blueimp-canvas-to-blob": "3.14.0", "buffer-loader": "0.0.1", + "canvas-toBlob": "1.0.0", "copy-webpack-plugin": "4.2.1", "decode-html": "2.0.0", "escape-html": "1.0.3", diff --git a/src/virtual-machine.js b/src/virtual-machine.js index e06bb16dd..6843c5c04 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -18,7 +18,7 @@ const Variable = require('./engine/variable'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); -require('blueimp-canvas-to-blob'); +require('canvas-toBlob'); const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; From 009253a8d1f6a93b02a57fa21f2291ee3de81771 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 26 Apr 2018 18:34:35 -0400 Subject: [PATCH 4/8] Code review --- package.json | 1 - src/virtual-machine.js | 41 ++++++++++++++++++----------------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index b39779db8..73dde72c0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "babel-loader": "^7.0.0", "babel-preset-es2015": "^6.24.1", "buffer-loader": "0.0.1", - "canvas-toBlob": "1.0.0", "copy-webpack-plugin": "4.2.1", "decode-html": "2.0.0", "escape-html": "1.0.3", diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 6843c5c04..609d83291 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -2,7 +2,6 @@ const TextEncoder = require('text-encoding').TextEncoder; const EventEmitter = require('events'); const JSZip = require('jszip'); -const Buffer = require('buffer').Buffer; const centralDispatch = require('./dispatch/central-dispatch'); const ExtensionManager = require('./extension-support/extension-manager'); const log = require('./util/log'); @@ -18,7 +17,6 @@ const Variable = require('./engine/variable'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); -require('canvas-toBlob'); const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; @@ -554,8 +552,9 @@ class VirtualMachine extends EventEmitter { const format = this.runtime.storage.get(id).dataFormat; if (format === this.runtime.storage.DataFormat.SVG) { return this.runtime.storage.get(id).decodeText(); - } else if (format === this.runtime.storage.DataFormat.PNG) { - const data = this.runtime.storage.get(id).encodeDataURI('image/png'); + } else if (format === this.runtime.storage.DataFormat.PNG || + format === this.runtime.storage.DataFormat.JPG) { + const data = this.runtime.storage.get(id).encodeDataURI(); const image = new Image(); image.src = data; return image; @@ -570,32 +569,28 @@ class VirtualMachine extends EventEmitter { * @param {HTMLCanvasElement} bitmap - new bitmap for the renderer. * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner + * @param {number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, 2 for double-resolution bitmaps */ - updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY) { + updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) { const costume = this.editingTarget.getCostumes()[costumeIndex]; if (costume && this.runtime && this.runtime.renderer) { costume.rotationCenterX = rotationCenterX; costume.rotationCenterY = rotationCenterY; - this.runtime.renderer.updateBitmapSkin(costume.skinId, bitmap, [rotationCenterX, rotationCenterY]); + this.runtime.renderer.updateBitmapSkin( + costume.skinId, bitmap, bitmapResolution, [rotationCenterX, rotationCenterY]); } - bitmap.toBlob(blob => { - const reader = new FileReader(); - reader.addEventListener('loadend', () => { - const storage = this.runtime.storage; - costume.assetId = storage.builtinHelper.cache( - storage.AssetType.ImageBitmap, - storage.DataFormat.PNG, - Buffer.from(reader.result) - ); - // @todo is there a better way to make sure all info is up to date on the costume? - costume.dataFormat = storage.DataFormat.PNG; - costume.bitmapResolution = 2; - this.emitTargetsUpdate(); - }); - reader.readAsArrayBuffer(blob); - }); - + const storage = this.runtime.storage; + costume.assetId = storage.builtinHelper.cache( + storage.AssetType.ImageBitmap, + storage.DataFormat.PNG, + bitmap.getContext('2d').getImageData(0, 0, bitmap.width, bitmap.height).data + ); + costume.dataFormat = storage.DataFormat.PNG; + costume.bitmapResolution = bitmapResolution; + costume.size = [bitmap.width, bitmap.height]; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + this.emitTargetsUpdate(); } /** From b2aa87da77a0d9ad286cfcfad251f208bafab963 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 26 Apr 2018 18:43:49 -0400 Subject: [PATCH 5/8] fix lint --- src/virtual-machine.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 609d83291..b8749f320 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -569,7 +569,8 @@ class VirtualMachine extends EventEmitter { * @param {HTMLCanvasElement} bitmap - new bitmap for the renderer. * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner - * @param {number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, 2 for double-resolution bitmaps + * @param {number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, + * 2 for double-resolution bitmaps */ updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) { const costume = this.editingTarget.getCostumes()[costumeIndex]; From 9f0f2fcd2745fb1789d3d68778b0344a5c43a3de Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 27 Apr 2018 17:23:30 -0400 Subject: [PATCH 6/8] Pass a data uri --- src/virtual-machine.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index b8749f320..89c032458 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -541,10 +541,10 @@ class VirtualMachine extends EventEmitter { } /** - * Get an SVG string from storage. + * Get a string representation of the image from storage. * @param {int} costumeIndex - the index of the costume to be got. - * @return {string|HTMLImageElement} the costume's SVG string if it's SVG, - * an HTMLImageElement if it's a PNG, or null if it couldn't be found or decoded. + * @return {string} the costume's SVG string if it's SVG, + * a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded. */ getCostume (costumeIndex) { const id = this.editingTarget.getCostumes()[costumeIndex].assetId; @@ -554,10 +554,7 @@ class VirtualMachine extends EventEmitter { return this.runtime.storage.get(id).decodeText(); } else if (format === this.runtime.storage.DataFormat.PNG || format === this.runtime.storage.DataFormat.JPG) { - const data = this.runtime.storage.get(id).encodeDataURI(); - const image = new Image(); - image.src = data; - return image; + return this.runtime.storage.get(id).encodeDataURI('image/png'); } log.error(`Unhandled format: ${this.runtime.storage.get(id).dataFormat}`); return null; @@ -566,7 +563,7 @@ class VirtualMachine extends EventEmitter { /** * Update a costume with the given bitmap * @param {int} costumeIndex - the index of the costume to be updated. - * @param {HTMLCanvasElement} bitmap - new bitmap for the renderer. + * @param {ImageData} bitmap - new bitmap for the renderer. * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner * @param {number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, @@ -577,20 +574,28 @@ class VirtualMachine extends EventEmitter { if (costume && this.runtime && this.runtime.renderer) { costume.rotationCenterX = rotationCenterX; costume.rotationCenterY = rotationCenterY; + + // @todo: updateBitmapSkin does not take ImageData + const canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const context = canvas.getContext('2d'); + context.putImageData(bitmap, 0, 0); + this.runtime.renderer.updateBitmapSkin( - costume.skinId, bitmap, bitmapResolution, [rotationCenterX, rotationCenterY]); + costume.skinId, canvas, bitmapResolution, [rotationCenterX, rotationCenterY]); } const storage = this.runtime.storage; costume.assetId = storage.builtinHelper.cache( storage.AssetType.ImageBitmap, storage.DataFormat.PNG, - bitmap.getContext('2d').getImageData(0, 0, bitmap.width, bitmap.height).data + bitmap.data ); costume.dataFormat = storage.DataFormat.PNG; costume.bitmapResolution = bitmapResolution; - costume.size = [bitmap.width, bitmap.height]; - costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + // costume.size = [bitmap.width, bitmap.height]; + // costume.md5 = `${costume.assetId}.${costume.dataFormat}`; this.emitTargetsUpdate(); } From 78cd3272376011003df6ba1939a1a4710487e2cb Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 30 Apr 2018 16:35:24 -0400 Subject: [PATCH 7/8] Move the place where we divide rotation center by resolution into the VM. Also revert the method of putting data into storage to the one that was convoluted but working --- package.json | 1 + src/virtual-machine.js | 73 +++++++++++++++++++++++++----------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 73dde72c0..b39779db8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "babel-loader": "^7.0.0", "babel-preset-es2015": "^6.24.1", "buffer-loader": "0.0.1", + "canvas-toBlob": "1.0.0", "copy-webpack-plugin": "4.2.1", "decode-html": "2.0.0", "escape-html": "1.0.3", diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 89c032458..34188183d 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -2,6 +2,7 @@ const TextEncoder = require('text-encoding').TextEncoder; const EventEmitter = require('events'); const JSZip = require('jszip'); +const Buffer = require('buffer').Buffer; const centralDispatch = require('./dispatch/central-dispatch'); const ExtensionManager = require('./extension-support/extension-manager'); const log = require('./util/log'); @@ -17,6 +18,7 @@ const Variable = require('./engine/variable'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); +require('canvas-toBlob'); const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; @@ -554,7 +556,7 @@ class VirtualMachine extends EventEmitter { return this.runtime.storage.get(id).decodeText(); } else if (format === this.runtime.storage.DataFormat.PNG || format === this.runtime.storage.DataFormat.JPG) { - return this.runtime.storage.get(id).encodeDataURI('image/png'); + return this.runtime.storage.get(id).encodeDataURI(); } log.error(`Unhandled format: ${this.runtime.storage.get(id).dataFormat}`); return null; @@ -562,41 +564,54 @@ class VirtualMachine extends EventEmitter { /** * Update a costume with the given bitmap - * @param {int} costumeIndex - the index of the costume to be updated. - * @param {ImageData} bitmap - new bitmap for the renderer. - * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner - * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner - * @param {number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, + * @param {!int} costumeIndex - the index of the costume to be updated. + * @param {!ImageData} bitmap - new bitmap for the renderer. + * @param {!number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner + * @param {!number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner + * @param {!number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, * 2 for double-resolution bitmaps */ updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) { const costume = this.editingTarget.getCostumes()[costumeIndex]; - if (costume && this.runtime && this.runtime.renderer) { - costume.rotationCenterX = rotationCenterX; - costume.rotationCenterY = rotationCenterY; + if (!(costume && this.runtime && this.runtime.renderer)) return; - // @todo: updateBitmapSkin does not take ImageData - const canvas = document.createElement('canvas'); - canvas.width = bitmap.width; - canvas.height = bitmap.height; - const context = canvas.getContext('2d'); - context.putImageData(bitmap, 0, 0); - - this.runtime.renderer.updateBitmapSkin( - costume.skinId, canvas, bitmapResolution, [rotationCenterX, rotationCenterY]); - } + costume.rotationCenterX = rotationCenterX; + costume.rotationCenterY = rotationCenterY; - const storage = this.runtime.storage; - costume.assetId = storage.builtinHelper.cache( - storage.AssetType.ImageBitmap, - storage.DataFormat.PNG, - bitmap.data + // @todo: updateBitmapSkin does not take ImageData + const canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const context = canvas.getContext('2d'); + context.putImageData(bitmap, 0, 0); + + // Divide by resolution because the renderer's definition of the rotation center + // is the rotation center divided by the bitmap resolution + this.runtime.renderer.updateBitmapSkin( + costume.skinId, + canvas, + bitmapResolution, + [rotationCenterX / bitmapResolution, rotationCenterY / bitmapResolution] ); - costume.dataFormat = storage.DataFormat.PNG; - costume.bitmapResolution = bitmapResolution; - // costume.size = [bitmap.width, bitmap.height]; - // costume.md5 = `${costume.assetId}.${costume.dataFormat}`; - this.emitTargetsUpdate(); + + // @todo there should be a better way to get from ImageData to a decodable storage format + canvas.toBlob(blob => { + const reader = new FileReader(); + reader.addEventListener('loadend', () => { + const storage = this.runtime.storage; + costume.assetId = storage.builtinHelper.cache( + storage.AssetType.ImageBitmap, + storage.DataFormat.PNG, + Buffer.from(reader.result) + ); + costume.dataFormat = storage.DataFormat.PNG; + costume.bitmapResolution = bitmapResolution; + costume.size = [bitmap.width, bitmap.height]; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + this.emitTargetsUpdate(); + }); + reader.readAsArrayBuffer(blob); + }); } /** From bd772e5f77b3d8473e3ab8caf4a83d0357a1f009 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 30 Apr 2018 19:58:48 -0400 Subject: [PATCH 8/8] Remove bitmap resolution from svgs --- src/virtual-machine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 34188183d..87e63fda7 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -639,6 +639,7 @@ class VirtualMachine extends EventEmitter { // so the dataFormat should be 'svg' costume.dataFormat = storage.DataFormat.SVG; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + delete costume.bitmapResolution; this.emitTargetsUpdate(); }