@scratch/scratch-svg-renderer 11.0.0-UEPR-176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,628 @@
1
+ const Matrix = require('transformation-matrix');
2
+ const SvgElement = require('./svg-element');
3
+ const log = require('./util/log');
4
+
5
+ /**
6
+ * @fileOverview Apply transforms to match stroke width appearance in 2.0 and 3.0
7
+ */
8
+
9
+ // Adapted from paper.js's Path.applyTransform
10
+ const _parseTransform = function (domElement) {
11
+ let matrix = Matrix.identity();
12
+ const string = domElement.attributes && domElement.attributes.transform && domElement.attributes.transform.value;
13
+ if (!string) return matrix;
14
+ // https://www.w3.org/TR/SVG/types.html#DataTypeTransformList
15
+ // Parse SVG transform string. First we split at /)\s*/, to separate
16
+ // commands
17
+ const transforms = string.split(/\)\s*/g);
18
+ for (const transform of transforms) {
19
+ if (!transform) break;
20
+ // Command come before the '(', values after
21
+ const parts = transform.split(/\(\s*/);
22
+ const command = parts[0].trim();
23
+ const v = parts[1].split(/[\s,]+/g);
24
+ // Convert values to floats
25
+ for (let j = 0; j < v.length; j++) {
26
+ v[j] = parseFloat(v[j]);
27
+ }
28
+ switch (command) {
29
+ case 'matrix':
30
+ matrix = Matrix.compose(matrix, {a: v[0], b: v[1], c: v[2], d: v[3], e: v[4], f: v[5]});
31
+ break;
32
+ case 'rotate':
33
+ matrix = Matrix.compose(matrix, Matrix.rotateDEG(v[0], v[1] || 0, v[2] || 0));
34
+ break;
35
+ case 'translate':
36
+ matrix = Matrix.compose(matrix, Matrix.translate(v[0], v[1] || 0));
37
+ break;
38
+ case 'scale':
39
+ matrix = Matrix.compose(matrix, Matrix.scale(v[0], v[1] || v[0]));
40
+ break;
41
+ case 'skewX':
42
+ matrix = Matrix.compose(matrix, Matrix.skewDEG(v[0], 0));
43
+ break;
44
+ case 'skewY':
45
+ matrix = Matrix.compose(matrix, Matrix.skewDEG(0, v[0]));
46
+ break;
47
+ default:
48
+ log.error(`Couldn't parse: ${command}`);
49
+ }
50
+ }
51
+ return matrix;
52
+ };
53
+
54
+ // Adapted from paper.js's Matrix.decompose
55
+ // Given a matrix, return the x and y scale factors of the matrix
56
+ const _getScaleFactor = function (matrix) {
57
+ const a = matrix.a;
58
+ const b = matrix.b;
59
+ const c = matrix.c;
60
+ const d = matrix.d;
61
+ const det = (a * d) - (b * c);
62
+
63
+ if (a !== 0 || b !== 0) {
64
+ const r = Math.sqrt((a * a) + (b * b));
65
+ return {x: r, y: det / r};
66
+ }
67
+ if (c !== 0 || d !== 0) {
68
+ const s = Math.sqrt((c * c) + (d * d));
69
+ return {x: det / s, y: s};
70
+ }
71
+ // a = b = c = d = 0
72
+ return {x: 0, y: 0};
73
+ };
74
+
75
+ // Returns null if matrix is not invertible. Otherwise returns given ellipse
76
+ // transformed by transform, an object {radiusX, radiusY, rotation}.
77
+ const _calculateTransformedEllipse = function (radiusX, radiusY, theta, transform) {
78
+ theta = -theta * Math.PI / 180;
79
+ const a = transform.a;
80
+ const b = -transform.c;
81
+ const c = -transform.b;
82
+ const d = transform.d;
83
+ // Since other parameters determine the translation of the ellipse in SVG, we do not need to worry
84
+ // about what e and f are.
85
+ const det = (a * d) - (b * c);
86
+ // Non-invertible matrix
87
+ if (det === 0) return null;
88
+
89
+ // rotA, rotB, and rotC represent Ax^2 + Bxy + Cy^2 = 1 coefficients for a rotated ellipse formula
90
+ const sinT = Math.sin(theta);
91
+ const cosT = Math.cos(theta);
92
+ const sin2T = Math.sin(2 * theta);
93
+ const rotA = (cosT * cosT / radiusX / radiusX) + (sinT * sinT / radiusY / radiusY);
94
+ const rotB = (sin2T / radiusX / radiusX) - (sin2T / radiusY / radiusY);
95
+ const rotC = (sinT * sinT / radiusX / radiusX) + (cosT * cosT / radiusY / radiusY);
96
+
97
+ // Calculate the ellipse formula of the transformed ellipse
98
+ // A, B, and C represent Ax^2 + Bxy + Cy^2 = 1 / det / det coefficients in a transformed ellipse formula
99
+ // scaled by inverse det squared (to preserve accuracy)
100
+ const A = ((rotA * d * d) - (rotB * d * c) + (rotC * c * c));
101
+ const B = ((-2 * rotA * b * d) + (rotB * a * d) + (rotB * b * c) - (2 * rotC * a * c));
102
+ const C = ((rotA * b * b) - (rotB * a * b) + (rotC * a * a));
103
+
104
+ // Derive new radii and theta from the transformed ellipse formula
105
+ const newRadiusXOverDet = Math.sqrt(2) *
106
+ Math.sqrt(
107
+ (A + C - Math.sqrt((A * A) + (B * B) - (2 * A * C) + (C * C))) /
108
+ ((-B * B) + (4 * A * C))
109
+ );
110
+ const newRadiusYOverDet = 1 / Math.sqrt(A + C - (1 / newRadiusXOverDet / newRadiusXOverDet));
111
+ let temp = (A - (1 / newRadiusXOverDet / newRadiusXOverDet)) /
112
+ ((1 / newRadiusYOverDet / newRadiusYOverDet) - (1 / newRadiusXOverDet / newRadiusXOverDet));
113
+ if (temp < 0 && Math.abs(temp) < 1e-8) temp = 0; // Fix floating point issue
114
+ temp = Math.sqrt(temp);
115
+ if (Math.abs(1 - temp) < 1e-8) temp = 1; // Fix floating point issue
116
+ // Solve for which of the two possible thetas is correct
117
+ let newTheta = Math.asin(temp);
118
+ temp = (B / (
119
+ (1 / newRadiusXOverDet / newRadiusXOverDet) -
120
+ (1 / newRadiusYOverDet / newRadiusYOverDet)));
121
+ const newTheta2 = -newTheta;
122
+ if (Math.abs(Math.sin(2 * newTheta2) - temp) <
123
+ Math.abs(Math.sin(2 * newTheta) - temp)) {
124
+ newTheta = newTheta2;
125
+ }
126
+
127
+ return {
128
+ radiusX: newRadiusXOverDet * det,
129
+ radiusY: newRadiusYOverDet * det,
130
+ rotation: -newTheta * 180 / Math.PI
131
+ };
132
+ };
133
+
134
+ // Adapted from paper.js's PathItem.setPathData
135
+ const _transformPath = function (pathString, transform) {
136
+ if (!transform || Matrix.toString(transform) === Matrix.toString(Matrix.identity())) return pathString;
137
+ // First split the path data into parts of command-coordinates pairs
138
+ // Commands are any of these characters: mzlhvcsqta
139
+ const parts = pathString && pathString.match(/[mlhvcsqtaz][^mlhvcsqtaz]*/ig);
140
+ let coords;
141
+ let relative = false;
142
+ let previous;
143
+ let control;
144
+ let current = {x: 0, y: 0};
145
+ let start = {x: 0, y: 0};
146
+ let result = '';
147
+
148
+ const getCoord = function (index, coord) {
149
+ let val = +coords[index];
150
+ if (relative) {
151
+ val += current[coord];
152
+ }
153
+ return val;
154
+ };
155
+
156
+ const getPoint = function (index) {
157
+ return {x: getCoord(index, 'x'), y: getCoord(index + 1, 'y')};
158
+ };
159
+
160
+ const roundTo4Places = function (num) {
161
+ return Number(num.toFixed(4));
162
+ };
163
+
164
+ // Returns the transformed point as a string
165
+ const getString = function (point) {
166
+ const transformed = Matrix.applyToPoint(transform, point);
167
+ return `${roundTo4Places(transformed.x)} ${roundTo4Places(transformed.y)} `;
168
+ };
169
+
170
+ for (let i = 0, l = parts && parts.length; i < l; i++) {
171
+ const part = parts[i];
172
+ const command = part[0];
173
+ const lower = command.toLowerCase();
174
+ // Match all coordinate values
175
+ coords = part.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
176
+ const length = coords && coords.length;
177
+ relative = command === lower;
178
+ // Fix issues with z in the middle of SVG path data, not followed by
179
+ // a m command, see paper.js#413:
180
+ if (previous === 'z' && !/[mz]/.test(lower)) {
181
+ result += `M ${current.x} ${current.y} `;
182
+ }
183
+ switch (lower) {
184
+ case 'm': // Move to
185
+ case 'l': // Line to
186
+ {
187
+ let move = lower === 'm';
188
+ for (let j = 0; j < length; j += 2) {
189
+ result += move ? 'M ' : 'L ';
190
+ current = getPoint(j);
191
+ result += getString(current);
192
+ if (move) {
193
+ start = current;
194
+ move = false;
195
+ }
196
+ }
197
+ control = current;
198
+ break;
199
+ }
200
+ case 'h': // Horizontal line
201
+ case 'v': // Vertical line
202
+ {
203
+ const coord = lower === 'h' ? 'x' : 'y';
204
+ current = {x: current.x, y: current.y}; // Clone as we're going to modify it.
205
+ for (let j = 0; j < length; j++) {
206
+ current[coord] = getCoord(j, coord);
207
+ result += `L ${getString(current)}`;
208
+ }
209
+ control = current;
210
+ break;
211
+ }
212
+ case 'c':
213
+ // Cubic Bezier curve
214
+ for (let j = 0; j < length; j += 6) {
215
+ const handle1 = getPoint(j);
216
+ control = getPoint(j + 2);
217
+ current = getPoint(j + 4);
218
+ result += `C ${getString(handle1)}${getString(control)}${getString(current)}`;
219
+ }
220
+ break;
221
+ case 's':
222
+ // Smooth cubic Bezier curve
223
+ for (let j = 0; j < length; j += 4) {
224
+ const handle1 = /[cs]/.test(previous) ?
225
+ {x: (current.x * 2) - control.x, y: (current.y * 2) - control.y} :
226
+ current;
227
+ control = getPoint(j);
228
+ current = getPoint(j + 2);
229
+
230
+ result += `C ${getString(handle1)}${getString(control)}${getString(current)}`;
231
+ previous = lower;
232
+ }
233
+ break;
234
+ case 'q':
235
+ // Quadratic Bezier curve
236
+ for (let j = 0; j < length; j += 4) {
237
+ control = getPoint(j);
238
+ current = getPoint(j + 2);
239
+ result += `Q ${getString(control)}${getString(current)}`;
240
+ }
241
+ break;
242
+ case 't':
243
+ // Smooth quadratic Bezier curve
244
+ for (let j = 0; j < length; j += 2) {
245
+ control = /[qt]/.test(previous) ?
246
+ {x: (current.x * 2) - control.x, y: (current.y * 2) - control.y} :
247
+ current;
248
+ current = getPoint(j);
249
+
250
+ result += `Q ${getString(control)}${getString(current)}`;
251
+ previous = lower;
252
+ }
253
+ break;
254
+ case 'a':
255
+ // Elliptical arc curve
256
+ for (let j = 0; j < length; j += 7) {
257
+ current = getPoint(j + 5);
258
+ const rx = +coords[j];
259
+ const ry = +coords[j + 1];
260
+ const rotation = +coords[j + 2];
261
+ const largeArcFlag = +coords[j + 3];
262
+ let clockwiseFlag = +coords[j + 4];
263
+ const newEllipse = _calculateTransformedEllipse(rx, ry, rotation, transform);
264
+ const matrixScale = _getScaleFactor(transform);
265
+ if (newEllipse) {
266
+ if ((matrixScale.x > 0 && matrixScale.y < 0) ||
267
+ (matrixScale.x < 0 && matrixScale.y > 0)) {
268
+ clockwiseFlag = clockwiseFlag ^ 1;
269
+ }
270
+ result += `A ${roundTo4Places(Math.abs(newEllipse.radiusX))} ` +
271
+ `${roundTo4Places(Math.abs(newEllipse.radiusY))} ` +
272
+ `${roundTo4Places(newEllipse.rotation)} ${largeArcFlag} ` +
273
+ `${clockwiseFlag} ${getString(current)}`;
274
+ } else {
275
+ result += `L ${getString(current)}`;
276
+ }
277
+ }
278
+ break;
279
+ case 'z':
280
+ // Close path
281
+ result += `Z `;
282
+ // Correctly handle relative m commands, see paper.js#1101:
283
+ current = start;
284
+ break;
285
+ }
286
+ previous = lower;
287
+ }
288
+ return result;
289
+ };
290
+
291
+ const GRAPHICS_ELEMENTS = ['circle', 'ellipse', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'text', 'use'];
292
+ const CONTAINER_ELEMENTS = ['a', 'defs', 'g', 'marker', 'glyph', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol'];
293
+ const _isContainerElement = function (element) {
294
+ return element.tagName && CONTAINER_ELEMENTS.includes(element.tagName.toLowerCase());
295
+ };
296
+ const _isGraphicsElement = function (element) {
297
+ return element.tagName && GRAPHICS_ELEMENTS.includes(element.tagName.toLowerCase());
298
+ };
299
+ const _isPathWithTransformAndStroke = function (element, strokeWidth) {
300
+ if (!element.attributes) return false;
301
+ strokeWidth = element.attributes['stroke-width'] ?
302
+ Number(element.attributes['stroke-width'].value) : Number(strokeWidth);
303
+ return strokeWidth &&
304
+ element.tagName && element.tagName.toLowerCase() === 'path' &&
305
+ element.attributes.d && element.attributes.d.value;
306
+ };
307
+ const _quadraticMean = function (a, b) {
308
+ return Math.sqrt(((a * a) + (b * b)) / 2);
309
+ };
310
+
311
+ const _createGradient = function (gradientId, svgTag, bbox, matrix) {
312
+ // Adapted from Paper.js's SvgImport.getValue
313
+ const getValue = function (node, name, isString, allowNull, allowPercent, defaultValue) {
314
+ // Interpret value as number. Never return NaN, but 0 instead.
315
+ // If the value is a sequence of numbers, parseFloat will
316
+ // return the first occurring number, which is enough for now.
317
+ let value = SvgElement.get(node, name);
318
+ let res;
319
+ if (value === null) {
320
+ if (defaultValue) {
321
+ res = defaultValue;
322
+ if (/%\s*$/.test(res)) {
323
+ value = defaultValue;
324
+ res = parseFloat(value);
325
+ }
326
+ } else if (allowNull) {
327
+ res = null;
328
+ } else if (isString) {
329
+ res = '';
330
+ } else {
331
+ res = 0;
332
+ }
333
+ } else if (isString) {
334
+ res = value;
335
+ } else {
336
+ res = parseFloat(value);
337
+ }
338
+ // Support for dimensions in percentage of the root size. If root-size
339
+ // is not set (e.g. during <defs>), just scale the percentage value to
340
+ // 0..1, as required by gradients with gradientUnits="objectBoundingBox"
341
+ if (/%\s*$/.test(value)) {
342
+ const size = allowPercent ? 1 : bbox[/x|^width/.test(name) ? 'width' : 'height'];
343
+ return res / 100 * size;
344
+ }
345
+ return res;
346
+ };
347
+ const getPoint = function (node, x, y, allowNull, allowPercent, defaultX, defaultY) {
348
+ x = getValue(node, x || 'x', false, allowNull, allowPercent, defaultX);
349
+ y = getValue(node, y || 'y', false, allowNull, allowPercent, defaultY);
350
+ return allowNull && (x === null || y === null) ? null : {x, y};
351
+ };
352
+
353
+ let defs = svgTag.getElementsByTagName('defs');
354
+ if (defs.length === 0) {
355
+ defs = SvgElement.create('defs');
356
+ svgTag.appendChild(defs);
357
+ } else {
358
+ defs = defs[0];
359
+ }
360
+
361
+ // Clone the old gradient. We'll make a new one, since the gradient might be reused elsewhere
362
+ // with different transform matrix
363
+ const oldGradient = svgTag.getElementById(gradientId);
364
+ if (!oldGradient) return;
365
+
366
+ const radial = oldGradient.tagName.toLowerCase() === 'radialgradient';
367
+ const newGradient = svgTag.getElementById(gradientId).cloneNode(true /* deep */);
368
+
369
+ // Give the new gradient a new ID
370
+ let matrixString = Matrix.toString(matrix);
371
+ matrixString = matrixString.substring(8, matrixString.length - 1);
372
+ const newGradientId = `${gradientId}-${matrixString}`;
373
+ newGradient.setAttribute('id', newGradientId);
374
+
375
+ // This gradient already exists and was transformed before. Just reuse the already-transformed one.
376
+ if (svgTag.getElementById(newGradientId)) {
377
+ // This is the same code as in the end of the function, but I don't feel like wrapping the next 80 lines
378
+ // in an `if (!svgTag.getElementById(newGradientId))` block
379
+ return `url(#${newGradientId})`;
380
+ }
381
+
382
+ const scaleToBounds = getValue(newGradient, 'gradientUnits', true) !==
383
+ 'userSpaceOnUse';
384
+ let origin;
385
+ let destination;
386
+ let radius;
387
+ let focal;
388
+ if (radial) {
389
+ origin = getPoint(newGradient, 'cx', 'cy', false, scaleToBounds, '50%', '50%');
390
+ radius = getValue(newGradient, 'r', false, false, scaleToBounds, '50%');
391
+ focal = getPoint(newGradient, 'fx', 'fy', true, scaleToBounds);
392
+ } else {
393
+ origin = getPoint(newGradient, 'x1', 'y1', false, scaleToBounds);
394
+ destination = getPoint(newGradient, 'x2', 'y2', false, scaleToBounds, '1');
395
+ if (origin.x === destination.x && origin.y === destination.y) {
396
+ // If it's degenerate, use the color of the last stop, as described by
397
+ // https://www.w3.org/TR/SVG/pservers.html#LinearGradientNotes
398
+ const stops = newGradient.getElementsByTagName('stop');
399
+ if (!stops.length || !stops[stops.length - 1].attributes ||
400
+ !stops[stops.length - 1].attributes['stop-color']) {
401
+ return null;
402
+ }
403
+ return stops[stops.length - 1].attributes['stop-color'].value;
404
+ }
405
+ }
406
+
407
+ // Transform points
408
+ // Emulate SVG's gradientUnits="objectBoundingBox"
409
+ if (scaleToBounds) {
410
+ const boundsMatrix = Matrix.compose(Matrix.translate(bbox.x, bbox.y), Matrix.scale(bbox.width, bbox.height));
411
+ origin = Matrix.applyToPoint(boundsMatrix, origin);
412
+ if (destination) destination = Matrix.applyToPoint(boundsMatrix, destination);
413
+ if (radius) {
414
+ radius = _quadraticMean(bbox.width, bbox.height) * radius;
415
+ }
416
+ if (focal) focal = Matrix.applyToPoint(boundsMatrix, focal);
417
+ }
418
+
419
+ if (radial) {
420
+ origin = Matrix.applyToPoint(matrix, origin);
421
+ const matrixScale = _getScaleFactor(matrix);
422
+ radius = _quadraticMean(matrixScale.x, matrixScale.y) * radius;
423
+ if (focal) focal = Matrix.applyToPoint(matrix, focal);
424
+ } else {
425
+ const dot = (a, b) => (a.x * b.x) + (a.y * b.y);
426
+ const multiply = (coefficient, v) => ({x: coefficient * v.x, y: coefficient * v.y});
427
+ const add = (a, b) => ({x: a.x + b.x, y: a.y + b.y});
428
+ const subtract = (a, b) => ({x: a.x - b.x, y: a.y - b.y});
429
+
430
+ // The line through origin and gradientPerpendicular is the line at which the gradient starts
431
+ let gradientPerpendicular = Math.abs(origin.x - destination.x) < 1e-8 ?
432
+ add(origin, {x: 1, y: (origin.x - destination.x) / (destination.y - origin.y)}) :
433
+ add(origin, {x: (destination.y - origin.y) / (origin.x - destination.x), y: 1});
434
+
435
+ // Transform points
436
+ gradientPerpendicular = Matrix.applyToPoint(matrix, gradientPerpendicular);
437
+ origin = Matrix.applyToPoint(matrix, origin);
438
+ destination = Matrix.applyToPoint(matrix, destination);
439
+
440
+ // Calculate the direction that the gradient has changed to
441
+ const originToPerpendicular = subtract(gradientPerpendicular, origin);
442
+ const originToDestination = subtract(destination, origin);
443
+ const gradientDirection = Math.abs(originToPerpendicular.x) < 1e-8 ?
444
+ {x: 1, y: -originToPerpendicular.x / originToPerpendicular.y} :
445
+ {x: -originToPerpendicular.y / originToPerpendicular.x, y: 1};
446
+
447
+ // Set the destination so that the gradient moves in the correct direction, by projecting the destination vector
448
+ // onto the gradient direction vector
449
+ const projectionCoeff = dot(originToDestination, gradientDirection) / dot(gradientDirection, gradientDirection);
450
+ const projection = multiply(projectionCoeff, gradientDirection);
451
+ destination = {x: origin.x + projection.x, y: origin.y + projection.y};
452
+ }
453
+
454
+ // Put values back into svg
455
+ if (radial) {
456
+ newGradient.setAttribute('cx', Number(origin.x.toFixed(4)));
457
+ newGradient.setAttribute('cy', Number(origin.y.toFixed(4)));
458
+ newGradient.setAttribute('r', Number(radius.toFixed(4)));
459
+ if (focal) {
460
+ newGradient.setAttribute('fx', Number(focal.x.toFixed(4)));
461
+ newGradient.setAttribute('fy', Number(focal.y.toFixed(4)));
462
+ }
463
+ } else {
464
+ newGradient.setAttribute('x1', Number(origin.x.toFixed(4)));
465
+ newGradient.setAttribute('y1', Number(origin.y.toFixed(4)));
466
+ newGradient.setAttribute('x2', Number(destination.x.toFixed(4)));
467
+ newGradient.setAttribute('y2', Number(destination.y.toFixed(4)));
468
+ }
469
+ newGradient.setAttribute('gradientUnits', 'userSpaceOnUse');
470
+ defs.appendChild(newGradient);
471
+
472
+ return `url(#${newGradientId})`;
473
+ };
474
+
475
+ // Adapted from paper.js's SvgImport.getDefinition
476
+ const _parseUrl = (value, windowRef) => {
477
+ // When url() comes from a style property, '#'' seems to be missing on
478
+ // WebKit. We also get variations of quotes or no quotes, single or
479
+ // double, so handle it all with one regular expression:
480
+ const match = value && value.match(/\((?:["'#]*)([^"')]+)/);
481
+ const name = match && match[1];
482
+ const res = name && windowRef ?
483
+ // This is required by Firefox, which can produce absolute
484
+ // urls for local gradients, see paperjs#1001:
485
+ name.replace(`${windowRef.location.href.split('#')[0]}#`, '') :
486
+ name;
487
+ return res;
488
+ };
489
+
490
+ /**
491
+ * Scratch 2.0 displays stroke widths in a "normalized" way, that is,
492
+ * if a shape with a stroke width has a transform applied, it will be
493
+ * rendered with a stroke that is the same width all the way around,
494
+ * instead of stretched looking.
495
+ *
496
+ * The vector paint editor also prefers to normalize the stroke width,
497
+ * rather than keep track of transforms at the group level, as this
498
+ * simplifies editing (e.g. stroke width 3 always means the same thickness)
499
+ *
500
+ * This function performs that normalization process, pushing transforms
501
+ * on groups down to the leaf level and averaging out the stroke width
502
+ * around the shapes. Note that this doens't just change stroke widths, it
503
+ * changes path data and attributes throughout the SVG.
504
+ *
505
+ * @param {SVGElement} svgTag The SVG dom object
506
+ * @param {Window} windowRef The window to use. Need to pass in for
507
+ * tests to work, as they get angry at even the mention of window.
508
+ * @param {object} bboxForTesting The bounds to use. Need to pass in for
509
+ * tests only, because getBBox doesn't work in Node. This should
510
+ * be the bounds of the svgTag without including stroke width or transforms.
511
+ * @return {void}
512
+ */
513
+ const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
514
+ const inherited = Matrix.identity();
515
+
516
+ const applyTransforms = (element, matrix, strokeWidth, fill, stroke) => {
517
+ if (_isContainerElement(element)) {
518
+ // Push fills and stroke width down to leaves
519
+ if (element.attributes['stroke-width']) {
520
+ strokeWidth = element.attributes['stroke-width'].value;
521
+ }
522
+ if (element.attributes) {
523
+ if (element.attributes.fill) fill = element.attributes.fill.value;
524
+ if (element.attributes.stroke) stroke = element.attributes.stroke.value;
525
+ }
526
+
527
+ // If any child nodes don't take attributes, leave the attributes
528
+ // at the parent level.
529
+ for (let i = 0; i < element.childNodes.length; i++) {
530
+ applyTransforms(
531
+ element.childNodes[i],
532
+ Matrix.compose(matrix, _parseTransform(element)),
533
+ strokeWidth,
534
+ fill,
535
+ stroke
536
+ );
537
+ }
538
+ element.removeAttribute('transform');
539
+ element.removeAttribute('stroke-width');
540
+ element.removeAttribute('fill');
541
+ element.removeAttribute('stroke');
542
+ } else if (_isPathWithTransformAndStroke(element, strokeWidth)) {
543
+ if (element.attributes['stroke-width']) {
544
+ strokeWidth = element.attributes['stroke-width'].value;
545
+ }
546
+ if (element.attributes.fill) fill = element.attributes.fill.value;
547
+ if (element.attributes.stroke) stroke = element.attributes.stroke.value;
548
+ matrix = Matrix.compose(matrix, _parseTransform(element));
549
+ if (Matrix.toString(matrix) === Matrix.toString(Matrix.identity())) {
550
+ element.removeAttribute('transform');
551
+ element.setAttribute('stroke-width', strokeWidth);
552
+ if (fill) element.setAttribute('fill', fill);
553
+ if (stroke) element.setAttribute('stroke', stroke);
554
+ return;
555
+ }
556
+
557
+ // Transform gradient
558
+ const fillGradientId = _parseUrl(fill, windowRef);
559
+ const strokeGradientId = _parseUrl(stroke, windowRef);
560
+
561
+ if (fillGradientId || strokeGradientId) {
562
+ const doc = windowRef.document;
563
+ // Need path bounds to transform gradient
564
+ const svgSpot = doc.createElement('span');
565
+ let bbox;
566
+ if (bboxForTesting) {
567
+ bbox = bboxForTesting;
568
+ } else {
569
+ try {
570
+ doc.body.appendChild(svgSpot);
571
+ const svg = SvgElement.set(doc.createElementNS(SvgElement.svg, 'svg'));
572
+ const path = SvgElement.set(doc.createElementNS(SvgElement.svg, 'path'));
573
+ path.setAttribute('d', element.attributes.d.value);
574
+ svg.appendChild(path);
575
+ svgSpot.appendChild(svg);
576
+ // Take the bounding box.
577
+ bbox = svg.getBBox();
578
+ } finally {
579
+ // Always destroy the element, even if, for example, getBBox throws.
580
+ doc.body.removeChild(svgSpot);
581
+ }
582
+ }
583
+
584
+ if (fillGradientId) {
585
+ const newFillRef = _createGradient(fillGradientId, svgTag, bbox, matrix);
586
+ if (newFillRef) fill = newFillRef;
587
+ }
588
+
589
+ if (strokeGradientId) {
590
+ const newStrokeRef = _createGradient(strokeGradientId, svgTag, bbox, matrix);
591
+ if (newStrokeRef) stroke = newStrokeRef;
592
+ }
593
+ }
594
+
595
+ // Transform path data
596
+ element.setAttribute('d', _transformPath(element.attributes.d.value, matrix));
597
+ element.removeAttribute('transform');
598
+
599
+ // Transform stroke width
600
+ const matrixScale = _getScaleFactor(matrix);
601
+ element.setAttribute('stroke-width', _quadraticMean(matrixScale.x, matrixScale.y) * strokeWidth);
602
+ if (fill) element.setAttribute('fill', fill);
603
+ if (stroke) element.setAttribute('stroke', stroke);
604
+ } else if (_isGraphicsElement(element)) {
605
+ // Push stroke width, fill, and stroke down to leaves
606
+ if (strokeWidth && !element.attributes['stroke-width']) {
607
+ element.setAttribute('stroke-width', strokeWidth);
608
+ }
609
+ if (fill && !element.attributes.fill) {
610
+ element.setAttribute('fill', fill);
611
+ }
612
+ if (stroke && !element.attributes.stroke) {
613
+ element.setAttribute('stroke', stroke);
614
+ }
615
+
616
+ // Push transform down to leaves
617
+ matrix = Matrix.compose(matrix, _parseTransform(element));
618
+ if (Matrix.toString(matrix) === Matrix.toString(Matrix.identity())) {
619
+ element.removeAttribute('transform');
620
+ } else {
621
+ element.setAttribute('transform', Matrix.toString(matrix));
622
+ }
623
+ }
624
+ };
625
+ applyTransforms(svgTag, inherited, 1 /* default SVG stroke width */);
626
+ };
627
+
628
+ module.exports = transformStrokeWidths;
@@ -0,0 +1,4 @@
1
+ const minilog = require('minilog');
2
+ minilog.enable();
3
+
4
+ module.exports = minilog('scratch-svg-render');