@stroke-stabilizer/core 0.2.16 → 0.3.1
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.
- package/README.md +108 -2
- package/dist/StabilizedPointer.d.ts +1 -1
- package/dist/filters/DouglasPeuckerFilter.d.ts +37 -0
- package/dist/filters/DouglasPeuckerFilter.d.ts.map +1 -0
- package/dist/filters/EmaFilter.d.ts.map +1 -1
- package/dist/filters/KalmanFilter.d.ts.map +1 -1
- package/dist/filters/LinearPredictionFilter.d.ts.map +1 -1
- package/dist/filters/MovingAverageFilter.d.ts.map +1 -1
- package/dist/filters/OneEuroFilter.d.ts.map +1 -1
- package/dist/filters/StringFilter.d.ts.map +1 -1
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/index.cjs +578 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -14
- package/dist/index.js.map +1 -1
- package/dist/prediction.d.ts +102 -0
- package/dist/prediction.d.ts.map +1 -0
- package/dist/spline.d.ts +53 -0
- package/dist/spline.d.ts.map +1 -0
- package/dist/svg.d.ts +79 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -502,10 +502,10 @@ class StabilizedPointer {
|
|
|
502
502
|
return "updateParams" in filter && typeof filter.updateParams === "function";
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
|
-
const FILTER_TYPE$
|
|
505
|
+
const FILTER_TYPE$7 = "noise";
|
|
506
506
|
class NoiseFilterImpl {
|
|
507
507
|
constructor(params) {
|
|
508
|
-
this.type = FILTER_TYPE$
|
|
508
|
+
this.type = FILTER_TYPE$7;
|
|
509
509
|
this.lastPoint = null;
|
|
510
510
|
this.params = { ...params };
|
|
511
511
|
}
|
|
@@ -533,10 +533,10 @@ class NoiseFilterImpl {
|
|
|
533
533
|
function noiseFilter(params) {
|
|
534
534
|
return new NoiseFilterImpl(params);
|
|
535
535
|
}
|
|
536
|
-
const FILTER_TYPE$
|
|
536
|
+
const FILTER_TYPE$6 = "kalman";
|
|
537
537
|
class KalmanFilterImpl {
|
|
538
538
|
constructor(params) {
|
|
539
|
-
this.type = FILTER_TYPE$
|
|
539
|
+
this.type = FILTER_TYPE$6;
|
|
540
540
|
this.state = null;
|
|
541
541
|
this.params = { ...params };
|
|
542
542
|
}
|
|
@@ -568,6 +568,8 @@ class KalmanFilterImpl {
|
|
|
568
568
|
x: newX,
|
|
569
569
|
y: newY,
|
|
570
570
|
pressure: point.pressure,
|
|
571
|
+
tiltX: point.tiltX,
|
|
572
|
+
tiltY: point.tiltY,
|
|
571
573
|
timestamp: point.timestamp
|
|
572
574
|
};
|
|
573
575
|
}
|
|
@@ -581,10 +583,10 @@ class KalmanFilterImpl {
|
|
|
581
583
|
function kalmanFilter(params) {
|
|
582
584
|
return new KalmanFilterImpl(params);
|
|
583
585
|
}
|
|
584
|
-
const FILTER_TYPE$
|
|
586
|
+
const FILTER_TYPE$5 = "movingAverage";
|
|
585
587
|
class MovingAverageFilterImpl {
|
|
586
588
|
constructor(params) {
|
|
587
|
-
this.type = FILTER_TYPE$
|
|
589
|
+
this.type = FILTER_TYPE$5;
|
|
588
590
|
this.window = [];
|
|
589
591
|
this.params = { ...params };
|
|
590
592
|
}
|
|
@@ -597,6 +599,10 @@ class MovingAverageFilterImpl {
|
|
|
597
599
|
let sumY = 0;
|
|
598
600
|
let sumPressure = 0;
|
|
599
601
|
let pressureCount = 0;
|
|
602
|
+
let sumTiltX = 0;
|
|
603
|
+
let tiltXCount = 0;
|
|
604
|
+
let sumTiltY = 0;
|
|
605
|
+
let tiltYCount = 0;
|
|
600
606
|
for (const p of this.window) {
|
|
601
607
|
sumX += p.x;
|
|
602
608
|
sumY += p.y;
|
|
@@ -604,14 +610,26 @@ class MovingAverageFilterImpl {
|
|
|
604
610
|
sumPressure += p.pressure;
|
|
605
611
|
pressureCount++;
|
|
606
612
|
}
|
|
613
|
+
if (p.tiltX !== void 0) {
|
|
614
|
+
sumTiltX += p.tiltX;
|
|
615
|
+
tiltXCount++;
|
|
616
|
+
}
|
|
617
|
+
if (p.tiltY !== void 0) {
|
|
618
|
+
sumTiltY += p.tiltY;
|
|
619
|
+
tiltYCount++;
|
|
620
|
+
}
|
|
607
621
|
}
|
|
608
622
|
const avgX = sumX / this.window.length;
|
|
609
623
|
const avgY = sumY / this.window.length;
|
|
610
624
|
const avgPressure = pressureCount > 0 ? sumPressure / pressureCount : void 0;
|
|
625
|
+
const avgTiltX = tiltXCount > 0 ? sumTiltX / tiltXCount : void 0;
|
|
626
|
+
const avgTiltY = tiltYCount > 0 ? sumTiltY / tiltYCount : void 0;
|
|
611
627
|
return {
|
|
612
628
|
x: avgX,
|
|
613
629
|
y: avgY,
|
|
614
630
|
pressure: avgPressure,
|
|
631
|
+
tiltX: avgTiltX,
|
|
632
|
+
tiltY: avgTiltY,
|
|
615
633
|
timestamp: point.timestamp
|
|
616
634
|
};
|
|
617
635
|
}
|
|
@@ -628,10 +646,10 @@ class MovingAverageFilterImpl {
|
|
|
628
646
|
function movingAverageFilter(params) {
|
|
629
647
|
return new MovingAverageFilterImpl(params);
|
|
630
648
|
}
|
|
631
|
-
const FILTER_TYPE$
|
|
649
|
+
const FILTER_TYPE$4 = "string";
|
|
632
650
|
class StringFilterImpl {
|
|
633
651
|
constructor(params) {
|
|
634
|
-
this.type = FILTER_TYPE$
|
|
652
|
+
this.type = FILTER_TYPE$4;
|
|
635
653
|
this.anchorPoint = null;
|
|
636
654
|
this.params = { ...params };
|
|
637
655
|
}
|
|
@@ -647,6 +665,8 @@ class StringFilterImpl {
|
|
|
647
665
|
return {
|
|
648
666
|
...this.anchorPoint,
|
|
649
667
|
pressure: point.pressure,
|
|
668
|
+
tiltX: point.tiltX,
|
|
669
|
+
tiltY: point.tiltY,
|
|
650
670
|
timestamp: point.timestamp
|
|
651
671
|
};
|
|
652
672
|
}
|
|
@@ -657,6 +677,8 @@ class StringFilterImpl {
|
|
|
657
677
|
x: newX,
|
|
658
678
|
y: newY,
|
|
659
679
|
pressure: point.pressure,
|
|
680
|
+
tiltX: point.tiltX,
|
|
681
|
+
tiltY: point.tiltY,
|
|
660
682
|
timestamp: point.timestamp
|
|
661
683
|
};
|
|
662
684
|
return this.anchorPoint;
|
|
@@ -671,10 +693,10 @@ class StringFilterImpl {
|
|
|
671
693
|
function stringFilter(params) {
|
|
672
694
|
return new StringFilterImpl(params);
|
|
673
695
|
}
|
|
674
|
-
const FILTER_TYPE$
|
|
696
|
+
const FILTER_TYPE$3 = "ema";
|
|
675
697
|
class EmaFilterImpl {
|
|
676
698
|
constructor(params) {
|
|
677
|
-
this.type = FILTER_TYPE$
|
|
699
|
+
this.type = FILTER_TYPE$3;
|
|
678
700
|
this.lastPoint = null;
|
|
679
701
|
this.params = { ...params };
|
|
680
702
|
}
|
|
@@ -692,10 +714,24 @@ class EmaFilterImpl {
|
|
|
692
714
|
} else {
|
|
693
715
|
newPressure = point.pressure;
|
|
694
716
|
}
|
|
717
|
+
let newTiltX;
|
|
718
|
+
if (point.tiltX !== void 0 && this.lastPoint.tiltX !== void 0) {
|
|
719
|
+
newTiltX = alpha * point.tiltX + (1 - alpha) * this.lastPoint.tiltX;
|
|
720
|
+
} else {
|
|
721
|
+
newTiltX = point.tiltX;
|
|
722
|
+
}
|
|
723
|
+
let newTiltY;
|
|
724
|
+
if (point.tiltY !== void 0 && this.lastPoint.tiltY !== void 0) {
|
|
725
|
+
newTiltY = alpha * point.tiltY + (1 - alpha) * this.lastPoint.tiltY;
|
|
726
|
+
} else {
|
|
727
|
+
newTiltY = point.tiltY;
|
|
728
|
+
}
|
|
695
729
|
this.lastPoint = {
|
|
696
730
|
x: newX,
|
|
697
731
|
y: newY,
|
|
698
732
|
pressure: newPressure,
|
|
733
|
+
tiltX: newTiltX,
|
|
734
|
+
tiltY: newTiltY,
|
|
699
735
|
timestamp: point.timestamp
|
|
700
736
|
};
|
|
701
737
|
return this.lastPoint;
|
|
@@ -710,7 +746,7 @@ class EmaFilterImpl {
|
|
|
710
746
|
function emaFilter(params) {
|
|
711
747
|
return new EmaFilterImpl(params);
|
|
712
748
|
}
|
|
713
|
-
const FILTER_TYPE$
|
|
749
|
+
const FILTER_TYPE$2 = "oneEuro";
|
|
714
750
|
class LowPassFilter {
|
|
715
751
|
constructor() {
|
|
716
752
|
this.y = null;
|
|
@@ -736,12 +772,14 @@ class LowPassFilter {
|
|
|
736
772
|
}
|
|
737
773
|
class OneEuroFilterImpl {
|
|
738
774
|
constructor(params) {
|
|
739
|
-
this.type = FILTER_TYPE$
|
|
775
|
+
this.type = FILTER_TYPE$2;
|
|
740
776
|
this.xFilter = new LowPassFilter();
|
|
741
777
|
this.yFilter = new LowPassFilter();
|
|
742
778
|
this.dxFilter = new LowPassFilter();
|
|
743
779
|
this.dyFilter = new LowPassFilter();
|
|
744
780
|
this.pressureFilter = new LowPassFilter();
|
|
781
|
+
this.tiltXFilter = new LowPassFilter();
|
|
782
|
+
this.tiltYFilter = new LowPassFilter();
|
|
745
783
|
this.lastTimestamp = null;
|
|
746
784
|
this.params = {
|
|
747
785
|
dCutoff: 1,
|
|
@@ -782,10 +820,24 @@ class OneEuroFilterImpl {
|
|
|
782
820
|
this.pressureFilter.setAlpha(alpha);
|
|
783
821
|
newPressure = this.pressureFilter.filter(point.pressure);
|
|
784
822
|
}
|
|
823
|
+
let newTiltX;
|
|
824
|
+
if (point.tiltX !== void 0) {
|
|
825
|
+
const alpha = this.computeAlpha(minCutoff, rate);
|
|
826
|
+
this.tiltXFilter.setAlpha(alpha);
|
|
827
|
+
newTiltX = this.tiltXFilter.filter(point.tiltX);
|
|
828
|
+
}
|
|
829
|
+
let newTiltY;
|
|
830
|
+
if (point.tiltY !== void 0) {
|
|
831
|
+
const alpha = this.computeAlpha(minCutoff, rate);
|
|
832
|
+
this.tiltYFilter.setAlpha(alpha);
|
|
833
|
+
newTiltY = this.tiltYFilter.filter(point.tiltY);
|
|
834
|
+
}
|
|
785
835
|
return {
|
|
786
836
|
x: newX,
|
|
787
837
|
y: newY,
|
|
788
838
|
pressure: newPressure,
|
|
839
|
+
tiltX: newTiltX,
|
|
840
|
+
tiltY: newTiltY,
|
|
789
841
|
timestamp: point.timestamp
|
|
790
842
|
};
|
|
791
843
|
}
|
|
@@ -817,16 +869,18 @@ class OneEuroFilterImpl {
|
|
|
817
869
|
this.dxFilter.reset();
|
|
818
870
|
this.dyFilter.reset();
|
|
819
871
|
this.pressureFilter.reset();
|
|
872
|
+
this.tiltXFilter.reset();
|
|
873
|
+
this.tiltYFilter.reset();
|
|
820
874
|
this.lastTimestamp = null;
|
|
821
875
|
}
|
|
822
876
|
}
|
|
823
877
|
function oneEuroFilter(params) {
|
|
824
878
|
return new OneEuroFilterImpl(params);
|
|
825
879
|
}
|
|
826
|
-
const FILTER_TYPE = "linearPrediction";
|
|
880
|
+
const FILTER_TYPE$1 = "linearPrediction";
|
|
827
881
|
class LinearPredictionFilterImpl {
|
|
828
882
|
constructor(params) {
|
|
829
|
-
this.type = FILTER_TYPE;
|
|
883
|
+
this.type = FILTER_TYPE$1;
|
|
830
884
|
this.history = [];
|
|
831
885
|
this.lastOutput = null;
|
|
832
886
|
this.params = {
|
|
@@ -851,6 +905,8 @@ class LinearPredictionFilterImpl {
|
|
|
851
905
|
let outputX = predictedX;
|
|
852
906
|
let outputY = predictedY;
|
|
853
907
|
let outputPressure = point.pressure;
|
|
908
|
+
let outputTiltX = point.tiltX;
|
|
909
|
+
let outputTiltY = point.tiltY;
|
|
854
910
|
if (this.lastOutput !== null && this.params.smoothing !== void 0) {
|
|
855
911
|
const s = this.params.smoothing;
|
|
856
912
|
outputX = s * predictedX + (1 - s) * this.lastOutput.x;
|
|
@@ -858,11 +914,19 @@ class LinearPredictionFilterImpl {
|
|
|
858
914
|
if (point.pressure !== void 0 && this.lastOutput.pressure !== void 0) {
|
|
859
915
|
outputPressure = s * point.pressure + (1 - s) * this.lastOutput.pressure;
|
|
860
916
|
}
|
|
917
|
+
if (point.tiltX !== void 0 && this.lastOutput.tiltX !== void 0) {
|
|
918
|
+
outputTiltX = s * point.tiltX + (1 - s) * this.lastOutput.tiltX;
|
|
919
|
+
}
|
|
920
|
+
if (point.tiltY !== void 0 && this.lastOutput.tiltY !== void 0) {
|
|
921
|
+
outputTiltY = s * point.tiltY + (1 - s) * this.lastOutput.tiltY;
|
|
922
|
+
}
|
|
861
923
|
}
|
|
862
924
|
this.lastOutput = {
|
|
863
925
|
x: outputX,
|
|
864
926
|
y: outputY,
|
|
865
927
|
pressure: outputPressure,
|
|
928
|
+
tiltX: outputTiltX,
|
|
929
|
+
tiltY: outputTiltY,
|
|
866
930
|
timestamp: point.timestamp
|
|
867
931
|
};
|
|
868
932
|
return this.lastOutput;
|
|
@@ -958,6 +1022,84 @@ class LinearPredictionFilterImpl {
|
|
|
958
1022
|
function linearPredictionFilter(params) {
|
|
959
1023
|
return new LinearPredictionFilterImpl(params);
|
|
960
1024
|
}
|
|
1025
|
+
const FILTER_TYPE = "douglasPeucker";
|
|
1026
|
+
class DouglasPeuckerFilterImpl {
|
|
1027
|
+
constructor(params) {
|
|
1028
|
+
this.type = FILTER_TYPE;
|
|
1029
|
+
this.points = [];
|
|
1030
|
+
this.params = { ...params };
|
|
1031
|
+
}
|
|
1032
|
+
process(point) {
|
|
1033
|
+
this.points.push(point);
|
|
1034
|
+
return point;
|
|
1035
|
+
}
|
|
1036
|
+
updateParams(params) {
|
|
1037
|
+
this.params = { ...this.params, ...params };
|
|
1038
|
+
}
|
|
1039
|
+
reset() {
|
|
1040
|
+
this.points = [];
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Get the simplified points without clearing the buffer
|
|
1044
|
+
*/
|
|
1045
|
+
getSimplified() {
|
|
1046
|
+
if (this.points.length <= 2) {
|
|
1047
|
+
return [...this.points];
|
|
1048
|
+
}
|
|
1049
|
+
return simplifyDouglasPeucker(this.points, this.params.epsilon);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Get all collected points
|
|
1053
|
+
*/
|
|
1054
|
+
getPoints() {
|
|
1055
|
+
return [...this.points];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function perpendicularDistance(point, lineStart, lineEnd) {
|
|
1059
|
+
const dx = lineEnd.x - lineStart.x;
|
|
1060
|
+
const dy = lineEnd.y - lineStart.y;
|
|
1061
|
+
const lengthSq = dx * dx + dy * dy;
|
|
1062
|
+
if (lengthSq === 0) {
|
|
1063
|
+
const pdx = point.x - lineStart.x;
|
|
1064
|
+
const pdy = point.y - lineStart.y;
|
|
1065
|
+
return Math.sqrt(pdx * pdx + pdy * pdy);
|
|
1066
|
+
}
|
|
1067
|
+
const numerator = Math.abs(
|
|
1068
|
+
dy * point.x - dx * point.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x
|
|
1069
|
+
);
|
|
1070
|
+
return numerator / Math.sqrt(lengthSq);
|
|
1071
|
+
}
|
|
1072
|
+
function simplifyDouglasPeucker(points, epsilon) {
|
|
1073
|
+
if (points.length <= 2) {
|
|
1074
|
+
return points;
|
|
1075
|
+
}
|
|
1076
|
+
let maxDistance = 0;
|
|
1077
|
+
let maxIndex = 0;
|
|
1078
|
+
const first = points[0];
|
|
1079
|
+
const last = points[points.length - 1];
|
|
1080
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
1081
|
+
const distance = perpendicularDistance(points[i], first, last);
|
|
1082
|
+
if (distance > maxDistance) {
|
|
1083
|
+
maxDistance = distance;
|
|
1084
|
+
maxIndex = i;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (maxDistance > epsilon) {
|
|
1088
|
+
const left = simplifyDouglasPeucker(points.slice(0, maxIndex + 1), epsilon);
|
|
1089
|
+
const right = simplifyDouglasPeucker(points.slice(maxIndex), epsilon);
|
|
1090
|
+
return [...left.slice(0, -1), ...right];
|
|
1091
|
+
}
|
|
1092
|
+
return [first, last];
|
|
1093
|
+
}
|
|
1094
|
+
function douglasPeuckerFilter(params) {
|
|
1095
|
+
return new DouglasPeuckerFilterImpl(params);
|
|
1096
|
+
}
|
|
1097
|
+
function simplify(points, epsilon) {
|
|
1098
|
+
if (points.length <= 2) {
|
|
1099
|
+
return [...points];
|
|
1100
|
+
}
|
|
1101
|
+
return simplifyDouglasPeucker(points, epsilon);
|
|
1102
|
+
}
|
|
961
1103
|
function createStabilizedPointer(level) {
|
|
962
1104
|
const clampedLevel = Math.max(0, Math.min(100, level));
|
|
963
1105
|
const pointer = new StabilizedPointer();
|
|
@@ -1078,20 +1220,442 @@ function bilateralKernel(params) {
|
|
|
1078
1220
|
}
|
|
1079
1221
|
};
|
|
1080
1222
|
}
|
|
1223
|
+
function toSVGPath(points, options = {}) {
|
|
1224
|
+
const { precision = 2, relative = false, closePath = false } = options;
|
|
1225
|
+
if (points.length === 0) {
|
|
1226
|
+
return "";
|
|
1227
|
+
}
|
|
1228
|
+
const format = (n) => n.toFixed(precision);
|
|
1229
|
+
const parts = [];
|
|
1230
|
+
const first = points[0];
|
|
1231
|
+
parts.push(`${relative ? "m" : "M"} ${format(first.x)} ${format(first.y)}`);
|
|
1232
|
+
if (relative) {
|
|
1233
|
+
let prevX = first.x;
|
|
1234
|
+
let prevY = first.y;
|
|
1235
|
+
for (let i = 1; i < points.length; i++) {
|
|
1236
|
+
const p = points[i];
|
|
1237
|
+
const dx = p.x - prevX;
|
|
1238
|
+
const dy = p.y - prevY;
|
|
1239
|
+
parts.push(`l ${format(dx)} ${format(dy)}`);
|
|
1240
|
+
prevX = p.x;
|
|
1241
|
+
prevY = p.y;
|
|
1242
|
+
}
|
|
1243
|
+
} else {
|
|
1244
|
+
for (let i = 1; i < points.length; i++) {
|
|
1245
|
+
const p = points[i];
|
|
1246
|
+
parts.push(`L ${format(p.x)} ${format(p.y)}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (closePath) {
|
|
1250
|
+
parts.push(relative ? "z" : "Z");
|
|
1251
|
+
}
|
|
1252
|
+
return parts.join(" ");
|
|
1253
|
+
}
|
|
1254
|
+
function toSVGPathSmooth(points, options = {}) {
|
|
1255
|
+
const {
|
|
1256
|
+
precision = 2,
|
|
1257
|
+
relative = false,
|
|
1258
|
+
closePath = false,
|
|
1259
|
+
tension = 0.5
|
|
1260
|
+
} = options;
|
|
1261
|
+
if (points.length === 0) {
|
|
1262
|
+
return "";
|
|
1263
|
+
}
|
|
1264
|
+
if (points.length === 1) {
|
|
1265
|
+
const p = points[0];
|
|
1266
|
+
return `${relative ? "m" : "M"} ${p.x.toFixed(precision)} ${p.y.toFixed(precision)}`;
|
|
1267
|
+
}
|
|
1268
|
+
if (points.length === 2) {
|
|
1269
|
+
return toSVGPath(points, options);
|
|
1270
|
+
}
|
|
1271
|
+
const format = (n) => n.toFixed(precision);
|
|
1272
|
+
const parts = [];
|
|
1273
|
+
const first = points[0];
|
|
1274
|
+
parts.push(`${relative ? "m" : "M"} ${format(first.x)} ${format(first.y)}`);
|
|
1275
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
1276
|
+
const prev = points[i - 1];
|
|
1277
|
+
const curr = points[i];
|
|
1278
|
+
const next = points[i + 1];
|
|
1279
|
+
const midX = (curr.x + next.x) / 2;
|
|
1280
|
+
const midY = (curr.y + next.y) / 2;
|
|
1281
|
+
const cpX = curr.x + (midX - curr.x) * (1 - tension);
|
|
1282
|
+
const cpY = curr.y + (midY - curr.y) * (1 - tension);
|
|
1283
|
+
if (relative) {
|
|
1284
|
+
const dx = cpX - prev.x;
|
|
1285
|
+
const dy = cpY - prev.y;
|
|
1286
|
+
const endDx = midX - prev.x;
|
|
1287
|
+
const endDy = midY - prev.y;
|
|
1288
|
+
parts.push(
|
|
1289
|
+
`q ${format(dx)} ${format(dy)} ${format(endDx)} ${format(endDy)}`
|
|
1290
|
+
);
|
|
1291
|
+
} else {
|
|
1292
|
+
parts.push(
|
|
1293
|
+
`Q ${format(cpX)} ${format(cpY)} ${format(midX)} ${format(midY)}`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const last = points[points.length - 1];
|
|
1298
|
+
const secondLast = points[points.length - 2];
|
|
1299
|
+
if (relative) {
|
|
1300
|
+
const midX = (secondLast.x + last.x) / 2;
|
|
1301
|
+
const midY = (secondLast.y + last.y) / 2;
|
|
1302
|
+
parts.push(`l ${format(last.x - midX)} ${format(last.y - midY)}`);
|
|
1303
|
+
} else {
|
|
1304
|
+
parts.push(`L ${format(last.x)} ${format(last.y)}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (closePath) {
|
|
1307
|
+
parts.push(relative ? "z" : "Z");
|
|
1308
|
+
}
|
|
1309
|
+
return parts.join(" ");
|
|
1310
|
+
}
|
|
1311
|
+
function toSVGPathCubic(points, options = {}) {
|
|
1312
|
+
const {
|
|
1313
|
+
precision = 2,
|
|
1314
|
+
relative = false,
|
|
1315
|
+
closePath = false,
|
|
1316
|
+
smoothing = 0.25
|
|
1317
|
+
} = options;
|
|
1318
|
+
if (points.length === 0) {
|
|
1319
|
+
return "";
|
|
1320
|
+
}
|
|
1321
|
+
if (points.length === 1) {
|
|
1322
|
+
const p = points[0];
|
|
1323
|
+
return `${relative ? "m" : "M"} ${p.x.toFixed(precision)} ${p.y.toFixed(precision)}`;
|
|
1324
|
+
}
|
|
1325
|
+
if (points.length === 2) {
|
|
1326
|
+
return toSVGPath(points, options);
|
|
1327
|
+
}
|
|
1328
|
+
const format = (n) => n.toFixed(precision);
|
|
1329
|
+
const parts = [];
|
|
1330
|
+
const first = points[0];
|
|
1331
|
+
parts.push(`${relative ? "m" : "M"} ${format(first.x)} ${format(first.y)}`);
|
|
1332
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1333
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
1334
|
+
const p1 = points[i];
|
|
1335
|
+
const p2 = points[i + 1];
|
|
1336
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
1337
|
+
const cp1x = p1.x + (p2.x - p0.x) * smoothing;
|
|
1338
|
+
const cp1y = p1.y + (p2.y - p0.y) * smoothing;
|
|
1339
|
+
const cp2x = p2.x - (p3.x - p1.x) * smoothing;
|
|
1340
|
+
const cp2y = p2.y - (p3.y - p1.y) * smoothing;
|
|
1341
|
+
if (relative) {
|
|
1342
|
+
const dx1 = cp1x - p1.x;
|
|
1343
|
+
const dy1 = cp1y - p1.y;
|
|
1344
|
+
const dx2 = cp2x - p1.x;
|
|
1345
|
+
const dy2 = cp2y - p1.y;
|
|
1346
|
+
const dx = p2.x - p1.x;
|
|
1347
|
+
const dy = p2.y - p1.y;
|
|
1348
|
+
parts.push(
|
|
1349
|
+
`c ${format(dx1)} ${format(dy1)} ${format(dx2)} ${format(dy2)} ${format(dx)} ${format(dy)}`
|
|
1350
|
+
);
|
|
1351
|
+
} else {
|
|
1352
|
+
parts.push(
|
|
1353
|
+
`C ${format(cp1x)} ${format(cp1y)} ${format(cp2x)} ${format(cp2y)} ${format(p2.x)} ${format(p2.y)}`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (closePath) {
|
|
1358
|
+
parts.push(relative ? "z" : "Z");
|
|
1359
|
+
}
|
|
1360
|
+
return parts.join(" ");
|
|
1361
|
+
}
|
|
1362
|
+
class StrokePredictor {
|
|
1363
|
+
constructor(config = {}) {
|
|
1364
|
+
this.history = [];
|
|
1365
|
+
this.config = {
|
|
1366
|
+
historySize: config.historySize ?? 4,
|
|
1367
|
+
maxPredictionMs: config.maxPredictionMs ?? 50,
|
|
1368
|
+
minVelocity: config.minVelocity ?? 0.1
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Add a point to the predictor's history
|
|
1373
|
+
*/
|
|
1374
|
+
addPoint(point) {
|
|
1375
|
+
let vx = 0;
|
|
1376
|
+
let vy = 0;
|
|
1377
|
+
if (this.history.length > 0) {
|
|
1378
|
+
const last = this.history[this.history.length - 1];
|
|
1379
|
+
const dt = point.timestamp - last.point.timestamp;
|
|
1380
|
+
if (dt > 0) {
|
|
1381
|
+
vx = (point.x - last.point.x) / dt;
|
|
1382
|
+
vy = (point.y - last.point.y) / dt;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
this.history.push({ point, vx, vy });
|
|
1386
|
+
while (this.history.length > this.config.historySize) {
|
|
1387
|
+
this.history.shift();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Predict the position at a given time ahead
|
|
1392
|
+
*
|
|
1393
|
+
* @param lookAheadMs Time in milliseconds to predict ahead
|
|
1394
|
+
* @returns Predicted point, or null if prediction is not possible
|
|
1395
|
+
*/
|
|
1396
|
+
predict(lookAheadMs) {
|
|
1397
|
+
if (this.history.length < 2) {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
const actualLookAhead = Math.min(lookAheadMs, this.config.maxPredictionMs);
|
|
1401
|
+
const { avgVx, avgVy } = this.calculateAverageVelocity();
|
|
1402
|
+
const speed = Math.sqrt(avgVx * avgVx + avgVy * avgVy);
|
|
1403
|
+
if (speed < this.config.minVelocity) {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
const lastPoint = this.history[this.history.length - 1].point;
|
|
1407
|
+
const predictedX = lastPoint.x + avgVx * actualLookAhead;
|
|
1408
|
+
const predictedY = lastPoint.y + avgVy * actualLookAhead;
|
|
1409
|
+
return {
|
|
1410
|
+
x: predictedX,
|
|
1411
|
+
y: predictedY,
|
|
1412
|
+
pressure: lastPoint.pressure,
|
|
1413
|
+
tiltX: lastPoint.tiltX,
|
|
1414
|
+
tiltY: lastPoint.tiltY,
|
|
1415
|
+
timestamp: lastPoint.timestamp + actualLookAhead
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get predicted points for rendering a preview stroke
|
|
1420
|
+
*
|
|
1421
|
+
* @param count Number of predicted points to generate
|
|
1422
|
+
* @param intervalMs Time interval between predicted points
|
|
1423
|
+
* @returns Array of predicted points
|
|
1424
|
+
*/
|
|
1425
|
+
predictMultiple(count, intervalMs = 8) {
|
|
1426
|
+
const points = [];
|
|
1427
|
+
for (let i = 1; i <= count; i++) {
|
|
1428
|
+
const predicted = this.predict(i * intervalMs);
|
|
1429
|
+
if (predicted) {
|
|
1430
|
+
points.push(predicted);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return points;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Calculate weighted average velocity from history
|
|
1437
|
+
* Recent points are weighted more heavily
|
|
1438
|
+
*/
|
|
1439
|
+
calculateAverageVelocity() {
|
|
1440
|
+
if (this.history.length < 2) {
|
|
1441
|
+
return { avgVx: 0, avgVy: 0 };
|
|
1442
|
+
}
|
|
1443
|
+
let totalWeight = 0;
|
|
1444
|
+
let weightedVx = 0;
|
|
1445
|
+
let weightedVy = 0;
|
|
1446
|
+
for (let i = 1; i < this.history.length; i++) {
|
|
1447
|
+
const weight = Math.pow(2, i);
|
|
1448
|
+
weightedVx += this.history[i].vx * weight;
|
|
1449
|
+
weightedVy += this.history[i].vy * weight;
|
|
1450
|
+
totalWeight += weight;
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
avgVx: totalWeight > 0 ? weightedVx / totalWeight : 0,
|
|
1454
|
+
avgVy: totalWeight > 0 ? weightedVy / totalWeight : 0
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Get the current average velocity
|
|
1459
|
+
*/
|
|
1460
|
+
getVelocity() {
|
|
1461
|
+
const { avgVx, avgVy } = this.calculateAverageVelocity();
|
|
1462
|
+
return { vx: avgVx, vy: avgVy };
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Get the current speed (magnitude of velocity)
|
|
1466
|
+
*/
|
|
1467
|
+
getSpeed() {
|
|
1468
|
+
const { avgVx, avgVy } = this.calculateAverageVelocity();
|
|
1469
|
+
return Math.sqrt(avgVx * avgVx + avgVy * avgVy);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Clear prediction history
|
|
1473
|
+
*/
|
|
1474
|
+
reset() {
|
|
1475
|
+
this.history = [];
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Get number of points in history
|
|
1479
|
+
*/
|
|
1480
|
+
get historyLength() {
|
|
1481
|
+
return this.history.length;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
function createPredictor(config) {
|
|
1485
|
+
return new StrokePredictor(config);
|
|
1486
|
+
}
|
|
1487
|
+
function interpolateCatmullRom(points, options = {}) {
|
|
1488
|
+
const {
|
|
1489
|
+
tension = 0.5,
|
|
1490
|
+
segmentDivisions = 10,
|
|
1491
|
+
interpolateAttributes = true
|
|
1492
|
+
} = options;
|
|
1493
|
+
if (points.length < 2) {
|
|
1494
|
+
return [...points];
|
|
1495
|
+
}
|
|
1496
|
+
if (points.length === 2) {
|
|
1497
|
+
return interpolateLinear(points[0], points[1], segmentDivisions);
|
|
1498
|
+
}
|
|
1499
|
+
const result = [];
|
|
1500
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1501
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
1502
|
+
const p1 = points[i];
|
|
1503
|
+
const p2 = points[i + 1];
|
|
1504
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
1505
|
+
if (i === 0) {
|
|
1506
|
+
result.push(p1);
|
|
1507
|
+
}
|
|
1508
|
+
for (let j = 1; j <= segmentDivisions; j++) {
|
|
1509
|
+
const t = j / segmentDivisions;
|
|
1510
|
+
const point = catmullRomPoint(p0, p1, p2, p3, t, tension);
|
|
1511
|
+
if (interpolateAttributes) {
|
|
1512
|
+
point.pressure = lerpOptional(p1.pressure, p2.pressure, t);
|
|
1513
|
+
point.tiltX = lerpOptional(p1.tiltX, p2.tiltX, t);
|
|
1514
|
+
point.tiltY = lerpOptional(p1.tiltY, p2.tiltY, t);
|
|
1515
|
+
}
|
|
1516
|
+
point.timestamp = lerp(p1.timestamp, p2.timestamp, t);
|
|
1517
|
+
result.push(point);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return result;
|
|
1521
|
+
}
|
|
1522
|
+
function catmullRomPoint(p0, p1, p2, p3, t, tension) {
|
|
1523
|
+
const alpha = tension;
|
|
1524
|
+
const t0 = 0;
|
|
1525
|
+
const t1 = getKnotValue(t0, p0, p1, alpha);
|
|
1526
|
+
const t2 = getKnotValue(t1, p1, p2, alpha);
|
|
1527
|
+
const t3 = getKnotValue(t2, p2, p3, alpha);
|
|
1528
|
+
const tActual = lerp(t1, t2, t);
|
|
1529
|
+
const A1x = remapPoint(t0, t1, p0.x, p1.x, tActual);
|
|
1530
|
+
const A1y = remapPoint(t0, t1, p0.y, p1.y, tActual);
|
|
1531
|
+
const A2x = remapPoint(t1, t2, p1.x, p2.x, tActual);
|
|
1532
|
+
const A2y = remapPoint(t1, t2, p1.y, p2.y, tActual);
|
|
1533
|
+
const A3x = remapPoint(t2, t3, p2.x, p3.x, tActual);
|
|
1534
|
+
const A3y = remapPoint(t2, t3, p2.y, p3.y, tActual);
|
|
1535
|
+
const B1x = remapPoint(t0, t2, A1x, A2x, tActual);
|
|
1536
|
+
const B1y = remapPoint(t0, t2, A1y, A2y, tActual);
|
|
1537
|
+
const B2x = remapPoint(t1, t3, A2x, A3x, tActual);
|
|
1538
|
+
const B2y = remapPoint(t1, t3, A2y, A3y, tActual);
|
|
1539
|
+
const Cx = remapPoint(t1, t2, B1x, B2x, tActual);
|
|
1540
|
+
const Cy = remapPoint(t1, t2, B1y, B2y, tActual);
|
|
1541
|
+
return {
|
|
1542
|
+
x: Cx,
|
|
1543
|
+
y: Cy,
|
|
1544
|
+
timestamp: 0
|
|
1545
|
+
// Will be set by caller
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function getKnotValue(ti, p0, p1, alpha) {
|
|
1549
|
+
const dx = p1.x - p0.x;
|
|
1550
|
+
const dy = p1.y - p0.y;
|
|
1551
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1552
|
+
return ti + Math.pow(dist, alpha);
|
|
1553
|
+
}
|
|
1554
|
+
function remapPoint(t0, t1, p0, p1, t) {
|
|
1555
|
+
const denominator = t1 - t0;
|
|
1556
|
+
if (Math.abs(denominator) < 1e-10) {
|
|
1557
|
+
return p0;
|
|
1558
|
+
}
|
|
1559
|
+
return (t1 - t) / denominator * p0 + (t - t0) / denominator * p1;
|
|
1560
|
+
}
|
|
1561
|
+
function lerp(a, b, t) {
|
|
1562
|
+
return a + (b - a) * t;
|
|
1563
|
+
}
|
|
1564
|
+
function lerpOptional(a, b, t) {
|
|
1565
|
+
if (a === void 0 || b === void 0) {
|
|
1566
|
+
return void 0;
|
|
1567
|
+
}
|
|
1568
|
+
return lerp(a, b, t);
|
|
1569
|
+
}
|
|
1570
|
+
function interpolateLinear(p1, p2, divisions) {
|
|
1571
|
+
const result = [p1];
|
|
1572
|
+
for (let i = 1; i <= divisions; i++) {
|
|
1573
|
+
const t = i / divisions;
|
|
1574
|
+
result.push({
|
|
1575
|
+
x: lerp(p1.x, p2.x, t),
|
|
1576
|
+
y: lerp(p1.y, p2.y, t),
|
|
1577
|
+
pressure: lerpOptional(p1.pressure, p2.pressure, t),
|
|
1578
|
+
tiltX: lerpOptional(p1.tiltX, p2.tiltX, t),
|
|
1579
|
+
tiltY: lerpOptional(p1.tiltY, p2.tiltY, t),
|
|
1580
|
+
timestamp: lerp(p1.timestamp, p2.timestamp, t)
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
function calculateArcLength(points) {
|
|
1586
|
+
if (points.length < 2) {
|
|
1587
|
+
return 0;
|
|
1588
|
+
}
|
|
1589
|
+
let length = 0;
|
|
1590
|
+
for (let i = 1; i < points.length; i++) {
|
|
1591
|
+
const dx = points[i].x - points[i - 1].x;
|
|
1592
|
+
const dy = points[i].y - points[i - 1].y;
|
|
1593
|
+
length += Math.sqrt(dx * dx + dy * dy);
|
|
1594
|
+
}
|
|
1595
|
+
return length;
|
|
1596
|
+
}
|
|
1597
|
+
function resampleByArcLength(points, spacing) {
|
|
1598
|
+
if (points.length < 2) {
|
|
1599
|
+
return [...points];
|
|
1600
|
+
}
|
|
1601
|
+
const result = [points[0]];
|
|
1602
|
+
let accumulated = 0;
|
|
1603
|
+
for (let i = 1; i < points.length; i++) {
|
|
1604
|
+
const prev = points[i - 1];
|
|
1605
|
+
const curr = points[i];
|
|
1606
|
+
const dx = curr.x - prev.x;
|
|
1607
|
+
const dy = curr.y - prev.y;
|
|
1608
|
+
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
1609
|
+
accumulated += segmentLength;
|
|
1610
|
+
while (accumulated >= spacing) {
|
|
1611
|
+
const overshoot = accumulated - spacing;
|
|
1612
|
+
const ratio = (segmentLength - overshoot) / segmentLength;
|
|
1613
|
+
const t = 1 - overshoot / segmentLength;
|
|
1614
|
+
result.push({
|
|
1615
|
+
x: prev.x + dx * ratio,
|
|
1616
|
+
y: prev.y + dy * ratio,
|
|
1617
|
+
pressure: lerpOptional(prev.pressure, curr.pressure, t),
|
|
1618
|
+
tiltX: lerpOptional(prev.tiltX, curr.tiltX, t),
|
|
1619
|
+
tiltY: lerpOptional(prev.tiltY, curr.tiltY, t),
|
|
1620
|
+
timestamp: lerp(prev.timestamp, curr.timestamp, t)
|
|
1621
|
+
});
|
|
1622
|
+
accumulated -= spacing;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const last = points[points.length - 1];
|
|
1626
|
+
const lastResult = result[result.length - 1];
|
|
1627
|
+
const finalDx = last.x - lastResult.x;
|
|
1628
|
+
const finalDy = last.y - lastResult.y;
|
|
1629
|
+
const finalDist = Math.sqrt(finalDx * finalDx + finalDy * finalDy);
|
|
1630
|
+
if (finalDist > spacing * 0.5) {
|
|
1631
|
+
result.push(last);
|
|
1632
|
+
}
|
|
1633
|
+
return result;
|
|
1634
|
+
}
|
|
1081
1635
|
exports.StabilizedPointer = StabilizedPointer;
|
|
1636
|
+
exports.StrokePredictor = StrokePredictor;
|
|
1082
1637
|
exports.bilateralKernel = bilateralKernel;
|
|
1083
1638
|
exports.boxKernel = boxKernel;
|
|
1639
|
+
exports.calculateArcLength = calculateArcLength;
|
|
1084
1640
|
exports.createFromPreset = createFromPreset;
|
|
1641
|
+
exports.createPredictor = createPredictor;
|
|
1085
1642
|
exports.createStabilizedPointer = createStabilizedPointer;
|
|
1643
|
+
exports.douglasPeuckerFilter = douglasPeuckerFilter;
|
|
1086
1644
|
exports.emaFilter = emaFilter;
|
|
1087
1645
|
exports.gaussianKernel = gaussianKernel;
|
|
1646
|
+
exports.interpolateCatmullRom = interpolateCatmullRom;
|
|
1088
1647
|
exports.isAdaptiveKernel = isAdaptiveKernel;
|
|
1089
1648
|
exports.kalmanFilter = kalmanFilter;
|
|
1090
1649
|
exports.linearPredictionFilter = linearPredictionFilter;
|
|
1091
1650
|
exports.movingAverageFilter = movingAverageFilter;
|
|
1092
1651
|
exports.noiseFilter = noiseFilter;
|
|
1093
1652
|
exports.oneEuroFilter = oneEuroFilter;
|
|
1653
|
+
exports.resampleByArcLength = resampleByArcLength;
|
|
1654
|
+
exports.simplify = simplify;
|
|
1094
1655
|
exports.smooth = smooth;
|
|
1095
1656
|
exports.stringFilter = stringFilter;
|
|
1657
|
+
exports.toSVGPath = toSVGPath;
|
|
1658
|
+
exports.toSVGPathCubic = toSVGPathCubic;
|
|
1659
|
+
exports.toSVGPathSmooth = toSVGPathSmooth;
|
|
1096
1660
|
exports.triangleKernel = triangleKernel;
|
|
1097
1661
|
//# sourceMappingURL=index.cjs.map
|