svg-path-commander 2.1.2 → 2.1.3

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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svg-path-commander",
3
3
  "author": "thednp",
4
4
  "license": "MIT",
5
- "version": "2.1.2",
5
+ "version": "2.1.3",
6
6
  "description": "Modern TypeScript tools for SVG",
7
7
  "source": "./src/index.ts",
8
8
  "main": "./dist/svg-path-commander.js",
@@ -1,5 +1,5 @@
1
1
  import { getPointAtLineLength } from './lineTools';
2
- import type { Point } from '../types';
2
+ import type { Point, PointTuple } from '../types';
3
3
 
4
4
  /**
5
5
  * Returns the Arc segment length.
@@ -37,10 +37,7 @@ const arcPoint = (cx: number, cy: number, rx: number, ry: number, alpha: number,
37
37
  const x = rx * cos(theta);
38
38
  const y = ry * sin(theta);
39
39
 
40
- return {
41
- x: cx + cosA * x - sinA * y,
42
- y: cy + sinA * x + cosA * y,
43
- };
40
+ return [cx + cosA * x - sinA * y, cy + sinA * x + cosA * y] as PointTuple;
44
41
  };
45
42
 
46
43
  /**
@@ -275,7 +272,9 @@ const getPointAtArcLength = (
275
272
  };
276
273
 
277
274
  /**
278
- * Returns the extrema for an Arc segment.
275
+ * Returns the extrema for an Arc segment in the following format:
276
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
277
+ *
279
278
  * @see https://github.com/herrstrietzel/svg-pathdata-getbbox
280
279
  *
281
280
  * @param x1 the starting point X
@@ -305,15 +304,9 @@ const getArcBBox = (
305
304
  const deltaAngle = endAngle - startAngle;
306
305
  const { min, max, tan, atan2, PI } = Math;
307
306
 
308
- // final on path point
309
- const p = { x, y };
310
-
311
307
  // circle/elipse center coordinates
312
308
  const { x: cx, y: cy } = center;
313
309
 
314
- // collect extreme points – add end point
315
- const extremes = [p];
316
-
317
310
  // rotation to radians
318
311
  const alpha = (angle * PI) / 180;
319
312
  const tangent = tan(alpha);
@@ -329,12 +322,10 @@ const getArcBBox = (
329
322
  const angle4 = angle3 + PI;
330
323
 
331
324
  // inner bounding box
332
- const xArr = [x1, x];
333
- const yArr = [y1, y];
334
- const xMin = min(...xArr);
335
- const xMax = max(...xArr);
336
- const yMin = min(...yArr);
337
- const yMax = max(...yArr);
325
+ let xMin = min(x1, x);
326
+ let xMax = max(x1, x);
327
+ let yMin = min(y1, y);
328
+ let yMax = max(y1, y);
338
329
 
339
330
  // on path point close after start
340
331
  const angleAfterStart = endAngle - deltaAngle * 0.001;
@@ -352,45 +343,46 @@ const getArcBBox = (
352
343
  */
353
344
 
354
345
  // right
355
- // istanbul ignore if @preserve
356
- if (pP2.x > xMax || pP3.x > xMax) {
346
+ if (pP2[0] > xMax || pP3[0] > xMax) {
357
347
  // get point for this theta
358
- extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle1));
348
+ const pxy = arcPoint(cx, cy, rx, ry, alpha, angle1);
349
+ xMin = min(xMin, pxy[0]);
350
+ yMin = min(yMin, pxy[1]);
351
+ xMax = max(xMax, pxy[0]);
352
+ yMax = max(yMax, pxy[1]);
359
353
  }
360
354
 
361
355
  // left
362
- // istanbul ignore if @preserve
363
- if (pP2.x < xMin || pP3.x < xMin) {
356
+ if (pP2[0] < xMin || pP3[0] < xMin) {
364
357
  // get anti-symmetric point
365
- extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle2));
358
+ const pxy = arcPoint(cx, cy, rx, ry, alpha, angle2);
359
+ xMin = min(xMin, pxy[0]);
360
+ yMin = min(yMin, pxy[1]);
361
+ xMax = max(xMax, pxy[0]);
362
+ yMax = max(yMax, pxy[1]);
366
363
  }
367
364
 
368
365
  // top
369
- // istanbul ignore if @preserve
370
- if (pP2.y < yMin || pP3.y < yMin) {
366
+ if (pP2[1] < yMin || pP3[1] < yMin) {
371
367
  // get anti-symmetric point
372
- extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle4));
368
+ const pxy = arcPoint(cx, cy, rx, ry, alpha, angle4);
369
+ xMin = min(xMin, pxy[0]);
370
+ yMin = min(yMin, pxy[1]);
371
+ xMax = max(xMax, pxy[0]);
372
+ yMax = max(yMax, pxy[1]);
373
373
  }
374
374
 
375
375
  // bottom
376
- // istanbul ignore if @preserve
377
- if (pP2.y > yMax || pP3.y > yMax) {
376
+ if (pP2[1] > yMax || pP3[1] > yMax) {
378
377
  // get point for this theta
379
- extremes.push(arcPoint(cx, cy, rx, ry, alpha, angle3));
378
+ const pxy = arcPoint(cx, cy, rx, ry, alpha, angle3);
379
+ xMin = min(xMin, pxy[0]);
380
+ yMin = min(yMin, pxy[1]);
381
+ xMax = max(xMax, pxy[0]);
382
+ yMax = max(yMax, pxy[1]);
380
383
  }
381
384
 
382
- return {
383
- min: {
384
- x: min(...extremes.map(n => n.x)),
385
- y: min(...extremes.map(n => n.y)),
386
- },
387
- max: {
388
- x: max(...extremes.map(n => n.x)),
389
- y: max(...extremes.map(n => n.y)),
390
- },
391
- };
385
+ return [xMin, yMin, xMax, yMax] as [number, number, number, number];
392
386
  };
393
387
 
394
388
  export { arcPoint, angleBetween, getArcLength, arcLength, getArcBBox, getArcProps, getPointAtArcLength };
395
-
396
- export {};
@@ -91,7 +91,8 @@ const getPointAtCubicLength = (
91
91
  };
92
92
 
93
93
  /**
94
- * Returns the boundig box of a CubicBezier segment.
94
+ * Returns the boundig box of a CubicBezier segment in the following format:
95
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
95
96
  *
96
97
  * @param x1 the starting point X
97
98
  * @param y1 the starting point Y
@@ -101,7 +102,7 @@ const getPointAtCubicLength = (
101
102
  * @param c2y the second control point Y
102
103
  * @param x2 the ending point X
103
104
  * @param y2 the ending point Y
104
- * @returns the point at CubicBezier length
105
+ * @returns the extrema of the CubicBezier segment
105
106
  */
106
107
  const getCubicBBox = (
107
108
  x1: number,
@@ -115,10 +116,8 @@ const getCubicBBox = (
115
116
  ) => {
116
117
  const cxMinMax = minmaxC([x1, c1x, c2x, x2]);
117
118
  const cyMinMax = minmaxC([y1, c1y, c2y, y2]);
118
- return {
119
- min: { x: cxMinMax[0], y: cyMinMax[0] },
120
- max: { x: cxMinMax[1], y: cyMinMax[1] },
121
- };
119
+
120
+ return [cxMinMax[0], cyMinMax[0], cxMinMax[1], cyMinMax[1]] as [number, number, number, number];
122
121
  };
123
122
 
124
123
  export { getCubicLength, getCubicBBox, getPointAtCubicLength, getPointAtCubicSegmentLength };
@@ -54,16 +54,8 @@ const getPointAtLineLength = (x1: number, y1: number, x2: number, y2: number, di
54
54
  */
55
55
  const getLineBBox = (x1: number, y1: number, x2: number, y2: number) => {
56
56
  const { min, max } = Math;
57
- return {
58
- min: {
59
- x: min(x1, x2),
60
- y: min(y1, y2),
61
- },
62
- max: {
63
- x: max(x1, x2),
64
- y: max(y1, y2),
65
- },
66
- };
57
+
58
+ return [min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)] as [number, number, number, number];
67
59
  };
68
60
 
69
61
  export { getPointAtLineLength, getLineBBox, getLineLength };
@@ -78,7 +78,8 @@ const getPointAtQuadLength = (
78
78
  };
79
79
 
80
80
  /**
81
- * Returns the boundig box of a QuadraticBezier segment.
81
+ * Returns the boundig box of a QuadraticBezier segment in the following format:
82
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
82
83
  *
83
84
  * @param x1 the starting point X
84
85
  * @param y1 the starting point Y
@@ -86,15 +87,12 @@ const getPointAtQuadLength = (
86
87
  * @param cy the control point Y
87
88
  * @param x2 the ending point X
88
89
  * @param y2 the ending point Y
89
- * @returns the point at CubicBezier length
90
+ * @returns the extrema of the QuadraticBezier segment
90
91
  */
91
92
  const getQuadBBox = (x1: number, y1: number, cx: number, cy: number, x2: number, y2: number) => {
92
93
  const cxMinMax = minmaxQ([x1, cx, x2]);
93
94
  const cyMinMax = minmaxQ([y1, cy, y2]);
94
- return {
95
- min: { x: cxMinMax[0], y: cyMinMax[0] },
96
- max: { x: cxMinMax[1], y: cyMinMax[1] },
97
- };
95
+ return [cxMinMax[0], cyMinMax[0], cxMinMax[1], cyMinMax[1]] as [number, number, number, number];
98
96
  };
99
97
 
100
98
  export { getPointAtQuadSegmentLength, getQuadLength, getQuadBBox, getPointAtQuadLength };
@@ -50,7 +50,11 @@ const absolutizeSegment = (segment: PathSegment, index: number, lastX: number, l
50
50
  } else {
51
51
  // use brakets for `eslint: no-case-declaration`
52
52
  // https://stackoverflow.com/a/50753272/803358
53
- const absValues = (segment.slice(1) as number[]).map((n, j) => n + (j % 2 ? lastY : lastX));
53
+ const absValues = [] as number[];
54
+ const seglen = segment.length;
55
+ for (let j = 1; j < seglen; j += 1) {
56
+ absValues.push((segment[j] as number) + (j % 2 ? lastX : lastY));
57
+ }
54
58
  // for c, s, q, t
55
59
  return [absCommand as typeof absCommand | number].concat(absValues) as
56
60
  | MSegment
@@ -1,7 +1,7 @@
1
+ import type { AbsoluteSegment, PathArray, PathCommand } from '../types';
1
2
  import pathToAbsolute from '../convert/pathToAbsolute';
2
3
  import shortenSegment from './shortenSegment';
3
4
  import paramsParser from '../parser/paramsParser';
4
- import type { AbsoluteSegment, PathArray, PathCommand } from '../types';
5
5
  import iterate from './iterate';
6
6
  import normalizeSegment from './normalizeSegment';
7
7
  import relativizeSegment from './relativizeSegment';
@@ -16,7 +16,7 @@ import roundSegment from './roundSegment';
16
16
  * @param roundOption the amount of decimals to round values to
17
17
  * @returns the optimized `pathArray`
18
18
  */
19
- const optimizePath = (pathInput: PathArray, roundOption: number) => {
19
+ const optimizePath = (pathInput: PathArray, roundOption?: number) => {
20
20
  const path = pathToAbsolute(pathInput);
21
21
  // allow for ZERO decimals or use an aggressive value of 2
22
22
  const round =
@@ -31,7 +31,6 @@ const optimizePath = (pathInput: PathArray, roundOption: number) => {
31
31
  return iterate(path, (seg, i, lastX, lastY) => {
32
32
  optimParams.x = lastX;
33
33
  optimParams.y = lastY;
34
- // const absoluteSegment = absolutizeSegment(seg, optimParams);
35
34
  const normalizedSegment = normalizeSegment(seg, optimParams);
36
35
  let result = seg;
37
36
  [pathCommand] = seg;
@@ -50,7 +50,11 @@ const relativizeSegment = (segment: PathSegment, index: number, lastX: number, l
50
50
  } else {
51
51
  // use brakets for `eslint: no-case-declaration`
52
52
  // https://stackoverflow.com/a/50753272/803358
53
- const relValues = (segment.slice(1) as number[]).map((n, j) => n - (j % 2 ? lastY : lastX));
53
+ const relValues = [] as number[];
54
+ const seglen = segment.length;
55
+ for (let j = 1; j < seglen; j += 1) {
56
+ relValues.push((segment[j] as number) - (j % 2 ? lastX : lastY));
57
+ }
54
58
  // for c, s, q, t
55
59
  return [relCommand as RelativeCommand | number].concat(relValues) as qSegment | tSegment | sSegment | cSegment;
56
60
  }
@@ -5,10 +5,8 @@ import type { AbsoluteArray, AbsoluteSegment, CSegment, LSegment, PathArray, Tra
5
5
  import type { TransformObject } from '../interface';
6
6
  import iterate from './iterate';
7
7
  import parsePathString from '../parser/parsePathString';
8
- import segmentToCubic from './segmentToCubic';
9
- import normalizeSegment from './normalizeSegment';
10
- import paramsParser from '../parser/paramsParser';
11
8
  import absolutizeSegment from './absolutizeSegment';
9
+ import arcToCubic from './arcToCubic';
12
10
 
13
11
  /**
14
12
  * Apply a 2D / 3D transformation to a `pathArray` instance.
@@ -32,7 +30,6 @@ const transformPath = (pathInput: PathArray | string, transform?: Partial<Transf
32
30
  let jj = 0;
33
31
  let pathCommand = 'M';
34
32
  // transform uses it's own set of params
35
- const transformParams = { ...paramsParser };
36
33
  const path = parsePathString(pathInput);
37
34
  const transformProps = transform && Object.keys(transform);
38
35
 
@@ -49,8 +46,6 @@ const transformPath = (pathInput: PathArray | string, transform?: Partial<Transf
49
46
  if (matrixInstance.isIdentity) return path.slice(0) as typeof path;
50
47
 
51
48
  return iterate<AbsoluteArray>(path, (seg, index, lastX, lastY) => {
52
- transformParams.x = lastX;
53
- transformParams.y = lastY;
54
49
  [pathCommand] = seg;
55
50
  const absCommand = pathCommand.toUpperCase();
56
51
  const isRelative = absCommand !== pathCommand;
@@ -60,9 +55,24 @@ const transformPath = (pathInput: PathArray | string, transform?: Partial<Transf
60
55
 
61
56
  let result =
62
57
  absCommand === 'A'
63
- ? segmentToCubic(absoluteSegment, transformParams)
64
- : ['V', 'H'].includes(absCommand)
65
- ? normalizeSegment(absoluteSegment, transformParams)
58
+ ? // ? segmentToCubic(absoluteSegment, transformParams)
59
+ (['C' as string | number].concat(
60
+ arcToCubic(
61
+ lastX,
62
+ lastY,
63
+ absoluteSegment[1] as number,
64
+ absoluteSegment[2] as number,
65
+ absoluteSegment[3] as number,
66
+ absoluteSegment[4] as number,
67
+ absoluteSegment[5] as number,
68
+ absoluteSegment[6] as number,
69
+ absoluteSegment[7] as number,
70
+ ),
71
+ ) as CSegment)
72
+ : absCommand === 'V'
73
+ ? (['L', lastX, absoluteSegment[1]] as LSegment)
74
+ : absCommand === 'H'
75
+ ? (['L', absoluteSegment[1], lastY] as LSegment)
66
76
  : absoluteSegment;
67
77
 
68
78
  // update pathCommand
@@ -97,12 +107,6 @@ const transformPath = (pathInput: PathArray | string, transform?: Partial<Transf
97
107
  x = lx;
98
108
  y = ly;
99
109
 
100
- const seglen = tempSegment.length;
101
- transformParams.x1 = +tempSegment[seglen - 2];
102
- transformParams.y1 = +tempSegment[seglen - 1];
103
- transformParams.x2 = +tempSegment[seglen - 4] || transformParams.x1;
104
- transformParams.y2 = +tempSegment[seglen - 3] || transformParams.y1;
105
-
106
110
  return result;
107
111
  });
108
112
  };
@@ -1,13 +1,12 @@
1
1
  import iterate from '../process/iterate';
2
2
  import { PathBBox } from '../interface';
3
- import { MSegment, PathArray, Point } from '../types';
3
+ import { LSegment, MSegment, PathArray, PointTuple } from '../types';
4
4
  import { getLineBBox } from '../math/lineTools';
5
5
  import { getArcBBox } from '../math/arcTools';
6
6
  import { getCubicBBox } from '../math/cubicTools';
7
7
  import { getQuadBBox } from '../math/quadTools';
8
8
  import parsePathString from '../parser/parsePathString';
9
- import paramsParser from '../parser/paramsParser';
10
- import normalizeSegment from '../process/normalizeSegment';
9
+ import absolutizeSegment from '../process/absolutizeSegment';
11
10
 
12
11
  const getPathBBox = (pathInput: PathArray | string) => {
13
12
  if (!pathInput) {
@@ -25,59 +24,133 @@ const getPathBBox = (pathInput: PathArray | string) => {
25
24
  }
26
25
 
27
26
  const path = parsePathString(pathInput);
28
- let data = [] as number[];
29
27
  let pathCommand = 'M';
30
- const x = 0;
31
- const y = 0;
32
28
  let mx = 0;
33
29
  let my = 0;
34
- const MIN = [] as Point[];
35
- const MAX = [] as Point[];
36
- let min = { x, y };
37
- let max = { x, y };
38
- const params = { ...paramsParser };
30
+ const { max, min } = Math;
31
+ let xMin = Infinity;
32
+ let yMin = Infinity;
33
+ let xMax = -Infinity;
34
+ let yMax = -Infinity;
35
+ let minX = 0;
36
+ let minY = 0;
37
+ let maxX = 0;
38
+ let maxY = 0;
39
+ let paramX1 = 0;
40
+ let paramY1 = 0;
41
+ let paramX2 = 0;
42
+ let paramY2 = 0;
43
+ let paramQX = 0;
44
+ let paramQY = 0;
39
45
 
40
- iterate(path, (seg, _, lastX, lastY) => {
41
- params.x = lastX;
42
- params.y = lastY;
43
- const result = normalizeSegment(seg, params);
44
- [pathCommand] = result;
45
- data = [lastX, lastY].concat(result.slice(1) as number[]);
46
+ iterate(path, (seg, index, lastX, lastY) => {
47
+ [pathCommand] = seg;
48
+ const absCommand = pathCommand.toUpperCase();
49
+ const isRelative = absCommand !== pathCommand;
50
+ const absoluteSegment = isRelative ? absolutizeSegment(seg, index, lastX, lastY) : (seg.slice(0) as typeof seg);
51
+
52
+ const normalSegment =
53
+ absCommand === 'V'
54
+ ? (['L', lastX, absoluteSegment[1]] as LSegment)
55
+ : absCommand === 'H'
56
+ ? (['L', absoluteSegment[1], lastY] as LSegment)
57
+ : absoluteSegment;
58
+
59
+ [pathCommand] = normalSegment;
60
+
61
+ if (!'TQ'.includes(absCommand)) {
62
+ // optional but good to be cautious
63
+ paramQX = 0;
64
+ paramQY = 0;
65
+ }
46
66
 
47
67
  // this segment is always ZERO
48
68
  /* istanbul ignore else @preserve */
49
69
  if (pathCommand === 'M') {
50
- // remember mx, my for Z
51
- [, mx, my] = result as MSegment;
52
- min = { x: mx, y: my };
53
- max = { x: mx, y: my };
70
+ [, mx, my] = normalSegment as MSegment;
71
+ minX = mx;
72
+ minY = my;
73
+ maxX = mx;
74
+ maxY = my;
54
75
  } else if (pathCommand === 'L') {
55
- ({ min, max } = getLineBBox(data[0], data[1], data[2], data[3]));
76
+ [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, normalSegment[1] as number, normalSegment[2] as number);
56
77
  } else if (pathCommand === 'A') {
57
- ({ min, max } = getArcBBox(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8]));
78
+ [minX, minY, maxX, maxY] = getArcBBox(
79
+ lastX,
80
+ lastY,
81
+ normalSegment[1] as number,
82
+ normalSegment[2] as number,
83
+ normalSegment[3] as number,
84
+ normalSegment[4] as number,
85
+ normalSegment[5] as number,
86
+ normalSegment[6] as number,
87
+ normalSegment[7] as number,
88
+ );
89
+ } else if (pathCommand === 'S') {
90
+ const cp1x = paramX1 * 2 - paramX2;
91
+ const cp1y = paramY1 * 2 - paramY2;
92
+
93
+ [minX, minY, maxX, maxY] = getCubicBBox(
94
+ lastX,
95
+ lastY,
96
+ cp1x,
97
+ cp1y,
98
+ normalSegment[1] as number,
99
+ normalSegment[2] as number,
100
+ normalSegment[3] as number,
101
+ normalSegment[4] as number,
102
+ );
58
103
  } else if (pathCommand === 'C') {
59
- ({ min, max } = getCubicBBox(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]));
104
+ [minX, minY, maxX, maxY] = getCubicBBox(
105
+ lastX,
106
+ lastY,
107
+ normalSegment[1] as number,
108
+ normalSegment[2] as number,
109
+ normalSegment[3] as number,
110
+ normalSegment[4] as number,
111
+ normalSegment[5] as number,
112
+ normalSegment[6] as number,
113
+ );
114
+ } else if (pathCommand === 'T') {
115
+ paramQX = paramX1 * 2 - paramQX;
116
+ paramQY = paramY1 * 2 - paramQY;
117
+ [minX, minY, maxX, maxY] = getQuadBBox(
118
+ lastX,
119
+ lastY,
120
+ paramQX,
121
+ paramQY,
122
+ normalSegment[1] as number,
123
+ normalSegment[2] as number,
124
+ );
60
125
  } else if (pathCommand === 'Q') {
61
- ({ min, max } = getQuadBBox(data[0], data[1], data[2], data[3], data[4], data[5]));
126
+ paramQX = normalSegment[1] as number;
127
+ paramQY = normalSegment[2] as number;
128
+ [minX, minY, maxX, maxY] = getQuadBBox(
129
+ lastX,
130
+ lastY,
131
+ normalSegment[1] as number,
132
+ normalSegment[2] as number,
133
+ normalSegment[3] as number,
134
+ normalSegment[4] as number,
135
+ );
62
136
  } else if (pathCommand === 'Z') {
63
- data = [lastX, lastY, mx, my];
64
- ({ min, max } = getLineBBox(data[0], data[1], data[2], data[3]));
137
+ [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, mx, my);
65
138
  }
139
+ xMin = min(minX, xMin);
140
+ yMin = min(minY, yMin);
141
+ xMax = max(maxX, xMax);
142
+ yMax = max(maxY, yMax);
66
143
 
67
- MIN.push(min);
68
- MAX.push(max);
69
-
70
- const seglen = result.length;
71
- params.x1 = +result[seglen - 2];
72
- params.y1 = +result[seglen - 1];
73
- params.x2 = +result[seglen - 4] || params.x1;
74
- params.y2 = +result[seglen - 3] || params.y1;
144
+ // update params
145
+ [paramX1, paramY1] = pathCommand === 'Z' ? [mx, my] : (normalSegment.slice(-2) as PointTuple);
146
+ [paramX2, paramY2] =
147
+ pathCommand === 'C'
148
+ ? ([normalSegment[3], normalSegment[4]] as PointTuple)
149
+ : pathCommand === 'S'
150
+ ? ([normalSegment[1], normalSegment[2]] as PointTuple)
151
+ : [paramX1, paramY1];
75
152
  });
76
153
 
77
- const xMin = Math.min(...MIN.map(n => n.x));
78
- const xMax = Math.max(...MAX.map(n => n.x));
79
- const yMin = Math.min(...MIN.map(n => n.y));
80
- const yMax = Math.max(...MAX.map(n => n.y));
81
154
  const width = xMax - xMin;
82
155
  const height = yMax - yMin;
83
156