@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.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$6 = "noise";
505
+ const FILTER_TYPE$7 = "noise";
506
506
  class NoiseFilterImpl {
507
507
  constructor(params) {
508
- this.type = FILTER_TYPE$6;
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$5 = "kalman";
536
+ const FILTER_TYPE$6 = "kalman";
537
537
  class KalmanFilterImpl {
538
538
  constructor(params) {
539
- this.type = FILTER_TYPE$5;
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$4 = "movingAverage";
586
+ const FILTER_TYPE$5 = "movingAverage";
585
587
  class MovingAverageFilterImpl {
586
588
  constructor(params) {
587
- this.type = FILTER_TYPE$4;
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$3 = "string";
649
+ const FILTER_TYPE$4 = "string";
632
650
  class StringFilterImpl {
633
651
  constructor(params) {
634
- this.type = FILTER_TYPE$3;
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$2 = "ema";
696
+ const FILTER_TYPE$3 = "ema";
675
697
  class EmaFilterImpl {
676
698
  constructor(params) {
677
- this.type = FILTER_TYPE$2;
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$1 = "oneEuro";
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$1;
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