svg-path-simplify 0.0.9 → 0.1.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.
@@ -25,9 +25,9 @@ function renderPoint(
25
25
  }
26
26
  }
27
27
 
28
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
28
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
29
29
 
30
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
30
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
31
31
 
32
32
  if (render) {
33
33
  svg.insertAdjacentHTML("beforeend", path);
@@ -105,18 +105,62 @@ function getAngle(p1, p2, normalize = false) {
105
105
  return angle
106
106
  }
107
107
 
108
+ function getDeltaAngle(centerPoint, startPoint, endPoint, largeArc = false) {
109
+
110
+ const normalizeAngle = (angle) => {
111
+ let normalized = angle % (2 * Math.PI);
112
+
113
+ if (normalized > Math.PI) {
114
+ normalized -= 2 * Math.PI;
115
+ } else if (normalized <= -Math.PI) {
116
+ normalized += 2 * Math.PI;
117
+ }
118
+ return normalized;
119
+ };
120
+
121
+ let startAngle = Math.atan2(
122
+ startPoint.y - centerPoint.y,
123
+ startPoint.x - centerPoint.x
124
+ );
125
+
126
+ let endAngle = Math.atan2(
127
+ endPoint.y - centerPoint.y,
128
+ endPoint.x - centerPoint.x
129
+ );
130
+
131
+ // Calculate raw delta angle (difference)
132
+ let deltaAngle = endAngle - startAngle;
133
+
134
+ // Normalize the delta angle to range (-π, π]
135
+ deltaAngle = normalizeAngle(deltaAngle);
136
+
137
+ if (largeArc) deltaAngle = Math.PI*2 - Math.abs(deltaAngle);
138
+
139
+ let phi = 180 / Math.PI;
140
+ let startAngleDeg = startAngle * phi;
141
+ let endAngleDeg = endAngle * phi;
142
+ let deltaAngleDeg = deltaAngle * phi;
143
+
144
+ return {
145
+ startAngle, endAngle, deltaAngle, startAngleDeg,
146
+ endAngleDeg,
147
+ deltaAngleDeg
148
+ };
149
+
150
+ }
151
+
108
152
  /**
109
153
  * based on: Justin C. Round's
110
154
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
111
155
  */
112
156
 
113
- function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
157
+ function checkLineIntersection(p1 = null, p2 = null, p3 = null, p4 = null, exact = true, debug = false) {
114
158
  // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
115
159
  let denominator, a, b, numerator1, numerator2;
116
160
  let intersectionPoint = {};
117
161
 
118
- if(!p1 || !p2 || !p3 || !p4){
119
- if(debug) console.warn('points missing');
162
+ if (!p1 || !p2 || !p3 || !p4) {
163
+ if (debug) console.warn('points missing');
120
164
  return false
121
165
  }
122
166
 
@@ -126,7 +170,7 @@ function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true,
126
170
  return false;
127
171
  }
128
172
  } catch {
129
- if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
173
+ if (debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
130
174
  return false
131
175
  }
132
176
 
@@ -483,6 +527,17 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
483
527
  return arcData;
484
528
  }
485
529
 
530
+ function rotatePoint(pt, cx, cy, rotation = 0, convertToRadians = false) {
531
+ if (!rotation) return pt;
532
+
533
+ rotation = convertToRadians ? (rotation / 180) * Math.PI : rotation;
534
+
535
+ return {
536
+ x: cx + (pt.x - cx) * Math.cos(rotation) - (pt.y - cy) * Math.sin(rotation),
537
+ y: cy + (pt.x - cx) * Math.sin(rotation) + (pt.y - cy) * Math.cos(rotation)
538
+ };
539
+ }
540
+
486
541
  function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
487
542
 
488
543
  // Convert degrees to radians
@@ -516,6 +571,18 @@ function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametri
516
571
  return pt
517
572
  }
518
573
 
574
+ // to parametric angle helper
575
+ function toParametricAngle(angle, rx, ry) {
576
+
577
+ if (rx === ry || (angle % PI$1 * 0.5 === 0)) return angle;
578
+ let angleP = atan$1(tan$1(angle) * (rx / ry));
579
+
580
+ // Ensure the parametric angle is in the correct quadrant
581
+ angleP = cos$1(angle) < 0 ? angleP + PI$1 : angleP;
582
+
583
+ return angleP
584
+ }
585
+
519
586
  function bezierhasExtreme(p0, cpts = [], angleThreshold = 0.05) {
520
587
  let isCubic = cpts.length === 3 ? true : false;
521
588
  let cp1 = cpts[0] || null;
@@ -2331,13 +2398,13 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2331
2398
  let cp2X = interpolate(p, cp2, 1.5);
2332
2399
 
2333
2400
  let dist0 = getDistAv(p0, p);
2334
- let threshold = dist0 * 0.01;
2401
+ let threshold = dist0 * 0.03;
2335
2402
  let dist1 = getDistAv(cp1X, cp2X);
2336
2403
 
2337
2404
  let cp1_Q = null;
2338
2405
  let type = 'C';
2339
2406
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2340
- let comN = {type, values};
2407
+ let comN = { type, values };
2341
2408
 
2342
2409
  if (dist1 && threshold && dist1 < threshold) {
2343
2410
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
@@ -2347,6 +2414,7 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2347
2414
  comN.values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2348
2415
  comN.p0 = p0;
2349
2416
  comN.cp1 = cp1_Q;
2417
+ comN.cp2 = null;
2350
2418
  comN.p = p;
2351
2419
  }
2352
2420
  }
@@ -2364,7 +2432,7 @@ function convertPathData(pathData, {
2364
2432
  if (toShorthands) pathData = pathDataToShorthands(pathData);
2365
2433
 
2366
2434
  // pre round - before relative conversion to minimize distortions
2367
- if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
2435
+ if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
2368
2436
  if (toRelative) pathData = pathDataToRelative(pathData);
2369
2437
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
2370
2438
 
@@ -2701,7 +2769,7 @@ function pathDataToShorthands(pathData, decimals = -1, test = false) {
2701
2769
  let com = pathData[i];
2702
2770
  let { type, values } = com;
2703
2771
  let valuesLen = values.length;
2704
- let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
2772
+ let valuesLast = [values[valuesLen - 2], values[valuesLen - 1]];
2705
2773
 
2706
2774
  // previoius command
2707
2775
  let comPrev = pathData[i - 1];
@@ -2829,6 +2897,108 @@ function pathDataToShorthands(pathData, decimals = -1, test = false) {
2829
2897
  return pathDataShorts;
2830
2898
  }
2831
2899
 
2900
+ /**
2901
+ * Convert a parametrized SVG arc to cubic Beziers
2902
+ * Assumes arc parameters are already resolved
2903
+ */
2904
+ function arcToBezierResolved({
2905
+
2906
+ // start / end points
2907
+ p0 = { x: 0, y: 0 },
2908
+ p = { x: 0, y: 0 },
2909
+
2910
+ // center
2911
+ centroid = { x: 0, y: 0 },
2912
+
2913
+ // radii
2914
+ rx = 0,
2915
+ ry = 0,
2916
+
2917
+ // SVG-style rotation
2918
+ xAxisRotation = 0,
2919
+ radToDegree = false,
2920
+
2921
+ // optional
2922
+ startAngle = null,
2923
+ endAngle = null,
2924
+ deltaAngle = null
2925
+
2926
+ } = {}) {
2927
+
2928
+ if (!rx || !ry) return [];
2929
+
2930
+ // new pathData
2931
+ let pathData = [];
2932
+
2933
+ // maximum delta for cubic approximations: Math.PI / 2 (90deg)
2934
+ const maxSegAngle = 1.5707963267948966;
2935
+
2936
+ // Pomax cubic constant
2937
+ const k = 0.551785;
2938
+
2939
+ // rotation
2940
+ let phi = radToDegree
2941
+ ? xAxisRotation
2942
+ : xAxisRotation * Math.PI / 180;
2943
+
2944
+ let cosphi = Math.cos(phi);
2945
+ let sinphi = Math.sin(phi);
2946
+
2947
+ // derive angles if not provided
2948
+ if (startAngle === null || endAngle === null || deltaAngle === null) {
2949
+ ({ startAngle, endAngle, deltaAngle } = getDeltaAngle(centroid, p0, p));
2950
+ }
2951
+
2952
+ // parametrize for elliptic arcs
2953
+ let startAngleParam = rx !== ry ? toParametricAngle(startAngle, rx, ry) : startAngle;
2954
+
2955
+ let deltaAngleParam = rx !== ry ? toParametricAngle(deltaAngle, rx, ry) : deltaAngle;
2956
+
2957
+ let segments = Math.max(1, Math.ceil(Math.abs(deltaAngleParam) / maxSegAngle));
2958
+ let angStep = deltaAngleParam / segments;
2959
+
2960
+ for (let i = 0; i < segments; i++) {
2961
+
2962
+ const a = Math.abs(angStep) === maxSegAngle ?
2963
+ Math.sign(angStep) * k :
2964
+ (4 / 3) * Math.tan(angStep / 4);
2965
+
2966
+ let cos0 = Math.cos(startAngleParam);
2967
+ let sin0 = Math.sin(startAngleParam);
2968
+ let cos1 = Math.cos(startAngleParam + angStep);
2969
+ let sin1 = Math.sin(startAngleParam + angStep);
2970
+
2971
+ // unit arc → cubic
2972
+ let c1 = { x: cos0 - sin0 * a, y: sin0 + cos0 * a };
2973
+ let c2 = { x: cos1 + sin1 * a, y: sin1 - cos1 * a };
2974
+ let e = { x: cos1, y: sin1 };
2975
+
2976
+ let values = [];
2977
+
2978
+ [c1, c2, e].forEach(pt => {
2979
+ let x = pt.x * rx;
2980
+ let y = pt.y * ry;
2981
+
2982
+ values.push(
2983
+ cosphi * x - sinphi * y + centroid.x,
2984
+ sinphi * x + cosphi * y + centroid.y
2985
+ );
2986
+ });
2987
+
2988
+ pathData.push({
2989
+ type: 'C',
2990
+ values,
2991
+ cp1: { x: values[0], y: values[1] },
2992
+ cp2: { x: values[2], y: values[3] },
2993
+ p: { x: values[4], y: values[5] },
2994
+ });
2995
+
2996
+ startAngleParam += angStep;
2997
+ }
2998
+
2999
+ return pathData;
3000
+ }
3001
+
2832
3002
  /**
2833
3003
  * convert arctocommands to cubic bezier
2834
3004
  * based on puzrin's a2c.js
@@ -2963,10 +3133,6 @@ function arcToBezier$1(p0, values, splitSegments = 1) {
2963
3133
  return pathDataArc;
2964
3134
  }
2965
3135
 
2966
- /**
2967
- * cubics to arcs
2968
- */
2969
-
2970
3136
  function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
2971
3137
 
2972
3138
  let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
@@ -3992,7 +4158,7 @@ function pathDataRemoveColinear(pathData, {
3992
4158
  let valsL = values.slice(-2);
3993
4159
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
3994
4160
 
3995
- let area = getPolygonArea([p0, p, p1], true);
4161
+ let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
3996
4162
 
3997
4163
  let distSquare = getSquareDistance(p0, p1);
3998
4164
 
@@ -4022,7 +4188,7 @@ function pathDataRemoveColinear(pathData, {
4022
4188
 
4023
4189
  // colinear – exclude arcs (as always =) as semicircles won't have an area
4024
4190
 
4025
- if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
4191
+ if ( isFlat && c < l - 1 && comN.type!=='A' && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
4026
4192
 
4027
4193
  /*
4028
4194
  console.log(area, distMax );
@@ -4170,25 +4336,43 @@ function pathDataToTopLeft(pathData) {
4170
4336
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4171
4337
  }
4172
4338
 
4173
- function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
4339
+ function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
4174
4340
 
4175
4341
  let pathDataNew = [];
4176
- let len = pathData.length;
4342
+ let l = pathData.length;
4177
4343
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4178
- let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
4344
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4179
4345
 
4180
4346
  let linetos = pathData.filter(com => com.type === 'L');
4181
4347
 
4182
4348
  // check if order is ideal
4183
- let penultimateCom = pathData[len - 2];
4349
+ let idxPenultimate = isClosed ? l-2 : l-1;
4350
+
4351
+ let penultimateCom = pathData[idxPenultimate];
4184
4352
  let penultimateType = penultimateCom.type;
4185
4353
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4186
4354
 
4187
4355
  // last L command ends at M
4188
4356
  let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4189
4357
 
4358
+ // add closepath Z to enable order optimizations
4359
+ if(!isClosed && autoClose && isClosingCommand){
4360
+
4361
+ /*
4362
+ // adjust final coords
4363
+ let valsLast = pathData[idxPenultimate].values
4364
+ let valsLastLen = valsLast.length;
4365
+ pathData[idxPenultimate].values[valsLastLen-2] = M.x
4366
+ pathData[idxPenultimate].values[valsLastLen-1] = M.y
4367
+ */
4368
+
4369
+ pathData.push({type:'Z', values:[]});
4370
+ isClosed = true;
4371
+ l++;
4372
+ }
4373
+
4190
4374
  // if last segment is not closing or a lineto
4191
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateType === 'L');
4375
+ let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
4192
4376
  skipReorder = false;
4193
4377
 
4194
4378
  // we can't change starting point for non closed paths
@@ -4201,7 +4385,7 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
4201
4385
  if (!skipReorder) {
4202
4386
 
4203
4387
  let indices = [];
4204
- for (let i = 0, len = pathData.length; i < len; i++) {
4388
+ for (let i = 0; i < l; i++) {
4205
4389
  let com = pathData[i];
4206
4390
  let { type, values } = com;
4207
4391
  if (values.length) {
@@ -4237,17 +4421,17 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
4237
4421
 
4238
4422
  M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4239
4423
 
4240
- len = pathData.length;
4424
+ l = pathData.length;
4241
4425
 
4242
4426
  // remove last lineto
4243
- penultimateCom = pathData[len - 2];
4427
+ penultimateCom = pathData[l - 2];
4244
4428
  penultimateType = penultimateCom.type;
4245
4429
  penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
4246
4430
 
4247
4431
  isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4248
4432
 
4249
4433
  if (removeFinalLineto && isClosingCommand) {
4250
- pathData.splice(len - 2, 1);
4434
+ pathData.splice(l - 2, 1);
4251
4435
  }
4252
4436
 
4253
4437
  pathDataNew.push(...pathData);
@@ -4509,49 +4693,47 @@ function refineAdjacentExtremes(pathData, {
4509
4693
  function removeEmptySVGEls(svg) {
4510
4694
  let els = svg.querySelectorAll('g, defs');
4511
4695
  els.forEach(el => {
4512
- if (!el.children.length) el.remove();
4696
+ if (!el.children.length) el.remove();
4513
4697
  });
4514
4698
  }
4515
4699
 
4516
4700
  function cleanUpSVG(svgMarkup, {
4517
- returnDom=false,
4518
- removeHidden=true,
4519
- removeUnused=true,
4520
- }={}) {
4701
+ returnDom = false,
4702
+ removeHidden = true,
4703
+ removeUnused = true,
4704
+ } = {}) {
4521
4705
  svgMarkup = cleanSvgPrologue(svgMarkup);
4522
-
4706
+
4523
4707
  // replace namespaced refs
4524
4708
  svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
4525
-
4709
+
4526
4710
  let svg = new DOMParser()
4527
- .parseFromString(svgMarkup, "text/html")
4528
- .querySelector("svg");
4529
-
4530
-
4531
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
4711
+ .parseFromString(svgMarkup, "text/html")
4712
+ .querySelector("svg");
4713
+
4714
+ let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
4532
4715
  removeExcludedAttribues(svg, allowed);
4533
-
4716
+
4534
4717
  let removeEls = ['metadata', 'script'];
4535
-
4718
+
4536
4719
  let els = svg.querySelectorAll('*');
4537
- els.forEach(el=>{
4538
- let name = el.nodeName;
4720
+ els.forEach(el => {
4721
+ let name = el.nodeName;
4539
4722
  // remove hidden elements
4540
4723
  let style = el.getAttribute('style') || '';
4541
4724
  let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
4542
4725
  let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
4543
- if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
4726
+ if (name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden)) {
4544
4727
  el.remove();
4545
- }else {
4728
+ } else {
4546
4729
  // remove BS elements
4547
4730
  removeNameSpaceAtts(el);
4548
4731
  }
4549
4732
  });
4550
4733
 
4551
- if(returnDom) return svg
4734
+ if (returnDom) return svg
4552
4735
 
4553
4736
  let markup = stringifySVG(svg);
4554
- console.log(markup);
4555
4737
 
4556
4738
  return markup;
4557
4739
  }
@@ -4570,7 +4752,7 @@ function cleanSvgPrologue(svgString) {
4570
4752
  );
4571
4753
  }
4572
4754
 
4573
- function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
4755
+ function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
4574
4756
  let atts = [...el.attributes].map((att) => att.name);
4575
4757
  atts.forEach((att) => {
4576
4758
  if (!allowed.includes(att)) {
@@ -4588,13 +4770,13 @@ function removeNameSpaceAtts(el) {
4588
4770
  });
4589
4771
  }
4590
4772
 
4591
- function stringifySVG(svg){
4592
- let markup = new XMLSerializer().serializeToString(svg);
4773
+ function stringifySVG(svg) {
4774
+ let markup = new XMLSerializer().serializeToString(svg);
4593
4775
  markup = markup
4594
- .replace(/\t/g, "")
4595
- .replace(/[\n\r|]/g, "\n")
4596
- .replace(/\n\s*\n/g, '\n')
4597
- .replace(/ +/g, ' ');
4776
+ .replace(/\t/g, "")
4777
+ .replace(/[\n\r|]/g, "\n")
4778
+ .replace(/\n\s*\n/g, '\n')
4779
+ .replace(/ +/g, ' ');
4598
4780
 
4599
4781
  return markup
4600
4782
  }
@@ -4604,12 +4786,19 @@ function refineRoundedCorners(pathData, {
4604
4786
  tolerance = 1
4605
4787
  } = {}) {
4606
4788
 
4789
+ // min size threshold for corners
4790
+ threshold *= tolerance;
4791
+
4607
4792
  let l = pathData.length;
4608
4793
 
4609
4794
  // add fist command
4610
4795
  let pathDataN = [pathData[0]];
4611
4796
 
4612
4797
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4798
+ let zIsLineto = isClosed ?
4799
+ (pathData[l-1].p.x === pathData[0].p0.x && pathData[l-1].p.y === pathData[0].p0.y)
4800
+ : false ;
4801
+
4613
4802
  let lastOff = isClosed ? 2 : 1;
4614
4803
 
4615
4804
  let comLast = pathData[l - lastOff];
@@ -4618,7 +4807,7 @@ function refineRoundedCorners(pathData, {
4618
4807
  let firstIsLine = pathData[1].type === 'L';
4619
4808
  let firstIsBez = pathData[1].type === 'C';
4620
4809
 
4621
- let normalizeClose = isClosed && firstIsBez;
4810
+ let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
4622
4811
 
4623
4812
  // normalize closepath to lineto
4624
4813
  if (normalizeClose) {
@@ -4635,9 +4824,8 @@ function refineRoundedCorners(pathData, {
4635
4824
  // search small cubic segments enclosed by linetos
4636
4825
  if ((type === 'L' && comN && comN.type === 'C') ||
4637
4826
  (type === 'C' && comN && comN.type === 'L')
4638
-
4639
4827
  ) {
4640
- let comL0 = com;
4828
+ let comL0 = type==='L' ? com : null;
4641
4829
  let comL1 = null;
4642
4830
  let comBez = [];
4643
4831
  let offset = 0;
@@ -4650,6 +4838,11 @@ function refineRoundedCorners(pathData, {
4650
4838
 
4651
4839
  }
4652
4840
 
4841
+ if(!comL0) {
4842
+ pathDataN.push(com);
4843
+ continue
4844
+ }
4845
+
4653
4846
  // closing corner to start
4654
4847
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4655
4848
  comL1 = pathData[1];
@@ -4689,43 +4882,41 @@ function refineRoundedCorners(pathData, {
4689
4882
 
4690
4883
  let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0);
4691
4884
 
4692
- if (comBez && !signChange && len3 < threshold && len1 > len3 && len2 > len3) {
4885
+ // exclude mid bezier segments that are larger than surrounding linetos
4886
+ let bezThresh = len3*0.5 * tolerance;
4887
+ let isSmall = bezThresh < len1 && bezThresh < len2 ;
4693
4888
 
4694
- let ptQ = checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false);
4695
- if (ptQ) {
4889
+ if (comBez.length && !signChange && isSmall ) {
4696
4890
 
4697
- /*
4698
- let dist1 = getDistAv(ptQ, comL0.p)
4699
- let dist2 = getDistAv(ptQ, comL1.p0)
4700
- let diff = Math.abs(dist1-dist2)
4701
- let rat = diff/Math.max(dist1, dist2)
4702
- console.log('rat', rat);
4703
- */
4891
+ let isFlatBezier = Math.abs(area2) <= getSquareDistance(comBez[0].p0, comBez[0].p)*0.005;
4892
+ let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false) : null;
4704
4893
 
4705
- /*
4706
- // adjust curve start and end to meet original
4707
- let t = 1
4894
+ if (!isFlatBezier && ptQ) {
4708
4895
 
4709
- let p0_2 = pointAtT([ptQ, comL0.p], t)
4896
+ // final check: mid point proximity
4897
+ let ptM = pointAtT([comL0.p, ptQ, comL1.p0], 0.5);
4710
4898
 
4711
- comL0.p = p0_2
4712
- comL0.values = [p0_2.x, p0_2.y]
4899
+ let ptM_bez = comBez.length===1 ? pointAtT( [comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5 ) : comBez[0].p ;
4713
4900
 
4714
- let p_2 = pointAtT([ptQ, comL1.p0], t)
4901
+ let dist1 = getDistAv(ptM, ptM_bez);
4715
4902
 
4716
- comL1.p0 = p_2
4903
+ // not in tolerance – rturn original command
4904
+ if(dist1>len3){
4717
4905
 
4718
- */
4906
+ pathDataN.push(com);
4907
+ } else {
4719
4908
 
4720
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4721
- comQ.p0 = comL0.p;
4722
- comQ.cp1 = ptQ;
4723
- comQ.p = comL1.p0;
4909
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4910
+ comQ.p0 = comL0.p;
4911
+ comQ.cp1 = ptQ;
4912
+ comQ.p = comL1.p0;
4913
+
4914
+ // add quadratic command
4915
+ pathDataN.push(comL0, comQ);
4916
+ i += offset;
4917
+ continue;
4918
+ }
4724
4919
 
4725
- // add quadratic command
4726
- pathDataN.push(comL0, comQ);
4727
- i += offset;
4728
- continue;
4729
4920
  }
4730
4921
  }
4731
4922
  }
@@ -4741,7 +4932,7 @@ function refineRoundedCorners(pathData, {
4741
4932
  }
4742
4933
 
4743
4934
  // revert close path normalization
4744
- if (normalizeClose) {
4935
+ if (normalizeClose || (isClosed && pathDataN[pathDataN.length-1].type!=='Z') ) {
4745
4936
  pathDataN.push({ type: 'Z', values: [] });
4746
4937
  }
4747
4938
 
@@ -4749,6 +4940,313 @@ function refineRoundedCorners(pathData, {
4749
4940
 
4750
4941
  }
4751
4942
 
4943
+ function getArcFromPoly(pts) {
4944
+ if (pts.length < 3) return false
4945
+
4946
+ // Pick 3 well-spaced points
4947
+ let p1 = pts[0];
4948
+ let p2 = pts[Math.floor(pts.length / 2)];
4949
+ let p3 = pts[pts.length - 1];
4950
+
4951
+ let x1 = p1.x, y1 = p1.y;
4952
+ let x2 = p2.x, y2 = p2.y;
4953
+ let x3 = p3.x, y3 = p3.y;
4954
+
4955
+ let a = x1 - x2;
4956
+ let b = y1 - y2;
4957
+ let c = x1 - x3;
4958
+ let d = y1 - y3;
4959
+
4960
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4961
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
4962
+
4963
+ let det = a * d - b * c;
4964
+
4965
+ if (Math.abs(det) < 1e-10) {
4966
+ console.warn("Points are collinear or numerically unstable");
4967
+ return false;
4968
+ }
4969
+
4970
+ // find center of arc
4971
+ let cx = (d * e - b * f) / det;
4972
+ let cy = (-c * e + a * f) / det;
4973
+ let centroid = { x: cx, y: cy };
4974
+
4975
+ // Radius (use start point)
4976
+ let r = getDistance(centroid, p1);
4977
+
4978
+ let angleData = getDeltaAngle(centroid, p1, p3);
4979
+ let {deltaAngle, startAngle, endAngle} = angleData;
4980
+
4981
+ return {
4982
+ centroid,
4983
+ r,
4984
+ startAngle,
4985
+ endAngle,
4986
+ deltaAngle
4987
+ };
4988
+ }
4989
+
4990
+ function refineRoundSegments(pathData, {
4991
+ threshold = 0,
4992
+ tolerance = 1,
4993
+ // take arcs or cubic beziers
4994
+ toCubic = false,
4995
+ debug = false
4996
+ } = {}) {
4997
+
4998
+ // min size threshold for corners
4999
+ threshold *= tolerance;
5000
+
5001
+ let l = pathData.length;
5002
+
5003
+ // add fist command
5004
+ let pathDataN = [pathData[0]];
5005
+
5006
+ // just for debugging
5007
+ let pathDataTest = [];
5008
+
5009
+ for (let i = 1; i < l; i++) {
5010
+ let com = pathData[i];
5011
+ let { type } = com;
5012
+ let comP = pathData[i - 1];
5013
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
5014
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
5015
+ let comN3 = pathData[i + 3] ? pathData[i + 3] : null;
5016
+ let comBez = null;
5017
+
5018
+ if ((com.type === 'C' || com.type === 'Q')) comBez = com;
5019
+ else if (comN && (comN.type === 'C' || comN.type === 'Q')) comBez = comN;
5020
+
5021
+ let cpts = comBez ? (comBez.type === 'C' ? [comBez.p0, comBez.cp1, comBez.cp2, comBez.p] : [comBez.p0, comBez.cp1, comBez.p]) : [];
5022
+
5023
+ let areaBez = 0;
5024
+ let areaLines = 0;
5025
+ let signChange = false;
5026
+ let L1, L2;
5027
+ let combine = false;
5028
+
5029
+ let p0_S, p_S;
5030
+ let poly = [];
5031
+ let pMid;
5032
+
5033
+ // 2. line-line-bezier-line-line
5034
+ if (
5035
+ comP.type === 'L' &&
5036
+ type === 'L' &&
5037
+ comBez &&
5038
+ comN2.type === 'L' &&
5039
+ comN3 && (comN3.type === 'L' || comN3.type === 'Z')
5040
+ ) {
5041
+
5042
+ L1 = [com.p0, com.p];
5043
+ L2 = [comN2.p0, comN2.p];
5044
+ p0_S = com.p0;
5045
+ p_S = comN2.p;
5046
+
5047
+ // don't allow sign changes
5048
+ areaBez = getPolygonArea(cpts, false);
5049
+ areaLines = getPolygonArea([...L1, ...L2], false);
5050
+ signChange = (areaBez < 0 && areaLines > 0) || (areaBez > 0 && areaLines < 0);
5051
+
5052
+ if (!signChange) {
5053
+
5054
+ // mid point of mid bezier
5055
+ pMid = pointAtT(cpts, 0.5);
5056
+
5057
+ // add to poly
5058
+ poly = [p0_S, pMid, p_S];
5059
+
5060
+ combine = true;
5061
+ }
5062
+
5063
+ }
5064
+
5065
+ // 1. line-bezier-bezier-line
5066
+ else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
5067
+
5068
+ // 1.2 next is cubic next is lineto
5069
+ if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
5070
+
5071
+ combine = true;
5072
+
5073
+ L1 = [comP.p0, comP.p];
5074
+ L2 = [comN2.p0, comN2.p];
5075
+ p0_S = comP.p;
5076
+ p_S = comN2.p0;
5077
+
5078
+ // mid point of mid bezier
5079
+ pMid = comBez.p;
5080
+
5081
+ // add to poly
5082
+ poly = [p0_S, comBez.p, p_S];
5083
+
5084
+ }
5085
+ }
5086
+
5087
+ /**
5088
+ * calculate either combined
5089
+ * cubic or arc commands
5090
+ */
5091
+ if (combine) {
5092
+
5093
+ // try to find center of arc
5094
+ let arcProps = getArcFromPoly(poly);
5095
+ if (arcProps) {
5096
+
5097
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
5098
+
5099
+ let xAxisRotation = 0;
5100
+ let sweep = deltaAngle > 0 ? 1 : 0;
5101
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
5102
+
5103
+ let pCM = rotatePoint(p0_S, centroid.x, centroid.y, deltaAngle * 0.5);
5104
+
5105
+ let dist2 = getDistAv(pCM, pMid);
5106
+ let thresh = getDistAv(p0_S, p_S) * 0.05;
5107
+ let bezierCommands;
5108
+
5109
+ // point is close enough
5110
+ if (dist2 < thresh) {
5111
+
5112
+ bezierCommands = arcToBezierResolved(
5113
+ {
5114
+ p0: p0_S,
5115
+ p: p_S,
5116
+ centroid,
5117
+ rx: r,
5118
+ ry: r,
5119
+ xAxisRotation,
5120
+ sweep,
5121
+ largeArc,
5122
+ deltaAngle,
5123
+ startAngle,
5124
+ endAngle
5125
+ }
5126
+ );
5127
+
5128
+ if(bezierCommands.length === 1){
5129
+
5130
+ // prefer more compact quadratic - otherwise arcs
5131
+ let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
5132
+
5133
+ if (comBezier.type === 'Q') {
5134
+ toCubic = true;
5135
+ }
5136
+
5137
+ com = comBezier;
5138
+ }
5139
+
5140
+ // prefer arcs if 2 cubics are required
5141
+ if (bezierCommands.length > 1) toCubic = false;
5142
+
5143
+ // return elliptic arc commands
5144
+ if (!toCubic) {
5145
+ // rewrite simplified command
5146
+ com.type = 'A';
5147
+ com.values = [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y];
5148
+ }
5149
+
5150
+ com.p0 = p0_S;
5151
+ com.p = p_S;
5152
+ com.extreme = false;
5153
+ com.corner = false;
5154
+
5155
+ // test rendering
5156
+
5157
+ if (debug) {
5158
+ // arcs
5159
+ if (!toCubic) {
5160
+ pathDataTest = [
5161
+ { type: 'M', values: [p0_S.x, p0_S.y] },
5162
+ { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
5163
+ ];
5164
+ }
5165
+ // cubics
5166
+ else {
5167
+ pathDataTest = [
5168
+ { type: 'M', values: [p0_S.x, p0_S.y] },
5169
+ ...bezierCommands
5170
+ ];
5171
+ }
5172
+
5173
+ let d = pathDataToD(pathDataTest);
5174
+ renderPath(markers, d, 'orange', '0.5%', '0.5');
5175
+ }
5176
+
5177
+ pathDataN.push(com);
5178
+ i++;
5179
+ continue
5180
+
5181
+ }
5182
+ }
5183
+ }
5184
+
5185
+ // pass through
5186
+ pathDataN.push(com);
5187
+ }
5188
+
5189
+ return pathDataN;
5190
+ }
5191
+
5192
+ function refineClosingCommand(pathData = [], {
5193
+ threshold = 0,
5194
+ } = {}) {
5195
+
5196
+ let l = pathData.length;
5197
+ let comLast = pathData[l - 1];
5198
+ let isClosed = comLast.type.toLowerCase() === 'z';
5199
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
5200
+ let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
5201
+ let valsPen = comPenultimate.values.slice(-2);
5202
+
5203
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5204
+ let pPen = { x: valsPen[0], y: valsPen[1] };
5205
+ let dist = getDistAv(M, pPen);
5206
+
5207
+ // adjust last coordinates for better reordering
5208
+ if (dist && dist < threshold) {
5209
+
5210
+ let valsLast = pathData[idxPenultimate].values;
5211
+ let valsLastLen = valsLast.length;
5212
+ pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
5213
+ pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
5214
+
5215
+ // adjust cpts
5216
+ let comFirst = pathData[1];
5217
+
5218
+ if (comFirst.type === 'C' && comPenultimate.type === 'C') {
5219
+ let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
5220
+ let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
5221
+
5222
+ let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
5223
+ let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
5224
+
5225
+ let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5226
+ let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5227
+
5228
+ let ver = dx2 < threshold && dx3 < threshold && dy1;
5229
+ let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5230
+
5231
+ if (dx1 && dx1 < threshold && ver) {
5232
+
5233
+ pathData[1].values[0] = M.x;
5234
+ pathData[idxPenultimate].values[2] = M.x;
5235
+ }
5236
+
5237
+ if (dy1 && dy1 < threshold && hor) {
5238
+
5239
+ pathData[1].values[1] = M.y;
5240
+ pathData[idxPenultimate].values[3] = M.y;
5241
+ }
5242
+
5243
+ }
5244
+ }
5245
+
5246
+ return pathData;
5247
+
5248
+ }
5249
+
4752
5250
  function svgPathSimplify(input = '', {
4753
5251
 
4754
5252
  // return svg markup or object
@@ -4767,13 +5265,15 @@ function svgPathSimplify(input = '', {
4767
5265
 
4768
5266
  simplifyBezier = true,
4769
5267
  optimizeOrder = true,
5268
+ autoClose = true,
4770
5269
  removeZeroLength = true,
5270
+ refineClosing = true,
4771
5271
  removeColinear = true,
4772
5272
  flatBezierToLinetos = true,
4773
5273
  revertToQuadratics = true,
4774
5274
 
4775
5275
  refineExtremes = true,
4776
- refineCorners = false,
5276
+ simplifyCorners = false,
4777
5277
 
4778
5278
  keepExtremes = true,
4779
5279
  keepCorners = true,
@@ -4782,6 +5282,8 @@ function svgPathSimplify(input = '', {
4782
5282
  addExtremes = false,
4783
5283
  removeOrphanSubpaths = false,
4784
5284
 
5285
+ simplifyRound = false,
5286
+
4785
5287
  // svg path optimizations
4786
5288
  decimals = 3,
4787
5289
  autoAccuracy = true,
@@ -4834,7 +5336,7 @@ function svgPathSimplify(input = '', {
4834
5336
 
4835
5337
  // stringify to compare lengths
4836
5338
 
4837
- let dStr = d.map(com=>{return `${com.type} ${com.values.join(' ')}`}).join(' ') ;
5339
+ let dStr = d.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
4838
5340
  svgSize = dStr.length;
4839
5341
 
4840
5342
  }
@@ -4899,8 +5401,9 @@ function svgPathSimplify(input = '', {
4899
5401
  // cleaned up pathData
4900
5402
 
4901
5403
  // reset array
4902
- let pathDataFlat = [];
5404
+ let pathDataPlusArr = [];
4903
5405
 
5406
+ // loop sub paths
4904
5407
  for (let i = 0; i < lenSub; i++) {
4905
5408
 
4906
5409
  let pathDataSub = subPathArr[i];
@@ -4924,6 +5427,9 @@ function svgPathSimplify(input = '', {
4924
5427
 
4925
5428
  // simplify beziers
4926
5429
  let { pathData, bb, dimA } = pathDataPlus;
5430
+
5431
+ if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
5432
+
4927
5433
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4928
5434
 
4929
5435
  // refine extremes
@@ -4937,7 +5443,7 @@ function svgPathSimplify(input = '', {
4937
5443
 
4938
5444
  let thresh = 1;
4939
5445
 
4940
- for(let c=0, l=pathData.length; c<l; c++){
5446
+ for (let c = 0, l = pathData.length; c < l; c++) {
4941
5447
  let com = pathData[c];
4942
5448
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4943
5449
  if (type === 'C') {
@@ -4950,6 +5456,9 @@ function svgPathSimplify(input = '', {
4950
5456
  // combine adjacent cubics
4951
5457
  pathData = combineArcs(pathData);
4952
5458
 
5459
+ /*
5460
+ */
5461
+
4953
5462
  }
4954
5463
 
4955
5464
  // post processing: remove flat beziers
@@ -4958,15 +5467,18 @@ function svgPathSimplify(input = '', {
4958
5467
  }
4959
5468
 
4960
5469
  // refine corners
4961
- if(refineCorners){
5470
+ if (simplifyCorners) {
5471
+
4962
5472
  let threshold = (bb.width + bb.height) / 2 * 0.1;
4963
5473
  pathData = refineRoundedCorners(pathData, { threshold, tolerance });
4964
-
4965
5474
  }
4966
5475
 
5476
+ // refine round segment sequences
5477
+ if (simplifyRound) pathData = refineRoundSegments(pathData);
5478
+
4967
5479
  // simplify to quadratics
4968
5480
  if (revertToQuadratics) {
4969
- for(let c=0, l=pathData.length; c<l; c++){
5481
+ for (let c = 0, l = pathData.length; c < l; c++) {
4970
5482
  let com = pathData[c];
4971
5483
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4972
5484
  if (type === 'C') {
@@ -4984,15 +5496,22 @@ function svgPathSimplify(input = '', {
4984
5496
  }
4985
5497
 
4986
5498
  // optimize close path
4987
- if (optimizeOrder) pathData = optimizeClosePath(pathData);
5499
+ if (optimizeOrder) pathData = optimizeClosePath(pathData, { autoClose });
4988
5500
 
4989
5501
  // update
4990
- pathDataFlat.push(...pathData);
5502
+
5503
+ pathDataPlusArr.push({ pathData, bb });
4991
5504
 
4992
5505
  }
4993
5506
 
5507
+ // sort to top left
5508
+ if (optimizeOrder) pathDataPlusArr = pathDataPlusArr.sort((a, b) => a.bb.y - b.bb.y || a.bb.x - b.bb.x);
5509
+
4994
5510
  // flatten compound paths
4995
- pathData = pathDataFlat;
5511
+ pathData = [];
5512
+ pathDataPlusArr.forEach(sub => {
5513
+ pathData.push(...sub.pathData);
5514
+ });
4996
5515
 
4997
5516
  if (autoAccuracy) {
4998
5517
  decimals = detectAccuracy(pathData);
@@ -5092,11 +5611,7 @@ const {
5092
5611
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
5093
5612
  } = Math;
5094
5613
 
5095
- /*
5096
- import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5097
- export {XMLSerializerPoly as XMLSerializerPoly};
5098
- export {DOMParserPoly as DOMParserPoly};
5099
- */
5614
+ // just for visual debugging
5100
5615
 
5101
5616
  // IIFE
5102
5617
  if (typeof window !== 'undefined') {