svg-path-simplify 0.0.4 → 0.0.7

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.
@@ -1,38 +1,13 @@
1
- function renderPoint(
2
- svg,
3
- coords,
4
- fill = "red",
5
- r = "1%",
6
- opacity = "1",
7
- title = '',
8
- render = true,
9
- id = "",
10
- className = ""
11
- ) {
12
- if (Array.isArray(coords)) {
13
- coords = {
14
- x: coords[0],
15
- y: coords[1]
16
- };
17
- }
18
- let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
19
- <title>${title}</title></circle>`;
20
-
21
- if (render) {
22
- svg.insertAdjacentHTML("beforeend", marker);
23
- } else {
24
- return marker;
25
- }
26
- }
27
-
28
1
  function detectInputType(input) {
29
2
  let type = 'string';
3
+ /*
30
4
  if (input instanceof HTMLImageElement) return "img";
31
5
  if (input instanceof SVGElement) return "svg";
32
6
  if (input instanceof HTMLCanvasElement) return "canvas";
33
7
  if (input instanceof File) return "file";
34
8
  if (input instanceof ArrayBuffer) return "buffer";
35
9
  if (input instanceof Blob) return "blob";
10
+ */
36
11
  if (Array.isArray(input)) return "array";
37
12
 
38
13
  if (typeof input === "string") {
@@ -882,6 +857,7 @@ function addExtemesToCommand(p0, values, tMin=0, tMax=1) {
882
857
 
883
858
  if(tArr.length){
884
859
  let commandsSplit = splitCommandAtTValues(p0, values, tArr);
860
+
885
861
  pathDataNew.push(...commandsSplit);
886
862
  extremeCount += commandsSplit.length;
887
863
  }else {
@@ -1535,6 +1511,7 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1535
1511
  let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1536
1512
  let r = sub(P, com1.p0);
1537
1513
 
1514
+
1538
1515
  t0 -= dot(r, dP) / dot(dP, dP);
1539
1516
 
1540
1517
  // construct merged cubic over [t0, 1]
@@ -1566,7 +1543,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1566
1543
  };
1567
1544
  }
1568
1545
 
1569
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true);
1546
+ let tMid = (1 - t0)*0.5 ;
1547
+
1548
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
1570
1549
  let seg1_cp2 = ptM.cpts[2];
1571
1550
 
1572
1551
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
@@ -2113,17 +2092,21 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2113
2092
  let cp1_Q = null;
2114
2093
  let type = 'C';
2115
2094
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2095
+ let comN = {type, values};
2116
2096
 
2117
2097
  if (dist1 < threshold) {
2118
2098
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2119
2099
  if (cp1_Q) {
2120
2100
 
2121
- type = 'Q';
2122
- values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2101
+ comN.type = 'Q';
2102
+ comN.values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2103
+ comN.p0 = p0;
2104
+ comN.cp1 = cp1_Q;
2105
+ comN.p = p;
2123
2106
  }
2124
2107
  }
2125
2108
 
2126
- return { type, values }
2109
+ return comN
2127
2110
 
2128
2111
  }
2129
2112
 
@@ -3500,8 +3483,11 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3500
3483
 
3501
3484
  let area = getPolygonArea([p0, p, p1], true);
3502
3485
 
3486
+ getSquareDistance(p0, p);
3487
+ getSquareDistance(p, p1);
3503
3488
  let distSquare = getSquareDistance(p0, p1);
3504
- let distMax = distSquare / 100 * tolerance;
3489
+
3490
+ let distMax = distSquare / 200 * tolerance;
3505
3491
 
3506
3492
  let isFlat = area < distMax;
3507
3493
  let isFlatBez = false;
@@ -3531,7 +3517,10 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3531
3517
  p0 = p;
3532
3518
 
3533
3519
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3534
- if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
3520
+
3521
+ if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
3522
+
3523
+
3535
3524
 
3536
3525
  continue;
3537
3526
  }
@@ -3554,6 +3543,21 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3554
3543
 
3555
3544
  }
3556
3545
 
3546
+ // remove zero-length segments introduced by rounding
3547
+ function removeZeroLengthLinetos_post(pathData) {
3548
+ let pathDataOpt = [];
3549
+ pathData.forEach((com, i) => {
3550
+ let { type, values } = com;
3551
+ if (type === 'l' || type === 'v' || type === 'h') {
3552
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
3553
+ if (hasLength) pathDataOpt.push(com);
3554
+ } else {
3555
+ pathDataOpt.push(com);
3556
+ }
3557
+ });
3558
+ return pathDataOpt
3559
+ }
3560
+
3557
3561
  function removeZeroLengthLinetos(pathData) {
3558
3562
 
3559
3563
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
@@ -3607,12 +3611,98 @@ function pathDataToTopLeft(pathData) {
3607
3611
  }
3608
3612
 
3609
3613
  // reorder to top left most
3610
- indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) || a.x - b.x);
3614
+
3615
+ indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) );
3611
3616
  newIndex = indices[0].index;
3612
3617
 
3613
3618
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3614
3619
  }
3615
3620
 
3621
+ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3622
+
3623
+ let pathDataNew = [];
3624
+ let len = pathData.length;
3625
+ let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
3626
+ let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3627
+
3628
+ let linetos = pathData.filter(com => com.type === 'L');
3629
+
3630
+ // check if order is ideal
3631
+ let penultimateCom = pathData[len - 2];
3632
+ let penultimateType = penultimateCom.type;
3633
+ let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
3634
+
3635
+ // last L command ends at M
3636
+ let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3637
+
3638
+ // if last segment is not closing or a lineto
3639
+ let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateType === 'L');
3640
+ skipReorder = false;
3641
+
3642
+ // we can't change starting point for non closed paths
3643
+ if (!isClosed) {
3644
+ return pathData
3645
+ }
3646
+
3647
+ let newIndex = 0;
3648
+
3649
+ if (!skipReorder) {
3650
+
3651
+ let indices = [];
3652
+ for (let i = 0, len = pathData.length; i < len; i++) {
3653
+ let com = pathData[i];
3654
+ let { type, values } = com;
3655
+ if (values.length) {
3656
+ let valsL = values.slice(-2);
3657
+ let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
3658
+ let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
3659
+ let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
3660
+ let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
3661
+ let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
3662
+ p.index = i;
3663
+ indices.push(p);
3664
+ }
3665
+ }
3666
+
3667
+ // find top most lineto
3668
+
3669
+ if (linetos.length) {
3670
+ let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
3671
+ com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
3672
+
3673
+ newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
3674
+
3675
+ }
3676
+ // use top most command
3677
+ else {
3678
+ indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x);
3679
+ newIndex = indices[0].index;
3680
+ }
3681
+
3682
+ // reorder
3683
+ pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3684
+ }
3685
+
3686
+ M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(7) };
3687
+
3688
+ len = pathData.length;
3689
+
3690
+ // remove last lineto
3691
+ penultimateCom = pathData[len - 2];
3692
+ penultimateType = penultimateCom.type;
3693
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
3694
+
3695
+ isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3696
+
3697
+ if (removeFinalLineto && isClosingCommand) {
3698
+ pathData.splice(len - 2, 1);
3699
+ }
3700
+
3701
+ pathDataNew.push(...pathData);
3702
+
3703
+ return pathDataNew
3704
+ }
3705
+
3616
3706
  /**
3617
3707
  * shift starting point
3618
3708
  */
@@ -3826,6 +3916,13 @@ function reversePathData(pathData, {
3826
3916
  return pathDataNew;
3827
3917
  }
3828
3918
 
3919
+ function removeEmptySVGEls(svg) {
3920
+ let els = svg.querySelectorAll('g, defs');
3921
+ els.forEach(el => {
3922
+ if (!el.children.length) el.remove();
3923
+ });
3924
+ }
3925
+
3829
3926
  function cleanUpSVG(svgMarkup, {
3830
3927
  returnDom=false,
3831
3928
  removeHidden=true,
@@ -3913,10 +4010,13 @@ function stringifySVG(svg){
3913
4010
  }
3914
4011
 
3915
4012
  function svgPathSimplify(input = '', {
4013
+
4014
+ // return svg markup or object
4015
+ getObject = false,
4016
+
3916
4017
  toAbsolute = true,
3917
4018
  toRelative = true,
3918
4019
  toShorthands = true,
3919
- decimals = 3,
3920
4020
 
3921
4021
  // not necessary unless you need cubics only
3922
4022
  quadraticToCubic = true,
@@ -3925,30 +4025,31 @@ function svgPathSimplify(input = '', {
3925
4025
  arcToCubic = false,
3926
4026
  cubicToArc = false,
3927
4027
 
3928
- // arc to cubic precision - adds more segments for better precision
3929
- arcAccuracy = 4,
3930
- keepExtremes = true,
3931
- keepCorners = true,
3932
- keepInflections = true,
3933
- extrapolateDominant = false,
3934
- addExtremes = false,
4028
+ simplifyBezier = true,
3935
4029
  optimizeOrder = true,
3936
4030
  removeColinear = true,
3937
- simplifyBezier = true,
3938
- autoAccuracy = true,
3939
4031
  flatBezierToLinetos = true,
3940
4032
  revertToQuadratics = true,
4033
+
4034
+ keepExtremes = true,
4035
+ keepCorners = true,
4036
+ extrapolateDominant = true,
4037
+ keepInflections = false,
4038
+ addExtremes = false,
4039
+
4040
+ // svg path optimizations
4041
+ decimals = 3,
4042
+ autoAccuracy = true,
4043
+
3941
4044
  minifyD = 0,
3942
4045
  tolerance = 1,
3943
4046
  reverse = false,
3944
4047
 
3945
4048
  // svg cleanup options
4049
+ mergePaths = false,
3946
4050
  removeHidden = true,
3947
4051
  removeUnused = true,
3948
4052
 
3949
- // return svg markup or object
3950
- getObject = false
3951
-
3952
4053
  } = {}) {
3953
4054
 
3954
4055
  // clamp tolerance
@@ -4000,6 +4101,17 @@ function svgPathSimplify(input = '', {
4000
4101
  /**
4001
4102
  * process all paths
4002
4103
  */
4104
+
4105
+ // SVG optimization options
4106
+ let pathOptions = {
4107
+ toRelative,
4108
+ toShorthands,
4109
+ decimals,
4110
+ };
4111
+
4112
+ // combinded path data for SVGs with mergePaths enabled
4113
+ let pathData_merged = [];
4114
+
4003
4115
  paths.forEach(path => {
4004
4116
  let { d, el } = path;
4005
4117
 
@@ -4046,7 +4158,7 @@ function svgPathSimplify(input = '', {
4046
4158
  // simplify beziers
4047
4159
  let { pathData, bb, dimA } = pathDataPlus;
4048
4160
 
4049
- pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4161
+ pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4050
4162
 
4051
4163
  // cubic to arcs
4052
4164
  if (cubicToArc) {
@@ -4075,11 +4187,27 @@ function svgPathSimplify(input = '', {
4075
4187
  if (type === 'C') {
4076
4188
 
4077
4189
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4078
- if (comQ.type === 'Q') pathData[c] = comQ;
4190
+ if (comQ.type === 'Q') {
4191
+ /*
4192
+ comQ.p0 = com.p0
4193
+ comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
4194
+ comQ.p = com.p
4195
+ */
4196
+ comQ.extreme = com.extreme;
4197
+ comQ.corner = com.corner;
4198
+ comQ.dimA = com.dimA;
4199
+
4200
+ pathData[c] = comQ;
4201
+ }
4079
4202
  }
4080
4203
  });
4081
4204
  }
4082
4205
 
4206
+ // optimize close path
4207
+ if (optimizeOrder) pathData = optimizeClosePath(pathData);
4208
+
4209
+ // poly
4210
+
4083
4211
  // update
4084
4212
  pathDataArrN.push(pathData);
4085
4213
  }
@@ -4087,64 +4215,76 @@ function svgPathSimplify(input = '', {
4087
4215
  // flatten compound paths
4088
4216
  pathData = pathDataArrN.flat();
4089
4217
 
4090
- /**
4091
- * detect accuracy
4092
- */
4093
- if (autoAccuracy) {
4094
- decimals = detectAccuracy(pathData);
4218
+ // collect for merged svg paths
4219
+ if (el && mergePaths) {
4220
+ pathData_merged.push(...pathData);
4095
4221
  }
4222
+ // single output
4223
+ else {
4096
4224
 
4097
- // optimize
4098
- let pathOptions = {
4099
- toRelative,
4100
- toShorthands,
4101
- decimals,
4102
- };
4225
+ /**
4226
+ * detect accuracy
4227
+ */
4228
+ if (autoAccuracy) {
4229
+ decimals = detectAccuracy(pathData);
4230
+ }
4103
4231
 
4104
- // optimize path data
4105
- pathData = convertPathData(pathData, pathOptions);
4232
+ // optimize path data
4233
+ pathData = convertPathData(pathData, pathOptions);
4106
4234
 
4107
- // remove zero-length segments introduced by rounding
4108
- let pathDataOpt = [];
4235
+ // remove zero-length segments introduced by rounding
4236
+ pathData = removeZeroLengthLinetos_post(pathData);
4109
4237
 
4110
- pathData.forEach((com, i) => {
4111
- let { type, values } = com;
4112
- if (type === 'l' || type === 'v' || type === 'h') {
4113
- let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
4114
- if (hasLength) pathDataOpt.push(com);
4115
- } else {
4116
- pathDataOpt.push(com);
4117
- }
4118
- });
4238
+ // compare command count
4239
+ let comCountS = pathData.length;
4119
4240
 
4120
- pathData = pathDataOpt;
4241
+ let dOpt = pathDataToD(pathData, minifyD);
4242
+ svgSizeOpt = new Blob([dOpt]).size;
4243
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4121
4244
 
4122
- // compare command count
4123
- let comCountS = pathData.length;
4245
+ path.d = dOpt;
4246
+ path.report = {
4247
+ original: comCount,
4248
+ new: comCountS,
4249
+ saved: comCount - comCountS,
4250
+ compression,
4251
+ decimals,
4124
4252
 
4125
- let dOpt = pathDataToD(pathData, minifyD);
4126
- svgSizeOpt = new Blob([dOpt]).size;
4253
+ };
4127
4254
 
4128
- compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4255
+ // apply new path for svgs
4256
+ if (el) el.setAttribute('d', dOpt);
4257
+ }
4258
+ });
4129
4259
 
4130
- path.d = dOpt;
4131
- path.report = {
4132
- original: comCount,
4133
- new: comCountS,
4134
- saved: comCount - comCountS,
4135
- compression,
4136
- decimals,
4260
+ /**
4261
+ * stringify new SVG
4262
+ */
4263
+ if (mode) {
4137
4264
 
4138
- };
4265
+ if (pathData_merged.length) {
4266
+ // optimize path data
4267
+ let pathData = convertPathData(pathData_merged, pathOptions);
4139
4268
 
4140
- // apply new path for svgs
4141
- if (el) el.setAttribute('d', dOpt);
4269
+ // remove zero-length segments introduced by rounding
4270
+ pathData = removeZeroLengthLinetos_post(pathData);
4142
4271
 
4143
- });
4272
+ let dOpt = pathDataToD(pathData, minifyD);
4144
4273
 
4145
- // stringify new SVG
4146
- if (mode) {
4147
- svg = new XMLSerializer().serializeToString(svg);
4274
+ // apply new path for svgs
4275
+ paths[0].el.setAttribute('d', dOpt);
4276
+
4277
+ // remove other paths
4278
+ for (let i = 1; i < paths.length; i++) {
4279
+ let pathEl = paths[i].el;
4280
+ if (pathEl) pathEl.remove();
4281
+ }
4282
+
4283
+ // remove empty groups e.g groups
4284
+ removeEmptySVGEls(svg);
4285
+ }
4286
+
4287
+ svg = stringifySVG(svg);
4148
4288
  svgSizeOpt = new Blob([svg]).size;
4149
4289
 
4150
4290
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
@@ -4166,7 +4306,7 @@ function svgPathSimplify(input = '', {
4166
4306
 
4167
4307
  }
4168
4308
 
4169
- function simplifyPathData(pathData, {
4309
+ function simplifyPathDataCubic(pathData, {
4170
4310
  keepExtremes = true,
4171
4311
  keepInflections = true,
4172
4312
  keepCorners = true,
@@ -4210,6 +4350,8 @@ function simplifyPathData(pathData, {
4210
4350
  if (combined.length === 1) {
4211
4351
  com = combined[0];
4212
4352
  let offset = 1;
4353
+
4354
+ // add cumulative error to prevent distortions
4213
4355
  error += com.error;
4214
4356
 
4215
4357
  // find next candidates
@@ -4227,6 +4369,9 @@ function simplifyPathData(pathData, {
4227
4369
 
4228
4370
  let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4229
4371
  if (combined.length === 1) {
4372
+ // add cumulative error to prevent distortions
4373
+
4374
+ error += combined[0].error * 0.5;
4230
4375
  offset++;
4231
4376
  }
4232
4377
  com = combined[0];
@@ -4258,39 +4403,6 @@ function simplifyPathData(pathData, {
4258
4403
  return pathDataN
4259
4404
  }
4260
4405
 
4261
- /**
4262
- * get viewBox
4263
- * either from explicit attribute or
4264
- * width and height attributes
4265
- */
4266
-
4267
- function getViewBox(svg = null, round = false) {
4268
-
4269
- // browser default
4270
- if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
4271
-
4272
- let style = window.getComputedStyle(svg);
4273
-
4274
- // the baseVal API method also converts physical units to pixels/user-units
4275
- let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
4276
- let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
4277
-
4278
- let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
4279
-
4280
- // remove SVG constructor
4281
- let { x, y, width, height } = viewBox;
4282
- viewBox = { x, y, width, height };
4283
-
4284
- // round to integers
4285
- if (round) {
4286
- for (let prop in viewBox) {
4287
- viewBox[prop] = Math.ceil(viewBox[prop]);
4288
- }
4289
- }
4290
-
4291
- return viewBox
4292
- }
4293
-
4294
4406
  const {
4295
4407
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4296
4408
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
@@ -4301,8 +4413,7 @@ const {
4301
4413
  // IIFE
4302
4414
  if (typeof window !== 'undefined') {
4303
4415
  window.svgPathSimplify = svgPathSimplify;
4304
- window.getViewBox = getViewBox;
4305
- window.renderPoint = renderPoint;
4416
+
4306
4417
  }
4307
4418
 
4308
- export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, getViewBox, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };
4419
+ export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };