Move SVG renderer logic back into SVGSkin

This will allow for fancier stuff to be done with the SVG viewbox in the
future to avoid subpixel jitter.
This commit is contained in:
adroitwhiz 2021-01-07 20:48:13 -05:00
parent 9560de4d75
commit 801b0dab1f
2 changed files with 50 additions and 22 deletions

View File

@ -1,7 +1,7 @@
const twgl = require('twgl.js'); const twgl = require('twgl.js');
const Skin = require('./Skin'); const Skin = require('./Skin');
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer; const {loadSvgString, serializeSvgToString} = require('scratch-svg-renderer');
const ShaderManager = require('./ShaderManager'); const ShaderManager = require('./ShaderManager');
const MAX_TEXTURE_DIMENSION = 2048; const MAX_TEXTURE_DIMENSION = 2048;
@ -28,8 +28,17 @@ class SVGSkin extends Skin {
/** @type {RenderWebGL} */ /** @type {RenderWebGL} */
this._renderer = renderer; this._renderer = renderer;
/** @type {SvgRenderer} */ /** @type {HTMLImageElement} */
this._svgRenderer = new SvgRenderer(); this._svgImage = null;
/** @type {Array<number>} */
this._size = [0, 0];
/** @type {HTMLCanvasElement} */
this._canvas = document.createElement('canvas');
/** @type {CanvasRenderingContext2D} */
this._context = this._canvas.getContext('2d');
/** @type {Array<WebGLTexture>} */ /** @type {Array<WebGLTexture>} */
this._scaledMIPs = []; this._scaledMIPs = [];
@ -56,7 +65,7 @@ class SVGSkin extends Skin {
* @return {Array<number>} the natural size, in Scratch units, of this skin. * @return {Array<number>} the natural size, in Scratch units, of this skin.
*/ */
get size () { get size () {
return this._svgRenderer.size; return [this._size[0], this._size[1]];
} }
useNearest (scale, drawable) { useNearest (scale, drawable) {
@ -94,18 +103,27 @@ class SVGSkin extends Skin {
* @return {SVGMIP} An object that handles creating and updating SVG textures. * @return {SVGMIP} An object that handles creating and updating SVG textures.
*/ */
createMIP (scale) { createMIP (scale) {
this._svgRenderer.draw(scale); const [width, height] = this._size;
this._canvas.width = width * scale;
this._canvas.height = height * scale;
if (
this._canvas.width <= 0 ||
this._canvas.height <= 0 ||
// Even if the canvas at the current scale has a nonzero size, the image's dimensions are floored
// pre-scaling; e.g. if an image has a width of 0.4 and is being rendered at 3x scale, the canvas will have
// a width of 1, but the image's width will be rounded down to 0 on some browsers (Firefox) prior to being
// drawn at that scale, resulting in an IndexSizeError if we attempt to draw it.
this._svgImage.naturalWidth <= 0 ||
this._svgImage.naturalHeight <= 0
) return super.getTexture();
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._context.setTransform(scale, 0, 0, scale, 0, 0);
this._context.drawImage(this._svgImage, 0, 0);
// Pull out the ImageData from the canvas. ImageData speeds up // Pull out the ImageData from the canvas. ImageData speeds up
// updating Silhouette and is better handled by more browsers in // updating Silhouette and is better handled by more browsers in
// regards to memory. // regards to memory.
const canvas = this._svgRenderer.canvas; const textureData = this._context.getImageData(0, 0, this._canvas.width, this._canvas.height);
// If one of the canvas dimensions is 0, set this MIP to an empty image texture.
// This avoids an IndexSizeError from attempting to getImageData when one of the dimensions is 0.
if (canvas.width === 0 || canvas.height === 0) return super.getTexture();
const context = canvas.getContext('2d');
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
const textureOptions = { const textureOptions = {
auto: false, auto: false,
@ -147,7 +165,7 @@ class SVGSkin extends Skin {
// Can't use bitwise stuff here because we need to handle negative exponents // Can't use bitwise stuff here because we need to handle negative exponents
const mipScale = Math.pow(2, mipLevel - INDEX_OFFSET); const mipScale = Math.pow(2, mipLevel - INDEX_OFFSET);
if (this._svgRenderer.loaded && !this._scaledMIPs[mipLevel]) { if (this._svgImage.complete && !this._scaledMIPs[mipLevel]) {
this._scaledMIPs[mipLevel] = this.createMIP(mipScale); this._scaledMIPs[mipLevel] = this.createMIP(mipScale);
} }
@ -172,14 +190,21 @@ class SVGSkin extends Skin {
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG.
*/ */
setSVG (svgData, rotationCenter) { setSVG (svgData, rotationCenter) {
this._svgRenderer.loadSVG(svgData, false, () => { const svgTag = loadSvgString(svgData);
const svgSize = this._svgRenderer.size; this._svgImage = document.createElement('img');
if (svgSize[0] === 0 || svgSize[1] === 0) { const svgText = serializeSvgToString(svgTag, true /* shouldInjectFonts */);
this._svgImage.addEventListener('load', () => {
const {x, y, width, height} = svgTag.viewBox.baseVal;
this._size[0] = width;
this._size[1] = height;
if (width === 0 || height === 0) {
super.setEmptyImageData(); super.setEmptyImageData();
return; return;
} }
const maxDimension = Math.ceil(Math.max(this.size[0], this.size[1])); const maxDimension = Math.ceil(Math.max(width, height));
let testScale = 2; let testScale = 2;
for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) { for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
this._maxTextureScale = testScale; this._maxTextureScale = testScale;
@ -188,12 +213,15 @@ class SVGSkin extends Skin {
this.resetMIPs(); this.resetMIPs();
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter(); if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
const viewOffset = this._svgRenderer.viewOffset; // Compensate for viewbox offset.
this._rotationCenter[0] = rotationCenter[0] - viewOffset[0]; // See https://github.com/LLK/scratch-render/pull/90.
this._rotationCenter[1] = rotationCenter[1] - viewOffset[1]; this._rotationCenter[0] = rotationCenter[0] - x;
this._rotationCenter[1] = rotationCenter[1] - y;
this.emit(Skin.Events.WasAltered); this.emit(Skin.Events.WasAltered);
}); });
this._svgImage.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
} }
} }

View File

@ -14,7 +14,7 @@ window.waitForSVGSkinLoad = renderer => new Promise(resolve => {
for (const skin of renderer._allSkins) { for (const skin of renderer._allSkins) {
if (skin.constructor.name !== 'SVGSkin') continue; if (skin.constructor.name !== 'SVGSkin') continue;
numSVGSkins++; numSVGSkins++;
if (skin._svgRenderer.loaded) numLoadedSVGSkins++; if (skin._svgImage.complete) numLoadedSVGSkins++;
} }
if (numSVGSkins === numLoadedSVGSkins) { if (numSVGSkins === numLoadedSVGSkins) {
@ -47,7 +47,7 @@ window.initVM = render => {
vm.attachStorage(storage); vm.attachStorage(storage);
vm.attachRenderer(render); vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer()); vm.attachV2SVGAdapter(ScratchSVGRenderer.V2SVGAdapter);
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter()); vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
return vm; return vm;