svg-path-simplify 0.0.7 → 0.0.9

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 (38) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +1250 -562
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +4756 -4068
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +1250 -562
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +89 -29
  9. package/package.json +5 -3
  10. package/src/detect_input.js +17 -10
  11. package/src/dom-polyfill.js +29 -0
  12. package/src/dom-polyfill_back.js +22 -0
  13. package/src/index.js +10 -1
  14. package/src/pathData_simplify_cubic.js +114 -143
  15. package/src/pathData_simplify_cubic_extrapolate.js +64 -35
  16. package/src/pathSimplify-main.js +113 -165
  17. package/src/svgii/geometry.js +8 -155
  18. package/src/svgii/geometry_flatness.js +94 -0
  19. package/src/svgii/pathData_analyze.js +15 -596
  20. package/src/svgii/pathData_convert.js +26 -17
  21. package/src/svgii/pathData_interpolate.js +65 -0
  22. package/src/svgii/pathData_parse.js +25 -9
  23. package/src/svgii/pathData_parse_els.js +245 -0
  24. package/src/svgii/pathData_remove_collinear.js +33 -28
  25. package/src/svgii/pathData_remove_orphaned.js +21 -0
  26. package/src/svgii/pathData_remove_zerolength.js +17 -3
  27. package/src/svgii/pathData_reorder.js +9 -3
  28. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  29. package/src/svgii/pathData_simplify_refineExtremes.js +208 -0
  30. package/src/svgii/pathData_split.js +43 -15
  31. package/src/svgii/pathData_stringify.js +3 -12
  32. package/src/svgii/rounding.js +35 -27
  33. package/src/svgii/svg_cleanup.js +4 -1
  34. package/testSVG.js +39 -0
  35. package/src/pathData_simplify_cubic_arr.js +0 -50
  36. package/src/svgii/simplify.js +0 -248
  37. package/src/svgii/simplify_bezier.js +0 -470
  38. package/src/svgii/simplify_linetos.js +0 -93
@@ -1,8 +1,9 @@
1
1
  import { splitSubpaths } from './pathData_split.js';
2
- import { getAngle, bezierhasExtreme, getPathDataVertices, svgArcToCenterParam, getSquareDistance, commandIsFlat, getDistAv } from "./geometry.js";
2
+ import { getAngle, bezierhasExtreme, getPathDataVertices, svgArcToCenterParam, getSquareDistance, getDistAv } from "./geometry.js";
3
3
  import { getPolygonArea, getPathArea } from './geometry_area.js';
4
4
  import { getPolyBBox } from './geometry_bbox.js';
5
5
  import { renderPoint, renderPath } from "./visualize.js";
6
+ import { commandIsFlat } from './geometry_flatness.js';
6
7
 
7
8
 
8
9
 
@@ -111,6 +112,7 @@ export function analyzePathData(pathData = []) {
111
112
  com.directionChange = false;
112
113
  com.closePath = false;
113
114
  com.dimA = 0;
115
+ //com.flat = false;
114
116
 
115
117
 
116
118
  /**
@@ -179,13 +181,18 @@ export function analyzePathData(pathData = []) {
179
181
  if (type === 'C') commandPts.push(cp2);
180
182
  commandPts.push(p);
181
183
 
184
+ /*
185
+ //let commandFlatness = commandIsFlat(commandPts);
182
186
  let commandFlatness = commandIsFlat(commandPts);
183
187
  isFlat = commandFlatness.flat;
184
188
  com.flat = isFlat;
185
189
 
186
190
  if (isFlat) {
187
191
  com.extreme = false;
192
+ //renderPoint(markers, p, 'red', '1%', '0.5')
188
193
  }
194
+ */
195
+
189
196
  }
190
197
 
191
198
  /**
@@ -247,14 +254,17 @@ export function analyzePathData(pathData = []) {
247
254
  let thresh = (w + h) / 2 * 0.1;
248
255
  let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
249
256
 
250
- let flatness2 = commandIsFlat(pts1, thresh)
251
- let isFlat2 = flatness2.flat;
257
+ //let flatness2 = commandIsFlat(pts1, thresh)
258
+ //let isFlat2 = flatness2.flat;
252
259
 
253
260
  /**
254
261
  * if current and next cubic are flat
255
262
  * we don't flag them as extremes to allow simplification
256
263
  */
257
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
264
+ //let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
265
+
266
+ let hasExtremes = (isFlat) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
267
+
258
268
 
259
269
  //let bezierExtreme = bezierhasExtreme(p0, cpts, angleThreshold);
260
270
 
@@ -302,595 +312,4 @@ export function analyzePathData(pathData = []) {
302
312
  //console.log('pathDataPlus', pathDataPlus);
303
313
  return pathDataPlus
304
314
 
305
- }
306
-
307
-
308
-
309
-
310
- export function analyzePathData2(pathData = [], debug = true) {
311
-
312
- // clone
313
- pathData = JSON.parse(JSON.stringify(pathData));
314
-
315
- // split to sub paths
316
- let pathDataSubArr = splitSubpaths(pathData)
317
-
318
- // collect more verbose data
319
- let pathDataPlus = [];
320
-
321
- // log
322
- let simplyfy_debug_log = [];
323
-
324
- /**
325
- * analyze sub paths
326
- * add simplified bbox (based on on-path-points)
327
- * get area
328
- */
329
- pathDataSubArr.forEach(pathData => {
330
-
331
- let pathPoly = getPathDataVertices(pathData);
332
- //let pathDataArea = getPathArea(pathData);
333
- let bb = getPolyBBox(pathPoly)
334
- let { left, right, top, bottom, width, height } = bb;
335
-
336
- // initial starting point coordinates
337
- let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
338
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
339
- let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
340
- let p;
341
-
342
- // init starting point data
343
- pathData[0].p0 = M;
344
- pathData[0].p = M;
345
- pathData[0].lineto = false;
346
- pathData[0].corner = false;
347
- pathData[0].extreme = false;
348
- pathData[0].directionChange = false;
349
- pathData[0].closePath = false;
350
-
351
-
352
- // add first M command
353
- let pathDataProps = [pathData[0]];
354
- let area0 = 0;
355
- let len = pathData.length;
356
-
357
- for (let c = 2; len && c <= len; c++) {
358
-
359
- let com = pathData[c - 1];
360
- let { type, values } = com;
361
- let valsL = values.slice(-2);
362
-
363
- /**
364
- * get command points for
365
- * flatness checks:
366
- * this way we can skip certain tests
367
- */
368
- let commandPts = [p0];
369
- let isFlat = false;
370
-
371
- // init properties
372
- com.lineto = false;
373
- com.corner = false;
374
- com.extreme = false;
375
- com.directionChange = false;
376
- com.closePath = false;
377
-
378
- /**
379
- * define angle threshold for
380
- * corner detection
381
- */
382
- let angleThreshold = 0.05
383
- p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
384
-
385
-
386
- // update M for Z starting points
387
- if (type === 'M') {
388
- M = p;
389
- p0 = p
390
- //p0 = p
391
- }
392
- else if (type.toLowerCase() === 'z') {
393
- //p0 = M;
394
- p = M;
395
- }
396
-
397
- // add on-path points
398
- com.p0 = p0;
399
- com.p = p;
400
-
401
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
402
-
403
- /**
404
- * explicit and implicit linetos
405
- * - introduced by Z
406
- */
407
- if (type === 'L') com.lineto = true;
408
-
409
- if (type === 'Z') {
410
- com.closePath = true;
411
- // if Z introduces an implicit lineto with a length
412
- if (M.x !== M0.x && M.y !== M0.y) {
413
- com.lineto = true;
414
- }
415
- }
416
-
417
- // if bezier
418
- if (type === 'Q' || type === 'C') {
419
- cp1 = { x: values[0], y: values[1] }
420
- cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
421
- com.cp1 = cp1;
422
- if (cp2) com.cp2 = cp2;
423
- }
424
-
425
- /**
426
- * check command flatness
427
- * we leave it to the bezier simplifier
428
- * to convert flat beziers to linetos
429
- * otherwise we may strip rather flat starting segments
430
- * preventing a better simplification
431
- */
432
-
433
- if (values.length > 2) {
434
- if (type === 'Q' || type === 'C') commandPts.push(cp1);
435
- if (type === 'C') commandPts.push(cp2);
436
- commandPts.push(p);
437
-
438
- let commandFlatness = commandIsFlat(commandPts);
439
- isFlat = commandFlatness.flat;
440
- com.flat = isFlat;
441
-
442
- if (isFlat) {
443
- com.extreme = false;
444
- /*
445
- pathDataProps.push(com)
446
- p0 = p;
447
- continue;
448
- */
449
- }
450
- }
451
-
452
-
453
- /**
454
- * is extreme relative to bounding box
455
- * in case elements are rotated we can't rely on 90degree angles
456
- * so we interpret maximum x/y on-path points as well as extremes
457
- * but we ignore linetos to allow chunk compilation
458
- */
459
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
460
- com.extreme = true;
461
- }
462
-
463
-
464
- // add to average
465
- //let squareDist = getSquareDistance(p0, p)
466
- //com.size = squareDist;
467
-
468
- let dimA = (width + height) / 2;
469
- com.dimA = dimA;
470
- //console.log('decimals', decimals, size);
471
-
472
- //next command
473
- let comN = pathData[c] ? pathData[c] : null;
474
- let comNValsL = comN ? comN.values.slice(-2) : null;
475
- typeN = comN ? comN.type : null;
476
-
477
-
478
- // get bezier control points
479
- if (comN) {
480
- pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
481
-
482
- if (comN.type === 'Q' || comN.type === 'C') {
483
- cp1N = { x: comN.values[0], y: comN.values[1] }
484
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
485
- }
486
- }
487
-
488
-
489
- /**
490
- * Detect direction change points
491
- * this will prevent distortions when simplifying
492
- * e.g in the "spine" of an "S" glyph
493
- */
494
- area1 = getPolygonArea(commandPts)
495
- let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
496
- // update area
497
- area0 = area1
498
-
499
- if (signChange) {
500
- //renderPoint(svg1, p0, 'orange', '1%', '0.75')
501
- com.directionChange = true;
502
- }
503
-
504
-
505
- /**
506
- * check extremes or corners for adjacent curves by control point angles
507
- */
508
- if ((type === 'Q' || type === 'C')) {
509
-
510
- if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
511
-
512
- // check extremes
513
- let cpts = commandPts.slice(1);
514
-
515
- let w = Math.abs(pN.x - p0.x)
516
- let h = Math.abs(pN.y - p0.y)
517
- let thresh = (w + h) / 2 * 0.1;
518
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
519
-
520
- let flatness2 = commandIsFlat(pts1, thresh)
521
- let isFlat2 = flatness2.flat;
522
-
523
- //console.log('isFlat2', isFlat2, isFlat);
524
-
525
- /**
526
- * if current and next cubic are flat
527
- * we don't flag them as extremes to allow simplification
528
- */
529
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
530
-
531
- if (hasExtremes) {
532
- com.extreme = true
533
- }
534
-
535
- // check corners
536
- else {
537
-
538
- let cpts1 = cp2 ? [cp2, p] : [cp1, p];
539
- let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
540
-
541
- let angCom1 = getAngle(...cpts1, true)
542
- let angCom2 = getAngle(...cpts2, true)
543
- let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
544
-
545
-
546
- let cpDist1 = getSquareDistance(...cpts1)
547
- let cpDist2 = getSquareDistance(...cpts2)
548
-
549
- let cornerThreshold = 10
550
- let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
551
-
552
- if (isCorner) {
553
- com.corner = true;
554
- }
555
- }
556
- }
557
- }
558
-
559
-
560
-
561
- //let debug = false
562
- //debug = true
563
- if (debug) {
564
- if (com.signChange) {
565
- renderPoint(markers, p0, 'orange', '1.5%', '0.75')
566
- }
567
- if (com.extreme) {
568
- renderPoint(markers, p, 'cyan', '1%', '0.75')
569
- }
570
- if (com.corner) {
571
- renderPoint(markers, p, 'magenta')
572
- }
573
- }
574
-
575
- pathDataProps.push(com)
576
- p0 = p;
577
-
578
- }
579
-
580
-
581
- //decimalsAV = Array.from(decimalsAV)
582
- //decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
583
- //console.log('decimalsAV', decimalsAV);
584
- //pathDataProps[0].decimals = decimalsAV
585
-
586
- //decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
587
- let dimA = (width + height) / 2
588
- pathDataPlus.push({ pathData: pathDataProps, bb: bb, dimA: dimA })
589
-
590
-
591
- if (simplyfy_debug_log.length) {
592
- console.log(simplyfy_debug_log);
593
- }
594
-
595
- })
596
-
597
-
598
-
599
- return pathDataPlus
600
-
601
- }
602
-
603
-
604
-
605
- export function analyzePathData__(pathData = [], debug = true) {
606
-
607
- // clone
608
- pathData = JSON.parse(JSON.stringify(pathData));
609
-
610
- // split to sub paths
611
- let pathDataSubArr = splitSubpaths(pathData)
612
-
613
- // collect more verbose data
614
- let pathDataPlus = [];
615
-
616
- // log
617
- let simplyfy_debug_log = [];
618
-
619
- /**
620
- * analyze sub paths
621
- * add simplified bbox (based on on-path-points)
622
- * get area
623
- */
624
- pathDataSubArr.forEach(pathData => {
625
-
626
- let pathDataArea = getPathArea(pathData);
627
- let pathPoly = getPathDataVertices(pathData);
628
- let bb = getPolyBBox(pathPoly)
629
- let { left, right, top, bottom, width, height } = bb;
630
-
631
- // initial starting point coordinates
632
- let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
633
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
634
- let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
635
- let p;
636
-
637
- // init starting point data
638
- pathData[0].p0 = M;
639
- pathData[0].p = M;
640
- pathData[0].lineto = false;
641
- pathData[0].corner = false;
642
- pathData[0].extreme = false;
643
- pathData[0].directionChange = false;
644
- pathData[0].closePath = false;
645
-
646
-
647
- // add first M command
648
- let pathDataProps = [pathData[0]];
649
- let area0 = 0;
650
- let len = pathData.length;
651
-
652
- for (let c = 2; len && c <= len; c++) {
653
-
654
- let com = pathData[c - 1];
655
- let { type, values } = com;
656
- let valsL = values.slice(-2);
657
-
658
- /**
659
- * get command points for
660
- * flatness checks:
661
- * this way we can skip certain tests
662
- */
663
- let commandPts = [p0];
664
- let isFlat = false;
665
-
666
- // init properties
667
- com.lineto = false;
668
- com.corner = false;
669
- com.extreme = false;
670
- com.directionChange = false;
671
- com.closePath = false;
672
-
673
- /**
674
- * define angle threshold for
675
- * corner detection
676
- */
677
- let angleThreshold = 0.05
678
- p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
679
-
680
-
681
- // update M for Z starting points
682
- if (type === 'M') {
683
- M = p;
684
- p0 = p
685
- //p0 = p
686
- }
687
- else if (type.toLowerCase() === 'z') {
688
- //p0 = M;
689
- p = M;
690
- }
691
-
692
- // add on-path points
693
- com.p0 = p0;
694
- com.p = p;
695
-
696
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
697
-
698
- /**
699
- * explicit and implicit linetos
700
- * - introduced by Z
701
- */
702
- if (type === 'L') com.lineto = true;
703
-
704
- if (type === 'Z') {
705
- com.closePath = true;
706
- // if Z introduces an implicit lineto with a length
707
- if (M.x !== M0.x && M.y !== M0.y) {
708
- com.lineto = true;
709
- }
710
- }
711
-
712
- // if bezier
713
- if (type === 'Q' || type === 'C') {
714
- cp1 = { x: values[0], y: values[1] }
715
- cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
716
- com.cp1 = cp1;
717
- if (cp2) com.cp2 = cp2;
718
- }
719
-
720
- /**
721
- * check command flatness
722
- * we leave it to the bezier simplifier
723
- * to convert flat beziers to linetos
724
- * otherwise we may strip rather flat starting segments
725
- * preventing a better simplification
726
- */
727
-
728
- if (values.length > 2) {
729
- if (type === 'Q' || type === 'C') commandPts.push(cp1);
730
- if (type === 'C') commandPts.push(cp2);
731
- commandPts.push(p);
732
-
733
- let commandFlatness = commandIsFlat(commandPts);
734
- isFlat = commandFlatness.flat;
735
- com.flat = isFlat;
736
-
737
- if (isFlat) {
738
- com.extreme = false;
739
- /*
740
- pathDataProps.push(com)
741
- p0 = p;
742
- continue;
743
- */
744
- }
745
- }
746
-
747
-
748
- /**
749
- * is extreme relative to bounding box
750
- * in case elements are rotated we can't rely on 90degree angles
751
- * so we interpret maximum x/y on-path points as well as extremes
752
- * but we ignore linetos to allow chunk compilation
753
- */
754
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
755
- com.extreme = true;
756
- }
757
-
758
-
759
- // add to average
760
- //let squareDist = getSquareDistance(p0, p)
761
- //com.size = squareDist;
762
-
763
- let dimA = (width + height) / 2;
764
- com.dimA = dimA;
765
- //console.log('decimals', decimals, size);
766
-
767
- //next command
768
- let comN = pathData[c] ? pathData[c] : null;
769
- let comNValsL = comN ? comN.values.slice(-2) : null;
770
- typeN = comN ? comN.type : null;
771
-
772
-
773
- // get bezier control points
774
- if (comN) {
775
- pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
776
-
777
- if (comN.type === 'Q' || comN.type === 'C') {
778
- cp1N = { x: comN.values[0], y: comN.values[1] }
779
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
780
- }
781
- }
782
-
783
-
784
- /**
785
- * Detect direction change points
786
- * this will prevent distortions when simplifying
787
- * e.g in the "spine" of an "S" glyph
788
- */
789
- area1 = getPolygonArea(commandPts)
790
- let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
791
- // update area
792
- area0 = area1
793
-
794
- if (signChange) {
795
- //renderPoint(svg1, p0, 'orange', '1%', '0.75')
796
- com.directionChange = true;
797
- }
798
-
799
-
800
- /**
801
- * check extremes or corners for adjacent curves by control point angles
802
- */
803
- if ((type === 'Q' || type === 'C')) {
804
-
805
- if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
806
-
807
- // check extremes
808
- let cpts = commandPts.slice(1);
809
-
810
- let w = Math.abs(pN.x - p0.x)
811
- let h = Math.abs(pN.y - p0.y)
812
- let thresh = (w + h) / 2 * 0.1;
813
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
814
-
815
- let flatness2 = commandIsFlat(pts1, thresh)
816
- let isFlat2 = flatness2.flat;
817
-
818
- //console.log('isFlat2', isFlat2, isFlat);
819
-
820
- /**
821
- * if current and next cubic are flat
822
- * we don't flag them as extremes to allow simplification
823
- */
824
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
825
-
826
- if (hasExtremes) {
827
- com.extreme = true
828
- }
829
-
830
- // check corners
831
- else {
832
-
833
- let cpts1 = cp2 ? [cp2, p] : [cp1, p];
834
- let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
835
-
836
- let angCom1 = getAngle(...cpts1, true)
837
- let angCom2 = getAngle(...cpts2, true)
838
- let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
839
-
840
-
841
- let cpDist1 = getSquareDistance(...cpts1)
842
- let cpDist2 = getSquareDistance(...cpts2)
843
-
844
- let cornerThreshold = 10
845
- let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
846
-
847
- if (isCorner) {
848
- com.corner = true;
849
- }
850
- }
851
- }
852
- }
853
-
854
-
855
-
856
- let debug = false
857
- //debug = true
858
- if (debug) {
859
- if (com.signChange) {
860
- renderPoint(svg1, p0, 'orange', '1.5%', '0.75')
861
- }
862
- if (com.extreme) {
863
- renderPoint(svg1, p, 'cyan', '1%', '0.75')
864
- }
865
- if (com.corner) {
866
- renderPoint(svg1, p, 'magenta')
867
- }
868
- }
869
-
870
- pathDataProps.push(com)
871
- p0 = p;
872
-
873
- }
874
-
875
-
876
- //decimalsAV = Array.from(decimalsAV)
877
- //decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
878
- //console.log('decimalsAV', decimalsAV);
879
- //pathDataProps[0].decimals = decimalsAV
880
-
881
- //decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
882
- let dimA = (width + height) / 2
883
- pathDataPlus.push({ pathData: pathDataProps, bb: bb, area: pathDataArea, dimA: dimA })
884
-
885
-
886
- if (simplyfy_debug_log.length) {
887
- console.log(simplyfy_debug_log);
888
- }
889
-
890
- })
891
-
892
-
893
-
894
- return pathDataPlus
895
-
896
- }
315
+ }