@stroke-stabilizer/core 0.2.16 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @stroke-stabilizer/core
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@stroke-stabilizer/core.svg)](https://www.npmjs.com/package/@stroke-stabilizer/core)
4
+ [![CI](https://github.com/usapopopooon/stroke-stabilizer/actions/workflows/ci.yml/badge.svg)](https://github.com/usapopopooon/stroke-stabilizer/actions/workflows/ci.yml)
5
+ [![Coverage](https://img.shields.io/badge/coverage-99.65%25-brightgreen)](https://github.com/usapopopooon/stroke-stabilizer)
4
6
 
5
7
  [日本語](./docs/README.ja.md)
6
8
 
@@ -19,6 +21,10 @@ Reduce hand tremor and smooth pen/mouse input in real-time using a flexible filt
19
21
  - **Automatic Endpoint Correction** - Strokes end at the actual input point
20
22
  - **rAF Batch Processing** - Coalesce high-frequency pointer events into animation frames
21
23
  - **8 Built-in Filters** - From simple moving average to adaptive One Euro Filter
24
+ - **Douglas-Peucker Simplification** - Reduce point count while preserving shape
25
+ - **SVG Path Output** - Convert strokes to SVG path data
26
+ - **Stroke Prediction** - Reduce perceived latency with velocity-based prediction
27
+ - **Catmull-Rom Interpolation** - Generate smooth curves between points
22
28
  - **Edge-preserving Smoothing** - Bilateral kernel for sharp corner preservation
23
29
  - **TypeScript First** - Full type safety with exported types
24
30
  - **Zero Dependencies** - Pure JavaScript, works anywhere
@@ -120,6 +126,7 @@ Without `getCoalescedEvents()`, fast strokes will appear jagged regardless of fi
120
126
  | `stringFilter` | Lazy Brush algorithm | Delayed, smooth strokes |
121
127
  | `oneEuroFilter` | Adaptive lowpass filter | Best balance of smoothness/latency |
122
128
  | `linearPredictionFilter` | Predicts next position | Lag compensation |
129
+ | `douglasPeuckerFilter` | Simplifies point sequences | Reduce data size |
123
130
 
124
131
  ### Post-processing Kernels
125
132
 
@@ -390,6 +397,103 @@ bilateralKernel({
390
397
  })
391
398
  ```
392
399
 
400
+ ### Douglas-Peucker Simplification
401
+
402
+ Reduce the number of points while preserving the shape of the stroke.
403
+
404
+ ```ts
405
+ import { douglasPeuckerFilter, simplify } from '@stroke-stabilizer/core'
406
+
407
+ // As a filter in the pipeline
408
+ const pointer = new StabilizedPointer().addFilter(
409
+ douglasPeuckerFilter({ epsilon: 2 })
410
+ )
411
+
412
+ // As a standalone function
413
+ const simplified = simplify(points, 2) // epsilon = 2px tolerance
414
+ ```
415
+
416
+ ### SVG Path Output
417
+
418
+ Convert processed strokes to SVG path data for rendering or export.
419
+
420
+ ```ts
421
+ import {
422
+ toSVGPath,
423
+ toSVGPathSmooth,
424
+ toSVGPathCubic,
425
+ } from '@stroke-stabilizer/core'
426
+
427
+ const points = pointer.finish()
428
+
429
+ // Simple polyline (M/L commands)
430
+ const pathData = toSVGPath(points)
431
+ // "M 10.00 20.00 L 30.00 40.00 L 50.00 60.00"
432
+
433
+ // Quadratic Bezier curves (smoother)
434
+ const smoothPath = toSVGPathSmooth(points, { tension: 0.5 })
435
+
436
+ // Cubic Bezier curves (smoothest)
437
+ const cubicPath = toSVGPathCubic(points, { smoothing: 0.25 })
438
+
439
+ // Use in SVG
440
+ svgElement.innerHTML = `<path d="${pathData}" stroke="black" fill="none"/>`
441
+ ```
442
+
443
+ ### Stroke Prediction
444
+
445
+ Reduce perceived latency by predicting the next pen position based on velocity.
446
+
447
+ ```ts
448
+ import { StrokePredictor } from '@stroke-stabilizer/core'
449
+
450
+ const predictor = new StrokePredictor({
451
+ historySize: 4, // Points used for velocity estimation
452
+ maxPredictionMs: 50, // Maximum prediction time
453
+ minVelocity: 0.1, // Minimum velocity to trigger prediction
454
+ })
455
+
456
+ canvas.addEventListener('pointermove', (e) => {
457
+ const stabilized = pointer.process({
458
+ x: e.offsetX,
459
+ y: e.offsetY,
460
+ timestamp: e.timeStamp,
461
+ })
462
+
463
+ if (stabilized) {
464
+ predictor.addPoint(stabilized)
465
+
466
+ // Get predicted point 16ms ahead
467
+ const predicted = predictor.predict(16)
468
+ if (predicted) {
469
+ drawPreview(predicted.x, predicted.y)
470
+ }
471
+ }
472
+ })
473
+ ```
474
+
475
+ ### Catmull-Rom Spline Interpolation
476
+
477
+ Generate smooth curves through a series of points, useful for upsampling or rendering.
478
+
479
+ ```ts
480
+ import {
481
+ interpolateCatmullRom,
482
+ resampleByArcLength,
483
+ } from '@stroke-stabilizer/core'
484
+
485
+ const points = pointer.finish()
486
+
487
+ // Interpolate with Catmull-Rom spline
488
+ const smooth = interpolateCatmullRom(points, {
489
+ tension: 0.5, // 0=loose, 1=tight
490
+ segmentDivisions: 10, // Points per segment
491
+ })
492
+
493
+ // Resample at uniform arc length intervals
494
+ const uniform = resampleByArcLength(points, 5) // 5px between points
495
+ ```
496
+
393
497
  ## API Reference
394
498
 
395
499
  ### StabilizedPointer
@@ -435,8 +539,10 @@ interface Point {
435
539
  }
436
540
 
437
541
  interface PointerPoint extends Point {
438
- pressure?: number
439
- timestamp: number
542
+ pressure?: number // Pen pressure (0-1)
543
+ tiltX?: number // Pen tilt on X axis (-90 to 90 degrees)
544
+ tiltY?: number // Pen tilt on Y axis (-90 to 90 degrees)
545
+ timestamp: number // Event timestamp in ms
440
546
  }
441
547
 
442
548
  type PaddingMode = 'reflect' | 'edge' | 'zero'
@@ -21,7 +21,7 @@ export interface StabilizedPointerOptions {
21
21
  appendEndpoint?: boolean;
22
22
  }
23
23
  /**
24
- * Dynamic Pipeline Pattern implementation
24
+ * Mutable Pipeline Pattern implementation
25
25
  *
26
26
  * A pipeline that allows adding, removing, and updating filters at runtime.
27
27
  * Always ready to execute without requiring a .build() call.
@@ -0,0 +1,37 @@
1
+ import type { Filter, PointerPoint } from '../types';
2
+ export interface DouglasPeuckerFilterParams {
3
+ /** Distance threshold (px). Points with perpendicular distance less than this are removed */
4
+ epsilon: number;
5
+ }
6
+ /**
7
+ * Create a Douglas-Peucker simplification filter
8
+ *
9
+ * This filter reduces the number of points while preserving the overall shape
10
+ * of the stroke. Useful for reducing data size or smoothing jaggy lines.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const pointer = new StabilizedPointer()
15
+ * .addFilter(douglasPeuckerFilter({ epsilon: 2 }))
16
+ *
17
+ * // Process points
18
+ * points.forEach(p => pointer.process(p))
19
+ *
20
+ * // Get simplified result
21
+ * const filter = pointer.getFilter('douglasPeucker') as ReturnType<typeof douglasPeuckerFilter>
22
+ * const simplified = filter.getSimplified()
23
+ * ```
24
+ */
25
+ export declare function douglasPeuckerFilter(params: DouglasPeuckerFilterParams): Filter;
26
+ /**
27
+ * Simplify an array of points using the Douglas-Peucker algorithm
28
+ *
29
+ * Standalone function for one-shot simplification without using a filter.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const simplified = simplify(points, 2)
34
+ * ```
35
+ */
36
+ export declare function simplify(points: PointerPoint[], epsilon: number): PointerPoint[];
37
+ //# sourceMappingURL=DouglasPeuckerFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DouglasPeuckerFilter.d.ts","sourceRoot":"","sources":["../../src/filters/DouglasPeuckerFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAmB,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,0BAA0B;IACzC,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAA;CAChB;AA2HD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,0BAA0B,GACjC,MAAM,CAER;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,YAAY,EAAE,EACtB,OAAO,EAAE,MAAM,GACd,YAAY,EAAE,CAKhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmaFilter.d.ts","sourceRoot":"","sources":["../../src/filters/EmaFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;CACd;AA6DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAEzD"}
1
+ {"version":3,"file":"EmaFilter.d.ts","sourceRoot":"","sources":["../../src/filters/EmaFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;CACd;AA+ED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAEzD"}
@@ -1 +1 @@
1
- {"version":3,"file":"KalmanFilter.d.ts","sourceRoot":"","sources":["../../src/filters/KalmanFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,CAAA;CACzB;AA4ED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAE/D"}
1
+ {"version":3,"file":"KalmanFilter.d.ts","sourceRoot":"","sources":["../../src/filters/KalmanFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,CAAA;CACzB;AA8ED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAE/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"LinearPredictionFilter.d.ts","sourceRoot":"","sources":["../../src/filters/LinearPredictionFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8OD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,MAAM,CAER"}
1
+ {"version":3,"file":"LinearPredictionFilter.d.ts","sourceRoot":"","sources":["../../src/filters/LinearPredictionFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA0PD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,MAAM,CAER"}
@@ -1 +1 @@
1
- {"version":3,"file":"MovingAverageFilter.d.ts","sourceRoot":"","sources":["../../src/filters/MovingAverageFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,yBAAyB;IACxC,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB;AAoED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAE7E"}
1
+ {"version":3,"file":"MovingAverageFilter.d.ts","sourceRoot":"","sources":["../../src/filters/MovingAverageFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,yBAAyB;IACxC,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB;AAoFD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAE7E"}
@@ -1 +1 @@
1
- {"version":3,"file":"OneEuroFilter.d.ts","sourceRoot":"","sources":["../../src/filters/OneEuroFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAqKD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAEjE"}
1
+ {"version":3,"file":"OneEuroFilter.d.ts","sourceRoot":"","sources":["../../src/filters/OneEuroFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AA2LD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAEjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"StringFilter.d.ts","sourceRoot":"","sources":["../../src/filters/StringFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAA;CACrB;AA8DD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAE/D"}
1
+ {"version":3,"file":"StringFilter.d.ts","sourceRoot":"","sources":["../../src/filters/StringFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAA;AAErE,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAA;CACrB;AAkED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAE/D"}
@@ -12,4 +12,6 @@ export { oneEuroFilter } from './OneEuroFilter';
12
12
  export type { OneEuroFilterParams } from './OneEuroFilter';
13
13
  export { linearPredictionFilter } from './LinearPredictionFilter';
14
14
  export type { LinearPredictionFilterParams } from './LinearPredictionFilter';
15
+ export { douglasPeuckerFilter, simplify } from './DouglasPeuckerFilter';
16
+ export type { DouglasPeuckerFilterParams } from './DouglasPeuckerFilter';
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,YAAY,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,YAAY,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,YAAY,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,YAAY,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAE5E,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACvE,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAA"}