svg-path-commander 0.1.21 → 0.1.25

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 (52) hide show
  1. package/README.md +72 -12
  2. package/dist/svg-path-commander.esm.js +658 -197
  3. package/dist/svg-path-commander.esm.min.js +2 -2
  4. package/dist/svg-path-commander.js +679 -196
  5. package/dist/svg-path-commander.min.js +2 -2
  6. package/package.json +1 -1
  7. package/src/convert/pathToAbsolute.js +3 -2
  8. package/src/convert/pathToCurve.js +2 -1
  9. package/src/convert/pathToRelative.js +2 -1
  10. package/src/math/polygonArea.js +4 -4
  11. package/src/math/polygonLength.js +2 -2
  12. package/src/parser/isSpace.js +1 -1
  13. package/src/parser/paramsCount.js +1 -0
  14. package/src/parser/parsePathString.js +4 -3
  15. package/src/parser/pathParser.js +2 -1
  16. package/src/parser/scanSegment.js +0 -1
  17. package/src/process/fixPath.js +1 -1
  18. package/src/process/lineToCubic.js +4 -5
  19. package/src/process/normalizePath.js +4 -2
  20. package/src/util/getClosestPoint.js +12 -0
  21. package/src/util/getCubicSize.js +41 -39
  22. package/src/util/getPathArea.js +19 -19
  23. package/src/util/getPathBBox.js +0 -11
  24. package/src/util/getPathLength.js +9 -9
  25. package/src/util/getPointAtLength.js +6 -29
  26. package/src/util/getPointAtPathLength.js +15 -0
  27. package/src/util/getPropertiesAtLength.js +63 -0
  28. package/src/util/getPropertiesAtPoint.js +78 -0
  29. package/src/util/getSegmentAtLength.js +13 -0
  30. package/src/util/getSegmentOfPoint.js +14 -0
  31. package/src/util/getTotalLength.js +15 -0
  32. package/src/util/isAbsoluteArray.js +2 -1
  33. package/src/util/isCurveArray.js +2 -1
  34. package/src/util/isNormalizedArray.js +2 -1
  35. package/src/util/isPointInStroke.js +13 -0
  36. package/src/util/isRelativeArray.js +2 -1
  37. package/src/util/pathLengthFactory.js +100 -0
  38. package/src/util/segmentArcFactory.js +52 -0
  39. package/src/util/segmentCubicFactory.js +83 -0
  40. package/src/util/segmentLineFactory.js +30 -0
  41. package/src/util/segmentQuadFactory.js +79 -0
  42. package/src/util/shapeToPath.js +1 -1
  43. package/src/util/util.js +19 -1
  44. package/types/index.d.ts +14 -5
  45. package/types/more/modules.ts +17 -5
  46. package/types/more/svg.d.ts +21 -0
  47. package/types/svg-path-commander.d.ts +253 -129
  48. package/src/util/getPointAtSegLength.js +0 -28
  49. package/src/util/getSegArcLength.js +0 -27
  50. package/src/util/getSegCubicLength.js +0 -52
  51. package/src/util/getSegLineLength.js +0 -14
  52. package/src/util/getSegQuadLength.js +0 -31
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * SVGPathCommander v0.1.21 (http://thednp.github.io/svg-path-commander)
3
- * Copyright 2021 © thednp
2
+ * SVGPathCommander v0.1.25 (http://thednp.github.io/svg-path-commander)
3
+ * Copyright 2022 © thednp
4
4
  * Licensed under MIT (https://github.com/thednp/svg-path-commander/blob/master/LICENSE)
5
5
  */
6
6
  /**
@@ -14,6 +14,7 @@ const defaultOptions = {
14
14
 
15
15
  /**
16
16
  * Segment params length
17
+ * @type {Record<string, number>}
17
18
  */
18
19
  const paramsCount = {
19
20
  a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0,
@@ -195,7 +196,7 @@ function isSpace(ch) {
195
196
  return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029) // Line terminators
196
197
  // White spaces
197
198
  || (ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0)
198
- || (ch >= 0x1680 && specialSpaces.indexOf(ch) >= 0);
199
+ || (ch >= 0x1680 && specialSpaces.includes(ch));
199
200
  }
200
201
 
201
202
  /**
@@ -272,7 +273,6 @@ function isArcCommand(code) {
272
273
  function scanSegment(path) {
273
274
  const { max, pathValue, index } = path;
274
275
  const cmdCode = pathValue.charCodeAt(index);
275
- // @ts-ignore
276
276
  const reqParams = paramsCount[pathValue[index].toLowerCase()];
277
277
 
278
278
  path.segmentStart = index;
@@ -336,7 +336,8 @@ function clonePath(path) {
336
336
  }
337
337
 
338
338
  /**
339
- * The `PathParser` used by the parser.
339
+ * The `PathParser` is used by the `parsePathString` static method
340
+ * to generate a `pathArray`.
340
341
  *
341
342
  * @param {string} pathString
342
343
  */
@@ -381,12 +382,13 @@ function isPathArray(path) {
381
382
  * @returns {SVGPathCommander.pathArray} the resulted `pathArray`
382
383
  */
383
384
  function parsePathString(pathInput) {
384
- if (Array.isArray(pathInput) && isPathArray(pathInput)) {
385
+ if (isPathArray(pathInput)) {
386
+ // @ts-ignore -- isPathArray also checks if it's an `Array`
385
387
  return clonePath(pathInput);
386
388
  }
387
389
 
388
- // @ts-ignore
389
- const path = new PathParser(pathInput); // TS expects string
390
+ // @ts-ignore -- pathInput is now string
391
+ const path = new PathParser(pathInput);
390
392
 
391
393
  skipSpaces(path);
392
394
 
@@ -414,11 +416,12 @@ function parsePathString(pathInput) {
414
416
  * Iterates an array to check if it's a `pathArray`
415
417
  * with all absolute values.
416
418
  *
417
- * @param {SVGPathCommander.pathArray} path the `pathArray` to be checked
419
+ * @param {string | SVGPathCommander.pathArray} path the `pathArray` to be checked
418
420
  * @returns {boolean} iteration result
419
421
  */
420
422
  function isAbsoluteArray(path) {
421
423
  return isPathArray(path)
424
+ // @ts-ignore -- `isPathArray` also checks if it's `Array`
422
425
  && path.every((x) => x[0] === x[0].toUpperCase());
423
426
  }
424
427
 
@@ -426,11 +429,12 @@ function isAbsoluteArray(path) {
426
429
  * Parses a path string value or object and returns an array
427
430
  * of segments, all converted to absolute values.
428
431
  *
429
- * @param {SVGPathCommander.pathArray | string} pathInput the path string | object
432
+ * @param {string | SVGPathCommander.pathArray} pathInput the path string | object
430
433
  * @returns {SVGPathCommander.absoluteArray} the resulted `pathArray` with absolute values
431
434
  */
432
435
  function pathToAbsolute(pathInput) {
433
- if (Array.isArray(pathInput) && isAbsoluteArray(pathInput)) {
436
+ if (isAbsoluteArray(pathInput)) {
437
+ // @ts-ignore -- `isAbsoluteArray` checks if it's `pathArray`
434
438
  return clonePath(pathInput);
435
439
  }
436
440
 
@@ -515,11 +519,12 @@ function pathToAbsolute(pathInput) {
515
519
  * Iterates an array to check if it's a `pathArray`
516
520
  * with relative values.
517
521
  *
518
- * @param {SVGPathCommander.pathArray} path the `pathArray` to be checked
522
+ * @param {string | SVGPathCommander.pathArray} path the `pathArray` to be checked
519
523
  * @returns {boolean} iteration result
520
524
  */
521
525
  function isRelativeArray(path) {
522
526
  return isPathArray(path)
527
+ // @ts-ignore -- `isPathArray` checks if it's `Array`
523
528
  && path.slice(1).every((seg) => seg[0] === seg[0].toLowerCase());
524
529
  }
525
530
 
@@ -527,11 +532,12 @@ function isRelativeArray(path) {
527
532
  * Parses a path string value or object and returns an array
528
533
  * of segments, all converted to relative values.
529
534
  *
530
- * @param {SVGPathCommander.pathArray} pathInput the path string | object
535
+ * @param {string | SVGPathCommander.pathArray} pathInput the path string | object
531
536
  * @returns {SVGPathCommander.relativeArray} the resulted `pathArray` with relative values
532
537
  */
533
538
  function pathToRelative(pathInput) {
534
539
  if (isRelativeArray(pathInput)) {
540
+ // @ts-ignore -- `isRelativeArray` checks if it's `pathArray`
535
541
  return clonePath(pathInput);
536
542
  }
537
543
 
@@ -728,10 +734,11 @@ function normalizeSegment(segment, params, prevCommand) {
728
734
  * with all segments are in non-shorthand notation
729
735
  * with absolute values.
730
736
  *
731
- * @param {SVGPathCommander.pathArray} path the `pathArray` to be checked
737
+ * @param {string | SVGPathCommander.pathArray} path the `pathArray` to be checked
732
738
  * @returns {boolean} iteration result
733
739
  */
734
740
  function isNormalizedArray(path) {
741
+ // @ts-ignore -- `isAbsoluteArray` also checks if it's `Array`
735
742
  return isAbsoluteArray(path) && path.every((seg) => 'ACLMQZ'.includes(seg[0]));
736
743
  }
737
744
 
@@ -747,14 +754,17 @@ const paramsParser = {
747
754
  * * convert segments to absolute values
748
755
  * * convert shorthand path commands to their non-shorthand notation
749
756
  *
750
- * @param {SVGPathCommander.pathArray} pathInput the string to be parsed or 'pathArray'
757
+ * @param {string | SVGPathCommander.pathArray} pathInput the string to be parsed or 'pathArray'
751
758
  * @returns {SVGPathCommander.normalArray} the normalized `pathArray`
752
759
  */
753
760
  function normalizePath(pathInput) {
754
761
  if (isNormalizedArray(pathInput)) {
762
+ // @ts-ignore -- `isNormalizedArray` checks if it's `pathArray`
755
763
  return clonePath(pathInput);
756
764
  }
757
765
 
766
+ /** @type {SVGPathCommander.normalArray} */
767
+ // @ts-ignore -- `absoluteArray` will become a `normalArray`
758
768
  const path = pathToAbsolute(pathInput);
759
769
  const params = { ...paramsParser };
760
770
  const allPathCommands = [];
@@ -782,7 +792,6 @@ function normalizePath(pathInput) {
782
792
  params.y2 = +(segment[seglen - 3]) || params.y1;
783
793
  }
784
794
 
785
- // @ts-ignore -- a `normalArray` is absolutely an `absoluteArray`
786
795
  return path;
787
796
  }
788
797
 
@@ -809,7 +818,7 @@ function fixPath(pathInput) {
809
818
  const [x, y] = normalArray[segBeforeZ].slice(-2);
810
819
 
811
820
  if (isClosed && mx === x && my === y) {
812
- // @ts-ignore -- `pathSegment[]` is a `pathArray`
821
+ // @ts-ignore -- `pathSegment[]` is quite a `pathArray`
813
822
  return pathArray.slice(0, -1);
814
823
  }
815
824
  return pathArray;
@@ -819,10 +828,11 @@ function fixPath(pathInput) {
819
828
  * Iterates an array to check if it's a `pathArray`
820
829
  * with all C (cubic bezier) segments.
821
830
  *
822
- * @param {SVGPathCommander.pathArray} path the `Array` to be checked
831
+ * @param {string | SVGPathCommander.pathArray} path the `Array` to be checked
823
832
  * @returns {boolean} iteration result
824
833
  */
825
834
  function isCurveArray(path) {
835
+ // @ts-ignore -- `isPathArray` also checks if it's `Array`
826
836
  return isPathArray(path) && path.every((seg) => 'MC'.includes(seg[0]));
827
837
  }
828
838
 
@@ -977,35 +987,6 @@ function quadToCubic(x1, y1, qx, qy, x2, y2) {
977
987
  ];
978
988
  }
979
989
 
980
- /**
981
- * Returns the {x,y} coordinates of a point at a
982
- * given length of a cubic-bezier segment.
983
- *
984
- * @param {number} p1x the starting point X
985
- * @param {number} p1y the starting point Y
986
- * @param {number} c1x the first control point X
987
- * @param {number} c1y the first control point Y
988
- * @param {number} c2x the second control point X
989
- * @param {number} c2y the second control point Y
990
- * @param {number} p2x the ending point X
991
- * @param {number} p2y the ending point Y
992
- * @param {number} t a [0-1] ratio
993
- * @returns {{x: number, y: number}} the requested {x,y} coordinates
994
- */
995
- function getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
996
- const t1 = 1 - t;
997
- return {
998
- x: (t1 ** 3) * p1x
999
- + t1 * t1 * 3 * t * c1x
1000
- + t1 * 3 * t * t * c2x
1001
- + (t ** 3) * p2x,
1002
- y: (t1 ** 3) * p1y
1003
- + t1 * t1 * 3 * t * c1y
1004
- + t1 * 3 * t * t * c2y
1005
- + (t ** 3) * p2y,
1006
- };
1007
- }
1008
-
1009
990
  /**
1010
991
  * Returns the coordinates of a specified distance
1011
992
  * ratio between two points.
@@ -1020,6 +1001,49 @@ function midPoint(a, b, t) {
1020
1001
  return [ax + (bx - ax) * t, ay + (by - ay) * t];
1021
1002
  }
1022
1003
 
1004
+ /**
1005
+ * Returns the square root of the distance
1006
+ * between two given points.
1007
+ *
1008
+ * @param {[number, number]} a the first point coordinates
1009
+ * @param {[number, number]} b the second point coordinates
1010
+ * @returns {number} the distance value
1011
+ */
1012
+ function distanceSquareRoot(a, b) {
1013
+ return Math.sqrt(
1014
+ (a[0] - b[0]) * (a[0] - b[0])
1015
+ + (a[1] - b[1]) * (a[1] - b[1]),
1016
+ );
1017
+ }
1018
+
1019
+ /**
1020
+ * Returns the length of a line (L,V,H,Z) segment,
1021
+ * or a point at a given length.
1022
+ *
1023
+ * @param {number} x1 the starting point X
1024
+ * @param {number} y1 the starting point Y
1025
+ * @param {number} x2 the ending point X
1026
+ * @param {number} y2 the ending point Y
1027
+ * @param {number=} distance the distance to point
1028
+ * @returns {{x: number, y: number} | number} the segment length or point
1029
+ */
1030
+ function segmentLineFactory(x1, y1, x2, y2, distance) {
1031
+ const length = distanceSquareRoot([x1, y1], [x2, y2]);
1032
+ const margin = 0.001;
1033
+
1034
+ if (typeof distance === 'number') {
1035
+ if (distance < margin) {
1036
+ return { x: x1, y: y1 };
1037
+ }
1038
+ if (distance > length) {
1039
+ return { x: x2, y: y2 };
1040
+ }
1041
+ const [x, y] = midPoint([x1, y1], [x2, y2], distance / length);
1042
+ return { x, y };
1043
+ }
1044
+ return length;
1045
+ }
1046
+
1023
1047
  /**
1024
1048
  * Converts an L (line-to) segment to C (cubic-bezier).
1025
1049
  *
@@ -1040,15 +1064,14 @@ function lineToCubic(x1, y1, x2, y2) {
1040
1064
  const p4 = midPoint(p2, p3, t);
1041
1065
  const p5 = midPoint(p3, p4, t);
1042
1066
  const p6 = midPoint(p4, p5, t);
1043
- // const cp1 = getPointAtSegLength.apply(0, p0.concat(p2, p4, p6, t));
1044
1067
  const seg1 = [...p0, ...p2, ...p4, ...p6, t];
1045
1068
  // @ts-ignore
1046
- const cp1 = getPointAtSegLength(...seg1);
1047
- // const cp2 = getPointAtSegLength.apply(0, p6.concat(p5, p3, p1, 0));
1069
+ const cp1 = segmentLineFactory(...seg1);
1048
1070
  const seg2 = [...p6, ...p5, ...p3, ...p1, 0];
1049
1071
  // @ts-ignore
1050
- const cp2 = getPointAtSegLength(...seg2);
1072
+ const cp2 = segmentLineFactory(...seg2);
1051
1073
 
1074
+ // @ts-ignore
1052
1075
  return [cp1.x, cp1.y, cp2.x, cp2.y, x2, y2];
1053
1076
  }
1054
1077
 
@@ -1106,11 +1129,12 @@ function segmentToCubic(segment, params) {
1106
1129
  * In addition, un-necessary `Z` segment is removed if previous segment
1107
1130
  * extends to the `M` segment.
1108
1131
  *
1109
- * @param {SVGPathCommander.pathArray} pathInput the string to be parsed or 'pathArray'
1132
+ * @param {string | SVGPathCommander.pathArray} pathInput the string to be parsed or 'pathArray'
1110
1133
  * @returns {SVGPathCommander.curveArray} the resulted `pathArray` converted to cubic-bezier
1111
1134
  */
1112
1135
  function pathToCurve(pathInput) {
1113
1136
  if (isCurveArray(pathInput)) {
1137
+ // @ts-ignore -- `isCurveArray` checks if it's `pathArray`
1114
1138
  return clonePath(pathInput);
1115
1139
  }
1116
1140
 
@@ -2547,65 +2571,149 @@ function transformPath(path, transform) {
2547
2571
  }
2548
2572
 
2549
2573
  /**
2550
- * Returns the cubic-bezier segment length.
2574
+ * Returns a point at a given length of a C (cubic-bezier) segment.
2575
+ *
2576
+ * @param {number} x1 the starting point X
2577
+ * @param {number} y1 the starting point Y
2578
+ * @param {number} c1x the first control point X
2579
+ * @param {number} c1y the first control point Y
2580
+ * @param {number} c2x the second control point X
2581
+ * @param {number} c2y the second control point Y
2582
+ * @param {number} x2 the ending point X
2583
+ * @param {number} y2 the ending point Y
2584
+ * @param {number} t a [0-1] ratio
2585
+ * @returns {{x: number, y: number}} the cubic-bezier segment length
2586
+ */
2587
+ function getPointAtCubicSegmentLength(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t) {
2588
+ const t1 = 1 - t;
2589
+ return {
2590
+ x: (t1 ** 3) * x1
2591
+ + 3 * (t1 ** 2) * t * c1x
2592
+ + 3 * t1 * (t ** 2) * c2x
2593
+ + (t ** 3) * x2,
2594
+ y: (t1 ** 3) * y1
2595
+ + 3 * (t1 ** 2) * t * c1y
2596
+ + 3 * t1 * (t ** 2) * c2y
2597
+ + (t ** 3) * y2,
2598
+ };
2599
+ }
2600
+
2601
+ /**
2602
+ * Returns the length of a C (cubic-bezier) segment,
2603
+ * or an {x,y} point at a given length.
2604
+ *
2605
+ * @param {number} x1 the starting point X
2606
+ * @param {number} y1 the starting point Y
2607
+ * @param {number} c1x the first control point X
2608
+ * @param {number} c1y the first control point Y
2609
+ * @param {number} c2x the second control point X
2610
+ * @param {number} c2y the second control point Y
2611
+ * @param {number} x2 the ending point X
2612
+ * @param {number} y2 the ending point Y
2613
+ * @param {number=} distance the point distance
2614
+ * @returns {{x: number, y: number} | number} the segment length or point
2615
+ */
2616
+ function segmentCubicFactory(x1, y1, c1x, c1y, c2x, c2y, x2, y2, distance) {
2617
+ const distanceIsNumber = typeof distance === 'number';
2618
+ const lengthMargin = 0.001;
2619
+ let x = x1; let y = y1;
2620
+ let totalLength = 0;
2621
+ let prev = [x1, y1, totalLength];
2622
+ /** @type {[number, number]} */
2623
+ let cur = [x1, y1];
2624
+ let t = 0;
2625
+
2626
+ if (distanceIsNumber && distance < lengthMargin) {
2627
+ return { x, y };
2628
+ }
2629
+
2630
+ const n = 100;
2631
+ for (let j = 0; j <= n; j += 1) {
2632
+ t = j / n;
2633
+
2634
+ ({ x, y } = getPointAtCubicSegmentLength(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t));
2635
+ totalLength += distanceSquareRoot(cur, [x, y]);
2636
+ cur = [x, y];
2637
+
2638
+ if (distanceIsNumber && totalLength >= distance) {
2639
+ const dv = (totalLength - distance) / (totalLength - prev[2]);
2640
+
2641
+ return {
2642
+ x: cur[0] * (1 - dv) + prev[0] * dv,
2643
+ y: cur[1] * (1 - dv) + prev[1] * dv,
2644
+ };
2645
+ }
2646
+ prev = [x, y, totalLength];
2647
+ }
2648
+
2649
+ if (distanceIsNumber && distance >= totalLength) {
2650
+ return { x: x2, y: y2 };
2651
+ }
2652
+ return totalLength;
2653
+ }
2654
+
2655
+ /**
2656
+ * Returns the cubic-bezier segment bounding box.
2551
2657
  *
2552
- * @param {number} p1x the starting point X
2553
- * @param {number} p1y the starting point Y
2658
+ * @param {number} x1 the starting point X
2659
+ * @param {number} y1 the starting point Y
2554
2660
  * @param {number} c1x the first control point X
2555
2661
  * @param {number} c1y the first control point Y
2556
2662
  * @param {number} c2x the second control point X
2557
2663
  * @param {number} c2y the second control point Y
2558
- * @param {number} p2x the ending point X
2559
- * @param {number} p2y the ending point Y
2560
- * @returns {SVGPathCommander.segmentLimits} the length of the cubic-bezier segment
2561
- */
2562
- function getCubicSize(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
2563
- let a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x);
2564
- let b = 2 * (c1x - p1x) - 2 * (c2x - c1x);
2565
- let c = p1x - c1x;
2664
+ * @param {number} x2 the ending point X
2665
+ * @param {number} y2 the ending point Y
2666
+ * @returns {SVGPathCommander.segmentLimits} the bounding box of the cubic-bezier segment
2667
+ */
2668
+ function getCubicSize(x1, y1, c1x, c1y, c2x, c2y, x2, y2) {
2669
+ let a = (c2x - 2 * c1x + x1) - (x2 - 2 * c2x + c1x);
2670
+ let b = 2 * (c1x - x1) - 2 * (c2x - c1x);
2671
+ let c = x1 - c1x;
2566
2672
  let t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
2567
2673
  let t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
2568
- const y = [p1y, p2y];
2569
- const x = [p1x, p2x];
2570
- let dot;
2571
- // @ts-ignore
2572
- if (Math.abs(t1) > '1e12') t1 = 0.5;
2573
- // @ts-ignore
2574
- if (Math.abs(t2) > '1e12') t2 = 0.5;
2674
+ const X = [x1, x2];
2675
+ const Y = [y1, y2];
2676
+ let x = 0;
2677
+ let y = 0;
2678
+
2679
+ if (Math.abs(t1) > 1e12) t1 = 0.5;
2680
+ if (Math.abs(t2) > 1e12) t2 = 0.5;
2575
2681
 
2576
2682
  if (t1 > 0 && t1 < 1) {
2577
- dot = getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
2578
- x.push(dot.x);
2579
- y.push(dot.y);
2683
+ // @ts-ignore
2684
+ ({ x, y } = segmentCubicFactory(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t1));
2685
+ X.push(x);
2686
+ Y.push(y);
2580
2687
  }
2581
2688
  if (t2 > 0 && t2 < 1) {
2582
- dot = getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
2583
- x.push(dot.x);
2584
- y.push(dot.y);
2689
+ // @ts-ignore
2690
+ ({ x, y } = segmentCubicFactory(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t2));
2691
+ X.push(x);
2692
+ Y.push(y);
2585
2693
  }
2586
- a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
2587
- b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
2588
- c = p1y - c1y;
2694
+ a = (c2y - 2 * c1y + y1) - (y2 - 2 * c2y + c1y);
2695
+ b = 2 * (c1y - y1) - 2 * (c2y - c1y);
2696
+ c = y1 - c1y;
2589
2697
  t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
2590
2698
  t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
2591
- // @ts-ignore
2592
- if (Math.abs(t1) > '1e12') t1 = 0.5;
2593
- // @ts-ignore
2594
- if (Math.abs(t2) > '1e12') t2 = 0.5;
2699
+ if (Math.abs(t1) > 1e12) t1 = 0.5;
2700
+ if (Math.abs(t2) > 1e12) t2 = 0.5;
2595
2701
 
2596
2702
  if (t1 > 0 && t1 < 1) {
2597
- dot = getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
2598
- x.push(dot.x);
2599
- y.push(dot.y);
2703
+ // @ts-ignore
2704
+ ({ x, y } = segmentCubicFactory(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t1));
2705
+ X.push(x);
2706
+ Y.push(y);
2600
2707
  }
2601
2708
  if (t2 > 0 && t2 < 1) {
2602
- dot = getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
2603
- x.push(dot.x);
2604
- y.push(dot.y);
2709
+ // @ts-ignore
2710
+ ({ x, y } = segmentCubicFactory(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t2));
2711
+ X.push(x);
2712
+ Y.push(y);
2605
2713
  }
2606
2714
  return {
2607
- min: { x: Math.min.apply(0, x), y: Math.min.apply(0, y) },
2608
- max: { x: Math.max.apply(0, x), y: Math.max.apply(0, y) },
2715
+ min: { x: Math.min(...X), y: Math.min(...Y) },
2716
+ max: { x: Math.max(...X), y: Math.max(...Y) },
2609
2717
  };
2610
2718
  }
2611
2719
 
@@ -2641,27 +2749,16 @@ function getPathBBox(path) {
2641
2749
  // @ts-ignore -- this should be fine
2642
2750
  const dim = getCubicSize(...sizeArgs);
2643
2751
 
2644
- // X = X.concat(dim.min.x, dim.max.x);
2645
2752
  X = [...X, ...[dim.min.x, dim.max.x]];
2646
-
2647
- // Y = Y.concat(dim.min.y, dim.max.y);
2648
2753
  Y = [...Y, ...[dim.min.y, dim.max.y]];
2649
2754
  x = s1;
2650
2755
  y = s2;
2651
2756
  }
2652
2757
  });
2653
2758
 
2654
- // @ts-ignore
2655
- // const xTop = Math.min.apply(0, X);
2656
2759
  const xTop = Math.min(...X);
2657
- // @ts-ignore
2658
- // const yTop = Math.min.apply(0, Y);
2659
2760
  const yTop = Math.min(...Y);
2660
- // @ts-ignore
2661
- // const xBot = Math.max.apply(0, X);
2662
2761
  const xBot = Math.max(...X);
2663
- // @ts-ignore
2664
- // const yBot = Math.max.apply(0, Y);
2665
2762
  const yBot = Math.max(...Y);
2666
2763
  const width = xBot - xTop;
2667
2764
  const height = yBot - yTop;
@@ -2904,38 +3001,38 @@ class SVGPathCommander {
2904
3001
  }
2905
3002
 
2906
3003
  /**
2907
- * Returns the area of a single segment shape.
3004
+ * Returns the area of a single cubic-bezier segment.
2908
3005
  *
2909
3006
  * http://objectmix.com/graphics/133553-area-closed-bezier-curve.html
2910
3007
  *
2911
- * @param {number} x0 the starting point X
2912
- * @param {number} y0 the starting point Y
2913
- * @param {number} x1 the first control point X
2914
- * @param {number} y1 the first control point Y
2915
- * @param {number} x2 the second control point X
2916
- * @param {number} y2 the second control point Y
2917
- * @param {number} x3 the ending point X
2918
- * @param {number} y3 the ending point Y
3008
+ * @param {number} x1 the starting point X
3009
+ * @param {number} y1 the starting point Y
3010
+ * @param {number} c1x the first control point X
3011
+ * @param {number} c1y the first control point Y
3012
+ * @param {number} c2x the second control point X
3013
+ * @param {number} c2y the second control point Y
3014
+ * @param {number} x2 the ending point X
3015
+ * @param {number} y2 the ending point Y
2919
3016
  * @returns {number} the area of the cubic-bezier segment
2920
3017
  */
2921
- function getCubicSegArea(x0, y0, x1, y1, x2, y2, x3, y3) {
2922
- return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
2923
- + (y1 * (x0 - x2)) - (x1 * (y0 - y2))
2924
- + (y3 * (x2 + x0 / 3)) - (x3 * (y2 + y0 / 3)))) / 20;
3018
+ function getCubicSegArea(x1, y1, c1x, c1y, c2x, c2y, x2, y2) {
3019
+ return (3 * ((y2 - y1) * (c1x + c2x) - (x2 - x1) * (c1y + c2y)
3020
+ + (c1y * (x1 - c2x)) - (c1x * (y1 - c2y))
3021
+ + (y2 * (c2x + x1 / 3)) - (x2 * (c2y + y1 / 3)))) / 20;
2925
3022
  }
2926
3023
 
2927
3024
  /**
2928
3025
  * Returns the area of a shape.
2929
3026
  * @author Jürg Lehni & Jonathan Puckey
2930
3027
  *
2931
- * => https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
3028
+ * @see https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
2932
3029
  *
2933
3030
  * @param {SVGPathCommander.pathArray} path the shape `pathArray`
2934
3031
  * @returns {number} the length of the cubic-bezier segment
2935
3032
  */
2936
3033
  function getPathArea(path) {
2937
- let x = 0; let y = 0;
2938
- let len = 0;
3034
+ let x = 0; let y = 0; let len = 0;
3035
+
2939
3036
  return pathToCurve(path).map((seg) => {
2940
3037
  switch (seg[0]) {
2941
3038
  case 'M':
@@ -2943,86 +3040,268 @@ function getPathArea(path) {
2943
3040
  return 0;
2944
3041
  default:
2945
3042
  // @ts-ignore -- the utility will have proper amount of params
2946
- len = getCubicSegArea(...[x, y, ...seg.slice(1)]);
2947
-
2948
- [x, y] = seg.slice(-2).map(Number);
3043
+ len = getCubicSegArea(x, y, ...seg.slice(1));
3044
+ // @ts-ignore -- the segment always has numbers
3045
+ [x, y] = seg.slice(-2);
2949
3046
  return len;
2950
3047
  }
2951
3048
  }).reduce((a, b) => a + b, 0);
2952
3049
  }
2953
3050
 
2954
3051
  /**
2955
- * @param {number} p1
2956
- * @param {number} p2
2957
- * @param {number} p3
2958
- * @param {number} p4
3052
+ * Returns the shape total length, or the equivalent to `shape.getTotalLength()`.
3053
+ *
3054
+ * This is the `pathToCurve` version which is faster and more efficient for
3055
+ * paths that are `curveArray`.
3056
+ *
3057
+ * @param {string | SVGPathCommander.curveArray} path the target `pathArray`
3058
+ * @returns {number} the `curveArray` total length
3059
+ */
3060
+ function getPathLength(path) {
3061
+ let totalLength = 0;
3062
+ pathToCurve(path).forEach((s, i, curveArray) => {
3063
+ const args = s[0] !== 'M' ? [...curveArray[i - 1].slice(-2), ...s.slice(1)] : [];
3064
+ // @ts-ignore
3065
+ totalLength += s[0] === 'M' ? 0 : segmentCubicFactory(...args);
3066
+ });
3067
+ return totalLength;
3068
+ }
3069
+
3070
+ /**
3071
+ * Returns the length of a A (arc-to) segment,
3072
+ * or an {x,y} point at a given length.
3073
+ *
3074
+ * @param {number} X1 the starting x position
3075
+ * @param {number} Y1 the starting y position
3076
+ * @param {number} RX x-radius of the arc
3077
+ * @param {number} RY y-radius of the arc
3078
+ * @param {number} angle x-axis-rotation of the arc
3079
+ * @param {number} LAF large-arc-flag of the arc
3080
+ * @param {number} SF sweep-flag of the arc
3081
+ * @param {number} X2 the ending x position
3082
+ * @param {number} Y2 the ending y position
3083
+ * @param {number} distance the point distance
3084
+ * @returns {{x: number, y: number} | number} the segment length or point
3085
+ */
3086
+ function segmentArcFactory(X1, Y1, RX, RY, angle, LAF, SF, X2, Y2, distance) {
3087
+ const cubicSeg = arcToCubic(X1, Y1, RX, RY, angle, LAF, SF, X2, Y2);
3088
+ const distanceIsNumber = typeof distance === 'number';
3089
+ let [x, y] = [X1, Y1];
3090
+ const lengthMargin = 0.001;
3091
+ let totalLength = 0;
3092
+ let cubicSubseg = [];
3093
+ let argsc = [];
3094
+ let segLen = 0;
3095
+
3096
+ if (distanceIsNumber && distance < lengthMargin) {
3097
+ return { x, y };
3098
+ }
3099
+
3100
+ for (let i = 0, ii = cubicSeg.length; i < ii; i += 6) {
3101
+ cubicSubseg = cubicSeg.slice(i, i + 6);
3102
+ argsc = [x, y, ...cubicSubseg];
3103
+ // @ts-ignore
3104
+ segLen = segmentCubicFactory(...argsc);
3105
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3106
+ // @ts-ignore -- this is a `cubicSegment`
3107
+ return segmentCubicFactory(...argsc, distance - totalLength);
3108
+ }
3109
+ totalLength += segLen;
3110
+ [x, y] = cubicSubseg.slice(-2);
3111
+ }
3112
+
3113
+ if (distanceIsNumber && distance >= totalLength) {
3114
+ return { x: X2, y: Y2 };
3115
+ }
3116
+
3117
+ return totalLength;
3118
+ }
3119
+
3120
+ /**
3121
+ * Returns the {x,y} coordinates of a point at a
3122
+ * given length of a quad-bezier segment.
3123
+ *
3124
+ * @see https://github.com/substack/point-at-length
3125
+ *
3126
+ * @param {number} x1 the starting point X
3127
+ * @param {number} y1 the starting point Y
3128
+ * @param {number} cx the control point X
3129
+ * @param {number} cy the control point Y
3130
+ * @param {number} x2 the ending point X
3131
+ * @param {number} y2 the ending point Y
2959
3132
  * @param {number} t a [0-1] ratio
2960
- * @returns {number}
3133
+ * @returns {{x: number, y: number}} the requested {x,y} coordinates
2961
3134
  */
2962
- function base3(p1, p2, p3, p4, t) {
2963
- const t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4;
2964
- const t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
2965
- return t * t2 - 3 * p1 + 3 * p2;
3135
+ function getPointAtQuadSegmentLength(x1, y1, cx, cy, x2, y2, t) {
3136
+ const t1 = 1 - t;
3137
+ return {
3138
+ x: (t1 ** 2) * x1
3139
+ + 2 * t1 * t * cx
3140
+ + (t ** 2) * x2,
3141
+ y: (t1 ** 2) * y1
3142
+ + 2 * t1 * t * cy
3143
+ + (t ** 2) * y2,
3144
+ };
2966
3145
  }
2967
3146
 
2968
3147
  /**
2969
- * Returns the C (cubic-bezier) segment length.
3148
+ * Returns the Q (quadratic-bezier) segment length,
3149
+ * or an {x,y} point at a given length.
2970
3150
  *
2971
3151
  * @param {number} x1 the starting point X
2972
3152
  * @param {number} y1 the starting point Y
2973
- * @param {number} x2 the first control point X
2974
- * @param {number} y2 the first control point Y
2975
- * @param {number} x3 the second control point X
2976
- * @param {number} y3 the second control point Y
2977
- * @param {number} x4 the ending point X
2978
- * @param {number} y4 the ending point Y
2979
- * @param {number} z a [0-1] ratio
2980
- * @returns {number} the cubic-bezier segment length
2981
- */
2982
- function getSegCubicLength(x1, y1, x2, y2, x3, y3, x4, y4, z) {
2983
- let Z = z;
2984
- if (z === null || Number.isNaN(+z)) Z = 1;
2985
-
2986
- // Z = Z > 1 ? 1 : Z < 0 ? 0 : Z;
2987
- if (Z > 1) Z = 1;
2988
- if (Z < 0) Z = 0;
2989
-
2990
- const z2 = Z / 2; let ct = 0; let xbase = 0; let ybase = 0; let sum = 0;
2991
- const Tvalues = [-0.1252, 0.1252, -0.3678, 0.3678,
2992
- -0.5873, 0.5873, -0.7699, 0.7699,
2993
- -0.9041, 0.9041, -0.9816, 0.9816];
2994
- const Cvalues = [0.2491, 0.2491, 0.2335, 0.2335,
2995
- 0.2032, 0.2032, 0.1601, 0.1601,
2996
- 0.1069, 0.1069, 0.0472, 0.0472];
2997
-
2998
- Tvalues.forEach((T, i) => {
2999
- ct = z2 * T + z2;
3000
- xbase = base3(x1, x2, x3, x4, ct);
3001
- ybase = base3(y1, y2, y3, y4, ct);
3002
- sum += Cvalues[i] * Math.sqrt(xbase * xbase + ybase * ybase);
3003
- });
3004
- return z2 * sum;
3153
+ * @param {number} qx the control point X
3154
+ * @param {number} qy the control point Y
3155
+ * @param {number} x2 the ending point X
3156
+ * @param {number} y2 the ending point Y
3157
+ * @param {number=} distance the distance to point
3158
+ * @returns {{x: number, y: number} | number} the segment length or point
3159
+ */
3160
+ function segmentQuadFactory(x1, y1, qx, qy, x2, y2, distance) {
3161
+ const distanceIsNumber = typeof distance === 'number';
3162
+ const lengthMargin = 0.001;
3163
+ let x = x1; let y = y1;
3164
+ let totalLength = 0;
3165
+ let prev = [x1, y1, totalLength];
3166
+ /** @type {[number, number]} */
3167
+ let cur = [x1, y1];
3168
+ let t = 0;
3169
+
3170
+ if (distanceIsNumber && distance < lengthMargin) {
3171
+ return { x, y };
3172
+ }
3173
+
3174
+ const n = 100;
3175
+ for (let j = 0; j <= n; j += 1) {
3176
+ t = j / n;
3177
+
3178
+ ({ x, y } = getPointAtQuadSegmentLength(x1, y1, qx, qy, x2, y2, t));
3179
+ totalLength += distanceSquareRoot(cur, [x, y]);
3180
+ cur = [x, y];
3181
+
3182
+ if (distanceIsNumber && totalLength >= distance) {
3183
+ const dv = (totalLength - distance) / (totalLength - prev[2]);
3184
+
3185
+ return {
3186
+ x: cur[0] * (1 - dv) + prev[0] * dv,
3187
+ y: cur[1] * (1 - dv) + prev[1] * dv,
3188
+ };
3189
+ }
3190
+ prev = [x, y, totalLength];
3191
+ }
3192
+ if (distanceIsNumber && distance >= totalLength) {
3193
+ return { x: x2, y: y2 };
3194
+ }
3195
+ return totalLength;
3005
3196
  }
3006
3197
 
3007
3198
  /**
3008
- * Returns the shape total length,
3009
- * or the equivalent to `shape.getTotalLength()`
3010
- * pathToCurve version
3199
+ * Returns a {x,y} point at a given length of a shape or the shape total length.
3011
3200
  *
3012
- * @param {SVGPathCommander.pathArray} path the target `pathArray`
3013
- * @returns {number} the shape total length
3201
+ * @param {string | SVGPathCommander.pathArray} pathInput the `pathArray` to look into
3202
+ * @param {number=} distance the length of the shape to look at
3203
+ * @returns {{x: number, y: number} | number} the total length or point
3014
3204
  */
3015
- function getPathLength(path) {
3205
+ function pathLengthFactory(pathInput, distance) {
3206
+ const path = fixPath(normalizePath(pathInput));
3207
+ const distanceIsNumber = typeof distance === 'number';
3016
3208
  let totalLength = 0;
3017
- pathToCurve(path).forEach((s, i, curveArray) => {
3018
- const args = s[0] !== 'M' ? [...curveArray[i - 1].slice(-2), ...s.slice(1)] : [];
3019
- totalLength += s[0] === 'M' ? 0
3209
+ let isM = true;
3210
+ /** @type {number[]} */
3211
+ let data = [];
3212
+ let pathCommand = 'M';
3213
+ let segLen = 0;
3214
+ let x = 0;
3215
+ let y = 0;
3216
+ let mx = 0;
3217
+ let my = 0;
3218
+ let seg;
3219
+
3220
+ for (let i = 0, ll = path.length; i < ll; i += 1) {
3221
+ seg = path[i];
3222
+ [pathCommand] = seg;
3223
+ isM = pathCommand === 'M';
3224
+ // @ts-ignore
3225
+ data = !isM ? [x, y, ...seg.slice(1)] : data;
3226
+
3227
+ // this segment is always ZERO
3228
+ if (isM) {
3229
+ // remember mx, my for Z
3020
3230
  // @ts-ignore
3021
- : getSegCubicLength(...args);
3022
- });
3231
+ [, mx, my] = seg;
3232
+ if (distanceIsNumber && distance < 0.001) {
3233
+ return { x: mx, y: my };
3234
+ }
3235
+ } else if (pathCommand === 'L') {
3236
+ // @ts-ignore
3237
+ segLen = segmentLineFactory(...data);
3238
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3239
+ // @ts-ignore
3240
+ return segmentLineFactory(...data, distance - totalLength);
3241
+ }
3242
+ totalLength += segLen;
3243
+ } else if (pathCommand === 'A') {
3244
+ // @ts-ignore
3245
+ segLen = segmentArcFactory(...data);
3246
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3247
+ // @ts-ignore
3248
+ return segmentArcFactory(...data, distance - totalLength);
3249
+ }
3250
+ totalLength += segLen;
3251
+ } else if (pathCommand === 'C') {
3252
+ // @ts-ignore
3253
+ segLen = segmentCubicFactory(...data);
3254
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3255
+ // @ts-ignore
3256
+ return segmentCubicFactory(...data, distance - totalLength);
3257
+ }
3258
+ totalLength += segLen;
3259
+ } else if (pathCommand === 'Q') {
3260
+ // @ts-ignore
3261
+ segLen = segmentQuadFactory(...data);
3262
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3263
+ // @ts-ignore
3264
+ return segmentQuadFactory(...data, distance - totalLength);
3265
+ }
3266
+ totalLength += segLen;
3267
+ } else if (pathCommand === 'Z') {
3268
+ data = [x, y, mx, my];
3269
+ // @ts-ignore
3270
+ segLen = segmentLineFactory(...data);
3271
+ if (distanceIsNumber && totalLength + segLen >= distance) {
3272
+ // @ts-ignore
3273
+ return segmentLineFactory(...data, distance - totalLength);
3274
+ }
3275
+ totalLength += segLen;
3276
+ }
3277
+
3278
+ // @ts-ignore -- needed for the below
3279
+ [x, y] = pathCommand !== 'Z' ? seg.slice(-2) : [mx, my];
3280
+ }
3281
+
3282
+ // native `getPointAtLength` behavior when the given distance
3283
+ // is higher than total length
3284
+ if (distanceIsNumber && distance >= totalLength) {
3285
+ return { x, y };
3286
+ }
3287
+
3023
3288
  return totalLength;
3024
3289
  }
3025
3290
 
3291
+ /**
3292
+ * Returns the shape total length, or the equivalent to `shape.getTotalLength()`.
3293
+ *
3294
+ * The `normalizePath` version is lighter, faster, more efficient and more accurate
3295
+ * with paths that are not `curveArray`.
3296
+ *
3297
+ * @param {string | SVGPathCommander.pathArray} pathInput the target `pathArray`
3298
+ * @returns {number} the shape total length
3299
+ */
3300
+ function getTotalLength(pathInput) {
3301
+ // @ts-ignore - it's fine
3302
+ return pathLengthFactory(pathInput);
3303
+ }
3304
+
3026
3305
  /**
3027
3306
  * Check if a path is drawn clockwise and returns true if so,
3028
3307
  * false otherwise.
@@ -3037,34 +3316,208 @@ function getDrawDirection(path) {
3037
3316
  /**
3038
3317
  * Returns [x,y] coordinates of a point at a given length of a shape.
3039
3318
  *
3040
- * @param {string | SVGPathCommander.pathArray} path the `pathArray` to look into
3041
- * @param {number} length the length of the shape to look at
3042
- * @returns {number[]} the requested [x,y] coordinates
3319
+ * @param {string | SVGPathCommander.pathArray} pathInput the `pathArray` to look into
3320
+ * @param {number} distance the length of the shape to look at
3321
+ * @returns {{x: number, y: number}} the requested {x, y} point coordinates
3043
3322
  */
3044
- function getPointAtLength(path, length) {
3045
- let totalLength = 0;
3046
- let segLen;
3047
- let data;
3048
- let result;
3323
+ function getPointAtLength(pathInput, distance) {
3049
3324
  // @ts-ignore
3050
- return pathToCurve(path).map((seg, i, curveArray) => {
3051
- data = i ? [...curveArray[i - 1].slice(-2), ...seg.slice(1)] : seg.slice(1);
3052
- // @ts-ignore
3053
- segLen = i ? getSegCubicLength(...data) : 0;
3054
- totalLength += segLen;
3325
+ return pathLengthFactory(pathInput, distance);
3326
+ }
3327
+
3328
+ /**
3329
+ * Returns [x,y] coordinates of a point at a given length of a shape.
3330
+ * `pathToCurve` version.
3331
+ *
3332
+ * @deprecated
3333
+ *
3334
+ * @param {string | SVGPathCommander.pathArray} pathInput the `pathArray` to look into
3335
+ * @param {number} distance the length of the shape to look at
3336
+ * @returns {{x: number, y: number}} the requested {x, y} point coordinates
3337
+ */
3338
+ function getPointAtPathLength(pathInput, distance) {
3339
+ return getPointAtLength(pathInput, distance);
3340
+ }
3055
3341
 
3056
- if (i === 0) {
3057
- result = { x: data[0], y: data[1] };
3058
- } else if (totalLength > length && length > totalLength - segLen) {
3059
- const args = [...data, 1 - ((totalLength - length) / segLen)];
3342
+ /**
3343
+ * Returns the segment, its index and length as well as
3344
+ * the length to that segment at a given length in a path.
3345
+ *
3346
+ * @param {string | SVGPathCommander.pathArray} pathInput target `pathArray`
3347
+ * @param {number=} distance the given length
3348
+ * @returns {SVGPathCommander.segmentProperties=} the requested properties
3349
+ */
3350
+ function getPropertiesAtLength(pathInput, distance) {
3351
+ const pathArray = parsePathString(pathInput);
3352
+ const segments = [];
3353
+
3354
+ let pathTemp = [...pathArray];
3355
+ // @ts-ignore
3356
+ let pathLength = getTotalLength(pathTemp);
3357
+ let index = pathTemp.length - 1;
3358
+ let lengthAtSegment = 0;
3359
+ let length = 0;
3360
+ /** @type {SVGPathCommander.pathSegment} */
3361
+ let segment = pathArray[0];
3362
+ const [x, y] = segment.slice(-2);
3363
+ const point = { x, y };
3364
+
3365
+ // If the path is empty, return 0.
3366
+ if (index <= 0 || !distance || !Number.isFinite(distance)) {
3367
+ return {
3368
+ // @ts-ignore
3369
+ segment, index: 0, length, point, lengthAtSegment,
3370
+ };
3371
+ }
3372
+
3373
+ if (distance >= pathLength) {
3374
+ pathTemp = pathArray.slice(0, -1);
3375
+ // @ts-ignore
3376
+ lengthAtSegment = getTotalLength(pathTemp);
3377
+ // @ts-ignore
3378
+ length = pathLength - lengthAtSegment;
3379
+ return {
3060
3380
  // @ts-ignore
3061
- result = getPointAtSegLength(...args);
3381
+ segment: pathArray[index], index, length, lengthAtSegment,
3382
+ };
3383
+ }
3384
+
3385
+ while (index > 0) {
3386
+ segment = pathTemp[index];
3387
+ pathTemp = pathTemp.slice(0, -1);
3388
+ // @ts-ignore -- `pathTemp` === `pathSegment[]` === `pathArray`
3389
+ lengthAtSegment = getTotalLength(pathTemp);
3390
+ // @ts-ignore
3391
+ length = pathLength - lengthAtSegment;
3392
+ pathLength = lengthAtSegment;
3393
+ segments.push({
3394
+ segment, index, length, lengthAtSegment,
3395
+ });
3396
+ index -= 1;
3397
+ }
3398
+
3399
+ // @ts-ignore
3400
+ return segments.find(({ lengthAtSegment: l }) => l <= distance);
3401
+ }
3402
+
3403
+ /**
3404
+ * Returns the point and segment in path closest to a given point as well as
3405
+ * the distance to the path stroke.
3406
+ * @see https://bl.ocks.org/mbostock/8027637
3407
+ *
3408
+ * @param {string | SVGPathCommander.pathArray} pathInput target `pathArray`
3409
+ * @param {{x: number, y: number}} point the given point
3410
+ * @returns {SVGPathCommander.pointProperties} the requested properties
3411
+ */
3412
+ function getPropertiesAtPoint(pathInput, point) {
3413
+ const path = fixPath(parsePathString(pathInput));
3414
+ const normalPath = normalizePath(path);
3415
+ const pathLength = getTotalLength(path);
3416
+ /** @param {{x: number, y: number}} p */
3417
+ const distanceTo = (p) => {
3418
+ const dx = p.x - point.x;
3419
+ const dy = p.y - point.y;
3420
+ return dx * dx + dy * dy;
3421
+ };
3422
+ let precision = 8;
3423
+ let scan = { x: 0, y: 0 };
3424
+ let scanDistance = 0;
3425
+ let closest = scan;
3426
+ let bestLength = 0;
3427
+ let bestDistance = Infinity;
3428
+
3429
+ // linear scan for coarse approximation
3430
+ for (let scanLength = 0; scanLength <= pathLength; scanLength += precision) {
3431
+ scan = getPointAtLength(normalPath, scanLength);
3432
+ scanDistance = distanceTo(scan);
3433
+ if (scanDistance < bestDistance) {
3434
+ closest = scan;
3435
+ bestLength = scanLength;
3436
+ bestDistance = scanDistance;
3437
+ }
3438
+ }
3439
+
3440
+ // binary search for precise estimate
3441
+ precision /= 2;
3442
+ let before = { x: 0, y: 0 };
3443
+ let after = before;
3444
+ let beforeLength = 0;
3445
+ let afterLength = 0;
3446
+ let beforeDistance = 0;
3447
+ let afterDistance = 0;
3448
+
3449
+ while (precision > 0.5) {
3450
+ beforeLength = bestLength - precision;
3451
+ before = getPointAtLength(normalPath, beforeLength);
3452
+ beforeDistance = distanceTo(before);
3453
+ afterLength = bestLength + precision;
3454
+ after = getPointAtLength(normalPath, afterLength);
3455
+ afterDistance = distanceTo(after);
3456
+ if (beforeLength >= 0 && beforeDistance < bestDistance) {
3457
+ closest = before;
3458
+ bestLength = beforeLength;
3459
+ bestDistance = beforeDistance;
3460
+ } else if (afterLength <= pathLength && afterDistance < bestDistance) {
3461
+ closest = after;
3462
+ bestLength = afterLength;
3463
+ bestDistance = afterDistance;
3062
3464
  } else {
3063
- result = null;
3465
+ precision /= 2;
3064
3466
  }
3467
+ }
3065
3468
 
3066
- return result;
3067
- }).filter((x) => x).slice(-1)[0]; // isolate last segment
3469
+ const segment = getPropertiesAtLength(path, bestLength);
3470
+ const distance = Math.sqrt(bestDistance);
3471
+
3472
+ return { closest, distance, segment };
3473
+ }
3474
+
3475
+ /**
3476
+ * Returns the point in path closest to a given point.
3477
+ *
3478
+ * @param {string | SVGPathCommander.pathArray} pathInput target `pathArray`
3479
+ * @param {{x: number, y: number}} point the given point
3480
+ * @returns {{x: number, y: number}} the best match
3481
+ */
3482
+ function getClosestPoint(pathInput, point) {
3483
+ return getPropertiesAtPoint(pathInput, point).closest;
3484
+ }
3485
+
3486
+ /**
3487
+ * Returns the path segment which contains a given point.
3488
+ *
3489
+ * @param {string | SVGPathCommander.pathArray} path the `pathArray` to look into
3490
+ * @param {{x: number, y: number}} point the point of the shape to look for
3491
+ * @returns {SVGPathCommander.pathSegment?} the requested segment
3492
+ */
3493
+ function getSegmentOfPoint(path, point) {
3494
+ const props = getPropertiesAtPoint(path, point);
3495
+ const { segment } = props;
3496
+ return typeof segment !== 'undefined' ? segment.segment : null;
3497
+ }
3498
+
3499
+ /**
3500
+ * Returns the segment at a given length.
3501
+ * @param {string | SVGPathCommander.pathArray} pathInput the target `pathArray`
3502
+ * @param {number} distance the distance in path to look at
3503
+ * @returns {SVGPathCommander.pathSegment?} the requested segment
3504
+ */
3505
+ function getSegmentAtLength(pathInput, distance) {
3506
+ const props = getPropertiesAtLength(pathInput, distance);
3507
+ const { segment } = typeof props !== 'undefined' ? props : { segment: null };
3508
+ return segment;
3509
+ }
3510
+
3511
+ /**
3512
+ * Checks if a given point is in the stroke of a path.
3513
+ *
3514
+ * @param {string | SVGPathCommander.pathArray} pathInput target path
3515
+ * @param {{x: number, y: number}} point
3516
+ * @returns {boolean} the query result
3517
+ */
3518
+ function isPointInStroke(pathInput, point) {
3519
+ const { distance } = getPropertiesAtPoint(pathInput, point);
3520
+ return Math.abs(distance) < 0.01;
3068
3521
  }
3069
3522
 
3070
3523
  /**
@@ -3126,7 +3579,7 @@ function getPolyPath(attr) {
3126
3579
  /** @type {SVGPathCommander.pathArray} */
3127
3580
  // @ts-ignore -- it's an empty `pathArray`
3128
3581
  const pathArray = [];
3129
- const points = attr.points.split(/[\s|,]/).map(Number);
3582
+ const points = attr.points.trim().split(/[\s|,]/).map(Number);
3130
3583
 
3131
3584
  let index = 0;
3132
3585
  while (index < points.length) {
@@ -3320,8 +3773,16 @@ const Util = {
3320
3773
  getDrawDirection,
3321
3774
  getPathArea,
3322
3775
  getPathBBox,
3776
+ getTotalLength,
3323
3777
  getPathLength,
3324
3778
  getPointAtLength,
3779
+ getPointAtPathLength,
3780
+ getClosestPoint,
3781
+ getSegmentOfPoint,
3782
+ getPropertiesAtPoint,
3783
+ getPropertiesAtLength,
3784
+ getSegmentAtLength,
3785
+ isPointInStroke,
3325
3786
  clonePath,
3326
3787
  splitPath,
3327
3788
  fixPath,
@@ -3335,7 +3796,7 @@ const Util = {
3335
3796
  options: defaultOptions,
3336
3797
  };
3337
3798
 
3338
- var version = "0.1.21";
3799
+ var version = "0.1.25";
3339
3800
 
3340
3801
  // @ts-ignore
3341
3802