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