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,265 @@
1
+
2
+ //import { splitSubpaths } from "./convert_segments";
3
+ import { splitSubpaths } from './pathData_split.js';
4
+
5
+ import { pointAtT, svgArcToCenterParam, getAngle, checkLineIntersection } from "./geometry";
6
+ import { getSubPathBBoxes, checkBBoxIntersections } from "./geometry_bbox";
7
+ import { renderPoint } from './visualize.js';
8
+
9
+
10
+ /**
11
+ * get pathdata area
12
+ */
13
+
14
+ export function getPathArea(pathData, decimals = 9) {
15
+ let totalArea = 0;
16
+ let polyPoints = [];
17
+
18
+ //check subpaths
19
+ let subPathsData = splitSubpaths(pathData);
20
+ let isCompoundPath = subPathsData.length > 1 ? true : false;
21
+ let counterShapes = [];
22
+
23
+ // check intersections for compund paths
24
+ if (isCompoundPath) {
25
+ let bboxArr = getSubPathBBoxes(subPathsData);
26
+
27
+ bboxArr.forEach(function (bb, b) {
28
+ //let path1 = path;
29
+ for (let i = 0; i < bboxArr.length; i++) {
30
+ let bb2 = bboxArr[i];
31
+ if (bb != bb2) {
32
+ let intersects = checkBBoxIntersections(bb, bb2);
33
+ if (intersects) {
34
+ counterShapes.push(i);
35
+ }
36
+ }
37
+ }
38
+ });
39
+ }
40
+
41
+ subPathsData.forEach((pathData, d) => {
42
+ //reset polygon points for each segment
43
+ polyPoints = [];
44
+ let comArea = 0;
45
+ let pathArea = 0;
46
+ let multiplier = 1;
47
+ let pts = [];
48
+
49
+ pathData.forEach(function (com, i) {
50
+ let [type, values] = [com.type, com.values];
51
+ let valuesL = values.length;
52
+
53
+ if (values.length) {
54
+ let prevC = i > 0 ? pathData[i - 1] : pathData[0];
55
+ let prevCVals = prevC.values;
56
+ let prevCValsL = prevCVals.length;
57
+ let p0 = { x: prevCVals[prevCValsL - 2], y: prevCVals[prevCValsL - 1] };
58
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
59
+
60
+ // C commands
61
+ if (type === 'C' || type === 'Q') {
62
+ let cp1 = { x: values[0], y: values[1] };
63
+ pts = type === 'C' ? [p0, cp1, { x: values[2], y: values[3] }, p] : [p0, cp1, p];
64
+ let areaBez = Math.abs(getBezierArea(pts))
65
+ comArea += areaBez;
66
+
67
+ //push points to calculate inner/remaining polygon area
68
+ polyPoints.push(p0, p);
69
+ }
70
+
71
+
72
+ // A commands
73
+ else if (type === 'A') {
74
+ let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], com.values[3], com.values[4], p.x, p.y)
75
+ let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle } = arcData
76
+
77
+ let arcArea = Math.abs(getEllipseArea(rx, ry, startAngle, endAngle));
78
+
79
+ // subtract remaining polygon between p0, center and p
80
+ let polyArea = Math.abs(getPolygonArea([p0, { x: cx, y: cy }, p]));
81
+ arcArea -= polyArea;
82
+
83
+ //push points to calculate inner/remaining polygon area
84
+ polyPoints.push(p0, p);
85
+ comArea += arcArea;
86
+ }
87
+
88
+ // L commands
89
+ else {
90
+ polyPoints.push(p0, p);
91
+ }
92
+ }
93
+ });
94
+
95
+
96
+ let areaPoly = getPolygonArea(polyPoints);
97
+
98
+ //subtract area by negative multiplier
99
+ if (counterShapes.indexOf(d) !== -1) {
100
+ multiplier = -1;
101
+ }
102
+
103
+ //values have the same sign - subtract polygon area
104
+ if (
105
+ (areaPoly < 0 && comArea < 0)
106
+ ) {
107
+ // are negative
108
+ pathArea = (Math.abs(comArea) - Math.abs(areaPoly)) * multiplier;
109
+ //console.log('negative area', pathArea );
110
+ } else {
111
+ pathArea = (Math.abs(comArea) + Math.abs(areaPoly)) * multiplier;
112
+ }
113
+
114
+ totalArea += pathArea;
115
+ })
116
+
117
+ //if(decimals>-1) totalArea = +totalArea.toFixed(decimals)
118
+ //console.log('negative area', totalArea );
119
+
120
+ return totalArea;
121
+ }
122
+
123
+
124
+ /**
125
+ * compare bezier area diffs
126
+ */
127
+ export function getBezierAreaAccuracy(cpts = [], areaPath = 0, areaPoly = 0, tolerance = 0.75) {
128
+
129
+ let type = cpts.length === 4 ? 'C' : 'Q';
130
+ let p0 = cpts.shift();
131
+ let cp1 = cpts[0];
132
+ let p = cpts[cpts.length - 1];
133
+ let cp2 = type === 'C' ? cpts[1] : cp1;
134
+
135
+ let res = { accurate: false, areaDiff: Infinity, comArea: null, cpArea: null, signChange: null }
136
+
137
+ /**
138
+ * check self intersections
139
+ * won't work for simplifications
140
+ */
141
+ let selfIntersecting = checkLineIntersection(p0, p, cp1, cp2, true);
142
+ let selfIntersecting2 = checkLineIntersection(p0, cp1, p, cp2, true);
143
+ if (selfIntersecting || selfIntersecting2) {
144
+ //renderPoint(svg1, p, 'yellow')
145
+ return res
146
+ }
147
+
148
+
149
+
150
+ /**
151
+ * check sign changes
152
+ * from cpts poly
153
+ * they indicate a wrong approximation
154
+ */
155
+
156
+ //cpArea = getPolygonArea([p0, cp1, p]);
157
+ res.cpArea = getPolygonArea([p0, ...cpts]);
158
+ res.signChange = (res.cpArea < 0 && areaPoly > 0) || (res.cpArea > 0 && areaPoly < 0);
159
+
160
+ //console.log('signChange', areaDiff, signChange, cpArea, areaPoly);
161
+
162
+ if (res.signChange) {
163
+ //areaDiff = Infinity
164
+ return res
165
+ }
166
+
167
+ /**
168
+ * check bézier area
169
+ */
170
+ let com = [
171
+ { type: 'M', values: [p0.x, p0.y] },
172
+ { type: type, values: [...cpts.map(pt => [pt.x, pt.y]).flat()] }
173
+ ];
174
+ res.comArea = getPathArea(com);
175
+ res.areaDiff = getRelativeAreaDiff(areaPath, res.comArea);
176
+
177
+
178
+ // very accurate - used to skip alternative calculations
179
+ res.accurate = res.areaDiff < tolerance * 0.3;
180
+
181
+ return res
182
+ }
183
+
184
+
185
+ /**
186
+ * get ellipse area
187
+ * skips to circle calculation if rx===ry
188
+ */
189
+
190
+ export function getEllipseArea(rx, ry, startAngle, endAngle) {
191
+ const totalArea = Math.PI * rx * ry;
192
+ let angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
193
+ // If circle, use simple circular formula
194
+ if (rx === ry) return totalArea * (angleDiff / (2 * Math.PI));
195
+
196
+ // Convert absolute angles to parametric angles
197
+ const absoluteToParametric = (phi)=>{
198
+ return Math.atan2(rx * Math.sin(phi), ry * Math.cos(phi));
199
+ }
200
+ startAngle = absoluteToParametric(startAngle);
201
+ endAngle = absoluteToParametric(endAngle);
202
+ angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
203
+ return totalArea * (angleDiff / (2 * Math.PI));
204
+ }
205
+
206
+
207
+
208
+ /**
209
+ * compare areas
210
+ * for thresholds
211
+ * returns a percentage value
212
+ */
213
+
214
+ export function getRelativeAreaDiff(area0, area1) {
215
+ let diff = Math.abs(area0 - area1);
216
+ return Math.abs(100 - (100 / area0 * (area0 + diff)))
217
+ }
218
+
219
+
220
+
221
+
222
+ /**
223
+ * get bezier area
224
+ */
225
+ export function getBezierArea(pts, absolute=false) {
226
+
227
+ let [p0, cp1, cp2, p] = [pts[0], pts[1], pts[2], pts[pts.length - 1]]
228
+ let area;
229
+
230
+ if (pts.length < 3) return 0;
231
+
232
+ // quadratic beziers
233
+ if (pts.length === 3) {
234
+ cp1 = {
235
+ x: pts[0].x * 1 / 3 + pts[1].x * 2 / 3,
236
+ y: pts[0].y * 1 / 3 + pts[1].y * 2 / 3
237
+ }
238
+
239
+ cp2 = {
240
+ x: pts[2].x * 1 / 3 + pts[1].x * 2 / 3,
241
+ y: pts[2].y * 1 / 3 + pts[1].y * 2 / 3
242
+ }
243
+ }
244
+
245
+ area = ((p0.x * (-2 * cp1.y - cp2.y + 3 * p.y) +
246
+ cp1.x * (2 * p0.y - cp2.y - p.y) +
247
+ cp2.x * (p0.y + cp1.y - 2 * p.y) +
248
+ p.x * (-3 * p0.y + cp1.y + 2 * cp2.y)) *
249
+ 3) / 20;
250
+
251
+ return absolute ? Math.abs(area) : area;
252
+ }
253
+
254
+ export function getPolygonArea(points, absolute=false) {
255
+ let area = 0;
256
+ for (let i = 0, len = points.length; len && i < len; i++) {
257
+ let addX = points[i].x;
258
+ let addY = points[i === points.length - 1 ? 0 : i + 1].y;
259
+ let subX = points[i === points.length - 1 ? 0 : i + 1].x;
260
+ let subY = points[i].y;
261
+ area += addX * addY * 0.5 - subX * subY * 0.5;
262
+ }
263
+ if(absolute) area=Math.abs(area);
264
+ return area;
265
+ }
@@ -0,0 +1,223 @@
1
+
2
+ //import { splitSubpaths } from "./convert_segments";
3
+ import { pointAtT, svgArcToCenterParam, getBezierExtremeT, getArcExtemes, getDistance, interpolate, getPointOnEllipse } from "./geometry";
4
+ import { renderPoint } from "./visualize";
5
+ //import {arcToBezier} from'./pathData_convert';
6
+
7
+
8
+ /**
9
+ * calculate polygon bbox
10
+ */
11
+ export function getPolyBBox(vertices, decimals = -1) {
12
+ let xArr = vertices.map(pt => pt.x);
13
+ let yArr = vertices.map(pt => pt.y);
14
+ let left = Math.min(...xArr)
15
+ let right = Math.max(...xArr)
16
+ let top = Math.min(...yArr)
17
+ let bottom = Math.max(...yArr)
18
+ let bb = {
19
+ x: left,
20
+ left: left,
21
+ right: right,
22
+ y: top,
23
+ top: top,
24
+ bottom: bottom,
25
+ width: right - left,
26
+ height: bottom - top
27
+ };
28
+
29
+ // round
30
+
31
+ if (decimals > -1) {
32
+ for (let prop in bb) {
33
+ bb[prop] = +bb[prop].toFixed(decimals)
34
+ }
35
+ }
36
+ //console.log(bb);
37
+ return bb;
38
+ }
39
+
40
+ export function getSubPathBBoxes(subPaths) {
41
+ let bboxArr = [];
42
+ subPaths.forEach((pathData) => {
43
+ //let bb = getPathDataBBox(pathData)
44
+ let bb = getPathDataBBox_sloppy(pathData);
45
+ bboxArr.push(bb);
46
+ });
47
+ //console.log('bboxArr', bboxArr);
48
+ return bboxArr;
49
+ }
50
+
51
+ export function checkBBoxIntersections(bb, bb1) {
52
+ let [x, y, width, height, right, bottom] = [
53
+ bb.x,
54
+ bb.y,
55
+ bb.width,
56
+ bb.height,
57
+ bb.x + bb.width,
58
+ bb.y + bb.height
59
+ ];
60
+ let [x1, y1, width1, height1, right1, bottom1] = [
61
+ bb1.x,
62
+ bb1.y,
63
+ bb1.width,
64
+ bb1.height,
65
+ bb1.x + bb1.width,
66
+ bb1.y + bb1.height
67
+ ];
68
+ let intersects = false;
69
+ if (width * height != width1 * height1) {
70
+ if (width * height > width1 * height1) {
71
+ if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
72
+ intersects = true;
73
+ }
74
+ }
75
+ }
76
+ return intersects;
77
+ }
78
+
79
+
80
+ /**
81
+ * sloppy path bbox aaproximation
82
+ */
83
+
84
+ export function getPathDataBBox_sloppy(pathData) {
85
+ let pts = getPathDataPoly(pathData);
86
+ let bb = getPolyBBox(pts);
87
+ return bb;
88
+ }
89
+
90
+
91
+ /**
92
+ * get path data poly
93
+ * including command points
94
+ * handy for faster/sloppy bbox approximations
95
+ */
96
+
97
+ export function getPathDataPoly(pathData) {
98
+
99
+ let poly = [];
100
+ for (let i = 0; i < pathData.length; i++) {
101
+ let com = pathData[i]
102
+ let prev = i > 0 ? pathData[i - 1] : pathData[i];
103
+ let { type, values } = com;
104
+ let p0 = { x: prev.values[prev.values.length - 2], y: prev.values[prev.values.length - 1] };
105
+ let p = values.length ? { x: values[values.length - 2], y: values[values.length - 1] } : ''
106
+ let cp1 = values.length ? { x: values[0], y: values[1] } : ''
107
+
108
+ switch (type) {
109
+
110
+ // convert to cubic to get polygon
111
+ case 'A':
112
+
113
+ let [, , xAxisRotation, largeArc, sweep, x1, y1] = com;
114
+
115
+ if (typeof arcToBezier !== 'function') {
116
+
117
+ // get real radii
118
+ let rx = getDistance(p0, p) / 2;
119
+ let ptMid = interpolate(p0, p, 0.5);
120
+
121
+ let pt1 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, 0)
122
+ let pt2 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, Math.PI)
123
+ poly.push(pt1, pt2, p)
124
+
125
+ //console.log('has no arc to cubic conversion');
126
+ break;
127
+ }
128
+ let cubic = arcToBezier(p0, values)
129
+ cubic.forEach(com => {
130
+ let vals = com.values
131
+ let cp1 = { x: vals[0], y: vals[1] }
132
+ let cp2 = { x: vals[2], y: vals[3] }
133
+ let p = { x: vals[4], y: vals[5] }
134
+ poly.push(cp1, cp2, p)
135
+ })
136
+ break;
137
+
138
+ case 'C':
139
+ let cp2 = { x: values[2], y: values[3] }
140
+ poly.push(cp1, cp2)
141
+ break;
142
+ case 'Q':
143
+ poly.push(cp1)
144
+ break;
145
+ }
146
+
147
+ // M and L commands
148
+ if (type.toLowerCase() !== 'z') {
149
+ poly.push(p)
150
+ }
151
+ }
152
+
153
+ return poly;
154
+ }
155
+
156
+
157
+ /**
158
+ * get exact path BBox
159
+ * calculating extremes for all command types
160
+ */
161
+
162
+ export function getPathDataBBox(pathData) {
163
+
164
+ // save extreme values
165
+ let xMin = Infinity;
166
+ let xMax = -Infinity;
167
+ let yMin = Infinity;
168
+ let yMax = -Infinity;
169
+
170
+ const setXYmaxMin = (pt) => {
171
+ if (pt.x < xMin) {
172
+ xMin = pt.x
173
+ }
174
+ if (pt.x > xMax) {
175
+ xMax = pt.x
176
+ }
177
+ if (pt.y < yMin) {
178
+ yMin = pt.y
179
+ }
180
+ if (pt.y > yMax) {
181
+ yMax = pt.y
182
+ }
183
+ }
184
+
185
+ for (let i = 0; i < pathData.length; i++) {
186
+ let com = pathData[i]
187
+ let { type, values } = com;
188
+ let valuesL = values.length;
189
+ let comPrev = pathData[i - 1] ? pathData[i - 1] : pathData[i];
190
+ let valuesPrev = comPrev.values;
191
+ let valuesPrevL = valuesPrev.length;
192
+
193
+ if (valuesL) {
194
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
195
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
196
+ // add final on path point
197
+ setXYmaxMin(p)
198
+
199
+ if (type === 'C' || type === 'Q') {
200
+ let cp1 = { x: values[0], y: values[1] };
201
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
202
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
203
+
204
+ let bezierExtremesT = getBezierExtremeT(pts)
205
+ bezierExtremesT.forEach(t => {
206
+ let pt = pointAtT(pts, t);
207
+ setXYmaxMin(pt)
208
+ })
209
+ }
210
+
211
+ else if (type === 'A') {
212
+ let arcExtremes = getArcExtemes(p0, values)
213
+ arcExtremes.forEach(pt => {
214
+ setXYmaxMin(pt)
215
+ })
216
+ }
217
+ }
218
+ }
219
+
220
+ let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }
221
+ return bbox
222
+ }
223
+