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,363 @@
1
+ //import { quadratic2Cubic } from './convert.js';
2
+ //import { splitSubpaths, shiftSvgStartingPoint } from './convert_segments.js';
3
+ import { shiftSvgStartingPoint, reorderPathData } from './pathData_reorder.js';
4
+ import { splitSubpaths, addExtemesToCommand } from './pathData_split.js';
5
+ import { getComThresh, commandIsFlat, getPathDataVertices } from './geometry.js';
6
+
7
+ import { getPolyBBox } from './geometry_bbox.js';
8
+
9
+
10
+
11
+
12
+ /**
13
+ * remove zero length commands
14
+ * replace flat beziers with lintos
15
+ * replace closing lines with z
16
+ * rearrange commands to avoid unnessessary linetos
17
+ */
18
+
19
+
20
+ export function cleanUpPathData(pathData, addExtremes = false, removeClosingLines = true, startToTop = true, debug = false) {
21
+
22
+ //collect logs
23
+ let simplyfy_debug_log = [];
24
+
25
+ pathData = JSON.parse(JSON.stringify(pathData));
26
+ let pathDataNew = [pathData[0]];
27
+
28
+ /**
29
+ * get poly bbox to define
30
+ * an appropriate relative threshold
31
+ * for flat or short segment detection
32
+ */
33
+ let pathPoly = getPathDataVertices(pathData);
34
+ let bb = getPolyBBox(pathPoly)
35
+ let { width, height } = bb;
36
+ let tolerance = (width + height) / 2 * 0.001
37
+
38
+
39
+ // previous on path point
40
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
41
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
42
+
43
+ let addedExtremes = false;
44
+
45
+
46
+ for (let c = 1, len = pathData.length; len && c < len; c++) {
47
+ let com = pathData[c];
48
+ //let comPrev = pathData[c - 1];
49
+ let comN = pathData[c + 1] ? pathData[c + 1] : '';
50
+ let { type, values } = com;
51
+ //let typeRel = type.toLowerCase();
52
+ let valsL = values.slice(-2);
53
+ let p = { x: valsL[0], y: valsL[1] };
54
+
55
+ // segment command points - including previous final on-path
56
+ let pts = [p0, p]
57
+ if (type === 'C' || type === 'Q') pts.push({ x: values[0], y: values[1] })
58
+ if (type === 'C') pts.push({ x: values[2], y: values[3] })
59
+
60
+
61
+ // get relative threshold based on averaged command dimensions
62
+ let xArr = pts.map(pt => { return pt.x });
63
+ let yArr = pts.map(pt => { return pt.y });
64
+ let xMax = Math.max(...xArr)
65
+ let xMin = Math.min(...xArr)
66
+ let yMax = Math.max(...yArr)
67
+ let yMin = Math.min(...yArr)
68
+
69
+ let w = xMax - xMin
70
+ let h = yMax - yMin
71
+ let dimA = (w + h) / 2 || 0;
72
+
73
+ if (type.toLowerCase() !== 'z') {
74
+
75
+ // zero length
76
+ //|| (type==='L' && dimA<tolerance)
77
+ if ((p.x === p0.x && p.y === p0.y) || (type === 'L' && dimA < tolerance)) {
78
+ //console.log('zero', com, dimA, tolerance, w, h);
79
+ if (debug) simplyfy_debug_log.push(`removed zero length ${type}`)
80
+ continue
81
+ }
82
+
83
+ /**
84
+ * simplify adjacent linetos
85
+ * based on their flatness
86
+ */
87
+ else if (type === 'L') {
88
+
89
+ //unnessecary closing linto
90
+ if (removeClosingLines && p.x === M.x && p.y === M.y && comN.type.toLowerCase() === 'z') {
91
+ if (debug) simplyfy_debug_log.push(`unnessecary closing linto`)
92
+ continue
93
+ }
94
+
95
+
96
+ if (comN.type === 'L') {
97
+
98
+ let valuesNL = comN.values.slice(-2)
99
+ let pN = { x: valuesNL[0], y: valuesNL[1] }
100
+
101
+
102
+ // check if adjacent linetos are flat
103
+ //let flatness = commandIsFlat([p0, p, pN], tolerance)
104
+ let flatness = commandIsFlat([p0, p, pN], tolerance)
105
+ let isFlatN = flatness.flat;
106
+
107
+
108
+ // next lineto is flat – don't add command
109
+ if (isFlatN) {
110
+ //console.log('flat', flatness, [p0, p, pN]);
111
+ if (debug) simplyfy_debug_log.push(`remove flat linetos`)
112
+ continue
113
+ }
114
+ }
115
+ }
116
+
117
+
118
+ if (type === 'C') {
119
+ /**
120
+ * detect flat beziers
121
+ * often used for morphing
122
+ * animation
123
+ */
124
+
125
+ let cp1 = { x: values[0], y: values[1] }
126
+ let cp2 = { x: values[2], y: values[3] }
127
+ let pts = [p0, cp1, cp2, p];
128
+
129
+ let flatness = commandIsFlat(pts, tolerance)
130
+ let isFlat = flatness.flat
131
+ let ratio = flatness.ratio;
132
+ //console.log('flatness', flatness);
133
+
134
+ /*
135
+ // zero length control points
136
+ let zeroCpts = (cp1.x === p0.x && cp1.y === p0.y && ratio<0.2 ) || (cp2.x === p.x && cp2.y === p.y && ratio<0.2 );
137
+ if(zeroCpts){
138
+ console.log('flat cpts');
139
+ //com = { type: 'L', values: [p.x, p.y] };
140
+ }
141
+ */
142
+
143
+ if(isFlat){
144
+ //console.log('!!!flat cpts');
145
+ //com = { type: 'L', values: [p.x, p.y] };
146
+ }
147
+
148
+
149
+
150
+ let valuesNL = comN ? comN.values.slice(-2) : '';
151
+ let pN = valuesNL.length ? { x: valuesNL[0], y: valuesNL[1] } : ''
152
+
153
+
154
+ //check adjacent flat C - convert to linetos
155
+ if (isFlat) {
156
+
157
+ let flatnessN, isFlatN=false;
158
+
159
+ if (comN.type === 'C') {
160
+
161
+ // check if adjacent curves are also flat
162
+ flatnessN = commandIsFlat([p0, p, pN], tolerance)
163
+ isFlatN = flatnessN.flat;
164
+
165
+
166
+ if (isFlatN) {
167
+ //console.log('is flat');
168
+ //console.log(flatnessN);
169
+ if (debug) simplyfy_debug_log.push(`skip cubic - actually a lineto: area-ratio: ${ratio}, flatness next:${flatnessN}`)
170
+
171
+ //com = { type: 'L', values: [p.x, p.y] };
172
+ //continue
173
+ }
174
+ }
175
+
176
+ if (ratio < 0.1 ) {
177
+ //console.log('simplify cubic to lineto');
178
+ simplyfy_debug_log.push(`simplify cubic to lineto`)
179
+ //com = { type: 'L', values: [p.x, p.y] };
180
+ }
181
+
182
+
183
+ }
184
+ // not flat
185
+ else {
186
+ // add extremes
187
+ if (addExtremes) {
188
+ addedExtremes = addExtemesToCommand(p0, values);
189
+ com = addedExtremes.pathData
190
+ }
191
+
192
+ //add extremes
193
+ if (addExtremes && addedExtremes.count) simplyfy_debug_log.push(`added extremes: ${addedExtremes.count}`)
194
+ }
195
+ }
196
+
197
+ }
198
+
199
+ // add new commands
200
+ if (com.length) {
201
+ pathDataNew.push(...com);
202
+ } else {
203
+ pathDataNew.push(com);
204
+ }
205
+
206
+
207
+ if (type.toLowerCase() === "z") {
208
+ p0 = M;
209
+ } else if (type === "M") {
210
+ M = { x: valsL[0], y: valsL[1] };
211
+ }
212
+
213
+ // new previous point
214
+ p0 = { x: valsL[0], y: valsL[1] };
215
+
216
+
217
+
218
+ }//end for
219
+
220
+ //optimize starting point
221
+ pathDataNew = optimizeStartingPoints(pathDataNew, removeClosingLines, startToTop);
222
+
223
+ simplyfy_debug_log.push(`original command count: ${pathData.length}; removed:${pathData.length - pathDataNew.length} `)
224
+
225
+ if (debug) console.log(simplyfy_debug_log);
226
+
227
+ //console.log(pathData.length, pathDataNew.length)
228
+ return pathDataNew;
229
+ }
230
+
231
+
232
+ /**
233
+ * avoids starting points in the middle of 2 smooth curves
234
+ * can replace linetos with closepaths
235
+ */
236
+
237
+ export function optimizeStartingPoints(pathData, removeFinalLineto = false, startToTop = false) {
238
+
239
+
240
+ let pathDataArr = splitSubpaths(pathData);
241
+ //console.log(pathDataArr);
242
+
243
+ let pathDataNew = [];
244
+ let len = pathDataArr.length;
245
+
246
+ for (let i = 0; i < len; i++) {
247
+ let pathData = pathDataArr[i]
248
+
249
+ // move starting point to first lineto
250
+ let firstLIndex = pathData.findIndex(cmd => cmd.type === 'L');
251
+ let firstBezierIndex = pathData.findIndex(cmd => cmd.type === 'C' || cmd.type === 'Q');
252
+ let commands = new Set([...pathData.map(com => com.type)]);
253
+ let hasLinetos = commands.has('L')
254
+ let hasBeziers = commands.has('C') || commands.has('Q')
255
+
256
+
257
+ let len = pathData.length
258
+ let isClosed = pathData[len - 1].type.toLowerCase() === 'z'
259
+
260
+ if (!isClosed) {
261
+ pathDataNew.push(...pathData);
262
+ continue
263
+ }
264
+
265
+ if (isClosed) {
266
+
267
+ let extremeIndex = -1;
268
+ let newIndex = 0
269
+
270
+ if (startToTop) {
271
+ //get top most index
272
+ let indices = [];
273
+ for (let i = 0, len = pathData.length; i < len; i++) {
274
+ let com = pathData[i];
275
+ let { type, values } = com;
276
+ if (values.length) {
277
+
278
+ let valsL = values.slice(-2)
279
+ let p = { x: valsL[0], y: valsL[1], index: i }
280
+ indices.push(p)
281
+
282
+ }
283
+ }
284
+
285
+
286
+ // find top most
287
+ indices = indices.sort((a, b) => {
288
+ a.y - b.y
289
+ //a.x - b.x || a.y - b.y
290
+ /*
291
+ let n = b.y - a.y;
292
+ if (n !== 0) {
293
+ return n;
294
+ }
295
+ return b.x - a.x;
296
+ */
297
+ });
298
+ newIndex = indices[0].index
299
+
300
+ } else {
301
+ //find extreme
302
+ let pathPoly = getPathDataVertices(pathData);
303
+ let bb = getPolyBBox(pathPoly)
304
+ let { left, right, top, bottom, width, height } = bb;
305
+ let minX = Infinity;
306
+ let minY = Infinity;
307
+
308
+
309
+ // get extreme
310
+ if (hasBeziers) {
311
+
312
+ for (let i = 0, len = pathData.length; i < len; i++) {
313
+ let com = pathData[i];
314
+ let { type, values } = com;
315
+ if (type === 'C' || type === 'Q') {
316
+
317
+ let valsL = values.slice(-2)
318
+ let p = { x: valsL[0], y: valsL[1] }
319
+ // is extreme relative to bounding box
320
+ if (p.x === left || p.y === top || p.x === right || p.y === bottom) {
321
+ if (p.x < minX && p.y < minY) {
322
+ extremeIndex = i;
323
+ minX = p.x;
324
+ minY = p.y
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+
332
+ // set to first bezier extreme or first L
333
+ firstBezierIndex = extremeIndex > -1 ? extremeIndex : firstBezierIndex
334
+ newIndex = hasLinetos ? firstLIndex : firstBezierIndex;
335
+
336
+ }
337
+
338
+
339
+ // reorder
340
+ pathData = shiftSvgStartingPoint(pathData, newIndex)
341
+ len = pathData.length
342
+
343
+ // remove last lineto
344
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] }
345
+ let penultimateCom = pathData[len - 2];
346
+ let penultimateType = penultimateCom.type;
347
+ let penultimateComCoords = penultimateCom.values.slice(-2)
348
+
349
+ let isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
350
+
351
+ if (removeFinalLineto && isClosingCommand) {
352
+ //console.log('remove l');
353
+ pathData.splice(len - 2, 1)
354
+ }
355
+ pathDataNew.push(...pathData);
356
+
357
+ }
358
+ }
359
+ return pathDataNew
360
+ }
361
+
362
+
363
+
@@ -0,0 +1,172 @@
1
+ import { checkLineIntersection, getAngle, getPointOnEllipse, getSquareDistance, pointAtT, reducePoints } from "./geometry";
2
+ import { getPolygonArea } from "./geometry_area";
3
+ import { getPolyBBox } from "./geometry_bbox";
4
+ import { renderPoint } from "./visualize";
5
+
6
+ export function analyzePoly(pts) {
7
+
8
+ let l = pts.length;
9
+ let polyArea = getPolygonArea(pts, true)
10
+ //console.log(polyArea);
11
+
12
+
13
+ // get areas
14
+ for (let i = 0; i < l; i++) {
15
+ let pt0 = i > 0 ? pts[i - 1] : pts[l - 1];
16
+ let pt1 = pts[i];
17
+ let pt2 = i < l - 1 ? pts[i + 1] : pts[0];
18
+
19
+ let area = getPolygonArea([pt0, pt1, pt2], false);
20
+ let ang1 = getAngle(pt1, pt0, true);
21
+ let ang2 = getAngle(pt1, pt2, true);
22
+ let delta = Math.abs(ang1 - ang2);
23
+ let deltaDeg = delta * 180 / Math.PI;
24
+
25
+
26
+
27
+ /**
28
+ * get local extremes
29
+ * my coincide with corners or
30
+ * direction changes
31
+ */
32
+ let { left, right, top, bottom } = getPolyBBox([pt0, pt2]);
33
+ let isExtreme = (pt1.x < left || pt1.x > right || pt1.y < top || pt1.y > bottom);
34
+
35
+
36
+ /**
37
+ * check corners by
38
+ * adjacent angle differences
39
+ */
40
+ let isCorner = deltaDeg < 120 || deltaDeg > 270;
41
+
42
+
43
+ /**
44
+ * get direction changes
45
+ * e.g the spine of a "S" shape
46
+ */
47
+ let directionChange = pt0.isCorner === false && ((pt0.area < 0 && area > 0) || (pt0.area > 0 && area < 0));
48
+
49
+
50
+
51
+ if (pt0.isExtreme &&
52
+ (pt1.y === pt0.y || pt1.x === pt0.x)
53
+ ) {
54
+ isExtreme = true;
55
+ }
56
+
57
+
58
+ if (directionChange && isExtreme) {
59
+ isCorner = true;
60
+ }
61
+
62
+ // if segment is too large relative to total area - don't interpret as corner
63
+ let areaRat = Math.abs(area / polyArea);
64
+
65
+ if (areaRat > 0.2) {
66
+ isCorner = false;
67
+ }
68
+
69
+
70
+ /**
71
+ * visualize significant points for
72
+ * debugging
73
+ */
74
+
75
+ /*
76
+ */
77
+
78
+ if ((isExtreme && isCorner)) {
79
+ isExtreme = false;
80
+ directionChange = false;
81
+ //isCorner = false;
82
+ }
83
+
84
+ if (isExtreme) {
85
+ renderPoint(markers, pt1, 'cyan', '1%');
86
+ }
87
+
88
+ if (isCorner) {
89
+ renderPoint(markers, pt1, 'purple', '0.5%');
90
+ }
91
+
92
+ if (directionChange) {
93
+ renderPoint(markers, pt1, 'orange', '1.5%', '0.5');
94
+ }
95
+
96
+
97
+ /**
98
+ * save point analysis properties
99
+ * to point objects
100
+ */
101
+ pt1.isExtreme = isExtreme;
102
+ pt1.isCorner = isCorner;
103
+ pt1.directionChange = directionChange;
104
+
105
+ pt1.area = area;
106
+ pt1.delta = delta;
107
+ pt1.deltaDeg = deltaDeg;
108
+
109
+ }
110
+
111
+
112
+ //getControlPoints(pts)
113
+
114
+
115
+ return pts
116
+ }
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+ export function getPathDataChunks(pathData) {
126
+
127
+ let chunks = [[]];
128
+ let lastType = 'M'
129
+ let ind = 0;
130
+ let wasExtreme, wasCorner, wasDirectionchange;
131
+
132
+ pathData.forEach(com => {
133
+
134
+ let { isCorner, isExtreme, directionChange, type } = com;
135
+
136
+ if (type !== lastType || wasExtreme || wasCorner || directionChange || wasDirectionchange) {
137
+ chunks.push([])
138
+ ind++
139
+ }
140
+ chunks[ind].push(com)
141
+
142
+ wasExtreme = isExtreme
143
+ wasCorner = isCorner
144
+ wasDirectionchange = directionChange;
145
+ lastType = type
146
+ })
147
+
148
+
149
+ return chunks;
150
+
151
+ }
152
+
153
+
154
+
155
+
156
+ /**
157
+ * check whether a polygon is likely
158
+ * to be closed
159
+ * or an open polyline
160
+ */
161
+ export function isClosedPolygon(pts, reduce = 24) {
162
+
163
+ let ptsR = reducePoints(pts, reduce);
164
+ let { width, height } = getPolyBBox(ptsR);
165
+ //let dimAvg = Math.max(width, height);
166
+ let dimAvg = (width + height) / 2;
167
+ //let closingThresh = (dimAvg / pts.length) ** 2
168
+ let closingThresh = (dimAvg) ** 2
169
+ let closingDist = getSquareDistance(pts[0], pts[pts.length - 1]);
170
+
171
+ return closingDist < closingThresh;
172
+ }