@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/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$6 = "noise";
503
+ const FILTER_TYPE$7 = "noise";
504
504
  class NoiseFilterImpl {
505
505
  constructor(params) {
506
- this.type = FILTER_TYPE$6;
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$5 = "kalman";
534
+ const FILTER_TYPE$6 = "kalman";
535
535
  class KalmanFilterImpl {
536
536
  constructor(params) {
537
- this.type = FILTER_TYPE$5;
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$4 = "movingAverage";
584
+ const FILTER_TYPE$5 = "movingAverage";
583
585
  class MovingAverageFilterImpl {
584
586
  constructor(params) {
585
- this.type = FILTER_TYPE$4;
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$3 = "string";
647
+ const FILTER_TYPE$4 = "string";
630
648
  class StringFilterImpl {
631
649
  constructor(params) {
632
- this.type = FILTER_TYPE$3;
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$2 = "ema";
694
+ const FILTER_TYPE$3 = "ema";
673
695
  class EmaFilterImpl {
674
696
  constructor(params) {
675
- this.type = FILTER_TYPE$2;
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$1 = "oneEuro";
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$1;
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