@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.
- package/README.md +397 -0
- package/dist/StabilizedPointer.d.ts +246 -0
- package/dist/StabilizedPointer.d.ts.map +1 -0
- package/dist/filters/EmaFilter.d.ts +25 -0
- package/dist/filters/EmaFilter.d.ts.map +1 -0
- package/dist/filters/KalmanFilter.d.ts +21 -0
- package/dist/filters/KalmanFilter.d.ts.map +1 -0
- package/dist/filters/LinearPredictionFilter.d.ts +54 -0
- package/dist/filters/LinearPredictionFilter.d.ts.map +1 -0
- package/dist/filters/MovingAverageFilter.d.ts +16 -0
- package/dist/filters/MovingAverageFilter.d.ts.map +1 -0
- package/dist/filters/NoiseFilter.d.ts +16 -0
- package/dist/filters/NoiseFilter.d.ts.map +1 -0
- package/dist/filters/OneEuroFilter.d.ts +45 -0
- package/dist/filters/OneEuroFilter.d.ts.map +1 -0
- package/dist/filters/StringFilter.d.ts +16 -0
- package/dist/filters/StringFilter.d.ts.map +1 -0
- package/dist/filters/index.d.ts +15 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/index.cjs +1086 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1086 -0
- package/dist/index.js.map +1 -0
- package/dist/kernels/BilateralKernel.d.ts +48 -0
- package/dist/kernels/BilateralKernel.d.ts.map +1 -0
- package/dist/kernels/boxKernel.d.ts +16 -0
- package/dist/kernels/boxKernel.d.ts.map +1 -0
- package/dist/kernels/gaussianKernel.d.ts +17 -0
- package/dist/kernels/gaussianKernel.d.ts.map +1 -0
- package/dist/kernels/index.d.ts +11 -0
- package/dist/kernels/index.d.ts.map +1 -0
- package/dist/kernels/triangleKernel.d.ts +16 -0
- package/dist/kernels/triangleKernel.d.ts.map +1 -0
- package/dist/kernels/types.d.ts +38 -0
- package/dist/kernels/types.d.ts.map +1 -0
- package/dist/presets.d.ts +36 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/smooth.d.ts +27 -0
- package/dist/smooth.d.ts.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# @stroke-stabilizer/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@stroke-stabilizer/core)
|
|
4
|
+
|
|
5
|
+
[日本語](./docs/README.ja.md)
|
|
6
|
+
|
|
7
|
+
> This is part of the [stroke-stabilizer](https://github.com/usapopopooon/stroke-stabilizer) monorepo
|
|
8
|
+
|
|
9
|
+
A lightweight, framework-agnostic stroke stabilization library for digital drawing applications.
|
|
10
|
+
|
|
11
|
+
Reduce hand tremor and smooth pen/mouse input in real-time using a flexible filter pipeline.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **[Dynamic Pipeline Pattern](https://dev.to/usapopopooon/the-dynamic-pipeline-pattern-a-mutable-method-chaining-for-real-time-processing-16e1)** - Add, remove, and update filters at runtime without rebuilding
|
|
16
|
+
- **Two-layer Processing** - Real-time filters + post-processing convolution
|
|
17
|
+
- **rAF Batch Processing** - Coalesce high-frequency pointer events into animation frames
|
|
18
|
+
- **8 Built-in Filters** - From simple moving average to adaptive One Euro Filter
|
|
19
|
+
- **Edge-preserving Smoothing** - Bilateral kernel for sharp corner preservation
|
|
20
|
+
- **TypeScript First** - Full type safety with exported types
|
|
21
|
+
- **Zero Dependencies** - Pure JavaScript, works anywhere
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @stroke-stabilizer/core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import {
|
|
33
|
+
StabilizedPointer,
|
|
34
|
+
emaFilter,
|
|
35
|
+
oneEuroFilter,
|
|
36
|
+
} from '@stroke-stabilizer/core'
|
|
37
|
+
|
|
38
|
+
const pointer = new StabilizedPointer()
|
|
39
|
+
.addFilter(emaFilter({ alpha: 0.5 }))
|
|
40
|
+
.addFilter(oneEuroFilter({ minCutoff: 1.0, beta: 0.007 }))
|
|
41
|
+
|
|
42
|
+
canvas.addEventListener('pointermove', (e) => {
|
|
43
|
+
const result = pointer.process({
|
|
44
|
+
x: e.clientX,
|
|
45
|
+
y: e.clientY,
|
|
46
|
+
pressure: e.pressure,
|
|
47
|
+
timestamp: e.timeStamp,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (result) {
|
|
51
|
+
draw(result.x, result.y)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
canvas.addEventListener('pointerup', () => {
|
|
56
|
+
pointer.reset()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Filters
|
|
61
|
+
|
|
62
|
+
> **📖 [Detailed Filter Reference](../../docs/filters.md)** - Mathematical formulas, technical explanations, and usage recommendations
|
|
63
|
+
|
|
64
|
+
### Real-time Filters
|
|
65
|
+
|
|
66
|
+
| Filter | Description | Use Case |
|
|
67
|
+
| ------------------------ | --------------------------------- | ---------------------------------- |
|
|
68
|
+
| `noiseFilter` | Rejects points too close together | Remove jitter |
|
|
69
|
+
| `movingAverageFilter` | Simple moving average (FIR) | Basic smoothing |
|
|
70
|
+
| `emaFilter` | Exponential moving average (IIR) | Low-latency smoothing |
|
|
71
|
+
| `kalmanFilter` | Kalman filter | Noisy input smoothing |
|
|
72
|
+
| `stringFilter` | Lazy Brush algorithm | Delayed, smooth strokes |
|
|
73
|
+
| `oneEuroFilter` | Adaptive lowpass filter | Best balance of smoothness/latency |
|
|
74
|
+
| `linearPredictionFilter` | Predicts next position | Lag compensation |
|
|
75
|
+
|
|
76
|
+
### Post-processing Kernels
|
|
77
|
+
|
|
78
|
+
| Kernel | Description |
|
|
79
|
+
| ----------------- | ------------------------- |
|
|
80
|
+
| `gaussianKernel` | Gaussian blur |
|
|
81
|
+
| `boxKernel` | Simple average |
|
|
82
|
+
| `triangleKernel` | Linear falloff |
|
|
83
|
+
| `bilateralKernel` | Edge-preserving smoothing |
|
|
84
|
+
|
|
85
|
+
## Usage Examples
|
|
86
|
+
|
|
87
|
+
### Basic Real-time Stabilization
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { StabilizedPointer, oneEuroFilter } from '@stroke-stabilizer/core'
|
|
91
|
+
|
|
92
|
+
const pointer = new StabilizedPointer().addFilter(
|
|
93
|
+
oneEuroFilter({ minCutoff: 1.0, beta: 0.007 })
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Process each point
|
|
97
|
+
const smoothed = pointer.process({ x, y, timestamp })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Dynamic Filter Updates
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// Add filter
|
|
104
|
+
pointer.addFilter(emaFilter({ alpha: 0.3 }))
|
|
105
|
+
|
|
106
|
+
// Update parameters at runtime
|
|
107
|
+
pointer.updateFilter('ema', { alpha: 0.5 })
|
|
108
|
+
|
|
109
|
+
// Remove filter
|
|
110
|
+
pointer.removeFilter('ema')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Post-processing with Bidirectional Convolution
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { StabilizedPointer, gaussianKernel } from '@stroke-stabilizer/core'
|
|
117
|
+
|
|
118
|
+
const pointer = new StabilizedPointer()
|
|
119
|
+
.addFilter(oneEuroFilter({ minCutoff: 1.0, beta: 0.007 }))
|
|
120
|
+
.addPostProcess(gaussianKernel({ size: 7 }), { padding: 'reflect' })
|
|
121
|
+
|
|
122
|
+
// Process points in real-time
|
|
123
|
+
pointer.process(point)
|
|
124
|
+
|
|
125
|
+
// After stroke ends, apply post-processing
|
|
126
|
+
const finalPoints = pointer.finish()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Re-applying Post-processing
|
|
130
|
+
|
|
131
|
+
Use `finishWithoutReset()` to preview or re-apply post-processing with different settings without losing the buffer.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import {
|
|
135
|
+
StabilizedPointer,
|
|
136
|
+
gaussianKernel,
|
|
137
|
+
bilateralKernel,
|
|
138
|
+
} from '@stroke-stabilizer/core'
|
|
139
|
+
|
|
140
|
+
const pointer = new StabilizedPointer()
|
|
141
|
+
|
|
142
|
+
// Process points
|
|
143
|
+
pointer.process(point1)
|
|
144
|
+
pointer.process(point2)
|
|
145
|
+
pointer.process(point3)
|
|
146
|
+
|
|
147
|
+
// Preview with gaussian kernel
|
|
148
|
+
pointer.addPostProcess(gaussianKernel({ size: 5 }))
|
|
149
|
+
const preview1 = pointer.finishWithoutReset()
|
|
150
|
+
draw(preview1)
|
|
151
|
+
|
|
152
|
+
// Change to bilateral kernel and re-apply
|
|
153
|
+
pointer.removePostProcess('gaussian')
|
|
154
|
+
pointer.addPostProcess(bilateralKernel({ size: 7, sigmaValue: 10 }))
|
|
155
|
+
const preview2 = pointer.finishWithoutReset()
|
|
156
|
+
draw(preview2)
|
|
157
|
+
|
|
158
|
+
// Finalize when satisfied (resets buffer)
|
|
159
|
+
const final = pointer.finish()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Difference between `finishWithoutReset()` and `finish()`:**
|
|
163
|
+
|
|
164
|
+
| Method | Post-process | Reset buffer |
|
|
165
|
+
| ---------------------- | ------------ | ------------ |
|
|
166
|
+
| `finishWithoutReset()` | ✅ | ❌ |
|
|
167
|
+
| `finish()` | ✅ | ✅ |
|
|
168
|
+
|
|
169
|
+
### Edge-preserving Smoothing
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { smooth, bilateralKernel } from '@stroke-stabilizer/core'
|
|
173
|
+
|
|
174
|
+
// Smooth while preserving sharp corners
|
|
175
|
+
const smoothed = smooth(points, {
|
|
176
|
+
kernel: bilateralKernel({ size: 7, sigmaValue: 10 }),
|
|
177
|
+
padding: 'reflect',
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Endpoint Preservation
|
|
182
|
+
|
|
183
|
+
By default, `smooth()` preserves exact start and end points so the stroke reaches the actual pointer position.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { smooth, gaussianKernel } from '@stroke-stabilizer/core'
|
|
187
|
+
|
|
188
|
+
// Default: endpoints preserved (recommended)
|
|
189
|
+
const smoothed = smooth(points, {
|
|
190
|
+
kernel: gaussianKernel({ size: 5 }),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Disable endpoint preservation
|
|
194
|
+
const smoothedAll = smooth(points, {
|
|
195
|
+
kernel: gaussianKernel({ size: 5 }),
|
|
196
|
+
preserveEndpoints: false,
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### rAF Batch Processing
|
|
201
|
+
|
|
202
|
+
For high-frequency input devices (pen tablets, etc.), batch processing reduces CPU load by coalescing pointer events into animation frames.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { StabilizedPointer, oneEuroFilter } from '@stroke-stabilizer/core'
|
|
206
|
+
|
|
207
|
+
const pointer = new StabilizedPointer()
|
|
208
|
+
.addFilter(oneEuroFilter({ minCutoff: 1.0, beta: 0.007 }))
|
|
209
|
+
.enableBatching({
|
|
210
|
+
onBatch: (points) => {
|
|
211
|
+
// Called once per frame with all processed points
|
|
212
|
+
drawPoints(points)
|
|
213
|
+
},
|
|
214
|
+
onPoint: (point) => {
|
|
215
|
+
// Called for each processed point (optional)
|
|
216
|
+
updatePreview(point)
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
canvas.addEventListener('pointermove', (e) => {
|
|
221
|
+
// Points are queued and processed on next animation frame
|
|
222
|
+
pointer.queue({
|
|
223
|
+
x: e.clientX,
|
|
224
|
+
y: e.clientY,
|
|
225
|
+
pressure: e.pressure,
|
|
226
|
+
timestamp: e.timeStamp,
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
canvas.addEventListener('pointerup', () => {
|
|
231
|
+
// Flush any pending points and apply post-processing
|
|
232
|
+
const finalPoints = pointer.finish()
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Batch processing methods:**
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Enable/disable batching (method chaining)
|
|
240
|
+
pointer.enableBatching({ onBatch, onPoint })
|
|
241
|
+
pointer.disableBatching()
|
|
242
|
+
|
|
243
|
+
// Queue points for batch processing
|
|
244
|
+
pointer.queue(point)
|
|
245
|
+
pointer.queueAll(points)
|
|
246
|
+
|
|
247
|
+
// Force immediate processing
|
|
248
|
+
pointer.flushBatch()
|
|
249
|
+
|
|
250
|
+
// Check state
|
|
251
|
+
pointer.isBatchingEnabled // boolean
|
|
252
|
+
pointer.pendingCount // number of queued points
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Presets
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { createFromPreset } from '@stroke-stabilizer/core'
|
|
259
|
+
|
|
260
|
+
// Quick setup with predefined configurations
|
|
261
|
+
const pointer = createFromPreset('smooth') // Heavy smoothing
|
|
262
|
+
const pointer = createFromPreset('responsive') // Low latency
|
|
263
|
+
const pointer = createFromPreset('balanced') // Default balance
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Filter Parameters
|
|
267
|
+
|
|
268
|
+
### oneEuroFilter (Recommended)
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
oneEuroFilter({
|
|
272
|
+
minCutoff: 1.0, // Smoothing at low speed (lower = smoother)
|
|
273
|
+
beta: 0.007, // Speed adaptation (higher = more responsive)
|
|
274
|
+
dCutoff: 1.0, // Derivative cutoff (usually 1.0)
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### emaFilter
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
emaFilter({
|
|
282
|
+
alpha: 0.5, // 0-1, higher = more responsive
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### kalmanFilter
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
kalmanFilter({
|
|
290
|
+
processNoise: 0.1, // Expected movement variance
|
|
291
|
+
measurementNoise: 0.5, // Input noise level
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### linearPredictionFilter
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
linearPredictionFilter({
|
|
299
|
+
historySize: 4, // Points used for prediction
|
|
300
|
+
predictionFactor: 0.5, // Prediction strength (0-1)
|
|
301
|
+
smoothing: 0.6, // Output smoothing
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### stringFilter (Lazy Brush)
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
stringFilter({
|
|
309
|
+
stringLength: 10, // Distance before anchor moves
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### bilateralKernel
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
bilateralKernel({
|
|
317
|
+
size: 7, // Kernel size (odd number)
|
|
318
|
+
sigmaValue: 10, // Edge preservation (lower = sharper edges)
|
|
319
|
+
sigmaSpace: 2, // Spatial falloff (optional)
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## API Reference
|
|
324
|
+
|
|
325
|
+
### StabilizedPointer
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
class StabilizedPointer {
|
|
329
|
+
// Filter management
|
|
330
|
+
addFilter(filter: Filter): this
|
|
331
|
+
removeFilter(type: string): boolean
|
|
332
|
+
updateFilter<T>(type: string, params: Partial<T>): boolean
|
|
333
|
+
getFilter(type: string): Filter | undefined
|
|
334
|
+
|
|
335
|
+
// Post-processing
|
|
336
|
+
addPostProcess(kernel: Kernel, options?: { padding?: PaddingMode }): this
|
|
337
|
+
removePostProcess(type: string): boolean
|
|
338
|
+
|
|
339
|
+
// Processing
|
|
340
|
+
process(point: PointerPoint): PointerPoint | null
|
|
341
|
+
finish(): Point[] // Apply post-process and reset
|
|
342
|
+
finishWithoutReset(): Point[] // Apply post-process without reset (for preview)
|
|
343
|
+
reset(): void // Reset filters and clear buffer
|
|
344
|
+
|
|
345
|
+
// Batch processing (rAF)
|
|
346
|
+
enableBatching(config?: BatchConfig): this
|
|
347
|
+
disableBatching(): this
|
|
348
|
+
queue(point: PointerPoint): this
|
|
349
|
+
queueAll(points: PointerPoint[]): this
|
|
350
|
+
flushBatch(): PointerPoint[]
|
|
351
|
+
isBatchingEnabled: boolean
|
|
352
|
+
pendingCount: number
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Types
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
interface Point {
|
|
360
|
+
x: number
|
|
361
|
+
y: number
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interface PointerPoint extends Point {
|
|
365
|
+
pressure?: number
|
|
366
|
+
timestamp: number
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
type PaddingMode = 'reflect' | 'edge' | 'zero'
|
|
370
|
+
|
|
371
|
+
interface BatchConfig {
|
|
372
|
+
onBatch?: (points: PointerPoint[]) => void
|
|
373
|
+
onPoint?: (point: PointerPoint) => void
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Architecture
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
Input → [Real-time Filters] → process() → Output
|
|
381
|
+
↓
|
|
382
|
+
[Buffer]
|
|
383
|
+
↓
|
|
384
|
+
[Post-processors] → finish() → Final Output
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Real-time filters** run on each input point with O(1) complexity.
|
|
388
|
+
**Post-processors** run once at stroke end with bidirectional convolution.
|
|
389
|
+
|
|
390
|
+
## Framework Adapters
|
|
391
|
+
|
|
392
|
+
- `@stroke-stabilizer/react` - React hooks
|
|
393
|
+
- `@stroke-stabilizer/vue` - Vue composables
|
|
394
|
+
|
|
395
|
+
## License
|
|
396
|
+
|
|
397
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { Filter, Point, PointerPoint } from './types';
|
|
2
|
+
import type { Kernel, PaddingMode } from './kernels/types';
|
|
3
|
+
/**
|
|
4
|
+
* Batch processing configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface BatchConfig {
|
|
7
|
+
/** Callback when a batch of points is processed */
|
|
8
|
+
onBatch?: (points: PointerPoint[]) => void;
|
|
9
|
+
/** Callback for each processed point */
|
|
10
|
+
onPoint?: (point: PointerPoint) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Dynamic Pipeline Pattern implementation
|
|
14
|
+
*
|
|
15
|
+
* A pipeline that allows adding, removing, and updating filters at runtime.
|
|
16
|
+
* Always ready to execute without requiring a .build() call.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const pointer = new StabilizedPointer()
|
|
21
|
+
* // Real-time layer
|
|
22
|
+
* .addFilter(noiseFilter({ minDistance: 2 }))
|
|
23
|
+
* .addFilter(kalmanFilter({ processNoise: 0.1 }))
|
|
24
|
+
* // Post-processing layer
|
|
25
|
+
* .addPostProcess(gaussianKernel({ size: 7 }))
|
|
26
|
+
*
|
|
27
|
+
* // During drawing
|
|
28
|
+
* pointer.process(point)
|
|
29
|
+
*
|
|
30
|
+
* // On completion
|
|
31
|
+
* const finalStroke = pointer.finish()
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class StabilizedPointer {
|
|
35
|
+
private filters;
|
|
36
|
+
private postProcessors;
|
|
37
|
+
private buffer;
|
|
38
|
+
private lastRawPoint;
|
|
39
|
+
private batchConfig;
|
|
40
|
+
private pendingPoints;
|
|
41
|
+
private rafId;
|
|
42
|
+
/**
|
|
43
|
+
* Add a filter to the pipeline
|
|
44
|
+
* @returns this (for method chaining)
|
|
45
|
+
*/
|
|
46
|
+
addFilter(filter: Filter): this;
|
|
47
|
+
/**
|
|
48
|
+
* Remove a filter by type
|
|
49
|
+
* @returns true if removed, false if not found
|
|
50
|
+
*/
|
|
51
|
+
removeFilter(type: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Update parameters of a filter by type
|
|
54
|
+
* @returns true if updated, false if not found
|
|
55
|
+
*/
|
|
56
|
+
updateFilter<TParams>(type: string, params: Partial<TParams>): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Get a filter by type
|
|
59
|
+
*/
|
|
60
|
+
getFilter(type: string): Filter | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Get list of current filter types
|
|
63
|
+
*/
|
|
64
|
+
getFilterTypes(): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Check if a filter exists
|
|
67
|
+
*/
|
|
68
|
+
hasFilter(type: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Process a point through the pipeline
|
|
71
|
+
* @returns Processed result (null if rejected by a filter)
|
|
72
|
+
*/
|
|
73
|
+
process(point: PointerPoint): PointerPoint | null;
|
|
74
|
+
/**
|
|
75
|
+
* Process multiple points at once
|
|
76
|
+
* @returns Array of processed results (nulls excluded)
|
|
77
|
+
*/
|
|
78
|
+
processAll(points: PointerPoint[]): PointerPoint[];
|
|
79
|
+
/**
|
|
80
|
+
* Get the processed buffer
|
|
81
|
+
*/
|
|
82
|
+
getBuffer(): readonly PointerPoint[];
|
|
83
|
+
/**
|
|
84
|
+
* Clear and return the processed buffer
|
|
85
|
+
*/
|
|
86
|
+
flushBuffer(): PointerPoint[];
|
|
87
|
+
/**
|
|
88
|
+
* Reset all filters and clear the buffer
|
|
89
|
+
*
|
|
90
|
+
* Call this to prepare for a new stroke without destroying the pipeline configuration.
|
|
91
|
+
* Filters are reset to their initial state and the buffer is cleared.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* // After finishing a stroke
|
|
96
|
+
* const result = pointer.finish() // automatically calls reset()
|
|
97
|
+
*
|
|
98
|
+
* // Or manually reset without finishing
|
|
99
|
+
* pointer.reset()
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
reset(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Clear the pipeline (remove all filters)
|
|
105
|
+
*/
|
|
106
|
+
clear(): void;
|
|
107
|
+
/**
|
|
108
|
+
* Get number of filters
|
|
109
|
+
*/
|
|
110
|
+
get length(): number;
|
|
111
|
+
/**
|
|
112
|
+
* Add post-processor to the pipeline
|
|
113
|
+
* @returns this (for method chaining)
|
|
114
|
+
*/
|
|
115
|
+
addPostProcess(kernel: Kernel, options?: {
|
|
116
|
+
padding?: PaddingMode;
|
|
117
|
+
}): this;
|
|
118
|
+
/**
|
|
119
|
+
* Remove a post-processor by type
|
|
120
|
+
* @returns true if removed, false if not found
|
|
121
|
+
*/
|
|
122
|
+
removePostProcess(type: string): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Check if a post-processor exists
|
|
125
|
+
*/
|
|
126
|
+
hasPostProcess(type: string): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Get list of post-processor types
|
|
129
|
+
*/
|
|
130
|
+
getPostProcessTypes(): string[];
|
|
131
|
+
/**
|
|
132
|
+
* Get number of post-processors
|
|
133
|
+
*/
|
|
134
|
+
get postProcessLength(): number;
|
|
135
|
+
/**
|
|
136
|
+
* Finish the stroke and return post-processed results, without resetting
|
|
137
|
+
*
|
|
138
|
+
* Use this when you want to get the final result but keep the buffer intact.
|
|
139
|
+
* Useful for previewing post-processing results or comparing different settings.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* pointer.addPostProcess(gaussianKernel({ size: 5 }))
|
|
144
|
+
* const preview1 = pointer.finishWithoutReset()
|
|
145
|
+
*
|
|
146
|
+
* // Change settings and re-apply
|
|
147
|
+
* pointer.removePostProcess('gaussian')
|
|
148
|
+
* pointer.addPostProcess(bilateralKernel({ size: 7, sigmaValue: 10 }))
|
|
149
|
+
* const preview2 = pointer.finishWithoutReset()
|
|
150
|
+
*
|
|
151
|
+
* // Finalize when done
|
|
152
|
+
* const final = pointer.finish()
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
finishWithoutReset(): Point[];
|
|
156
|
+
/**
|
|
157
|
+
* Track if drain has been called to avoid duplicate draining
|
|
158
|
+
*/
|
|
159
|
+
private hasDrained;
|
|
160
|
+
/**
|
|
161
|
+
* Append the final raw input point to ensure stroke ends at the actual endpoint
|
|
162
|
+
*
|
|
163
|
+
* Instead of draining filters (which adds many extra points),
|
|
164
|
+
* we simply append the raw endpoint. The post-processing phase
|
|
165
|
+
* with bidirectional convolution will naturally smooth the transition.
|
|
166
|
+
*/
|
|
167
|
+
private appendEndpoint;
|
|
168
|
+
/**
|
|
169
|
+
* Finish the stroke and return post-processed results
|
|
170
|
+
*
|
|
171
|
+
* This applies all post-processors to the buffer, then resets filters and clears the buffer.
|
|
172
|
+
* Use this at the end of a stroke to get the final smoothed result.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* // During drawing
|
|
177
|
+
* pointer.process(point)
|
|
178
|
+
*
|
|
179
|
+
* // On stroke end
|
|
180
|
+
* const finalStroke = pointer.finish()
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
finish(): Point[];
|
|
184
|
+
/**
|
|
185
|
+
* Enable requestAnimationFrame batch processing
|
|
186
|
+
*
|
|
187
|
+
* When enabled, points queued via queue() are batched and processed
|
|
188
|
+
* on the next animation frame, reducing CPU load for high-frequency
|
|
189
|
+
* pointer events.
|
|
190
|
+
*
|
|
191
|
+
* @returns this (for method chaining)
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* const pointer = new StabilizedPointer()
|
|
196
|
+
* .addFilter(noiseFilter({ minDistance: 2 }))
|
|
197
|
+
* .enableBatching({
|
|
198
|
+
* onBatch: (points) => drawPoints(points),
|
|
199
|
+
* onPoint: (point) => updatePreview(point)
|
|
200
|
+
* })
|
|
201
|
+
*
|
|
202
|
+
* canvas.onpointermove = (e) => {
|
|
203
|
+
* pointer.queue({ x: e.clientX, y: e.clientY, timestamp: e.timeStamp })
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
enableBatching(config?: BatchConfig): this;
|
|
208
|
+
/**
|
|
209
|
+
* Disable batch processing
|
|
210
|
+
* Flushes any pending points before disabling
|
|
211
|
+
* @returns this (for method chaining)
|
|
212
|
+
*/
|
|
213
|
+
disableBatching(): this;
|
|
214
|
+
/**
|
|
215
|
+
* Check if batch processing is enabled
|
|
216
|
+
*/
|
|
217
|
+
get isBatchingEnabled(): boolean;
|
|
218
|
+
/**
|
|
219
|
+
* Queue a point for batch processing
|
|
220
|
+
*
|
|
221
|
+
* If batching is enabled, the point is queued and processed on the next
|
|
222
|
+
* animation frame. If batching is disabled, the point is processed immediately.
|
|
223
|
+
*
|
|
224
|
+
* @returns this (for method chaining, useful for queueing multiple points)
|
|
225
|
+
*/
|
|
226
|
+
queue(point: PointerPoint): this;
|
|
227
|
+
/**
|
|
228
|
+
* Queue multiple points for batch processing
|
|
229
|
+
* @returns this (for method chaining)
|
|
230
|
+
*/
|
|
231
|
+
queueAll(points: PointerPoint[]): this;
|
|
232
|
+
/**
|
|
233
|
+
* Force flush pending batched points immediately
|
|
234
|
+
* @returns Array of processed points
|
|
235
|
+
*/
|
|
236
|
+
flushBatch(): PointerPoint[];
|
|
237
|
+
/**
|
|
238
|
+
* Get number of pending points in the batch queue
|
|
239
|
+
*/
|
|
240
|
+
get pendingCount(): number;
|
|
241
|
+
private scheduleFlush;
|
|
242
|
+
private cancelScheduledFlush;
|
|
243
|
+
private processPendingPoints;
|
|
244
|
+
private isUpdatableFilter;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=StabilizedPointer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StabilizedPointer.d.ts","sourceRoot":"","sources":["../src/StabilizedPointer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Filter } from '../types';
|
|
2
|
+
export interface EmaFilterParams {
|
|
3
|
+
/**
|
|
4
|
+
* Smoothing coefficient (0-1)
|
|
5
|
+
* - Lower value: stronger smoothing (emphasizes past values)
|
|
6
|
+
* - Higher value: more responsive (emphasizes new values)
|
|
7
|
+
*/
|
|
8
|
+
alpha: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create an Exponential Moving Average (EMA) filter
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // Strong smoothing
|
|
16
|
+
* const pointer = new StabilizedPointer()
|
|
17
|
+
* .addFilter(emaFilter({ alpha: 0.2 }))
|
|
18
|
+
*
|
|
19
|
+
* // Light smoothing
|
|
20
|
+
* const pointer = new StabilizedPointer()
|
|
21
|
+
* .addFilter(emaFilter({ alpha: 0.7 }))
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function emaFilter(params: EmaFilterParams): Filter;
|
|
25
|
+
//# sourceMappingURL=EmaFilter.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Filter } from '../types';
|
|
2
|
+
export interface KalmanFilterParams {
|
|
3
|
+
/** Process noise (Q): Lower values trust prediction more */
|
|
4
|
+
processNoise: number;
|
|
5
|
+
/** Measurement noise (R): Higher values result in stronger smoothing */
|
|
6
|
+
measurementNoise: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Create a Kalman filter
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const pointer = new StabilizedPointer()
|
|
14
|
+
* .addFilter(kalmanFilter({
|
|
15
|
+
* processNoise: 0.1,
|
|
16
|
+
* measurementNoise: 0.5
|
|
17
|
+
* }))
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function kalmanFilter(params: KalmanFilterParams): Filter;
|
|
21
|
+
//# sourceMappingURL=KalmanFilter.d.ts.map
|
|
@@ -0,0 +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"}
|