Merge pull request #232 from cwillisf/rasterize-scaled-svg

Re-rasterize SVGs when scaled up
This commit is contained in:
Ray Schamp
2018-01-17 10:07:13 -05:00
committed by GitHub
3 changed files with 108 additions and 39 deletions

View File

@@ -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,12 +65,29 @@ class SVGSkin extends Skin {
}
/**
* @param {Array<number>} scale - The scaling factors to be used.
* @param {Array<number>} 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) {
/** @todo re-render a scaled version if the requested scale is significantly larger than the current render */
// 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);
}
});
}
return this._texture;
}
@@ -74,8 +99,9 @@ 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;
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 +117,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);

View File

@@ -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;

View File

@@ -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;
}
/**
@@ -52,14 +53,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);
}
/**
@@ -91,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');
@@ -298,33 +300,49 @@ 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();
const bbox = this._measurements;
_draw (scale, onFinish) {
// 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 (this._onFinish) {
this._onFinish();
}
};
const svgText = this._toString();
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
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;
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();
}
}
/**