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