@stroke-stabilizer/core 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +397 -0
  2. package/dist/StabilizedPointer.d.ts +246 -0
  3. package/dist/StabilizedPointer.d.ts.map +1 -0
  4. package/dist/filters/EmaFilter.d.ts +25 -0
  5. package/dist/filters/EmaFilter.d.ts.map +1 -0
  6. package/dist/filters/KalmanFilter.d.ts +21 -0
  7. package/dist/filters/KalmanFilter.d.ts.map +1 -0
  8. package/dist/filters/LinearPredictionFilter.d.ts +54 -0
  9. package/dist/filters/LinearPredictionFilter.d.ts.map +1 -0
  10. package/dist/filters/MovingAverageFilter.d.ts +16 -0
  11. package/dist/filters/MovingAverageFilter.d.ts.map +1 -0
  12. package/dist/filters/NoiseFilter.d.ts +16 -0
  13. package/dist/filters/NoiseFilter.d.ts.map +1 -0
  14. package/dist/filters/OneEuroFilter.d.ts +45 -0
  15. package/dist/filters/OneEuroFilter.d.ts.map +1 -0
  16. package/dist/filters/StringFilter.d.ts +16 -0
  17. package/dist/filters/StringFilter.d.ts.map +1 -0
  18. package/dist/filters/index.d.ts +15 -0
  19. package/dist/filters/index.d.ts.map +1 -0
  20. package/dist/index.cjs +1086 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +1086 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/kernels/BilateralKernel.d.ts +48 -0
  27. package/dist/kernels/BilateralKernel.d.ts.map +1 -0
  28. package/dist/kernels/boxKernel.d.ts +16 -0
  29. package/dist/kernels/boxKernel.d.ts.map +1 -0
  30. package/dist/kernels/gaussianKernel.d.ts +17 -0
  31. package/dist/kernels/gaussianKernel.d.ts.map +1 -0
  32. package/dist/kernels/index.d.ts +11 -0
  33. package/dist/kernels/index.d.ts.map +1 -0
  34. package/dist/kernels/triangleKernel.d.ts +16 -0
  35. package/dist/kernels/triangleKernel.d.ts.map +1 -0
  36. package/dist/kernels/types.d.ts +38 -0
  37. package/dist/kernels/types.d.ts.map +1 -0
  38. package/dist/presets.d.ts +36 -0
  39. package/dist/presets.d.ts.map +1 -0
  40. package/dist/smooth.d.ts +27 -0
  41. package/dist/smooth.d.ts.map +1 -0
  42. package/dist/types.d.ts +33 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/package.json +46 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1086 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function isAdaptiveKernel(kernel) {
4
+ return "computeWeights" in kernel && typeof kernel.computeWeights === "function";
5
+ }
6
+ function applyPadding(points, halfSize, mode) {
7
+ if (points.length === 0) return [];
8
+ const padded = [];
9
+ for (let i = halfSize; i > 0; i--) {
10
+ switch (mode) {
11
+ case "reflect":
12
+ padded.push(points[Math.min(i, points.length - 1)]);
13
+ break;
14
+ case "edge":
15
+ padded.push(points[0]);
16
+ break;
17
+ case "zero":
18
+ padded.push({ x: 0, y: 0 });
19
+ break;
20
+ }
21
+ }
22
+ padded.push(...points);
23
+ for (let i = 1; i <= halfSize; i++) {
24
+ switch (mode) {
25
+ case "reflect":
26
+ padded.push(points[Math.max(0, points.length - 1 - i)]);
27
+ break;
28
+ case "edge":
29
+ padded.push(points[points.length - 1]);
30
+ break;
31
+ case "zero":
32
+ padded.push({ x: 0, y: 0 });
33
+ break;
34
+ }
35
+ }
36
+ return padded;
37
+ }
38
+ function smooth(points, options) {
39
+ const { kernel, padding = "reflect", preserveEndpoints = true } = options;
40
+ if (points.length === 0) return [];
41
+ const originalStart = points[0];
42
+ const originalEnd = points[points.length - 1];
43
+ let result;
44
+ if (isAdaptiveKernel(kernel)) {
45
+ const halfSize = Math.floor(kernel.size / 2);
46
+ const padded = applyPadding(points, halfSize, padding);
47
+ result = [];
48
+ for (let i = 0; i < points.length; i++) {
49
+ const centerIdx = i + halfSize;
50
+ const center = padded[centerIdx];
51
+ const neighbors = [];
52
+ for (let k = 0; k < kernel.size; k++) {
53
+ neighbors.push(padded[i + k]);
54
+ }
55
+ const weights = kernel.computeWeights(center, neighbors);
56
+ let sumX = 0;
57
+ let sumY = 0;
58
+ for (let k = 0; k < weights.length; k++) {
59
+ sumX += neighbors[k].x * weights[k];
60
+ sumY += neighbors[k].y * weights[k];
61
+ }
62
+ result.push({ x: sumX, y: sumY });
63
+ }
64
+ } else {
65
+ const fixedKernel = kernel;
66
+ const { weights } = fixedKernel;
67
+ if (weights.length <= 1) return [...points];
68
+ const halfSize = Math.floor(weights.length / 2);
69
+ const padded = applyPadding(points, halfSize, padding);
70
+ result = [];
71
+ for (let i = 0; i < points.length; i++) {
72
+ let sumX = 0;
73
+ let sumY = 0;
74
+ for (let k = 0; k < weights.length; k++) {
75
+ const point = padded[i + k];
76
+ sumX += point.x * weights[k];
77
+ sumY += point.y * weights[k];
78
+ }
79
+ result.push({ x: sumX, y: sumY });
80
+ }
81
+ }
82
+ if (preserveEndpoints && result.length > 0) {
83
+ result[0] = { ...originalStart };
84
+ if (result.length > 1) {
85
+ result[result.length - 1] = { ...originalEnd };
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ class StabilizedPointer {
91
+ constructor() {
92
+ this.filters = [];
93
+ this.postProcessors = [];
94
+ this.buffer = [];
95
+ this.lastRawPoint = null;
96
+ this.batchConfig = null;
97
+ this.pendingPoints = [];
98
+ this.rafId = null;
99
+ this.hasDrained = false;
100
+ }
101
+ /**
102
+ * Add a filter to the pipeline
103
+ * @returns this (for method chaining)
104
+ */
105
+ addFilter(filter) {
106
+ this.filters.push(filter);
107
+ return this;
108
+ }
109
+ /**
110
+ * Remove a filter by type
111
+ * @returns true if removed, false if not found
112
+ */
113
+ removeFilter(type) {
114
+ const index = this.filters.findIndex((f) => f.type === type);
115
+ if (index === -1) return false;
116
+ this.filters.splice(index, 1);
117
+ return true;
118
+ }
119
+ /**
120
+ * Update parameters of a filter by type
121
+ * @returns true if updated, false if not found
122
+ */
123
+ updateFilter(type, params) {
124
+ const filter = this.filters.find((f) => f.type === type);
125
+ if (!filter) return false;
126
+ if (this.isUpdatableFilter(filter)) {
127
+ filter.updateParams(params);
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+ /**
133
+ * Get a filter by type
134
+ */
135
+ getFilter(type) {
136
+ return this.filters.find((f) => f.type === type);
137
+ }
138
+ /**
139
+ * Get list of current filter types
140
+ */
141
+ getFilterTypes() {
142
+ return this.filters.map((f) => f.type);
143
+ }
144
+ /**
145
+ * Check if a filter exists
146
+ */
147
+ hasFilter(type) {
148
+ return this.filters.some((f) => f.type === type);
149
+ }
150
+ /**
151
+ * Process a point through the pipeline
152
+ * @returns Processed result (null if rejected by a filter)
153
+ */
154
+ process(point) {
155
+ this.lastRawPoint = point;
156
+ let current = point;
157
+ for (const filter of this.filters) {
158
+ if (current === null) break;
159
+ current = filter.process(current);
160
+ }
161
+ if (current !== null) {
162
+ this.buffer.push(current);
163
+ }
164
+ return current;
165
+ }
166
+ /**
167
+ * Process multiple points at once
168
+ * @returns Array of processed results (nulls excluded)
169
+ */
170
+ processAll(points) {
171
+ const results = [];
172
+ for (const point of points) {
173
+ const result = this.process(point);
174
+ if (result !== null) {
175
+ results.push(result);
176
+ }
177
+ }
178
+ return results;
179
+ }
180
+ /**
181
+ * Get the processed buffer
182
+ */
183
+ getBuffer() {
184
+ return this.buffer;
185
+ }
186
+ /**
187
+ * Clear and return the processed buffer
188
+ */
189
+ flushBuffer() {
190
+ const flushed = [...this.buffer];
191
+ this.buffer = [];
192
+ return flushed;
193
+ }
194
+ /**
195
+ * Reset all filters and clear the buffer
196
+ *
197
+ * Call this to prepare for a new stroke without destroying the pipeline configuration.
198
+ * Filters are reset to their initial state and the buffer is cleared.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * // After finishing a stroke
203
+ * const result = pointer.finish() // automatically calls reset()
204
+ *
205
+ * // Or manually reset without finishing
206
+ * pointer.reset()
207
+ * ```
208
+ */
209
+ reset() {
210
+ for (const filter of this.filters) {
211
+ filter.reset();
212
+ }
213
+ this.buffer = [];
214
+ this.lastRawPoint = null;
215
+ this.hasDrained = false;
216
+ }
217
+ /**
218
+ * Clear the pipeline (remove all filters)
219
+ */
220
+ clear() {
221
+ this.filters = [];
222
+ this.postProcessors = [];
223
+ this.buffer = [];
224
+ }
225
+ /**
226
+ * Get number of filters
227
+ */
228
+ get length() {
229
+ return this.filters.length;
230
+ }
231
+ // ========================================
232
+ // Post-processing layer
233
+ // ========================================
234
+ /**
235
+ * Add post-processor to the pipeline
236
+ * @returns this (for method chaining)
237
+ */
238
+ addPostProcess(kernel, options = {}) {
239
+ this.postProcessors.push({
240
+ kernel,
241
+ padding: options.padding ?? "reflect"
242
+ });
243
+ return this;
244
+ }
245
+ /**
246
+ * Remove a post-processor by type
247
+ * @returns true if removed, false if not found
248
+ */
249
+ removePostProcess(type) {
250
+ const index = this.postProcessors.findIndex((p) => p.kernel.type === type);
251
+ if (index === -1) return false;
252
+ this.postProcessors.splice(index, 1);
253
+ return true;
254
+ }
255
+ /**
256
+ * Check if a post-processor exists
257
+ */
258
+ hasPostProcess(type) {
259
+ return this.postProcessors.some((p) => p.kernel.type === type);
260
+ }
261
+ /**
262
+ * Get list of post-processor types
263
+ */
264
+ getPostProcessTypes() {
265
+ return this.postProcessors.map((p) => p.kernel.type);
266
+ }
267
+ /**
268
+ * Get number of post-processors
269
+ */
270
+ get postProcessLength() {
271
+ return this.postProcessors.length;
272
+ }
273
+ /**
274
+ * Finish the stroke and return post-processed results, without resetting
275
+ *
276
+ * Use this when you want to get the final result but keep the buffer intact.
277
+ * Useful for previewing post-processing results or comparing different settings.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * pointer.addPostProcess(gaussianKernel({ size: 5 }))
282
+ * const preview1 = pointer.finishWithoutReset()
283
+ *
284
+ * // Change settings and re-apply
285
+ * pointer.removePostProcess('gaussian')
286
+ * pointer.addPostProcess(bilateralKernel({ size: 7, sigmaValue: 10 }))
287
+ * const preview2 = pointer.finishWithoutReset()
288
+ *
289
+ * // Finalize when done
290
+ * const final = pointer.finish()
291
+ * ```
292
+ */
293
+ finishWithoutReset() {
294
+ if (this.batchConfig) {
295
+ this.flushBatch();
296
+ }
297
+ this.appendEndpoint();
298
+ let points = [...this.buffer];
299
+ for (const processor of this.postProcessors) {
300
+ points = smooth(points, {
301
+ kernel: processor.kernel,
302
+ padding: processor.padding
303
+ });
304
+ }
305
+ return points;
306
+ }
307
+ /**
308
+ * Append the final raw input point to ensure stroke ends at the actual endpoint
309
+ *
310
+ * Instead of draining filters (which adds many extra points),
311
+ * we simply append the raw endpoint. The post-processing phase
312
+ * with bidirectional convolution will naturally smooth the transition.
313
+ */
314
+ appendEndpoint() {
315
+ if (this.hasDrained) {
316
+ return;
317
+ }
318
+ if (this.lastRawPoint === null || this.buffer.length === 0) {
319
+ return;
320
+ }
321
+ this.hasDrained = true;
322
+ const target = this.lastRawPoint;
323
+ const lastBufferPoint = this.buffer[this.buffer.length - 1];
324
+ const dx = target.x - lastBufferPoint.x;
325
+ const dy = target.y - lastBufferPoint.y;
326
+ const distance = Math.sqrt(dx * dx + dy * dy);
327
+ if (distance < 1) {
328
+ return;
329
+ }
330
+ this.buffer.push({
331
+ x: target.x,
332
+ y: target.y,
333
+ pressure: target.pressure ?? 1,
334
+ timestamp: target.timestamp + 8
335
+ });
336
+ }
337
+ /**
338
+ * Finish the stroke and return post-processed results
339
+ *
340
+ * This applies all post-processors to the buffer, then resets filters and clears the buffer.
341
+ * Use this at the end of a stroke to get the final smoothed result.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * // During drawing
346
+ * pointer.process(point)
347
+ *
348
+ * // On stroke end
349
+ * const finalStroke = pointer.finish()
350
+ * ```
351
+ */
352
+ finish() {
353
+ const points = this.finishWithoutReset();
354
+ this.reset();
355
+ return points;
356
+ }
357
+ // ========================================
358
+ // Batch processing layer (rAF)
359
+ // ========================================
360
+ /**
361
+ * Enable requestAnimationFrame batch processing
362
+ *
363
+ * When enabled, points queued via queue() are batched and processed
364
+ * on the next animation frame, reducing CPU load for high-frequency
365
+ * pointer events.
366
+ *
367
+ * @returns this (for method chaining)
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * const pointer = new StabilizedPointer()
372
+ * .addFilter(noiseFilter({ minDistance: 2 }))
373
+ * .enableBatching({
374
+ * onBatch: (points) => drawPoints(points),
375
+ * onPoint: (point) => updatePreview(point)
376
+ * })
377
+ *
378
+ * canvas.onpointermove = (e) => {
379
+ * pointer.queue({ x: e.clientX, y: e.clientY, timestamp: e.timeStamp })
380
+ * }
381
+ * ```
382
+ */
383
+ enableBatching(config = {}) {
384
+ this.batchConfig = config;
385
+ return this;
386
+ }
387
+ /**
388
+ * Disable batch processing
389
+ * Flushes any pending points before disabling
390
+ * @returns this (for method chaining)
391
+ */
392
+ disableBatching() {
393
+ this.flushBatch();
394
+ this.batchConfig = null;
395
+ return this;
396
+ }
397
+ /**
398
+ * Check if batch processing is enabled
399
+ */
400
+ get isBatchingEnabled() {
401
+ return this.batchConfig !== null;
402
+ }
403
+ /**
404
+ * Queue a point for batch processing
405
+ *
406
+ * If batching is enabled, the point is queued and processed on the next
407
+ * animation frame. If batching is disabled, the point is processed immediately.
408
+ *
409
+ * @returns this (for method chaining, useful for queueing multiple points)
410
+ */
411
+ queue(point) {
412
+ if (this.batchConfig) {
413
+ this.pendingPoints.push(point);
414
+ this.scheduleFlush();
415
+ } else {
416
+ this.process(point);
417
+ }
418
+ return this;
419
+ }
420
+ /**
421
+ * Queue multiple points for batch processing
422
+ * @returns this (for method chaining)
423
+ */
424
+ queueAll(points) {
425
+ if (this.batchConfig) {
426
+ this.pendingPoints.push(...points);
427
+ this.scheduleFlush();
428
+ } else {
429
+ this.processAll(points);
430
+ }
431
+ return this;
432
+ }
433
+ /**
434
+ * Force flush pending batched points immediately
435
+ * @returns Array of processed points
436
+ */
437
+ flushBatch() {
438
+ this.cancelScheduledFlush();
439
+ return this.processPendingPoints();
440
+ }
441
+ /**
442
+ * Get number of pending points in the batch queue
443
+ */
444
+ get pendingCount() {
445
+ return this.pendingPoints.length;
446
+ }
447
+ // ----------------------------------------
448
+ // Private batch processing methods
449
+ // ----------------------------------------
450
+ scheduleFlush() {
451
+ if (this.rafId !== null) return;
452
+ if (typeof requestAnimationFrame !== "undefined") {
453
+ this.rafId = requestAnimationFrame(() => {
454
+ this.rafId = null;
455
+ this.processPendingPoints();
456
+ });
457
+ } else {
458
+ this.rafId = setTimeout(() => {
459
+ this.rafId = null;
460
+ this.processPendingPoints();
461
+ }, 16);
462
+ }
463
+ }
464
+ cancelScheduledFlush() {
465
+ if (this.rafId === null) return;
466
+ if (typeof cancelAnimationFrame !== "undefined") {
467
+ cancelAnimationFrame(this.rafId);
468
+ } else {
469
+ clearTimeout(this.rafId);
470
+ }
471
+ this.rafId = null;
472
+ }
473
+ processPendingPoints() {
474
+ var _a, _b, _c, _d;
475
+ if (this.pendingPoints.length === 0) return [];
476
+ const points = this.pendingPoints;
477
+ this.pendingPoints = [];
478
+ const results = [];
479
+ for (const point of points) {
480
+ const result = this.process(point);
481
+ if (result !== null) {
482
+ results.push(result);
483
+ (_b = (_a = this.batchConfig) == null ? void 0 : _a.onPoint) == null ? void 0 : _b.call(_a, result);
484
+ }
485
+ }
486
+ if (results.length > 0) {
487
+ (_d = (_c = this.batchConfig) == null ? void 0 : _c.onBatch) == null ? void 0 : _d.call(_c, results);
488
+ }
489
+ return results;
490
+ }
491
+ isUpdatableFilter(filter) {
492
+ return "updateParams" in filter && typeof filter.updateParams === "function";
493
+ }
494
+ }
495
+ const FILTER_TYPE$6 = "noise";
496
+ class NoiseFilterImpl {
497
+ constructor(params) {
498
+ this.type = FILTER_TYPE$6;
499
+ this.lastPoint = null;
500
+ this.params = { ...params };
501
+ }
502
+ process(point) {
503
+ if (this.lastPoint === null) {
504
+ this.lastPoint = point;
505
+ return point;
506
+ }
507
+ const dx = point.x - this.lastPoint.x;
508
+ const dy = point.y - this.lastPoint.y;
509
+ const distance = Math.sqrt(dx * dx + dy * dy);
510
+ if (distance < this.params.minDistance) {
511
+ return null;
512
+ }
513
+ this.lastPoint = point;
514
+ return point;
515
+ }
516
+ updateParams(params) {
517
+ this.params = { ...this.params, ...params };
518
+ }
519
+ reset() {
520
+ this.lastPoint = null;
521
+ }
522
+ }
523
+ function noiseFilter(params) {
524
+ return new NoiseFilterImpl(params);
525
+ }
526
+ const FILTER_TYPE$5 = "kalman";
527
+ class KalmanFilterImpl {
528
+ constructor(params) {
529
+ this.type = FILTER_TYPE$5;
530
+ this.state = null;
531
+ this.params = { ...params };
532
+ }
533
+ process(point) {
534
+ if (this.state === null) {
535
+ this.state = {
536
+ x: point.x,
537
+ y: point.y,
538
+ p: 1,
539
+ lastTimestamp: point.timestamp
540
+ };
541
+ return point;
542
+ }
543
+ const { processNoise: Q, measurementNoise: R } = this.params;
544
+ const predictedX = this.state.x;
545
+ const predictedY = this.state.y;
546
+ const predictedP = this.state.p + Q;
547
+ const K = predictedP / (predictedP + R);
548
+ const newX = predictedX + K * (point.x - predictedX);
549
+ const newY = predictedY + K * (point.y - predictedY);
550
+ const newP = (1 - K) * predictedP;
551
+ this.state = {
552
+ x: newX,
553
+ y: newY,
554
+ p: newP,
555
+ lastTimestamp: point.timestamp
556
+ };
557
+ return {
558
+ x: newX,
559
+ y: newY,
560
+ pressure: point.pressure,
561
+ timestamp: point.timestamp
562
+ };
563
+ }
564
+ updateParams(params) {
565
+ this.params = { ...this.params, ...params };
566
+ }
567
+ reset() {
568
+ this.state = null;
569
+ }
570
+ }
571
+ function kalmanFilter(params) {
572
+ return new KalmanFilterImpl(params);
573
+ }
574
+ const FILTER_TYPE$4 = "movingAverage";
575
+ class MovingAverageFilterImpl {
576
+ constructor(params) {
577
+ this.type = FILTER_TYPE$4;
578
+ this.window = [];
579
+ this.params = { ...params };
580
+ }
581
+ process(point) {
582
+ this.window.push(point);
583
+ while (this.window.length > this.params.windowSize) {
584
+ this.window.shift();
585
+ }
586
+ let sumX = 0;
587
+ let sumY = 0;
588
+ let sumPressure = 0;
589
+ let pressureCount = 0;
590
+ for (const p of this.window) {
591
+ sumX += p.x;
592
+ sumY += p.y;
593
+ if (p.pressure !== void 0) {
594
+ sumPressure += p.pressure;
595
+ pressureCount++;
596
+ }
597
+ }
598
+ const avgX = sumX / this.window.length;
599
+ const avgY = sumY / this.window.length;
600
+ const avgPressure = pressureCount > 0 ? sumPressure / pressureCount : void 0;
601
+ return {
602
+ x: avgX,
603
+ y: avgY,
604
+ pressure: avgPressure,
605
+ timestamp: point.timestamp
606
+ };
607
+ }
608
+ updateParams(params) {
609
+ this.params = { ...this.params, ...params };
610
+ while (this.window.length > this.params.windowSize) {
611
+ this.window.shift();
612
+ }
613
+ }
614
+ reset() {
615
+ this.window = [];
616
+ }
617
+ }
618
+ function movingAverageFilter(params) {
619
+ return new MovingAverageFilterImpl(params);
620
+ }
621
+ const FILTER_TYPE$3 = "string";
622
+ class StringFilterImpl {
623
+ constructor(params) {
624
+ this.type = FILTER_TYPE$3;
625
+ this.anchorPoint = null;
626
+ this.params = { ...params };
627
+ }
628
+ process(point) {
629
+ if (this.anchorPoint === null) {
630
+ this.anchorPoint = point;
631
+ return point;
632
+ }
633
+ const dx = point.x - this.anchorPoint.x;
634
+ const dy = point.y - this.anchorPoint.y;
635
+ const distance = Math.sqrt(dx * dx + dy * dy);
636
+ if (distance <= this.params.stringLength) {
637
+ return {
638
+ ...this.anchorPoint,
639
+ pressure: point.pressure,
640
+ timestamp: point.timestamp
641
+ };
642
+ }
643
+ const ratio = (distance - this.params.stringLength) / distance;
644
+ const newX = this.anchorPoint.x + dx * ratio;
645
+ const newY = this.anchorPoint.y + dy * ratio;
646
+ this.anchorPoint = {
647
+ x: newX,
648
+ y: newY,
649
+ pressure: point.pressure,
650
+ timestamp: point.timestamp
651
+ };
652
+ return this.anchorPoint;
653
+ }
654
+ updateParams(params) {
655
+ this.params = { ...this.params, ...params };
656
+ }
657
+ reset() {
658
+ this.anchorPoint = null;
659
+ }
660
+ }
661
+ function stringFilter(params) {
662
+ return new StringFilterImpl(params);
663
+ }
664
+ const FILTER_TYPE$2 = "ema";
665
+ class EmaFilterImpl {
666
+ constructor(params) {
667
+ this.type = FILTER_TYPE$2;
668
+ this.lastPoint = null;
669
+ this.params = { ...params };
670
+ }
671
+ process(point) {
672
+ if (this.lastPoint === null) {
673
+ this.lastPoint = point;
674
+ return point;
675
+ }
676
+ const { alpha } = this.params;
677
+ const newX = alpha * point.x + (1 - alpha) * this.lastPoint.x;
678
+ const newY = alpha * point.y + (1 - alpha) * this.lastPoint.y;
679
+ let newPressure;
680
+ if (point.pressure !== void 0 && this.lastPoint.pressure !== void 0) {
681
+ newPressure = alpha * point.pressure + (1 - alpha) * this.lastPoint.pressure;
682
+ } else {
683
+ newPressure = point.pressure;
684
+ }
685
+ this.lastPoint = {
686
+ x: newX,
687
+ y: newY,
688
+ pressure: newPressure,
689
+ timestamp: point.timestamp
690
+ };
691
+ return this.lastPoint;
692
+ }
693
+ updateParams(params) {
694
+ this.params = { ...this.params, ...params };
695
+ }
696
+ reset() {
697
+ this.lastPoint = null;
698
+ }
699
+ }
700
+ function emaFilter(params) {
701
+ return new EmaFilterImpl(params);
702
+ }
703
+ const FILTER_TYPE$1 = "oneEuro";
704
+ class LowPassFilter {
705
+ constructor() {
706
+ this.y = null;
707
+ this.alpha = 1;
708
+ }
709
+ setAlpha(alpha) {
710
+ this.alpha = Math.max(0, Math.min(1, alpha));
711
+ }
712
+ filter(value) {
713
+ if (this.y === null) {
714
+ this.y = value;
715
+ } else {
716
+ this.y = this.alpha * value + (1 - this.alpha) * this.y;
717
+ }
718
+ return this.y;
719
+ }
720
+ reset() {
721
+ this.y = null;
722
+ }
723
+ lastValue() {
724
+ return this.y;
725
+ }
726
+ }
727
+ class OneEuroFilterImpl {
728
+ constructor(params) {
729
+ this.type = FILTER_TYPE$1;
730
+ this.xFilter = new LowPassFilter();
731
+ this.yFilter = new LowPassFilter();
732
+ this.dxFilter = new LowPassFilter();
733
+ this.dyFilter = new LowPassFilter();
734
+ this.pressureFilter = new LowPassFilter();
735
+ this.lastTimestamp = null;
736
+ this.params = {
737
+ dCutoff: 1,
738
+ ...params
739
+ };
740
+ }
741
+ process(point) {
742
+ let rate = 60;
743
+ if (this.lastTimestamp !== null) {
744
+ const dt = (point.timestamp - this.lastTimestamp) / 1e3;
745
+ if (dt > 0) {
746
+ rate = 1 / dt;
747
+ }
748
+ }
749
+ this.lastTimestamp = point.timestamp;
750
+ const { minCutoff, beta, dCutoff } = this.params;
751
+ const newX = this.filterAxis(
752
+ point.x,
753
+ this.xFilter,
754
+ this.dxFilter,
755
+ rate,
756
+ minCutoff,
757
+ beta,
758
+ dCutoff
759
+ );
760
+ const newY = this.filterAxis(
761
+ point.y,
762
+ this.yFilter,
763
+ this.dyFilter,
764
+ rate,
765
+ minCutoff,
766
+ beta,
767
+ dCutoff
768
+ );
769
+ let newPressure;
770
+ if (point.pressure !== void 0) {
771
+ const alpha = this.computeAlpha(minCutoff, rate);
772
+ this.pressureFilter.setAlpha(alpha);
773
+ newPressure = this.pressureFilter.filter(point.pressure);
774
+ }
775
+ return {
776
+ x: newX,
777
+ y: newY,
778
+ pressure: newPressure,
779
+ timestamp: point.timestamp
780
+ };
781
+ }
782
+ filterAxis(value, valueFilter, derivFilter, rate, minCutoff, beta, dCutoff) {
783
+ const prevValue = valueFilter.lastValue();
784
+ let dValue = 0;
785
+ if (prevValue !== null) {
786
+ dValue = (value - prevValue) * rate;
787
+ }
788
+ const dAlpha = this.computeAlpha(dCutoff, rate);
789
+ derivFilter.setAlpha(dAlpha);
790
+ const filteredDValue = derivFilter.filter(dValue);
791
+ const cutoff = minCutoff + beta * Math.abs(filteredDValue);
792
+ const alpha = this.computeAlpha(cutoff, rate);
793
+ valueFilter.setAlpha(alpha);
794
+ return valueFilter.filter(value);
795
+ }
796
+ computeAlpha(cutoff, rate) {
797
+ const tau = 1 / (2 * Math.PI * cutoff);
798
+ const te = 1 / rate;
799
+ return 1 / (1 + tau / te);
800
+ }
801
+ updateParams(params) {
802
+ this.params = { ...this.params, ...params };
803
+ }
804
+ reset() {
805
+ this.xFilter.reset();
806
+ this.yFilter.reset();
807
+ this.dxFilter.reset();
808
+ this.dyFilter.reset();
809
+ this.pressureFilter.reset();
810
+ this.lastTimestamp = null;
811
+ }
812
+ }
813
+ function oneEuroFilter(params) {
814
+ return new OneEuroFilterImpl(params);
815
+ }
816
+ const FILTER_TYPE = "linearPrediction";
817
+ class LinearPredictionFilterImpl {
818
+ constructor(params) {
819
+ this.type = FILTER_TYPE;
820
+ this.history = [];
821
+ this.lastOutput = null;
822
+ this.params = {
823
+ smoothing: 0.6,
824
+ ...params
825
+ };
826
+ }
827
+ process(point) {
828
+ this.history.push(point);
829
+ while (this.history.length > this.params.historySize + 1) {
830
+ this.history.shift();
831
+ }
832
+ if (this.history.length === 1) {
833
+ this.lastOutput = point;
834
+ return point;
835
+ }
836
+ const { velocity, acceleration } = this.estimateMotion();
837
+ const dt = this.history.length >= 2 ? (this.history[this.history.length - 1].timestamp - this.history[this.history.length - 2].timestamp) / 1e3 : 1 / 60;
838
+ const { predictionFactor } = this.params;
839
+ const predictedX = point.x + velocity.x * dt * predictionFactor + 0.5 * acceleration.x * dt * dt * predictionFactor;
840
+ const predictedY = point.y + velocity.y * dt * predictionFactor + 0.5 * acceleration.y * dt * dt * predictionFactor;
841
+ let outputX = predictedX;
842
+ let outputY = predictedY;
843
+ let outputPressure = point.pressure;
844
+ if (this.lastOutput !== null && this.params.smoothing !== void 0) {
845
+ const s = this.params.smoothing;
846
+ outputX = s * predictedX + (1 - s) * this.lastOutput.x;
847
+ outputY = s * predictedY + (1 - s) * this.lastOutput.y;
848
+ if (point.pressure !== void 0 && this.lastOutput.pressure !== void 0) {
849
+ outputPressure = s * point.pressure + (1 - s) * this.lastOutput.pressure;
850
+ }
851
+ }
852
+ this.lastOutput = {
853
+ x: outputX,
854
+ y: outputY,
855
+ pressure: outputPressure,
856
+ timestamp: point.timestamp
857
+ };
858
+ return this.lastOutput;
859
+ }
860
+ /**
861
+ * Estimate velocity and acceleration using least squares
862
+ */
863
+ estimateMotion() {
864
+ const n = this.history.length;
865
+ if (n < 2) {
866
+ return {
867
+ velocity: { x: 0, y: 0 },
868
+ acceleration: { x: 0, y: 0 }
869
+ };
870
+ }
871
+ const t0 = this.history[0].timestamp;
872
+ const times = this.history.map((p) => (p.timestamp - t0) / 1e3);
873
+ const xs = this.history.map((p) => p.x);
874
+ const ys = this.history.map((p) => p.y);
875
+ if (n === 2) {
876
+ const dt = times[1] - times[0];
877
+ if (dt <= 0) {
878
+ return { velocity: { x: 0, y: 0 }, acceleration: { x: 0, y: 0 } };
879
+ }
880
+ return {
881
+ velocity: {
882
+ x: (xs[1] - xs[0]) / dt,
883
+ y: (ys[1] - ys[0]) / dt
884
+ },
885
+ acceleration: { x: 0, y: 0 }
886
+ };
887
+ }
888
+ const fitX = this.polynomialFit(times, xs);
889
+ const fitY = this.polynomialFit(times, ys);
890
+ const lastT = times[times.length - 1];
891
+ return {
892
+ velocity: {
893
+ x: fitX.b + 2 * fitX.c * lastT,
894
+ y: fitY.b + 2 * fitY.c * lastT
895
+ },
896
+ acceleration: {
897
+ x: 2 * fitX.c,
898
+ y: 2 * fitY.c
899
+ }
900
+ };
901
+ }
902
+ /**
903
+ * Quadratic polynomial least squares fitting
904
+ * y = a + b*x + c*x^2
905
+ */
906
+ polynomialFit(x, y) {
907
+ const n = x.length;
908
+ let sumX = 0, sumX2 = 0, sumX3 = 0, sumX4 = 0;
909
+ let sumY = 0, sumXY = 0, sumX2Y = 0;
910
+ for (let i = 0; i < n; i++) {
911
+ const xi = x[i];
912
+ const yi = y[i];
913
+ const xi2 = xi * xi;
914
+ sumX += xi;
915
+ sumX2 += xi2;
916
+ sumX3 += xi2 * xi;
917
+ sumX4 += xi2 * xi2;
918
+ sumY += yi;
919
+ sumXY += xi * yi;
920
+ sumX2Y += xi2 * yi;
921
+ }
922
+ const det = n * (sumX2 * sumX4 - sumX3 * sumX3) - sumX * (sumX * sumX4 - sumX3 * sumX2) + sumX2 * (sumX * sumX3 - sumX2 * sumX2);
923
+ if (Math.abs(det) < 1e-10) {
924
+ const avgX = sumX / n;
925
+ const avgY = sumY / n;
926
+ let num = 0, den = 0;
927
+ for (let i = 0; i < n; i++) {
928
+ num += (x[i] - avgX) * (y[i] - avgY);
929
+ den += (x[i] - avgX) * (x[i] - avgX);
930
+ }
931
+ const b2 = den > 0 ? num / den : 0;
932
+ const a2 = avgY - b2 * avgX;
933
+ return { a: a2, b: b2, c: 0 };
934
+ }
935
+ const a = (sumY * (sumX2 * sumX4 - sumX3 * sumX3) - sumX * (sumXY * sumX4 - sumX3 * sumX2Y) + sumX2 * (sumXY * sumX3 - sumX2 * sumX2Y)) / det;
936
+ const b = (n * (sumXY * sumX4 - sumX3 * sumX2Y) - sumY * (sumX * sumX4 - sumX3 * sumX2) + sumX2 * (sumX * sumX2Y - sumXY * sumX2)) / det;
937
+ const c = (n * (sumX2 * sumX2Y - sumXY * sumX3) - sumX * (sumX * sumX2Y - sumXY * sumX2) + sumY * (sumX * sumX3 - sumX2 * sumX2)) / det;
938
+ return { a, b, c };
939
+ }
940
+ updateParams(params) {
941
+ this.params = { ...this.params, ...params };
942
+ }
943
+ reset() {
944
+ this.history = [];
945
+ this.lastOutput = null;
946
+ }
947
+ }
948
+ function linearPredictionFilter(params) {
949
+ return new LinearPredictionFilterImpl(params);
950
+ }
951
+ function createStabilizedPointer(level) {
952
+ const clampedLevel = Math.max(0, Math.min(100, level));
953
+ const pointer = new StabilizedPointer();
954
+ if (clampedLevel === 0) {
955
+ return pointer;
956
+ }
957
+ const minDistance = 1 + clampedLevel / 100 * 2;
958
+ pointer.addFilter(noiseFilter({ minDistance }));
959
+ if (clampedLevel >= 21) {
960
+ const processNoise = 0.12 - clampedLevel / 100 * 0.08;
961
+ const measurementNoise = 0.4 + clampedLevel / 100 * 0.6;
962
+ pointer.addFilter(kalmanFilter({ processNoise, measurementNoise }));
963
+ }
964
+ if (clampedLevel >= 41) {
965
+ const windowSize = clampedLevel >= 61 ? 7 : 5;
966
+ pointer.addFilter(movingAverageFilter({ windowSize }));
967
+ }
968
+ if (clampedLevel >= 61) {
969
+ const stringLength = clampedLevel >= 81 ? 15 : 8;
970
+ pointer.addFilter(stringFilter({ stringLength }));
971
+ }
972
+ return pointer;
973
+ }
974
+ const presetLevels = {
975
+ none: 0,
976
+ light: 20,
977
+ medium: 50,
978
+ heavy: 75,
979
+ extreme: 100
980
+ };
981
+ function createFromPreset(preset) {
982
+ return createStabilizedPointer(presetLevels[preset]);
983
+ }
984
+ function gaussianKernel(params) {
985
+ const { size } = params;
986
+ const sigma = params.sigma ?? size / 3;
987
+ const actualSize = size % 2 === 0 ? size + 1 : size;
988
+ const halfSize = Math.floor(actualSize / 2);
989
+ const weights = [];
990
+ let sum = 0;
991
+ for (let i = 0; i < actualSize; i++) {
992
+ const x = i - halfSize;
993
+ const weight = Math.exp(-(x * x) / (2 * sigma * sigma));
994
+ weights.push(weight);
995
+ sum += weight;
996
+ }
997
+ for (let i = 0; i < weights.length; i++) {
998
+ weights[i] /= sum;
999
+ }
1000
+ return {
1001
+ type: "gaussian",
1002
+ weights
1003
+ };
1004
+ }
1005
+ function boxKernel(params) {
1006
+ const { size } = params;
1007
+ const actualSize = size % 2 === 0 ? size + 1 : size;
1008
+ const weight = 1 / actualSize;
1009
+ const weights = Array(actualSize).fill(weight);
1010
+ return {
1011
+ type: "box",
1012
+ weights
1013
+ };
1014
+ }
1015
+ function triangleKernel(params) {
1016
+ const { size } = params;
1017
+ const actualSize = size % 2 === 0 ? size + 1 : size;
1018
+ const halfSize = Math.floor(actualSize / 2);
1019
+ const weights = [];
1020
+ let sum = 0;
1021
+ for (let i = 0; i < actualSize; i++) {
1022
+ const weight = halfSize + 1 - Math.abs(i - halfSize);
1023
+ weights.push(weight);
1024
+ sum += weight;
1025
+ }
1026
+ for (let i = 0; i < weights.length; i++) {
1027
+ weights[i] /= sum;
1028
+ }
1029
+ return {
1030
+ type: "triangle",
1031
+ weights
1032
+ };
1033
+ }
1034
+ function bilateralKernel(params) {
1035
+ const { size, sigmaValue } = params;
1036
+ const actualSize = size % 2 === 0 ? size + 1 : size;
1037
+ const sigmaSpace = params.sigmaSpace ?? actualSize / 3;
1038
+ const halfSize = Math.floor(actualSize / 2);
1039
+ const spatialWeights = [];
1040
+ for (let i = 0; i < actualSize; i++) {
1041
+ const d = i - halfSize;
1042
+ spatialWeights.push(Math.exp(-(d * d) / (2 * sigmaSpace * sigmaSpace)));
1043
+ }
1044
+ return {
1045
+ type: "bilateral",
1046
+ size: actualSize,
1047
+ sigmaSpace,
1048
+ sigmaValue,
1049
+ computeWeights(center, neighbors) {
1050
+ const weights = [];
1051
+ let sum = 0;
1052
+ for (let i = 0; i < neighbors.length; i++) {
1053
+ const dx = neighbors[i].x - center.x;
1054
+ const dy = neighbors[i].y - center.y;
1055
+ const valueDiff = dx * dx + dy * dy;
1056
+ const valueWeight = Math.exp(-valueDiff / (2 * sigmaValue * sigmaValue));
1057
+ const weight = spatialWeights[i] * valueWeight;
1058
+ weights.push(weight);
1059
+ sum += weight;
1060
+ }
1061
+ if (sum > 0) {
1062
+ for (let i = 0; i < weights.length; i++) {
1063
+ weights[i] /= sum;
1064
+ }
1065
+ }
1066
+ return weights;
1067
+ }
1068
+ };
1069
+ }
1070
+ exports.StabilizedPointer = StabilizedPointer;
1071
+ exports.bilateralKernel = bilateralKernel;
1072
+ exports.boxKernel = boxKernel;
1073
+ exports.createFromPreset = createFromPreset;
1074
+ exports.createStabilizedPointer = createStabilizedPointer;
1075
+ exports.emaFilter = emaFilter;
1076
+ exports.gaussianKernel = gaussianKernel;
1077
+ exports.isAdaptiveKernel = isAdaptiveKernel;
1078
+ exports.kalmanFilter = kalmanFilter;
1079
+ exports.linearPredictionFilter = linearPredictionFilter;
1080
+ exports.movingAverageFilter = movingAverageFilter;
1081
+ exports.noiseFilter = noiseFilter;
1082
+ exports.oneEuroFilter = oneEuroFilter;
1083
+ exports.smooth = smooth;
1084
+ exports.stringFilter = stringFilter;
1085
+ exports.triangleKernel = triangleKernel;
1086
+ //# sourceMappingURL=index.cjs.map