svg-path-simplify 0.0.1 → 0.0.4

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 (44) hide show
  1. package/LICENSE +339 -21
  2. package/README.md +61 -2
  3. package/dist/svg-path-simplify.esm.js +4308 -0
  4. package/dist/svg-path-simplify.esm.min.js +1 -0
  5. package/dist/svg-path-simplify.js +4334 -0
  6. package/dist/svg-path-simplify.min.js +1 -0
  7. package/dist/svg-path-simplify.node.js +4331 -0
  8. package/dist/svg-path-simplify.node.min.js +1 -0
  9. package/index.html +230 -0
  10. package/package.json +5 -6
  11. package/src/constants.js +4 -0
  12. package/src/detect_input.js +42 -0
  13. package/src/index.js +21 -3
  14. package/src/pathData_simplify_cubic.js +324 -0
  15. package/src/pathData_simplify_cubic_arr.js +50 -0
  16. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  17. package/src/pathSimplify-main.js +400 -0
  18. package/src/svg_getViewbox.js +32 -0
  19. package/src/svgii/...parse.js +402 -0
  20. package/src/svgii/geometry.js +1143 -0
  21. package/src/svgii/geometry_area.js +265 -0
  22. package/src/svgii/geometry_bbox.js +223 -0
  23. package/src/svgii/pathData_analyze.js +896 -0
  24. package/src/svgii/pathData_convert.js +1180 -0
  25. package/src/svgii/pathData_parse.js +487 -0
  26. package/src/svgii/pathData_remove_collinear.js +98 -0
  27. package/src/svgii/pathData_remove_zerolength.js +28 -0
  28. package/src/svgii/pathData_reorder.js +238 -0
  29. package/src/svgii/pathData_reverse.js +124 -0
  30. package/src/svgii/pathData_scale.js +42 -0
  31. package/src/svgii/pathData_split.js +449 -0
  32. package/src/svgii/pathData_stringify.js +145 -0
  33. package/src/svgii/pathData_toPolygon.js +92 -0
  34. package/src/svgii/pathdata_cleanup.js +363 -0
  35. package/src/svgii/poly_analyze.js +172 -0
  36. package/src/svgii/poly_to_pathdata.js +185 -0
  37. package/src/svgii/rounding.js +162 -0
  38. package/src/svgii/simplify.js +248 -0
  39. package/src/svgii/simplify_bezier.js +470 -0
  40. package/src/svgii/simplify_linetos.js +93 -0
  41. package/src/svgii/simplify_polygon.js +135 -0
  42. package/src/svgii/stringify.js +103 -0
  43. package/src/svgii/svg_cleanup.js +86 -0
  44. package/src/svgii/visualize.js +317 -0
@@ -0,0 +1,185 @@
1
+
2
+ /**
3
+ * Chord-Length Parameterization
4
+ * based on
5
+ * https://francoisromain.medium.com/smooth-a-svg-path-with-cubic-bezier-curves-e37b49d46c74
6
+ */
7
+
8
+ import { checkLineIntersection, interpolate, mirrorCpts } from "./geometry";
9
+ import { getPolyBBox } from "./geometry_bbox";
10
+ import { isClosedPolygon } from "./poly_analyze";
11
+
12
+
13
+ // Render the svg <path> element
14
+ export function getCurvePathData(pts, t = 0.666, closed = 'auto', keepCorners = true) {
15
+
16
+
17
+ //auto detect closed polygon
18
+ if(closed==='auto'){
19
+ closed = isClosedPolygon(pts)
20
+ }
21
+
22
+
23
+ // append first 2 pts for closed paths
24
+ if (closed) {
25
+ pts = pts.concat(pts.slice(0, 2));
26
+ }
27
+
28
+
29
+ // Position of a control point
30
+ const controlPoint = (pt1, pt0, pt2, reverse = false, t = 0.666) => {
31
+
32
+ let p = pt0 || pt1;
33
+ let n = pt2 || pt1;
34
+
35
+ let dx = n.x - p.x
36
+ let dy = n.y - p.y
37
+ let sign = reverse ? -1 : 1;
38
+
39
+ let cp0 = {
40
+ x: pt1.x + dx * sign,
41
+ y: pt1.y + dy * sign
42
+ };
43
+
44
+
45
+ let t2 = 0.1 / (1 - t * 0.5)
46
+ let cp = interpolate(pt1, cp0, t2)
47
+
48
+ return cp;
49
+ };
50
+
51
+
52
+ // collect smoothed pathData
53
+ let pathData = [];
54
+ pathData.push({ type: "M", values: [pts[0].x, pts[0].y], p0:{x:pts[0].x, y:pts[0].y} });
55
+
56
+ let cp2_0 = pts[0];
57
+ let l = pts.length;
58
+
59
+
60
+ for (let i = 1; i < l; i++) {
61
+
62
+ let drawLine = false;
63
+ let ptPrev = i > 1 ? pts[i - 2] : pts[l - 1];
64
+ let ptNext = i < l - 1 ? pts[i + 1] : pts[0];
65
+ //console.log(ptPrev, ptNext);
66
+
67
+ let pt0 = pts[i - 1];
68
+ let pt1 = pts[i];
69
+ let cp1 = controlPoint(pt0, ptPrev, pt1, false, t);
70
+ let cp2 = controlPoint(pt1, pt0, ptNext, true, t);
71
+
72
+ let {isExtreme, isCorner,directionChange} = pt1;
73
+
74
+ // get cp vector intersections
75
+ let cpI = checkLineIntersection(pt0, cp1, pt1, cp2, false);
76
+
77
+
78
+ // harmonize cpts
79
+ if (cpI) {
80
+ let { left, top, right, bottom, width, height } = getPolyBBox([pt0, pt1]);
81
+ let outside = cpI ? (cpI.x < left || cpI.x > right || cpI.y < top || cpI.y > bottom) : false;
82
+
83
+ // adjust/harmonize control points
84
+ if (!outside) {
85
+ cp1 = interpolate(pt0, cpI, t)
86
+ cp2 = interpolate(pt1, cpI, t)
87
+ } else {
88
+
89
+ // check exact cp self intersections
90
+ let cpI2 = checkLineIntersection(pt0, cp1, pt1, cp2, true);
91
+
92
+ // control points are diverging - connction between cps and start/end point
93
+ let interH = checkLineIntersection(pt0, pt1, cp1, cp2, true);
94
+
95
+ cpI = !interH ? cpI : (cpI2 ? cpI2 : null)
96
+
97
+ //&& i < l - 3
98
+ if (cpI ) {
99
+ cp1 = interpolate(pt0, cpI, t)
100
+ cp2 = interpolate(pt1, cpI, t)
101
+ //renderPoint(svg, cpI, 'magenta')
102
+ }
103
+ }
104
+
105
+ }
106
+
107
+
108
+ if (keepCorners) {
109
+
110
+ // mirror cpts
111
+ if ((pt1.isCorner && !pt0.isCorner) || (!pt1.isCorner && pt0.isCorner)) {
112
+ let outgoing = !pt1.isCorner && pt0.isCorner;
113
+
114
+ let cps = mirrorCpts(cp2_0, pt0, cp2, pt1, outgoing, t);
115
+ let cp1_2 = cps.cp1
116
+ let cp2_2 = cps.cp2
117
+
118
+ cp1 = cp1_2;
119
+ cp2 = cp2_2;
120
+
121
+ }
122
+
123
+ // withdraw cpts for sharp corners - tag as lineto
124
+ else if ((pt1.isCorner && pt0.isCorner)) {
125
+
126
+ cp1 = { x: pt0.x, y: pt0.y };
127
+ cp2 = { x: pt1.x, y: pt1.y };
128
+ drawLine = true
129
+ }
130
+
131
+ }
132
+
133
+
134
+ // update last cp2
135
+ cp2_0 = cp2;
136
+
137
+ let com = { type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, pt1.x, pt1.y],
138
+ drawLine,
139
+ // add properties for chunk based simplification
140
+ isExtreme, isCorner,directionChange
141
+ };
142
+
143
+
144
+ let values = com.values
145
+ com.p0 = pt0
146
+ com.cp1 = {x:values[0], y:values[1]}
147
+ com.cp2 = {x:values[2], y:values[3]}
148
+ com.p = {x:values[4], y:values[5]}
149
+
150
+
151
+ pathData.push(com);
152
+ }
153
+
154
+ // copy last commands 1st controlpoint to first curveto
155
+ if (closed) {
156
+ let comLast = pathData[pathData.length - 1];
157
+ let valuesLastC = comLast.values;
158
+ let valuesFirstC = pathData[1].values;
159
+
160
+ pathData[1].type = 'C'
161
+ pathData[1].values = [valuesLastC[0], valuesLastC[1], ...valuesFirstC.slice(2)]
162
+ let values0 = pathData[0].values
163
+ let values = pathData[1].values
164
+ pathData[1].p0 = {x:values0[0], y:values0[1]}
165
+ pathData[1].cp1 = {x:values[0], y:values[1]}
166
+ pathData[1].cp2 = {x:values[2], y:values[3]}
167
+ pathData[1].p = {x:values[4], y:values[5]}
168
+
169
+ // delete last curveto
170
+ pathData = pathData.slice(0, pathData.length - 1);
171
+ pathData.push({ type: 'z', values: [] })
172
+
173
+ }
174
+
175
+ // convert flat curves to linetos
176
+ pathData.forEach((com, i) => {
177
+ if (com.drawLine) {
178
+ pathData[i].type = 'L'
179
+ pathData[i].values = com.values.slice(-2);
180
+ }
181
+ })
182
+
183
+ //console.log(pathData);
184
+ return pathData;
185
+ };
@@ -0,0 +1,162 @@
1
+
2
+ /**
3
+ * detect suitable floating point accuracy
4
+ * for further rounding/optimizations
5
+ */
6
+
7
+ import { getDistAv } from "./geometry";
8
+
9
+
10
+ export function detectAccuracy(pathData) {
11
+
12
+ // Reference first MoveTo command (M)
13
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
14
+ let p0 = M
15
+ let p = M
16
+ pathData[0].decimals = 0
17
+ let lastDec = 0;
18
+ let maxDecimals = 0
19
+ let minDim = Infinity
20
+ let maxDim = 0
21
+
22
+ //console.log('detectAccuracy');
23
+
24
+ let dims = new Set();
25
+
26
+ // add average distances
27
+ for (let i = 0, len = pathData.length; i < len; i++) {
28
+ let com = pathData[i];
29
+ let { type, values } = com;
30
+
31
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
32
+ p={x:lastVals[0], y:lastVals[1]}
33
+
34
+ // use existing averave dimension value or calculate
35
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0
36
+ //let dimA = +getDistAv(p0, p).toFixed(8)
37
+ //console.log('dimA', dimA, com.dimA, type);
38
+
39
+ if(dimA) dims.add(dimA);
40
+
41
+ if(dimA && dimA<minDim) minDim = dimA;
42
+ if(dimA && dimA>maxDim) maxDim = dimA;
43
+
44
+
45
+ if(type==='M'){
46
+ M=p;
47
+ }
48
+ p0 = p;
49
+ }
50
+
51
+
52
+ let dim_min = Array.from(dims).sort()
53
+ let sliceIdx = Math.ceil(dim_min.length/8);
54
+ dim_min = dim_min.slice(0, sliceIdx );
55
+
56
+ let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
57
+
58
+ let threshold = 50
59
+ let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length
60
+
61
+ // clamp
62
+ return Math.min(Math.max(0, decimalsAuto), 8)
63
+
64
+ }
65
+
66
+
67
+
68
+ export function detectAccuracy_back(pathData) {
69
+
70
+ // Reference first MoveTo command (M)
71
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
72
+ let p0 = { ...M };
73
+ pathData[0].decimals = 0
74
+ let lastDec = 0;
75
+ let maxDecimals = 0
76
+
77
+ for (let i = 1, len = pathData.length; i < len; i++) {
78
+ let com = pathData[i];
79
+ let { type, values } = com;
80
+
81
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
82
+ let lastX = lastVals[0];
83
+ let lastY = lastVals[1];
84
+
85
+ if (type === 'Z' || type === 'z') {
86
+ lastX = M.x;
87
+ lastY = M.y;
88
+ }
89
+
90
+ let w = Math.abs(p0.x - lastX);
91
+ let h = Math.abs(p0.y - lastY);
92
+ let dimA = (w + h) / 2 || 0;
93
+
94
+
95
+ // Determine decimal places dynamically
96
+ let decimals = (type !== 'Z' && type !== 'z') ? Math.ceil((1 / dimA)).toString().length + 1 : 0;
97
+
98
+ //console.log(type, dimA, decimals);
99
+
100
+
101
+ if (dimA === 0) {
102
+ //console.log('zero length');
103
+ decimals = lastDec;
104
+ }
105
+
106
+ else if (decimals && dimA < 0.5) {
107
+ decimals++
108
+ }
109
+
110
+ //console.log('dimA', type, dimA, decimals);
111
+
112
+
113
+ // Update previous coordinates
114
+ p0 = { x: lastX, y: lastY };
115
+
116
+ // Track MoveTo for closing paths
117
+ if (type === 'M') {
118
+ M = { x: values[0], y: values[1] };
119
+ com.decimals = decimals;
120
+ } else {
121
+
122
+ // Store ideal precision for next pass
123
+ com.decimals = decimals;
124
+
125
+ }
126
+
127
+ maxDecimals = decimals > maxDecimals ? decimals : maxDecimals;
128
+ lastDec = decimals;
129
+ }
130
+
131
+ // set max decimal for M
132
+ return maxDecimals
133
+ //pathData[0].decimals = maxDecimals
134
+ //return pathData
135
+ }
136
+
137
+
138
+ /**
139
+ * round path data
140
+ * either by explicit decimal value or
141
+ * based on suggested accuracy in path data
142
+ */
143
+ export function roundPathData(pathData, decimals = -1) {
144
+ // has recommended decimals
145
+ let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
146
+ //console.log('decimals', decimals, hasDecimal);
147
+
148
+ for(let c=0, len=pathData.length; c<len; c++){
149
+ let com=pathData[c];
150
+ let {type, values} = com
151
+
152
+ if (decimals >-1 || hasDecimal) {
153
+ decimals = hasDecimal ? com.decimals : decimals;
154
+
155
+
156
+ //console.log('decimals', type, decimals);
157
+ pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
158
+
159
+ }
160
+ };
161
+ return pathData;
162
+ }
@@ -0,0 +1,248 @@
1
+
2
+ /**
3
+ * split path data into chunks
4
+ * to detect subsequent cubic segments
5
+ * that could be combined
6
+ */
7
+
8
+ //import { splitSubpaths, shiftSvgStartingPoint } from "./convert_segments";
9
+ import { shiftSvgStartingPoint } from "./pathData_reorder.js";
10
+ import { splitSubpaths, getPathDataPlusChunks } from './pathData_split.js';
11
+
12
+ import { getAngle, bezierhasExtreme, getPathDataVertices } from "./geometry";
13
+ import { renderPoint, renderPath } from "./visualize";
14
+
15
+
16
+ //import { optimizeStartingPoints } from './cleanup.js';
17
+ //import { getPathDataVertices, getPointOnEllipse, pointAtT, checkLineIntersection, getDistance, interpolate } from './geometry.js';
18
+
19
+ import { getPolygonArea, getPathArea, getRelativeAreaDiff } from './geometry_area.js';
20
+ import { getPathDataBBox, getPolyBBox } from './geometry_bbox.js';
21
+
22
+ import { optimizeStartingPoints, cleanUpPathData } from './pathdata_cleanup.js';
23
+
24
+ import { pathDataArcsToCubics, pathDataQuadraticToCubic, quadratic2Cubic, pathDataToRelative, pathDataToAbsolute, pathDataToLonghands, pathDataToShorthands, pathDataToQuadratic, cubicToQuad, arcToBezier, pathDataToVerbose, convertArrayPathData, revertPathDataToArray, combineArcs, replaceCubicsByArcs } from './pathData_convert.js';
25
+
26
+ import {unitePolygon} from './simplify_polygon.js';
27
+
28
+ import { simplifyBezierSequence } from './simplify_bezier.js';
29
+ //import { simplifyBezierSequence } from './simplify_bezier_back16_working.js';
30
+ //import { simplifyBezierSequence } from './simplify_bezier_back17_working.js';
31
+
32
+
33
+
34
+ import { simplifyLinetoSequence } from './simplify_linetos.js';
35
+ import { analyzePathData } from "./pathData_anylyse.js";
36
+ import { scalePathData } from "./pathData_scale.js";
37
+ //import { analyzePathData } from "./pathData_anylyse_back1.js";
38
+
39
+
40
+
41
+ export function simplifyPathData(pathData, tolerance = 3, keepDetails = true, forceCubic = false, cubicToArc = true, multipass = false, debug = false) {
42
+
43
+ ///devcomment
44
+
45
+ //console.log('forceCubic simplifyPathData', forceCubic);
46
+
47
+ // unoptimized area
48
+ let area0 = getPathArea(pathData);
49
+
50
+ // get bbox for adjustment scaling
51
+ let bb = getPathDataBBox(pathData);
52
+ //console.log('bb', bb);
53
+
54
+ let dimA = (bb.width + bb.height) / 2;
55
+ let scale = dimA < 10 ? 100 / dimA : 1;
56
+
57
+ // scale small paths
58
+ if (scale != 1) pathData = scalePathData(pathData, scale, scale)
59
+
60
+ // remove zero length commands and shift starting point
61
+ let addExtremes = true;
62
+ addExtremes = false;
63
+
64
+ let removeFinalLineto = false
65
+ let startToTop = true;
66
+ //tolerance = 5;
67
+
68
+ // show chunks
69
+ //debug = true
70
+
71
+ /**
72
+ * optimize starting point
73
+ * remove zero length segments
74
+ */
75
+ pathData = cleanUpPathData(pathData, addExtremes, removeFinalLineto, startToTop, debug)
76
+
77
+
78
+ // get verbose pathdata properties
79
+ let pathDataPlus = analyzePathData(pathData);
80
+
81
+ // add chunks to path object
82
+ let pathDataPlusChunks = getPathDataPlusChunks(pathDataPlus, debug);
83
+
84
+ // create simplified pathData
85
+ let pathDataSimple = [];
86
+
87
+ // loop sup path
88
+ for (let s = 0, l = pathDataPlusChunks.length; l && s < l; s++) {
89
+ let sub = pathDataPlusChunks[s];
90
+ let { chunks, dimA, area } = sub;
91
+
92
+ let thresh = dimA * 0.1
93
+ let len = chunks.length;
94
+ let simplified;
95
+ //console.log('sub', chunks);
96
+
97
+ //forceCubic = true
98
+
99
+ for (let i = 0; i < len; i++) {
100
+ let chunk = chunks[i];
101
+ let type = chunk[0].type;
102
+
103
+ // try to convert cubic to quadratic
104
+
105
+ //forceCubic = true
106
+
107
+ if (!forceCubic && chunk.length === 1 && type === 'C') {
108
+ simplified = simplifyBezierSequence(chunk);
109
+ pathDataSimple.push(...simplified);
110
+ //console.log('simplified cubic to quadratic', simplified);
111
+ continue;
112
+ }
113
+
114
+ // nothing to combine
115
+ if (chunk.length < 2) {
116
+ pathDataSimple.push(...chunk);
117
+ //console.log('simple',chunk );
118
+ continue;
119
+ }
120
+
121
+ // simplify linetos
122
+ if (type === 'L' && chunk.length > 1) {
123
+ //simplified = simplifyLinetoSequence(chunk, thresh);
124
+ //console.log('lineto');
125
+ simplified = simplifyLinetoSequence(chunk);
126
+ pathDataSimple.push(...simplified);
127
+ }
128
+
129
+ // Béziers
130
+ else if (chunk.length > 1 && (type === 'C' || type === 'Q')) {
131
+ //console.log('hasCubics');
132
+ if (chunk.length) {
133
+
134
+ multipass = false
135
+ //multipass = true
136
+
137
+ let directionChange = chunk[0].directionChange;
138
+ //directionChange = false
139
+
140
+ /**
141
+ * prevent too aggressive simplification
142
+ * e.g for quadratic glyphs
143
+ * by splitting large chunks in two
144
+ */
145
+ //keepDetails = false
146
+
147
+ //(directionChange && chunk.length > 4) || (!directionChange && chunk.length > 4)
148
+ if (keepDetails && (chunk.length > 4) && !multipass) {
149
+ let split = Math.ceil((chunk.length - 1) / 2)
150
+ let chunk1 = chunk.slice(0, split)
151
+ let chunk2 = chunk.slice(split)
152
+ //console.log('chunk:', chunk);
153
+ //renderPoint(svg1,chunk[0].p0, 'magenta' )
154
+
155
+ //console.log('forceCubic keepDetails', forceCubic);
156
+ let simplified1 = simplifyBezierSequence(chunk1, tolerance, keepDetails, forceCubic);
157
+ let simplified2 = simplifyBezierSequence(chunk2, tolerance, keepDetails, forceCubic);
158
+
159
+ pathDataSimple.push(...simplified1, ...simplified2);
160
+ }
161
+
162
+ else {
163
+ simplified = simplifyBezierSequence(chunk, tolerance, keepDetails, forceCubic);
164
+ pathDataSimple.push(...simplified);
165
+ }
166
+ }
167
+ }
168
+
169
+ // No match, keep original commands
170
+ else {
171
+ //chunk.forEach(com => pathDataSimple.push({ type: com.type, values: com.values }));
172
+ pathDataSimple.push(...chunk);
173
+ }
174
+ }
175
+ }
176
+
177
+
178
+ /**
179
+ * try to replace cubics
180
+ * to arcs
181
+ */
182
+ //cubicToArc = false;
183
+ if (cubicToArc) {
184
+ //console.log();
185
+ pathDataSimple = replaceCubicsByArcs(pathDataSimple, tolerance * 0.5);
186
+
187
+ // combine adjacent arcs
188
+ pathDataSimple = combineArcs(pathDataSimple);
189
+
190
+ console.log('arcs', pathDataSimple);
191
+ }
192
+
193
+
194
+ // rescale small paths
195
+ if (scale != 1) pathDataSimple = scalePathData(pathDataSimple, 1 / scale, 1 / scale)
196
+
197
+
198
+ /**
199
+ * final area check
200
+ * fallback to original if difference is too large
201
+ */
202
+ /*
203
+ let areaS = getPathArea(pathDataSimple);
204
+ let areaDiff = getRelativeAreaDiff(area0, areaS)
205
+
206
+ if (areaDiff > tolerance) {
207
+ //pathDataSimple = pathData;
208
+ //console.log('take original', pathDataSimple);
209
+ }
210
+ */
211
+
212
+
213
+ /**
214
+ * final optimization
215
+ * simplify adjacent linetos
216
+ * optimize start points
217
+ * we done it before
218
+ * but we need to apply this again to
219
+ * avoid unnecessary close linetos
220
+ */
221
+
222
+ // prefer first lineto to allow implicit closing linetos by "Z"
223
+ removeFinalLineto = true;
224
+ startToTop = false;
225
+ addExtremes = false;
226
+ debug = false;
227
+
228
+ pathDataSimple = cleanUpPathData(pathDataSimple, addExtremes, removeFinalLineto, startToTop, debug)
229
+ console.log('pathDataSimple post', pathDataSimple);
230
+
231
+ return pathDataSimple;
232
+ }
233
+
234
+
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+
245
+
246
+
247
+
248
+