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