From 89b5ed32d06ab8fcd2d83b6dffd396d67afe5118 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 17 Jan 2018 00:16:19 -0500 Subject: [PATCH 1/2] First pass at re-rasterizing SVGs when scaled --- src/SVGSkin.js | 39 +++++++++++++++++++++++++++-- src/playground/index.html | 30 +++++++++++++++++----- src/svg-quirks-mode/svg-renderer.js | 20 +++++++-------- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/SVGSkin.js b/src/SVGSkin.js index 186eb16..fbfda2a 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -5,6 +5,8 @@ const SvgRenderer = require('./svg-quirks-mode/svg-renderer'); const Silhouette = require('./Silhouette'); +const MAX_TEXTURE_DIMENSION = 2048; + class SVGSkin extends Skin { /** * Create a new SVG skin. @@ -25,6 +27,12 @@ class SVGSkin extends Skin { /** @type {WebGLTexture} */ this._texture = null; + /** @type {number} */ + this._textureScale = 1; + + /** @type {Number} */ + this._maxTextureScale = 0; + this._silhouette = new Silhouette(); } @@ -57,11 +65,29 @@ class SVGSkin extends Skin { } /** - * @param {Array} scale - The scaling factors to be used. + * @param {Array} scale - The scaling factors to be used, each in the [0,100] range. * @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale. */ // eslint-disable-next-line no-unused-vars getTexture (scale) { + // The texture only ever gets uniform scale. Take the larger of the two axes. + const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100; + const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale); + let newScale = this._textureScale; + while ((newScale < this._maxTextureScale) && (requestedScale >= 1.5 * newScale)) { + newScale *= 2; + } + if (this._textureScale !== newScale) { + this._textureScale = newScale; + this._svgRenderer._draw(this._textureScale, () => { + if (this._textureScale === newScale) { + const gl = this._renderer.gl; + gl.bindTexture(gl.TEXTURE_2D, this._texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas); + } + }); + } + /** @todo re-render a scaled version if the requested scale is significantly larger than the current render */ return this._texture; } @@ -74,8 +100,10 @@ class SVGSkin extends Skin { * @fires Skin.event:WasAltered */ setSVG (svgData, rotationCenter) { - this._svgRenderer.fromString(svgData, () => { + this._svgRenderer.fromString(svgData, 1, () => { const gl = this._renderer.gl; + this._textureScale = this._maxTextureScale = 1; + this._svgRenderer.extraScale = 1; if (this._texture) { gl.bindTexture(gl.TEXTURE_2D, this._texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas); @@ -91,6 +119,13 @@ class SVGSkin extends Skin { this._texture = twgl.createTexture(gl, textureOptions); this._silhouette.update(this._svgRenderer.canvas); } + + const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height); + let testScale = 2; + for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) { + this._maxTextureScale = testScale; + } + if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter(); this.setRotationCenter.apply(this, rotationCenter); this.emit(Skin.Events.WasAltered); diff --git a/src/playground/index.html b/src/playground/index.html index e64d503..0d041c3 100644 --- a/src/playground/index.html +++ b/src/playground/index.html @@ -43,17 +43,35 @@ scale: [100, 100], direction: 90 }); + var drawableID2 = renderer.createDrawable(); + var wantBitmapSkin = false; + + // Bitmap (squirrel) var image = new Image(); image.onload = function () { - var skinId = renderer.createBitmapSkin(image); - renderer.updateDrawableProperties(drawableID2, { - skinId: skinId - }); + var bitmapSkinId = renderer.createBitmapSkin(image); + if (wantBitmapSkin) { + renderer.updateDrawableProperties(drawableID2, { + skinId: bitmapSkinId + }); + } }; image.crossOrigin = 'anonymous'; - image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + - '09dc888b0b7df19f70d81588ae73420e.svg/get/'; + image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/'; + + // SVG (cat 1-a) + var xhr = new XMLHttpRequest(); + xhr.addEventListener("load", function () { + var skinId = renderer.createSVGSkin(xhr.responseText); + if (!wantBitmapSkin) { + renderer.updateDrawableProperties(drawableID2, { + skinId: skinId + }); + } + }); + xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/f88bf1935daea28f8ca098462a31dbb0.svg/get/'); + xhr.send(); var fudgeSelect = document.getElementById('fudgeproperty'); var posX = 0; diff --git a/src/svg-quirks-mode/svg-renderer.js b/src/svg-quirks-mode/svg-renderer.js index 5b6147c..e679ab6 100644 --- a/src/svg-quirks-mode/svg-renderer.js +++ b/src/svg-quirks-mode/svg-renderer.js @@ -52,14 +52,12 @@ class SvgRenderer { * This will be parsed and transformed, and finally drawn. * When drawing is finished, the `onFinish` callback is called. * @param {string} svgString String of SVG data to draw in quirks-mode. + * @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`). * @param {Function} [onFinish] Optional callback for when drawing finished. */ - fromString (svgString, onFinish) { - // Store the callback for later. - this._onFinish = onFinish; + fromString (svgString, scale, onFinish) { this._loadString(svgString); - // Draw to a canvas. - this._draw(); + this._draw(scale, onFinish); } /** @@ -298,10 +296,12 @@ class SvgRenderer { } /** - * Draw the SVG to a canvas. + * Draw the SVG to a canvas. The canvas will automatically be scaled by the value returned by `getDrawRatio`. + * @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`). + * @param {Function} [onFinish] - An optional callback to call when the draw operation is complete. */ - _draw () { - const ratio = this.getDrawRatio(); + _draw (scale, onFinish) { + const ratio = this.getDrawRatio() * (Number.isFinite(scale) ? scale : 1); const bbox = this._measurements; // Convert the SVG text to an Image, and then draw it to the canvas. @@ -319,8 +319,8 @@ class SvgRenderer { this._canvas.style.width = bbox.width; this._canvas.style.height = bbox.height; // All finished - call the callback if provided. - if (this._onFinish) { - this._onFinish(); + if (onFinish) { + onFinish(); } }; const svgText = this._toString(); From 8a04fcd54180ee556eee83dd0c867ffdd1e2aa6e Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 17 Jan 2018 09:42:20 -0500 Subject: [PATCH 2/2] Cache image element in the SVG renderer. --- src/SVGSkin.js | 2 - src/svg-quirks-mode/svg-renderer.js | 62 +++++++++++++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/SVGSkin.js b/src/SVGSkin.js index fbfda2a..3ca6099 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -88,7 +88,6 @@ class SVGSkin extends Skin { }); } - /** @todo re-render a scaled version if the requested scale is significantly larger than the current render */ return this._texture; } @@ -103,7 +102,6 @@ class SVGSkin extends Skin { this._svgRenderer.fromString(svgData, 1, () => { const gl = this._renderer.gl; this._textureScale = this._maxTextureScale = 1; - this._svgRenderer.extraScale = 1; if (this._texture) { gl.bindTexture(gl.TEXTURE_2D, this._texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas); diff --git a/src/svg-quirks-mode/svg-renderer.js b/src/svg-quirks-mode/svg-renderer.js index e679ab6..bf1e95f 100644 --- a/src/svg-quirks-mode/svg-renderer.js +++ b/src/svg-quirks-mode/svg-renderer.js @@ -38,6 +38,7 @@ class SvgRenderer { this._canvas = canvas || document.createElement('canvas'); this._context = this._canvas.getContext('2d'); this._measurements = {x: 0, y: 0, width: 0, height: 0}; + this._cachedImage = null; } /** @@ -89,6 +90,9 @@ class SvgRenderer { * @param {string} svgString String of SVG data to draw in quirks-mode. */ _loadString (svgString) { + // New svg string invalidates the cached image + this._cachedImage = null; + // Parse string into SVG XML. const parser = new DOMParser(); this._svgDom = parser.parseFromString(svgString, 'text/xml'); @@ -301,30 +305,44 @@ class SvgRenderer { * @param {Function} [onFinish] - An optional callback to call when the draw operation is complete. */ _draw (scale, onFinish) { + // Convert the SVG text to an Image, and then draw it to the canvas. + if (this._cachedImage) { + this._drawFromImage(scale, onFinish); + } else { + const img = new Image(); + img.onload = () => { + this._cachedImage = img; + this._drawFromImage(scale, onFinish); + }; + const svgText = this._toString(); + img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; + } + } + + /** + * Draw to the canvas from a loaded image element. + * @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`). + * @param {Function} [onFinish] - An optional callback to call when the draw operation is complete. + **/ + _drawFromImage (scale, onFinish) { + if (!this._cachedImage) return; + const ratio = this.getDrawRatio() * (Number.isFinite(scale) ? scale : 1); const bbox = this._measurements; - - // Convert the SVG text to an Image, and then draw it to the canvas. - const img = new Image(); - img.onload = () => { - // Set up the canvas for drawing. - this._canvas.width = bbox.width * ratio; - this._canvas.height = bbox.height * ratio; - this._context.clearRect(0, 0, this._canvas.width, this._canvas.height); - this._context.scale(ratio, ratio); - this._context.drawImage(img, 0, 0); - // Reset the canvas transform after drawing. - this._context.setTransform(1, 0, 0, 1, 0, 0); - // Set the CSS style of the canvas to the actual measurements. - this._canvas.style.width = bbox.width; - this._canvas.style.height = bbox.height; - // All finished - call the callback if provided. - if (onFinish) { - onFinish(); - } - }; - const svgText = this._toString(); - img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; + this._canvas.width = bbox.width * ratio; + this._canvas.height = bbox.height * ratio; + this._context.clearRect(0, 0, this._canvas.width, this._canvas.height); + this._context.scale(ratio, ratio); + this._context.drawImage(this._cachedImage, 0, 0); + // Reset the canvas transform after drawing. + this._context.setTransform(1, 0, 0, 1, 0, 0); + // Set the CSS style of the canvas to the actual measurements. + this._canvas.style.width = bbox.width; + this._canvas.style.height = bbox.height; + // All finished - call the callback if provided. + if (onFinish) { + onFinish(); + } } /**