svg-path-simplify 0.0.1 → 0.0.2

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 (42) hide show
  1. package/README.md +28 -1
  2. package/dist/svg-path-simplify.esm.js +4040 -0
  3. package/dist/svg-path-simplify.esm.min.js +1 -0
  4. package/dist/svg-path-simplify.js +4065 -0
  5. package/dist/svg-path-simplify.min.js +1 -0
  6. package/dist/svg-path-simplify.node.js +4062 -0
  7. package/dist/svg-path-simplify.node.min.js +1 -0
  8. package/index.html +222 -0
  9. package/package.json +2 -2
  10. package/src/constants.js +4 -0
  11. package/src/index.js +18 -3
  12. package/src/pathData_simplify_cubic.js +324 -0
  13. package/src/pathData_simplify_cubic_arr.js +50 -0
  14. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  15. package/src/pathSimplify-main.js +294 -0
  16. package/src/svgii/...parse.js +402 -0
  17. package/src/svgii/geometry.js +1096 -0
  18. package/src/svgii/geometry_area.js +265 -0
  19. package/src/svgii/geometry_bbox.js +223 -0
  20. package/src/svgii/pathData_analyze.js +896 -0
  21. package/src/svgii/pathData_convert.js +1180 -0
  22. package/src/svgii/pathData_parse.js +487 -0
  23. package/src/svgii/pathData_remove_collinear.js +85 -0
  24. package/src/svgii/pathData_remove_zerolength.js +28 -0
  25. package/src/svgii/pathData_reorder.js +204 -0
  26. package/src/svgii/pathData_reverse.js +124 -0
  27. package/src/svgii/pathData_scale.js +42 -0
  28. package/src/svgii/pathData_split.js +449 -0
  29. package/src/svgii/pathData_stringify.js +146 -0
  30. package/src/svgii/pathData_toPolygon.js +92 -0
  31. package/src/svgii/pathdata_cleanup.js +363 -0
  32. package/src/svgii/poly_analyze.js +172 -0
  33. package/src/svgii/poly_to_pathdata.js +185 -0
  34. package/src/svgii/rounding.js +154 -0
  35. package/src/svgii/simplify.js +248 -0
  36. package/src/svgii/simplify_bezier.js +470 -0
  37. package/src/svgii/simplify_linetos.js +93 -0
  38. package/src/svgii/simplify_polygon.js +135 -0
  39. package/src/svgii/stringify.js +103 -0
  40. package/src/svgii/svg_cleanup.js +80 -0
  41. package/src/svgii/visualize.js +317 -0
  42. package/LICENSE +0 -21
@@ -0,0 +1,50 @@
1
+ import { getDistance, getSquareDistance, checkLineIntersection, pointAtT, getDistAv } from "./svgii/geometry";
2
+ import { getBezierArea, getPolygonArea } from "./svgii/geometry_area";
3
+ import { renderPoint } from "./svgii/visualize";
4
+
5
+ export function combineCubicArray(commands = [], threshold = 0.05) {
6
+
7
+
8
+ let [comF, comM, comE] = commands;
9
+
10
+ commands.forEach(com => {
11
+ let area = getBezierArea([com.p0, com.cp1, com.cp2, com.p], true)
12
+ com.area = area
13
+ })
14
+
15
+ // sort by area
16
+ let commandsFiltered = JSON.parse(JSON.stringify(commands)).sort((a,b)=>b.area-a.area )
17
+
18
+ let comL = commandsFiltered[0]
19
+
20
+ // find largest segment
21
+ //let area2 = getBezierArea(comF)
22
+
23
+ console.log('area1', commands, commandsFiltered);
24
+
25
+ // largest segment
26
+ comM = commandsFiltered[0]
27
+
28
+
29
+ //let segLargest =
30
+ let pt_mid = pointAtT([comM.p0, comM.cp1, comM.cp2, comM.p], 0.5, false, true)
31
+
32
+ let seg1_cp2 = pt_mid.cpts[2]
33
+ let seg2_cp1 = pt_mid.cpts[3]
34
+ renderPoint(markers, seg1_cp2, 'orange')
35
+ renderPoint(markers, seg2_cp1, 'cyan')
36
+
37
+
38
+ console.log('comM', pt_mid);
39
+
40
+ renderPoint(markers, comF.p0, 'green')
41
+ renderPoint(markers, comL.p, 'purple')
42
+ //renderPoint(markers, comE.cp2, 'orange')
43
+
44
+ renderPoint(markers, pt_mid, 'magenta')
45
+ //let ptI_1 = checkLineIntersection(comF)
46
+
47
+
48
+ return commands;
49
+ }
50
+
@@ -0,0 +1,220 @@
1
+ import { checkLineIntersection, getAngle, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint } from "./svgii/geometry";
2
+ import { getPathArea } from "./svgii/geometry_area";
3
+ import { pathDataToD } from "./svgii/pathData_stringify";
4
+ import { renderPath, renderPoint } from "./svgii/visualize";
5
+
6
+ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
7
+
8
+ // cubic Bézier derivative
9
+ const cubicDerivative = (p0, p1, p2, p3, t) => {
10
+ let mt = 1 - t;
11
+
12
+ return {
13
+ x:
14
+ 3 * mt * mt * (p1.x - p0.x) +
15
+ 6 * mt * t * (p2.x - p1.x) +
16
+ 3 * t * t * (p3.x - p2.x),
17
+ y:
18
+ 3 * mt * mt * (p1.y - p0.y) +
19
+ 6 * mt * t * (p2.y - p1.y) +
20
+ 3 * t * t * (p3.y - p2.y)
21
+ };
22
+ }
23
+
24
+
25
+
26
+ // if combining fails return original commands
27
+ let commands = [com1, com2]
28
+
29
+ // detect dominant
30
+ let dist1 = getSquareDistance(com1.p0, com1.p)
31
+ let dist2 = getSquareDistance(com2.p0, com2.p)
32
+ let reverse = dist1 > dist2;
33
+
34
+ //let ang1 = getAngle(com1.p0, com1.cp1)
35
+ //let ang2 = getAngle(com2.p, com1.cp2)
36
+
37
+ // backup original commands
38
+ let com1_o = JSON.parse(JSON.stringify(com1))
39
+ let com2_o = JSON.parse(JSON.stringify(com2))
40
+
41
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false)
42
+
43
+ if (!ptI) {
44
+ //renderPoint(markers, com1.p, 'purple')
45
+ return commands
46
+ }
47
+
48
+
49
+ if (reverse) {
50
+ let com2_R = {
51
+ p0: { x: com1.p.x, y: com1.p.y },
52
+ cp1: { x: com1.cp2.x, y: com1.cp2.y },
53
+ cp2: { x: com1.cp1.x, y: com1.cp1.y },
54
+ p: { x: com1.p0.x, y: com1.p0.y },
55
+ }
56
+
57
+ let com1_R = {
58
+ p0: { x: com2.p.x, y: com2.p.y },
59
+ cp1: { x: com2.cp2.x, y: com2.cp2.y },
60
+ cp2: { x: com2.cp1.x, y: com2.cp1.y },
61
+ p: { x: com2.p0.x, y: com2.p0.y },
62
+ }
63
+
64
+ com1 = com1_R;
65
+ com2 = com2_R;
66
+ }
67
+
68
+
69
+ let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
70
+ let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
71
+ let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
72
+ let dot = (a, b) => a.x * b.x + a.y * b.y;
73
+
74
+
75
+ // estimate extrapolation parameter t0
76
+
77
+ let B0 = com2.p0;
78
+ let D0 = cubicDerivative(
79
+ com2.p0,
80
+ com2.cp1,
81
+ com2.cp2,
82
+ com2.p,
83
+ 0
84
+ );
85
+
86
+ let v = sub(com1.p0, B0);
87
+
88
+ // first-order projection onto tangent
89
+ let t0 = dot(v, D0) / dot(D0, D0);
90
+
91
+
92
+ // refine with one Newton iteration (optional but cheap)
93
+ let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
94
+ let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
95
+ let r = sub(P, com1.p0);
96
+
97
+ //let t0_2 = t0 - dot(r, dP) / dot(dP, dP);
98
+ //console.log(t0, t0_2);
99
+
100
+ t0 -= dot(r, dP) / dot(dP, dP);
101
+
102
+
103
+ // construct merged cubic over [t0, 1]
104
+
105
+ let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
106
+ let Q3 = com2.p;
107
+
108
+ let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
109
+ let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
110
+
111
+ let scale = 1 - t0;
112
+
113
+ let Q1 = add(Q0, mul(d0, scale / 3));
114
+ let Q2 = sub(Q3, mul(d1, scale / 3));
115
+
116
+ let result = {
117
+ p0: Q0,
118
+ cp1: Q1,
119
+ cp2: Q2,
120
+ p: Q3,
121
+ };
122
+
123
+
124
+ if (reverse) {
125
+ result = {
126
+ p0: Q3,
127
+ cp1: Q2,
128
+ cp2: Q1,
129
+ p: Q0,
130
+ }
131
+ }
132
+
133
+
134
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true)
135
+ let seg1_cp2 = ptM.cpts[2]
136
+ //let seg2_cp1 = ptM.cpts[3]
137
+
138
+
139
+ let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false)
140
+ let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false)
141
+
142
+
143
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333)
144
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333)
145
+
146
+ // test self intersections and exit
147
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true )
148
+ if(cp_intersection){
149
+ //renderPoint(markers, cp_intersection )
150
+ return commands;
151
+ }
152
+
153
+
154
+ result.cp1 = cp1_2
155
+ result.cp2 = cp2_2
156
+
157
+ // check distances
158
+
159
+ let dist3 = getDistAv(com1_o.p0, result.p0)
160
+ let dist4 = getDistAv(com2_o.p, result.p)
161
+ let dist5 = (dist3 + dist4)
162
+
163
+ // use original points
164
+ result.p0 = com1_o.p0
165
+ result.p = com2_o.p
166
+ result.extreme = com2_o.extreme
167
+ result.corner = com2_o.corner
168
+ result.dimA = com2_o.dimA
169
+ result.directionChange = com2_o.directionChange
170
+ result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y]
171
+
172
+
173
+
174
+ // check if completely off
175
+ if (dist5 < maxDist) {
176
+
177
+ // compare combined with original area
178
+ let pathData0 = [
179
+ { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
180
+ { type: 'C', values: [com1_o.cp1.x, com1_o.cp1.y, com1_o.cp2.x, com1_o.cp2.y, com1_o.p.x, com1_o.p.y] },
181
+ { type: 'C', values: [com2_o.cp1.x, com2_o.cp1.y, com2_o.cp2.x, com2_o.cp2.y, com2_o.p.x, com2_o.p.y] },
182
+ ];
183
+
184
+ let area0 = getPathArea(pathData0)
185
+ let pathDataN = [
186
+ { type: 'M', values: [result.p0.x, result.p0.y] },
187
+ { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
188
+ ]
189
+
190
+ let areaN = getPathArea(pathDataN)
191
+ let areaDiff = Math.abs(areaN / area0 - 1)
192
+
193
+ result.error = areaDiff * 10 * tolerance;
194
+ //result.error = areaDiff + dist5;
195
+
196
+ let d = pathDataToD(pathDataN)
197
+
198
+ // success
199
+ if (areaDiff < 0.01) {
200
+ commands = [result];
201
+ //renderPath(markers, d, 'orange')
202
+ //console.log('areaDiff', areaDiff);
203
+
204
+ } else {
205
+ // renderPath(markers, d, 'red')
206
+ // console.log('areaDiff', areaDiff);
207
+ }
208
+
209
+
210
+ //renderPath(markers, d, 'orange')
211
+ }
212
+
213
+
214
+
215
+ //console.log(commands);
216
+
217
+ return commands
218
+
219
+ }
220
+
@@ -0,0 +1,294 @@
1
+ import { combineCubicPairs } from './pathData_simplify_cubic';
2
+ import { getPathDataVertices, pointAtT } from './svgii/geometry';
3
+ import { getPolyBBox } from './svgii/geometry_bbox';
4
+ import { analyzePathData, analyzePathData2 } from './svgii/pathData_analyze';
5
+ import { combineArcs, convertPathData, cubicCommandToArc, revertCubicQuadratic } from './svgii/pathData_convert';
6
+ import { parsePathDataNormalized } from './svgii/pathData_parse';
7
+ import { pathDataRemoveColinear } from './svgii/pathData_remove_collinear';
8
+ import { removeZeroLengthLinetos } from './svgii/pathData_remove_zerolength';
9
+ import { pathDataToTopLeft } from './svgii/pathData_reorder';
10
+ import { reversePathData } from './svgii/pathData_reverse';
11
+ import { addExtremePoints, splitSubpaths } from './svgii/pathData_split';
12
+ import { pathDataToD } from './svgii/pathData_stringify';
13
+ import { pathDataToPolyPlus } from './svgii/pathData_toPolygon';
14
+ import { analyzePoly } from './svgii/poly_analyze';
15
+ import { getCurvePathData } from './svgii/poly_to_pathdata';
16
+ import { detectAccuracy } from './svgii/rounding';
17
+ import { renderPoint } from './svgii/visualize';
18
+
19
+ export function svgPathSimplify(d = '', {
20
+ toAbsolute = true,
21
+ toRelative = true,
22
+ toShorthands = true,
23
+ decimals = 3,
24
+ //optimize = 0,
25
+
26
+ // not necessary unless you need cubics only
27
+ quadraticToCubic = true,
28
+
29
+ // mostly a fallback if arc calculations fail
30
+ arcToCubic = false,
31
+ cubicToArc = false,
32
+
33
+ // arc to cubic precision - adds more segments for better precision
34
+ arcAccuracy = 4,
35
+ keepExtremes = true,
36
+ keepCorners = true,
37
+ keepInflections = true,
38
+ extrapolateDominant = false,
39
+ addExtremes = false,
40
+ optimizeOrder = true,
41
+ removeColinear = true,
42
+ simplifyBezier = true,
43
+ autoAccuracy = true,
44
+ flatBezierToLinetos = true,
45
+ revertToQuadratics = true,
46
+ minifyD = 0,
47
+ tolerance = 1,
48
+ reverse = false
49
+ } = {}) {
50
+
51
+
52
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
53
+
54
+ // create clone for fallback
55
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
56
+
57
+ // count commands for evaluation
58
+ let comCount = pathDataO.length
59
+
60
+ /**
61
+ * get sub paths
62
+ */
63
+ let subPathArr = splitSubpaths(pathData);
64
+
65
+ // cleaned up pathData
66
+ let pathDataArrN = [];
67
+
68
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
69
+
70
+ //let { pathData, bb } = subPathArr[i];
71
+ let pathDataSub = subPathArr[i];
72
+
73
+ // try simplification in reversed order
74
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
75
+
76
+ // remove zero length linetos
77
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub)
78
+
79
+ // add extremes
80
+ //let tMin=0.2, tMax=0.8;
81
+ let tMin = 0, tMax = 1;
82
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax)
83
+
84
+
85
+ // sort to top left
86
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
87
+
88
+ // remove colinear/flat
89
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
90
+
91
+ // analyze pathdata to add info about signicant properties such as extremes, corners
92
+ let pathDataPlus = analyzePathData(pathDataSub);
93
+
94
+
95
+ // simplify beziers
96
+ let { pathData, bb, dimA } = pathDataPlus;
97
+
98
+
99
+
100
+
101
+
102
+ let pathDataN = pathData;
103
+
104
+
105
+
106
+
107
+ pathDataN = simplifyBezier ? simplifyPathData(pathDataN, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathDataN;
108
+
109
+
110
+
111
+ // cubic to arcs
112
+ if(cubicToArc){
113
+
114
+ let thresh = 3;
115
+
116
+ pathDataN.forEach((com, c) => {
117
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
118
+ if (type === 'C') {
119
+ //console.log(com);
120
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh)
121
+ if(comA.isArc) pathDataN[c] = comA.com;
122
+ //if (comQ.type === 'Q') pathDataN[c] = comQ
123
+ }
124
+ })
125
+
126
+ // combine adjacent cubics
127
+ pathDataN = combineArcs(pathDataN)
128
+
129
+ }
130
+
131
+
132
+ // simplify to quadratics
133
+ if (revertToQuadratics) {
134
+ pathDataN.forEach((com, c) => {
135
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
136
+ if (type === 'C') {
137
+ //console.log(com);
138
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p)
139
+ if (comQ.type === 'Q') pathDataN[c] = comQ
140
+ }
141
+ })
142
+ }
143
+
144
+
145
+
146
+
147
+ // update
148
+ pathDataArrN.push(pathDataN)
149
+ }
150
+
151
+
152
+ // merge pathdata
153
+ let pathDataFlat = pathDataArrN.flat();
154
+
155
+ /**
156
+ * detect accuracy
157
+ */
158
+ if (autoAccuracy) {
159
+ decimals = detectAccuracy(pathDataFlat)
160
+ }
161
+
162
+
163
+
164
+ // compare command count
165
+ let comCountS = pathDataFlat.length
166
+
167
+ // optimize
168
+ let pathOptions = {
169
+ toRelative,
170
+ toShorthands,
171
+ decimals,
172
+ }
173
+
174
+
175
+ // optimize path data
176
+ pathData = convertPathData(pathDataFlat, pathOptions)
177
+ let dOpt = pathDataToD(pathData, minifyD)
178
+
179
+ let report = {
180
+ original: comCount,
181
+ new: comCountS,
182
+ saved: comCount - comCountS,
183
+ decimals,
184
+ success: comCountS < comCount
185
+ }
186
+
187
+ return { pathData, d: dOpt, report };
188
+
189
+
190
+ }
191
+
192
+
193
+
194
+ function simplifyPathData(pathData, {
195
+ keepExtremes = true,
196
+ keepInflections = true,
197
+ keepCorners = true,
198
+ extrapolateDominant = true,
199
+ tolerance = 1,
200
+ reverse = false
201
+ } = {}) {
202
+
203
+ let pathDataN = [pathData[0]];
204
+
205
+ for (let i = 2, l = pathData.length; l && i <= l; i++) {
206
+ let com = pathData[i - 1];
207
+ let comN = i < l ? pathData[i] : null;
208
+ let typeN = comN?.type || null;
209
+ //let isCornerN = comN?.corner || null;
210
+ //let isExtremeN = comN?.extreme || null;
211
+ let isDirChange = com?.directionChange || null;
212
+ let isDirChangeN = comN?.directionChange || null;
213
+
214
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
215
+
216
+ // count simplifications
217
+ let success = 0;
218
+
219
+ // next is also cubic
220
+ if (type === 'C' && typeN === 'C') {
221
+
222
+ // cannot be combined as crossing extremes or corners
223
+ if (
224
+ (keepInflections && isDirChangeN) ||
225
+ (keepCorners && corner) ||
226
+ (!isDirChange && keepExtremes && extreme)
227
+ ) {
228
+ //renderPoint(markers, p, 'red', '1%')
229
+ pathDataN.push(com)
230
+ }
231
+
232
+ // try simplification
233
+ else {
234
+ //renderPoint(markers, p, 'magenta', '1%')
235
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance)
236
+ let error = 0;
237
+
238
+ // combining successful! try next segment
239
+ if (combined.length === 1) {
240
+ com = combined[0]
241
+ let offset = 1;
242
+ error += com.error;
243
+ //console.log('!error', error);
244
+
245
+ // find next candidates
246
+ for (let n = i + 1; error < tolerance && n < l; n++) {
247
+ let comN = pathData[n]
248
+ if (comN.type !== 'C' ||
249
+ (
250
+ (keepInflections && comN.directionChange) ||
251
+ (keepCorners && com.corner) ||
252
+ (keepExtremes && com.extreme)
253
+ )
254
+ ) {
255
+ break
256
+ }
257
+
258
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance)
259
+ if (combined.length === 1) {
260
+ offset++
261
+ }
262
+ com = combined[0]
263
+ }
264
+
265
+ //com.opt = true
266
+ pathDataN.push(com)
267
+
268
+ if (i < l) {
269
+ i += offset
270
+ }
271
+
272
+ } else {
273
+ pathDataN.push(com)
274
+ }
275
+ }
276
+
277
+ } // end of bezier command
278
+
279
+
280
+ // other commands
281
+ else {
282
+ pathDataN.push(com)
283
+ }
284
+
285
+ } // end command loop
286
+
287
+ // reverse back
288
+ if (reverse) pathDataN = reversePathData(pathDataN)
289
+
290
+ return pathDataN
291
+ }
292
+
293
+
294
+