Skip to content

Commit 6f36758

Browse files
committed
(WIP) correct subpixel positioning for SVGs
1 parent 8ccf3e8 commit 6f36758

File tree

1 file changed

+88
-38
lines changed

1 file changed

+88
-38
lines changed

src/svg-renderer.js

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ class SvgRenderer {
1818
this._canvas = canvas || document.createElement('canvas');
1919
this._context = this._canvas.getContext('2d');
2020
this._measurements = {x: 0, y: 0, width: 0, height: 0};
21+
this._textureSize = [0, 0];
2122
this._cachedImage = null;
23+
this._cachedRotationCenter = null;
24+
25+
this.viewOffset = [0, 0];
2226
}
2327

2428
/**
@@ -33,7 +37,7 @@ class SvgRenderer {
3337
* This will be parsed and transformed, and finally drawn.
3438
* When drawing is finished, the `onFinish` callback is called.
3539
* @param {string} svgString String of SVG data to draw in quirks-mode.
36-
* @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`).
40+
* @param {number} [scale] - Optionally, also scale the image by this factor.
3741
* @param {Function} [onFinish] Optional callback for when drawing finished.
3842
*/
3943
fromString (svgString, scale, onFinish) {
@@ -54,15 +58,15 @@ class SvgRenderer {
5458
/**
5559
* @return {Array<number>} the natural size, in Scratch units, of this SVG.
5660
*/
57-
get size () {
61+
get measuredSize () {
5862
return [this._measurements.width, this._measurements.height];
5963
}
6064

6165
/**
62-
* @return {Array<number>} the offset (upper left corner) of the SVG's view box.
66+
* @return {Array<number>} the size, in "logical" pixels, of the texture.
6367
*/
64-
get viewOffset () {
65-
return [this._measurements.x, this._measurements.y];
68+
get textureSize () {
69+
return this._textureSize;
6670
}
6771

6872
/**
@@ -111,6 +115,8 @@ class SvgRenderer {
111115
x: this._svgTag.viewBox.baseVal.x,
112116
y: this._svgTag.viewBox.baseVal.y
113117
};
118+
119+
this._svgTag.setAttribute('preserveAspectRatio', 'none');
114120
}
115121

116122
/**
@@ -366,59 +372,103 @@ class SvgRenderer {
366372
}
367373

368374
/**
369-
* Get the drawing ratio, adjusted for HiDPI screens.
370-
* @return {number} Scale ratio to draw to canvases with.
371-
*/
372-
getDrawRatio () {
373-
const devicePixelRatio = window.devicePixelRatio || 1;
374-
const backingStoreRatio = this._context.webkitBackingStorePixelRatio ||
375-
this._context.mozBackingStorePixelRatio ||
376-
this._context.msBackingStorePixelRatio ||
377-
this._context.oBackingStorePixelRatio ||
378-
this._context.backingStorePixelRatio || 1;
379-
return devicePixelRatio / backingStoreRatio;
380-
}
381-
382-
/**
383-
* Draw the SVG to a canvas. The canvas will automatically be scaled by the value returned by `getDrawRatio`.
384-
* @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`).
375+
* Draw the SVG to a canvas.
376+
* @param {number} [scale] - Optionally, also scale the image by this factor.
385377
* @param {Function} [onFinish] - An optional callback to call when the draw operation is complete.
378+
* @param {Array<number>} [rotationCenter] - Optionally, the rotation center of the Skin this SVG is used for.
379+
* @param {number} [minScale] - Optionally, the minimum scale this SVG will be rendered at
380+
* with proper subpixel positioning.
386381
*/
387-
_draw (scale, onFinish) {
382+
_draw (scale, onFinish, rotationCenter, minScale) {
383+
const measurements = this._measurements;
384+
385+
// Compensate for quirks-mode SVG viewbox offset.
386+
// Multiplied by the minimum drawing scale.
387+
const center = [
388+
(rotationCenter[0] - this._measurements.x) * minScale,
389+
(rotationCenter[1] - this._measurements.y) * minScale
390+
];
391+
392+
// Take the fractional part of the scaled rotation center.
393+
// We will translate the viewbox by this amount later for proper subpixel positioning.
394+
const centerFrac = [
395+
(center[0] % 1),
396+
(center[1] % 1)
397+
];
398+
399+
// Scale the viewbox dimensions by the minimum scale, add the offset, then take the ceiling
400+
// to get the rendered size (scaled by minScale).
401+
const scaledSize = [
402+
Math.ceil((this._measurements.width * minScale) + (1 - centerFrac[0])),
403+
Math.ceil((this._measurements.height * minScale) + (1 - centerFrac[1]))
404+
];
405+
406+
// Scale back up to the SVG size.
407+
const textureSize = [
408+
scaledSize[0] / minScale,
409+
scaledSize[1] / minScale
410+
];
411+
412+
this._textureSize = textureSize;
413+
this.viewOffset = [
414+
((center[0] + (1 - centerFrac[0])) / minScale),
415+
((center[1] + (1 - centerFrac[1])) / minScale)
416+
];
417+
418+
// Adjust the SVG tag's viewbox to match the texture dimensions and offset.
419+
// This will ensure that the SVG is rendered at the proper sub-pixel position,
420+
// and with integer dimensions at power-of-two sizes down to minScale.
421+
if (!this._cachedRotationCenter ||
422+
this._cachedRotationCenter[0] !== rotationCenter[0] ||
423+
this._cachedRotationCenter[1] !== rotationCenter[1]) {
424+
425+
this._svgTag.setAttribute('viewBox',
426+
`${
427+
measurements.x - ((1 - centerFrac[0]) / minScale)
428+
} ${
429+
measurements.y - ((1 - centerFrac[1]) / minScale)
430+
} ${
431+
textureSize[0]
432+
} ${
433+
textureSize[1]
434+
}`);
435+
436+
this._svgTag.setAttribute('width', textureSize[0]);
437+
this._svgTag.setAttribute('height', textureSize[1]);
438+
439+
440+
this._cachedRotationCenter = rotationCenter;
441+
}
388442
// Convert the SVG text to an Image, and then draw it to the canvas.
389443
if (this._cachedImage) {
390-
this._drawFromImage(scale, onFinish);
444+
this._drawFromImage(scale, rotationCenter, onFinish);
391445
} else {
392446
const img = new Image();
393-
img.onload = () => {
447+
img.addEventListener('load', () => {
394448
this._cachedImage = img;
395-
this._drawFromImage(scale, onFinish);
396-
};
449+
this._drawFromImage(scale, rotationCenter, onFinish);
450+
});
397451
const svgText = this.toString(true /* shouldInjectFonts */);
398452
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
399453
}
400454
}
401455

402456
/**
403457
* Draw to the canvas from a loaded image element.
404-
* @param {number} [scale] - Optionally, also scale the image by this factor (multiplied by `getDrawRatio()`).
458+
* @param {number} [scale] - Optionally, also scale the image by this factor.
459+
* @param {Array<number>} [rotationCenter] - Optionally, the rotation center of the Skin this SVG is used for.
405460
* @param {Function} [onFinish] - An optional callback to call when the draw operation is complete.
406461
**/
407-
_drawFromImage (scale, onFinish) {
462+
_drawFromImage (scale, rotationCenter, onFinish) {
408463
if (!this._cachedImage) return;
409464

410-
const ratio = this.getDrawRatio() * (Number.isFinite(scale) ? scale : 1);
411-
const bbox = this._measurements;
412-
this._canvas.width = bbox.width * ratio;
413-
this._canvas.height = bbox.height * ratio;
465+
const ratio = Number.isFinite(scale) ? scale : 1;
466+
this._canvas.width = this._textureSize[0] * ratio;
467+
this._canvas.height = this._textureSize[1] * ratio;
414468
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
415-
this._context.scale(ratio, ratio);
469+
// Scale the canvas up.
470+
this._context.setTransform(ratio, 0, 0, ratio, 0, 0);
416471
this._context.drawImage(this._cachedImage, 0, 0);
417-
// Reset the canvas transform after drawing.
418-
this._context.setTransform(1, 0, 0, 1, 0, 0);
419-
// Set the CSS style of the canvas to the actual measurements.
420-
this._canvas.style.width = bbox.width;
421-
this._canvas.style.height = bbox.height;
422472
// All finished - call the callback if provided.
423473
if (onFinish) {
424474
onFinish();

0 commit comments

Comments
 (0)