@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/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
|