svgo-v2 2.8.0

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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/svgo +10 -0
  4. package/dist/svgo.browser.js +1 -0
  5. package/lib/css-tools.js +239 -0
  6. package/lib/parser.js +259 -0
  7. package/lib/path.js +347 -0
  8. package/lib/stringifier.js +326 -0
  9. package/lib/style.js +283 -0
  10. package/lib/svgo/coa.js +517 -0
  11. package/lib/svgo/config.js +138 -0
  12. package/lib/svgo/css-class-list.js +72 -0
  13. package/lib/svgo/css-select-adapter.d.ts +2 -0
  14. package/lib/svgo/css-select-adapter.js +120 -0
  15. package/lib/svgo/css-style-declaration.js +232 -0
  16. package/lib/svgo/jsAPI.d.ts +2 -0
  17. package/lib/svgo/jsAPI.js +443 -0
  18. package/lib/svgo/plugins.js +109 -0
  19. package/lib/svgo/tools.js +137 -0
  20. package/lib/svgo-node.js +106 -0
  21. package/lib/svgo.js +83 -0
  22. package/lib/types.ts +172 -0
  23. package/lib/xast.js +102 -0
  24. package/package.json +130 -0
  25. package/plugins/_applyTransforms.js +335 -0
  26. package/plugins/_collections.js +2168 -0
  27. package/plugins/_path.js +816 -0
  28. package/plugins/_transforms.js +379 -0
  29. package/plugins/addAttributesToSVGElement.js +87 -0
  30. package/plugins/addClassesToSVGElement.js +87 -0
  31. package/plugins/cleanupAttrs.js +55 -0
  32. package/plugins/cleanupEnableBackground.js +75 -0
  33. package/plugins/cleanupIDs.js +297 -0
  34. package/plugins/cleanupListOfValues.js +154 -0
  35. package/plugins/cleanupNumericValues.js +113 -0
  36. package/plugins/collapseGroups.js +135 -0
  37. package/plugins/convertColors.js +152 -0
  38. package/plugins/convertEllipseToCircle.js +39 -0
  39. package/plugins/convertPathData.js +1023 -0
  40. package/plugins/convertShapeToPath.js +175 -0
  41. package/plugins/convertStyleToAttrs.js +132 -0
  42. package/plugins/convertTransform.js +432 -0
  43. package/plugins/inlineStyles.js +379 -0
  44. package/plugins/mergePaths.js +104 -0
  45. package/plugins/mergeStyles.js +93 -0
  46. package/plugins/minifyStyles.js +148 -0
  47. package/plugins/moveElemsAttrsToGroup.js +130 -0
  48. package/plugins/moveGroupAttrsToElems.js +62 -0
  49. package/plugins/plugins.js +56 -0
  50. package/plugins/prefixIds.js +241 -0
  51. package/plugins/preset-default.js +80 -0
  52. package/plugins/removeAttributesBySelector.js +99 -0
  53. package/plugins/removeAttrs.js +159 -0
  54. package/plugins/removeComments.js +31 -0
  55. package/plugins/removeDesc.js +41 -0
  56. package/plugins/removeDimensions.js +43 -0
  57. package/plugins/removeDoctype.js +42 -0
  58. package/plugins/removeEditorsNSData.js +68 -0
  59. package/plugins/removeElementsByAttr.js +78 -0
  60. package/plugins/removeEmptyAttrs.js +33 -0
  61. package/plugins/removeEmptyContainers.js +58 -0
  62. package/plugins/removeEmptyText.js +57 -0
  63. package/plugins/removeHiddenElems.js +318 -0
  64. package/plugins/removeMetadata.js +29 -0
  65. package/plugins/removeNonInheritableGroupAttrs.js +38 -0
  66. package/plugins/removeOffCanvasPaths.js +138 -0
  67. package/plugins/removeRasterImages.js +33 -0
  68. package/plugins/removeScriptElement.js +29 -0
  69. package/plugins/removeStyleElement.js +29 -0
  70. package/plugins/removeTitle.js +29 -0
  71. package/plugins/removeUnknownsAndDefaults.js +218 -0
  72. package/plugins/removeUnusedNS.js +61 -0
  73. package/plugins/removeUselessDefs.js +65 -0
  74. package/plugins/removeUselessStrokeAndFill.js +144 -0
  75. package/plugins/removeViewBox.js +51 -0
  76. package/plugins/removeXMLNS.js +30 -0
  77. package/plugins/removeXMLProcInst.js +30 -0
  78. package/plugins/reusePaths.js +113 -0
  79. package/plugins/sortAttrs.js +113 -0
  80. package/plugins/sortDefsChildren.js +60 -0
@@ -0,0 +1,1023 @@
1
+ 'use strict';
2
+
3
+ const { collectStylesheet, computeStyle } = require('../lib/style.js');
4
+ const { pathElems } = require('./_collections.js');
5
+ const { path2js, js2path } = require('./_path.js');
6
+ const { applyTransforms } = require('./_applyTransforms.js');
7
+ const { cleanupOutData } = require('../lib/svgo/tools');
8
+
9
+ exports.name = 'convertPathData';
10
+ exports.type = 'visitor';
11
+ exports.active = true;
12
+ exports.description =
13
+ 'optimizes path data: writes in shorter form, applies transformations';
14
+
15
+ exports.params = {
16
+ applyTransforms: true,
17
+ applyTransformsStroked: true,
18
+ makeArcs: {
19
+ threshold: 2.5, // coefficient of rounding error
20
+ tolerance: 0.5, // percentage of radius
21
+ },
22
+ straightCurves: true,
23
+ lineShorthands: true,
24
+ curveSmoothShorthands: true,
25
+ floatPrecision: 3,
26
+ transformPrecision: 5,
27
+ removeUseless: true,
28
+ collapseRepeated: true,
29
+ utilizeAbsolute: true,
30
+ leadingZero: true,
31
+ negativeExtraSpace: true,
32
+ noSpaceAfterFlags: false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
33
+ forceAbsolutePath: false,
34
+ };
35
+
36
+ let roundData;
37
+ let precision;
38
+ let error;
39
+ let arcThreshold;
40
+ let arcTolerance;
41
+
42
+ /**
43
+ * Convert absolute Path to relative,
44
+ * collapse repeated instructions,
45
+ * detect and convert Lineto shorthands,
46
+ * remove useless instructions like "l0,0",
47
+ * trim useless delimiters and leading zeros,
48
+ * decrease accuracy of floating-point numbers.
49
+ *
50
+ * @see https://www.w3.org/TR/SVG11/paths.html#PathData
51
+ *
52
+ * @param {Object} item current iteration item
53
+ * @param {Object} params plugin params
54
+ * @return {Boolean} if false, item will be filtered out
55
+ *
56
+ * @author Kir Belevich
57
+ */
58
+ exports.fn = (root, params) => {
59
+ const stylesheet = collectStylesheet(root);
60
+ return {
61
+ element: {
62
+ enter: (node) => {
63
+ if (pathElems.includes(node.name) && node.attributes.d != null) {
64
+ const computedStyle = computeStyle(stylesheet, node);
65
+ precision = params.floatPrecision;
66
+ error =
67
+ precision !== false
68
+ ? +Math.pow(0.1, precision).toFixed(precision)
69
+ : 1e-2;
70
+ roundData = precision > 0 && precision < 20 ? strongRound : round;
71
+ if (params.makeArcs) {
72
+ arcThreshold = params.makeArcs.threshold;
73
+ arcTolerance = params.makeArcs.tolerance;
74
+ }
75
+ const hasMarkerMid = computedStyle['marker-mid'] != null;
76
+
77
+ const maybeHasStroke =
78
+ computedStyle.stroke &&
79
+ (computedStyle.stroke.type === 'dynamic' ||
80
+ computedStyle.stroke.value !== 'none');
81
+ const maybeHasLinecap =
82
+ computedStyle['stroke-linecap'] &&
83
+ (computedStyle['stroke-linecap'].type === 'dynamic' ||
84
+ computedStyle['stroke-linecap'].value !== 'butt');
85
+ const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap;
86
+
87
+ var data = path2js(node);
88
+
89
+ // TODO: get rid of functions returns
90
+ if (data.length) {
91
+ if (params.applyTransforms) {
92
+ applyTransforms(node, data, params);
93
+ }
94
+
95
+ convertToRelative(data);
96
+
97
+ data = filters(data, params, {
98
+ maybeHasStrokeAndLinecap,
99
+ hasMarkerMid,
100
+ });
101
+
102
+ if (params.utilizeAbsolute) {
103
+ data = convertToMixed(data, params);
104
+ }
105
+
106
+ js2path(node, data, params);
107
+ }
108
+ }
109
+ },
110
+ },
111
+ };
112
+ };
113
+
114
+ /**
115
+ * Convert absolute path data coordinates to relative.
116
+ *
117
+ * @param {Array} path input path data
118
+ * @param {Object} params plugin params
119
+ * @return {Array} output path data
120
+ */
121
+ const convertToRelative = (pathData) => {
122
+ let start = [0, 0];
123
+ let cursor = [0, 0];
124
+ let prevCoords = [0, 0];
125
+
126
+ for (let i = 0; i < pathData.length; i += 1) {
127
+ const pathItem = pathData[i];
128
+ let { command, args } = pathItem;
129
+
130
+ // moveto (x y)
131
+ if (command === 'm') {
132
+ // update start and cursor
133
+ cursor[0] += args[0];
134
+ cursor[1] += args[1];
135
+ start[0] = cursor[0];
136
+ start[1] = cursor[1];
137
+ }
138
+ if (command === 'M') {
139
+ // M → m
140
+ // skip first moveto
141
+ if (i !== 0) {
142
+ command = 'm';
143
+ }
144
+ args[0] -= cursor[0];
145
+ args[1] -= cursor[1];
146
+ // update start and cursor
147
+ cursor[0] += args[0];
148
+ cursor[1] += args[1];
149
+ start[0] = cursor[0];
150
+ start[1] = cursor[1];
151
+ }
152
+
153
+ // lineto (x y)
154
+ if (command === 'l') {
155
+ cursor[0] += args[0];
156
+ cursor[1] += args[1];
157
+ }
158
+ if (command === 'L') {
159
+ // L → l
160
+ command = 'l';
161
+ args[0] -= cursor[0];
162
+ args[1] -= cursor[1];
163
+ cursor[0] += args[0];
164
+ cursor[1] += args[1];
165
+ }
166
+
167
+ // horizontal lineto (x)
168
+ if (command === 'h') {
169
+ cursor[0] += args[0];
170
+ }
171
+ if (command === 'H') {
172
+ // H → h
173
+ command = 'h';
174
+ args[0] -= cursor[0];
175
+ cursor[0] += args[0];
176
+ }
177
+
178
+ // vertical lineto (y)
179
+ if (command === 'v') {
180
+ cursor[1] += args[0];
181
+ }
182
+ if (command === 'V') {
183
+ // V → v
184
+ command = 'v';
185
+ args[0] -= cursor[1];
186
+ cursor[1] += args[0];
187
+ }
188
+
189
+ // curveto (x1 y1 x2 y2 x y)
190
+ if (command === 'c') {
191
+ cursor[0] += args[4];
192
+ cursor[1] += args[5];
193
+ }
194
+ if (command === 'C') {
195
+ // C → c
196
+ command = 'c';
197
+ args[0] -= cursor[0];
198
+ args[1] -= cursor[1];
199
+ args[2] -= cursor[0];
200
+ args[3] -= cursor[1];
201
+ args[4] -= cursor[0];
202
+ args[5] -= cursor[1];
203
+ cursor[0] += args[4];
204
+ cursor[1] += args[5];
205
+ }
206
+
207
+ // smooth curveto (x2 y2 x y)
208
+ if (command === 's') {
209
+ cursor[0] += args[2];
210
+ cursor[1] += args[3];
211
+ }
212
+ if (command === 'S') {
213
+ // S → s
214
+ command = 's';
215
+ args[0] -= cursor[0];
216
+ args[1] -= cursor[1];
217
+ args[2] -= cursor[0];
218
+ args[3] -= cursor[1];
219
+ cursor[0] += args[2];
220
+ cursor[1] += args[3];
221
+ }
222
+
223
+ // quadratic Bézier curveto (x1 y1 x y)
224
+ if (command === 'q') {
225
+ cursor[0] += args[2];
226
+ cursor[1] += args[3];
227
+ }
228
+ if (command === 'Q') {
229
+ // Q → q
230
+ command = 'q';
231
+ args[0] -= cursor[0];
232
+ args[1] -= cursor[1];
233
+ args[2] -= cursor[0];
234
+ args[3] -= cursor[1];
235
+ cursor[0] += args[2];
236
+ cursor[1] += args[3];
237
+ }
238
+
239
+ // smooth quadratic Bézier curveto (x y)
240
+ if (command === 't') {
241
+ cursor[0] += args[0];
242
+ cursor[1] += args[1];
243
+ }
244
+ if (command === 'T') {
245
+ // T → t
246
+ command = 't';
247
+ args[0] -= cursor[0];
248
+ args[1] -= cursor[1];
249
+ cursor[0] += args[0];
250
+ cursor[1] += args[1];
251
+ }
252
+
253
+ // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
254
+ if (command === 'a') {
255
+ cursor[0] += args[5];
256
+ cursor[1] += args[6];
257
+ }
258
+ if (command === 'A') {
259
+ // A → a
260
+ command = 'a';
261
+ args[5] -= cursor[0];
262
+ args[6] -= cursor[1];
263
+ cursor[0] += args[5];
264
+ cursor[1] += args[6];
265
+ }
266
+
267
+ // closepath
268
+ if (command === 'Z' || command === 'z') {
269
+ // reset cursor
270
+ cursor[0] = start[0];
271
+ cursor[1] = start[1];
272
+ }
273
+
274
+ pathItem.command = command;
275
+ pathItem.args = args;
276
+ // store absolute coordinates for later use
277
+ // base should preserve reference from other element
278
+ pathItem.base = prevCoords;
279
+ pathItem.coords = [cursor[0], cursor[1]];
280
+ prevCoords = pathItem.coords;
281
+ }
282
+
283
+ return pathData;
284
+ };
285
+
286
+ /**
287
+ * Main filters loop.
288
+ *
289
+ * @param {Array} path input path data
290
+ * @param {Object} params plugin params
291
+ * @return {Array} output path data
292
+ */
293
+ function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
294
+ var stringify = data2Path.bind(null, params),
295
+ relSubpoint = [0, 0],
296
+ pathBase = [0, 0],
297
+ prev = {};
298
+
299
+ path = path.filter(function (item, index, path) {
300
+ let command = item.command;
301
+ let data = item.args;
302
+ let next = path[index + 1];
303
+
304
+ if (command !== 'Z' && command !== 'z') {
305
+ var sdata = data,
306
+ circle;
307
+
308
+ if (command === 's') {
309
+ sdata = [0, 0].concat(data);
310
+
311
+ if (command === 'c' || command === 's') {
312
+ var pdata = prev.args,
313
+ n = pdata.length;
314
+
315
+ // (-x, -y) of the prev tangent point relative to the current point
316
+ sdata[0] = pdata[n - 2] - pdata[n - 4];
317
+ sdata[1] = pdata[n - 1] - pdata[n - 3];
318
+ }
319
+ }
320
+
321
+ // convert curves to arcs if possible
322
+ if (
323
+ params.makeArcs &&
324
+ (command == 'c' || command == 's') &&
325
+ isConvex(sdata) &&
326
+ (circle = findCircle(sdata))
327
+ ) {
328
+ var r = roundData([circle.radius])[0],
329
+ angle = findArcAngle(sdata, circle),
330
+ sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0,
331
+ arc = {
332
+ command: 'a',
333
+ args: [r, r, 0, 0, sweep, sdata[4], sdata[5]],
334
+ coords: item.coords.slice(),
335
+ base: item.base,
336
+ },
337
+ output = [arc],
338
+ // relative coordinates to adjust the found circle
339
+ relCenter = [
340
+ circle.center[0] - sdata[4],
341
+ circle.center[1] - sdata[5],
342
+ ],
343
+ relCircle = { center: relCenter, radius: circle.radius },
344
+ arcCurves = [item],
345
+ hasPrev = 0,
346
+ suffix = '',
347
+ nextLonghand;
348
+
349
+ if (
350
+ (prev.command == 'c' &&
351
+ isConvex(prev.args) &&
352
+ isArcPrev(prev.args, circle)) ||
353
+ (prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle))
354
+ ) {
355
+ arcCurves.unshift(prev);
356
+ arc.base = prev.base;
357
+ arc.args[5] = arc.coords[0] - arc.base[0];
358
+ arc.args[6] = arc.coords[1] - arc.base[1];
359
+ var prevData = prev.command == 'a' ? prev.sdata : prev.args;
360
+ var prevAngle = findArcAngle(prevData, {
361
+ center: [
362
+ prevData[4] + circle.center[0],
363
+ prevData[5] + circle.center[1],
364
+ ],
365
+ radius: circle.radius,
366
+ });
367
+ angle += prevAngle;
368
+ if (angle > Math.PI) arc.args[3] = 1;
369
+ hasPrev = 1;
370
+ }
371
+
372
+ // check if next curves are fitting the arc
373
+ for (
374
+ var j = index;
375
+ (next = path[++j]) && ~'cs'.indexOf(next.command);
376
+
377
+ ) {
378
+ var nextData = next.args;
379
+ if (next.command == 's') {
380
+ nextLonghand = makeLonghand(
381
+ { command: 's', args: next.args.slice() },
382
+ path[j - 1].args
383
+ );
384
+ nextData = nextLonghand.args;
385
+ nextLonghand.args = nextData.slice(0, 2);
386
+ suffix = stringify([nextLonghand]);
387
+ }
388
+ if (isConvex(nextData) && isArc(nextData, relCircle)) {
389
+ angle += findArcAngle(nextData, relCircle);
390
+ if (angle - 2 * Math.PI > 1e-3) break; // more than 360°
391
+ if (angle > Math.PI) arc.args[3] = 1;
392
+ arcCurves.push(next);
393
+ if (2 * Math.PI - angle > 1e-3) {
394
+ // less than 360°
395
+ arc.coords = next.coords;
396
+ arc.args[5] = arc.coords[0] - arc.base[0];
397
+ arc.args[6] = arc.coords[1] - arc.base[1];
398
+ } else {
399
+ // full circle, make a half-circle arc and add a second one
400
+ arc.args[5] = 2 * (relCircle.center[0] - nextData[4]);
401
+ arc.args[6] = 2 * (relCircle.center[1] - nextData[5]);
402
+ arc.coords = [
403
+ arc.base[0] + arc.args[5],
404
+ arc.base[1] + arc.args[6],
405
+ ];
406
+ arc = {
407
+ command: 'a',
408
+ args: [
409
+ r,
410
+ r,
411
+ 0,
412
+ 0,
413
+ sweep,
414
+ next.coords[0] - arc.coords[0],
415
+ next.coords[1] - arc.coords[1],
416
+ ],
417
+ coords: next.coords,
418
+ base: arc.coords,
419
+ };
420
+ output.push(arc);
421
+ j++;
422
+ break;
423
+ }
424
+ relCenter[0] -= nextData[4];
425
+ relCenter[1] -= nextData[5];
426
+ } else break;
427
+ }
428
+
429
+ if ((stringify(output) + suffix).length < stringify(arcCurves).length) {
430
+ if (path[j] && path[j].command == 's') {
431
+ makeLonghand(path[j], path[j - 1].args);
432
+ }
433
+ if (hasPrev) {
434
+ var prevArc = output.shift();
435
+ roundData(prevArc.args);
436
+ relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2];
437
+ relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1];
438
+ prev.command = 'a';
439
+ prev.args = prevArc.args;
440
+ item.base = prev.coords = prevArc.coords;
441
+ }
442
+ arc = output.shift();
443
+ if (arcCurves.length == 1) {
444
+ item.sdata = sdata.slice(); // preserve curve data for future checks
445
+ } else if (arcCurves.length - 1 - hasPrev > 0) {
446
+ // filter out consumed next items
447
+ path.splice.apply(
448
+ path,
449
+ [index + 1, arcCurves.length - 1 - hasPrev].concat(output)
450
+ );
451
+ }
452
+ if (!arc) return false;
453
+ command = 'a';
454
+ data = arc.args;
455
+ item.coords = arc.coords;
456
+ }
457
+ }
458
+
459
+ // Rounding relative coordinates, taking in account accummulating error
460
+ // to get closer to absolute coordinates. Sum of rounded value remains same:
461
+ // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
462
+ if (precision !== false) {
463
+ if (
464
+ command === 'm' ||
465
+ command === 'l' ||
466
+ command === 't' ||
467
+ command === 'q' ||
468
+ command === 's' ||
469
+ command === 'c'
470
+ ) {
471
+ for (var i = data.length; i--; ) {
472
+ data[i] += item.base[i % 2] - relSubpoint[i % 2];
473
+ }
474
+ } else if (command == 'h') {
475
+ data[0] += item.base[0] - relSubpoint[0];
476
+ } else if (command == 'v') {
477
+ data[0] += item.base[1] - relSubpoint[1];
478
+ } else if (command == 'a') {
479
+ data[5] += item.base[0] - relSubpoint[0];
480
+ data[6] += item.base[1] - relSubpoint[1];
481
+ }
482
+ roundData(data);
483
+
484
+ if (command == 'h') relSubpoint[0] += data[0];
485
+ else if (command == 'v') relSubpoint[1] += data[0];
486
+ else {
487
+ relSubpoint[0] += data[data.length - 2];
488
+ relSubpoint[1] += data[data.length - 1];
489
+ }
490
+ roundData(relSubpoint);
491
+
492
+ if (command === 'M' || command === 'm') {
493
+ pathBase[0] = relSubpoint[0];
494
+ pathBase[1] = relSubpoint[1];
495
+ }
496
+ }
497
+
498
+ // convert straight curves into lines segments
499
+ if (params.straightCurves) {
500
+ if (
501
+ (command === 'c' && isCurveStraightLine(data)) ||
502
+ (command === 's' && isCurveStraightLine(sdata))
503
+ ) {
504
+ if (next && next.command == 's') makeLonghand(next, data); // fix up next curve
505
+ command = 'l';
506
+ data = data.slice(-2);
507
+ } else if (command === 'q' && isCurveStraightLine(data)) {
508
+ if (next && next.command == 't') makeLonghand(next, data); // fix up next curve
509
+ command = 'l';
510
+ data = data.slice(-2);
511
+ } else if (
512
+ command === 't' &&
513
+ prev.command !== 'q' &&
514
+ prev.command !== 't'
515
+ ) {
516
+ command = 'l';
517
+ data = data.slice(-2);
518
+ } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) {
519
+ command = 'l';
520
+ data = data.slice(-2);
521
+ }
522
+ }
523
+
524
+ // horizontal and vertical line shorthands
525
+ // l 50 0 → h 50
526
+ // l 0 50 → v 50
527
+ if (params.lineShorthands && command === 'l') {
528
+ if (data[1] === 0) {
529
+ command = 'h';
530
+ data.pop();
531
+ } else if (data[0] === 0) {
532
+ command = 'v';
533
+ data.shift();
534
+ }
535
+ }
536
+
537
+ // collapse repeated commands
538
+ // h 20 h 30 -> h 50
539
+ if (
540
+ params.collapseRepeated &&
541
+ hasMarkerMid === false &&
542
+ (command === 'm' || command === 'h' || command === 'v') &&
543
+ prev.command &&
544
+ command == prev.command.toLowerCase() &&
545
+ ((command != 'h' && command != 'v') ||
546
+ prev.args[0] >= 0 == data[0] >= 0)
547
+ ) {
548
+ prev.args[0] += data[0];
549
+ if (command != 'h' && command != 'v') {
550
+ prev.args[1] += data[1];
551
+ }
552
+ prev.coords = item.coords;
553
+ path[index] = prev;
554
+ return false;
555
+ }
556
+
557
+ // convert curves into smooth shorthands
558
+ if (params.curveSmoothShorthands && prev.command) {
559
+ // curveto
560
+ if (command === 'c') {
561
+ // c + c → c + s
562
+ if (
563
+ prev.command === 'c' &&
564
+ data[0] === -(prev.args[2] - prev.args[4]) &&
565
+ data[1] === -(prev.args[3] - prev.args[5])
566
+ ) {
567
+ command = 's';
568
+ data = data.slice(2);
569
+ }
570
+
571
+ // s + c → s + s
572
+ else if (
573
+ prev.command === 's' &&
574
+ data[0] === -(prev.args[0] - prev.args[2]) &&
575
+ data[1] === -(prev.args[1] - prev.args[3])
576
+ ) {
577
+ command = 's';
578
+ data = data.slice(2);
579
+ }
580
+
581
+ // [^cs] + c → [^cs] + s
582
+ else if (
583
+ prev.command !== 'c' &&
584
+ prev.command !== 's' &&
585
+ data[0] === 0 &&
586
+ data[1] === 0
587
+ ) {
588
+ command = 's';
589
+ data = data.slice(2);
590
+ }
591
+ }
592
+
593
+ // quadratic Bézier curveto
594
+ else if (command === 'q') {
595
+ // q + q → q + t
596
+ if (
597
+ prev.command === 'q' &&
598
+ data[0] === prev.args[2] - prev.args[0] &&
599
+ data[1] === prev.args[3] - prev.args[1]
600
+ ) {
601
+ command = 't';
602
+ data = data.slice(2);
603
+ }
604
+
605
+ // t + q → t + t
606
+ else if (
607
+ prev.command === 't' &&
608
+ data[2] === prev.args[0] &&
609
+ data[3] === prev.args[1]
610
+ ) {
611
+ command = 't';
612
+ data = data.slice(2);
613
+ }
614
+ }
615
+ }
616
+
617
+ // remove useless non-first path segments
618
+ if (params.removeUseless && !maybeHasStrokeAndLinecap) {
619
+ // l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
620
+ if (
621
+ (command === 'l' ||
622
+ command === 'h' ||
623
+ command === 'v' ||
624
+ command === 'q' ||
625
+ command === 't' ||
626
+ command === 'c' ||
627
+ command === 's') &&
628
+ data.every(function (i) {
629
+ return i === 0;
630
+ })
631
+ ) {
632
+ path[index] = prev;
633
+ return false;
634
+ }
635
+
636
+ // a 25,25 -30 0,1 0,0
637
+ if (command === 'a' && data[5] === 0 && data[6] === 0) {
638
+ path[index] = prev;
639
+ return false;
640
+ }
641
+ }
642
+
643
+ item.command = command;
644
+ item.args = data;
645
+
646
+ prev = item;
647
+ } else {
648
+ // z resets coordinates
649
+ relSubpoint[0] = pathBase[0];
650
+ relSubpoint[1] = pathBase[1];
651
+ if (prev.command === 'Z' || prev.command === 'z') return false;
652
+ prev = item;
653
+ }
654
+
655
+ return true;
656
+ });
657
+
658
+ return path;
659
+ }
660
+
661
+ /**
662
+ * Writes data in shortest form using absolute or relative coordinates.
663
+ *
664
+ * @param {Array} data input path data
665
+ * @return {Boolean} output
666
+ */
667
+ function convertToMixed(path, params) {
668
+ var prev = path[0];
669
+
670
+ path = path.filter(function (item, index) {
671
+ if (index == 0) return true;
672
+ if (item.command === 'Z' || item.command === 'z') {
673
+ prev = item;
674
+ return true;
675
+ }
676
+
677
+ var command = item.command,
678
+ data = item.args,
679
+ adata = data.slice();
680
+
681
+ if (
682
+ command === 'm' ||
683
+ command === 'l' ||
684
+ command === 't' ||
685
+ command === 'q' ||
686
+ command === 's' ||
687
+ command === 'c'
688
+ ) {
689
+ for (var i = adata.length; i--; ) {
690
+ adata[i] += item.base[i % 2];
691
+ }
692
+ } else if (command == 'h') {
693
+ adata[0] += item.base[0];
694
+ } else if (command == 'v') {
695
+ adata[0] += item.base[1];
696
+ } else if (command == 'a') {
697
+ adata[5] += item.base[0];
698
+ adata[6] += item.base[1];
699
+ }
700
+
701
+ roundData(adata);
702
+
703
+ var absoluteDataStr = cleanupOutData(adata, params),
704
+ relativeDataStr = cleanupOutData(data, params);
705
+
706
+ // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
707
+ // v-20 -> V0
708
+ // Don't convert if it fits following previous command.
709
+ // l20 30-10-50 instead of l20 30L20 30
710
+ if (
711
+ params.forceAbsolutePath ||
712
+ (absoluteDataStr.length < relativeDataStr.length &&
713
+ !(
714
+ params.negativeExtraSpace &&
715
+ command == prev.command &&
716
+ prev.command.charCodeAt(0) > 96 &&
717
+ absoluteDataStr.length == relativeDataStr.length - 1 &&
718
+ (data[0] < 0 ||
719
+ (/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1))
720
+ ))
721
+ ) {
722
+ item.command = command.toUpperCase();
723
+ item.args = adata;
724
+ }
725
+
726
+ prev = item;
727
+
728
+ return true;
729
+ });
730
+
731
+ return path;
732
+ }
733
+
734
+ /**
735
+ * Checks if curve is convex. Control points of such a curve must form
736
+ * a convex quadrilateral with diagonals crosspoint inside of it.
737
+ *
738
+ * @param {Array} data input path data
739
+ * @return {Boolean} output
740
+ */
741
+ function isConvex(data) {
742
+ var center = getIntersection([
743
+ 0,
744
+ 0,
745
+ data[2],
746
+ data[3],
747
+ data[0],
748
+ data[1],
749
+ data[4],
750
+ data[5],
751
+ ]);
752
+
753
+ return (
754
+ center &&
755
+ data[2] < center[0] == center[0] < 0 &&
756
+ data[3] < center[1] == center[1] < 0 &&
757
+ data[4] < center[0] == center[0] < data[0] &&
758
+ data[5] < center[1] == center[1] < data[1]
759
+ );
760
+ }
761
+
762
+ /**
763
+ * Computes lines equations by two points and returns their intersection point.
764
+ *
765
+ * @param {Array} coords 8 numbers for 4 pairs of coordinates (x,y)
766
+ * @return {Array|undefined} output coordinate of lines' crosspoint
767
+ */
768
+ function getIntersection(coords) {
769
+ // Prev line equation parameters.
770
+ var a1 = coords[1] - coords[3], // y1 - y2
771
+ b1 = coords[2] - coords[0], // x2 - x1
772
+ c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1
773
+ // Next line equation parameters
774
+ a2 = coords[5] - coords[7], // y1 - y2
775
+ b2 = coords[6] - coords[4], // x2 - x1
776
+ c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
777
+ denom = a1 * b2 - a2 * b1;
778
+
779
+ if (!denom) return; // parallel lines havn't an intersection
780
+
781
+ var cross = [(b1 * c2 - b2 * c1) / denom, (a1 * c2 - a2 * c1) / -denom];
782
+ if (
783
+ !isNaN(cross[0]) &&
784
+ !isNaN(cross[1]) &&
785
+ isFinite(cross[0]) &&
786
+ isFinite(cross[1])
787
+ ) {
788
+ return cross;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Decrease accuracy of floating-point numbers
794
+ * in path data keeping a specified number of decimals.
795
+ * Smart rounds values like 2.3491 to 2.35 instead of 2.349.
796
+ * Doesn't apply "smartness" if the number precision fits already.
797
+ *
798
+ * @param {Array} data input data array
799
+ * @return {Array} output data array
800
+ */
801
+ function strongRound(data) {
802
+ for (var i = data.length; i-- > 0; ) {
803
+ if (data[i].toFixed(precision) != data[i]) {
804
+ var rounded = +data[i].toFixed(precision - 1);
805
+ data[i] =
806
+ +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
807
+ ? +data[i].toFixed(precision)
808
+ : rounded;
809
+ }
810
+ }
811
+ return data;
812
+ }
813
+
814
+ /**
815
+ * Simple rounding function if precision is 0.
816
+ *
817
+ * @param {Array} data input data array
818
+ * @return {Array} output data array
819
+ */
820
+ function round(data) {
821
+ for (var i = data.length; i-- > 0; ) {
822
+ data[i] = Math.round(data[i]);
823
+ }
824
+ return data;
825
+ }
826
+
827
+ /**
828
+ * Checks if a curve is a straight line by measuring distance
829
+ * from middle points to the line formed by end points.
830
+ *
831
+ * @param {Array} xs array of curve points x-coordinates
832
+ * @param {Array} ys array of curve points y-coordinates
833
+ * @return {Boolean}
834
+ */
835
+
836
+ function isCurveStraightLine(data) {
837
+ // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
838
+ var i = data.length - 2,
839
+ a = -data[i + 1], // y1 − y2 (y1 = 0)
840
+ b = data[i], // x2 − x1 (x1 = 0)
841
+ d = 1 / (a * a + b * b); // same part for all points
842
+
843
+ if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case
844
+
845
+ // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²))
846
+ while ((i -= 2) >= 0) {
847
+ if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error)
848
+ return false;
849
+ }
850
+
851
+ return true;
852
+ }
853
+
854
+ /**
855
+ * Converts next curve from shorthand to full form using the current curve data.
856
+ *
857
+ * @param {Object} item curve to convert
858
+ * @param {Array} data current curve data
859
+ */
860
+
861
+ function makeLonghand(item, data) {
862
+ switch (item.command) {
863
+ case 's':
864
+ item.command = 'c';
865
+ break;
866
+ case 't':
867
+ item.command = 'q';
868
+ break;
869
+ }
870
+ item.args.unshift(
871
+ data[data.length - 2] - data[data.length - 4],
872
+ data[data.length - 1] - data[data.length - 3]
873
+ );
874
+ return item;
875
+ }
876
+
877
+ /**
878
+ * Returns distance between two points
879
+ *
880
+ * @param {Array} point1 first point coordinates
881
+ * @param {Array} point2 second point coordinates
882
+ * @return {Number} distance
883
+ */
884
+
885
+ function getDistance(point1, point2) {
886
+ return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
887
+ }
888
+
889
+ /**
890
+ * Returns coordinates of the curve point corresponding to the certain t
891
+ * a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4,
892
+ * where pN are control points and p1 is zero due to relative coordinates.
893
+ *
894
+ * @param {Array} curve array of curve points coordinates
895
+ * @param {Number} t parametric position from 0 to 1
896
+ * @return {Array} Point coordinates
897
+ */
898
+
899
+ function getCubicBezierPoint(curve, t) {
900
+ var sqrT = t * t,
901
+ cubT = sqrT * t,
902
+ mt = 1 - t,
903
+ sqrMt = mt * mt;
904
+
905
+ return [
906
+ 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4],
907
+ 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5],
908
+ ];
909
+ }
910
+
911
+ /**
912
+ * Finds circle by 3 points of the curve and checks if the curve fits the found circle.
913
+ *
914
+ * @param {Array} curve
915
+ * @return {Object|undefined} circle
916
+ */
917
+
918
+ function findCircle(curve) {
919
+ var midPoint = getCubicBezierPoint(curve, 1 / 2),
920
+ m1 = [midPoint[0] / 2, midPoint[1] / 2],
921
+ m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2],
922
+ center = getIntersection([
923
+ m1[0],
924
+ m1[1],
925
+ m1[0] + m1[1],
926
+ m1[1] - m1[0],
927
+ m2[0],
928
+ m2[1],
929
+ m2[0] + (m2[1] - midPoint[1]),
930
+ m2[1] - (m2[0] - midPoint[0]),
931
+ ]),
932
+ radius = center && getDistance([0, 0], center),
933
+ tolerance = Math.min(arcThreshold * error, (arcTolerance * radius) / 100);
934
+
935
+ if (
936
+ center &&
937
+ radius < 1e15 &&
938
+ [1 / 4, 3 / 4].every(function (point) {
939
+ return (
940
+ Math.abs(
941
+ getDistance(getCubicBezierPoint(curve, point), center) - radius
942
+ ) <= tolerance
943
+ );
944
+ })
945
+ )
946
+ return { center: center, radius: radius };
947
+ }
948
+
949
+ /**
950
+ * Checks if a curve fits the given circle.
951
+ *
952
+ * @param {Object} circle
953
+ * @param {Array} curve
954
+ * @return {Boolean}
955
+ */
956
+
957
+ function isArc(curve, circle) {
958
+ var tolerance = Math.min(
959
+ arcThreshold * error,
960
+ (arcTolerance * circle.radius) / 100
961
+ );
962
+
963
+ return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) {
964
+ return (
965
+ Math.abs(
966
+ getDistance(getCubicBezierPoint(curve, point), circle.center) -
967
+ circle.radius
968
+ ) <= tolerance
969
+ );
970
+ });
971
+ }
972
+
973
+ /**
974
+ * Checks if a previous curve fits the given circle.
975
+ *
976
+ * @param {Object} circle
977
+ * @param {Array} curve
978
+ * @return {Boolean}
979
+ */
980
+
981
+ function isArcPrev(curve, circle) {
982
+ return isArc(curve, {
983
+ center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
984
+ radius: circle.radius,
985
+ });
986
+ }
987
+
988
+ /**
989
+ * Finds angle of a curve fitting the given arc.
990
+
991
+ * @param {Array} curve
992
+ * @param {Object} relCircle
993
+ * @return {Number} angle
994
+ */
995
+
996
+ function findArcAngle(curve, relCircle) {
997
+ var x1 = -relCircle.center[0],
998
+ y1 = -relCircle.center[1],
999
+ x2 = curve[4] - relCircle.center[0],
1000
+ y2 = curve[5] - relCircle.center[1];
1001
+
1002
+ return Math.acos(
1003
+ (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
1004
+ );
1005
+ }
1006
+
1007
+ /**
1008
+ * Converts given path data to string.
1009
+ *
1010
+ * @param {Object} params
1011
+ * @param {Array} pathData
1012
+ * @return {String}
1013
+ */
1014
+
1015
+ function data2Path(params, pathData) {
1016
+ return pathData.reduce(function (pathString, item) {
1017
+ var strData = '';
1018
+ if (item.args) {
1019
+ strData = cleanupOutData(roundData(item.args.slice()), params);
1020
+ }
1021
+ return pathString + item.command + strData;
1022
+ }, '');
1023
+ }