UNPKG

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