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,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,154 @@
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
+ // add average distances
25
+ for (let i = 0, len = pathData.length; i < len; i++) {
26
+ let com = pathData[i];
27
+ let { type, values } = com;
28
+
29
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
30
+ p={x:lastVals[0], y:lastVals[1]}
31
+
32
+ // use existing averave dimension value or calculate
33
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0
34
+ //let dimA = +getDistAv(p0, p).toFixed(8)
35
+ //console.log('dimA', dimA, com.dimA, type);
36
+
37
+ if(dimA && dimA<minDim) minDim = dimA;
38
+ //if(dimA && dimA>maxDim) maxDim = dimA;
39
+
40
+
41
+ if(type==='M'){
42
+ M=p;
43
+ }
44
+ p0 = p;
45
+ }
46
+
47
+ //minDim = +minDim.toFixed(8)
48
+ let decimalsAuto = Math.floor(50 / minDim).toString().length
49
+ //console.log('!!!minDim', minDim, 'maxDim', maxDim, decimalsAuto);
50
+
51
+ // clamp
52
+ return Math.min(Math.max(0, decimalsAuto), 8)
53
+
54
+ }
55
+
56
+
57
+
58
+
59
+
60
+ export function detectAccuracy_back(pathData) {
61
+
62
+ // Reference first MoveTo command (M)
63
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
64
+ let p0 = { ...M };
65
+ pathData[0].decimals = 0
66
+ let lastDec = 0;
67
+ let maxDecimals = 0
68
+
69
+ for (let i = 1, len = pathData.length; i < len; i++) {
70
+ let com = pathData[i];
71
+ let { type, values } = com;
72
+
73
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
74
+ let lastX = lastVals[0];
75
+ let lastY = lastVals[1];
76
+
77
+ if (type === 'Z' || type === 'z') {
78
+ lastX = M.x;
79
+ lastY = M.y;
80
+ }
81
+
82
+ let w = Math.abs(p0.x - lastX);
83
+ let h = Math.abs(p0.y - lastY);
84
+ let dimA = (w + h) / 2 || 0;
85
+
86
+
87
+ // Determine decimal places dynamically
88
+ let decimals = (type !== 'Z' && type !== 'z') ? Math.ceil((1 / dimA)).toString().length + 1 : 0;
89
+
90
+ //console.log(type, dimA, decimals);
91
+
92
+
93
+ if (dimA === 0) {
94
+ //console.log('zero length');
95
+ decimals = lastDec;
96
+ }
97
+
98
+ else if (decimals && dimA < 0.5) {
99
+ decimals++
100
+ }
101
+
102
+ //console.log('dimA', type, dimA, decimals);
103
+
104
+
105
+ // Update previous coordinates
106
+ p0 = { x: lastX, y: lastY };
107
+
108
+ // Track MoveTo for closing paths
109
+ if (type === 'M') {
110
+ M = { x: values[0], y: values[1] };
111
+ com.decimals = decimals;
112
+ } else {
113
+
114
+ // Store ideal precision for next pass
115
+ com.decimals = decimals;
116
+
117
+ }
118
+
119
+ maxDecimals = decimals > maxDecimals ? decimals : maxDecimals;
120
+ lastDec = decimals;
121
+ }
122
+
123
+ // set max decimal for M
124
+ return maxDecimals
125
+ //pathData[0].decimals = maxDecimals
126
+ //return pathData
127
+ }
128
+
129
+
130
+ /**
131
+ * round path data
132
+ * either by explicit decimal value or
133
+ * based on suggested accuracy in path data
134
+ */
135
+ export function roundPathData(pathData, decimals = -1) {
136
+ // has recommended decimals
137
+ let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
138
+ //console.log('decimals', decimals, hasDecimal);
139
+
140
+ for(let c=0, len=pathData.length; c<len; c++){
141
+ let com=pathData[c];
142
+ let {type, values} = com
143
+
144
+ if (decimals >-1 || hasDecimal) {
145
+ decimals = hasDecimal ? com.decimals : decimals;
146
+
147
+
148
+ //console.log('decimals', type, decimals);
149
+ pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
150
+
151
+ }
152
+ };
153
+ return pathData;
154
+ }
@@ -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
+