@@ -18,7 +18,11 @@ class SvgRenderer {
18
18
this . _canvas = canvas || document . createElement ( 'canvas' ) ;
19
19
this . _context = this . _canvas . getContext ( '2d' ) ;
20
20
this . _measurements = { x : 0 , y : 0 , width : 0 , height : 0 } ;
21
+ this . _textureSize = [ 0 , 0 ] ;
21
22
this . _cachedImage = null ;
23
+ this . _cachedRotationCenter = null ;
24
+
25
+ this . viewOffset = [ 0 , 0 ] ;
22
26
}
23
27
24
28
/**
@@ -33,7 +37,7 @@ class SvgRenderer {
33
37
* This will be parsed and transformed, and finally drawn.
34
38
* When drawing is finished, the `onFinish` callback is called.
35
39
* @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.
37
41
* @param {Function } [onFinish] Optional callback for when drawing finished.
38
42
*/
39
43
fromString ( svgString , scale , onFinish ) {
@@ -54,15 +58,15 @@ class SvgRenderer {
54
58
/**
55
59
* @return {Array<number> } the natural size, in Scratch units, of this SVG.
56
60
*/
57
- get size ( ) {
61
+ get measuredSize ( ) {
58
62
return [ this . _measurements . width , this . _measurements . height ] ;
59
63
}
60
64
61
65
/**
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 .
63
67
*/
64
- get viewOffset ( ) {
65
- return [ this . _measurements . x , this . _measurements . y ] ;
68
+ get textureSize ( ) {
69
+ return this . _textureSize ;
66
70
}
67
71
68
72
/**
@@ -111,6 +115,8 @@ class SvgRenderer {
111
115
x : this . _svgTag . viewBox . baseVal . x ,
112
116
y : this . _svgTag . viewBox . baseVal . y
113
117
} ;
118
+
119
+ this . _svgTag . setAttribute ( 'preserveAspectRatio' , 'none' ) ;
114
120
}
115
121
116
122
/**
@@ -366,59 +372,103 @@ class SvgRenderer {
366
372
}
367
373
368
374
/**
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.
385
377
* @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.
386
381
*/
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
+ }
388
442
// Convert the SVG text to an Image, and then draw it to the canvas.
389
443
if ( this . _cachedImage ) {
390
- this . _drawFromImage ( scale , onFinish ) ;
444
+ this . _drawFromImage ( scale , rotationCenter , onFinish ) ;
391
445
} else {
392
446
const img = new Image ( ) ;
393
- img . onload = ( ) => {
447
+ img . addEventListener ( 'load' , ( ) => {
394
448
this . _cachedImage = img ;
395
- this . _drawFromImage ( scale , onFinish ) ;
396
- } ;
449
+ this . _drawFromImage ( scale , rotationCenter , onFinish ) ;
450
+ } ) ;
397
451
const svgText = this . toString ( true /* shouldInjectFonts */ ) ;
398
452
img . src = `data:image/svg+xml;utf8,${ encodeURIComponent ( svgText ) } ` ;
399
453
}
400
454
}
401
455
402
456
/**
403
457
* 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.
405
460
* @param {Function } [onFinish] - An optional callback to call when the draw operation is complete.
406
461
**/
407
- _drawFromImage ( scale , onFinish ) {
462
+ _drawFromImage ( scale , rotationCenter , onFinish ) {
408
463
if ( ! this . _cachedImage ) return ;
409
464
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 ;
414
468
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 ) ;
416
471
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 ;
422
472
// All finished - call the callback if provided.
423
473
if ( onFinish ) {
424
474
onFinish ( ) ;
0 commit comments