source: frontend/node_modules/svgo/plugins/convertPathData.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

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