source: trip-planner-front/node_modules/svgo/plugins/convertPathData.js@ e29cc2e

Last change on this file since e29cc2e was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 27.7 KB
Line 
1'use strict';
2
3const { collectStylesheet, computeStyle } = require('../lib/style.js');
4const { pathElems } = require('./_collections.js');
5const { path2js, js2path } = require('./_path.js');
6const { applyTransforms } = require('./_applyTransforms.js');
7const { cleanupOutData } = require('../lib/svgo/tools');
8
9exports.name = 'convertPathData';
10exports.type = 'visitor';
11exports.active = true;
12exports.description =
13 'optimizes path data: writes in shorter form, applies transformations';
14
15exports.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
36let roundData;
37let precision;
38let error;
39let arcThreshold;
40let 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 */
58exports.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 */
121const 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 */
293function 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 */
667function 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 */
741function 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 */
768function 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 */
801function 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 */
820function 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
836function 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
861function 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
885function 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
899function 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
918function 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
957function 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
981function 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
996function 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
1015function 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}
Note: See TracBrowser for help on using the repository browser.