@jjlmoya/utils-hardware 1.2.0 → 1.3.0
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/package.json
CHANGED
|
@@ -232,6 +232,6 @@ const t = (ui ?? {}) as TestRatonUI;
|
|
|
232
232
|
placeholder: document.getElementById('tr-placeholder'),
|
|
233
233
|
canvas: document.getElementById('tr-canvas') as HTMLCanvasElement | null,
|
|
234
234
|
area: document.getElementById('tr-area')
|
|
235
|
-
}
|
|
235
|
+
});
|
|
236
236
|
</script>
|
|
237
237
|
</div>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { MovingAverage } from './MovingAverage';
|
|
2
|
+
import { StatisticalFilter } from './StatisticalFilter';
|
|
3
|
+
|
|
1
4
|
export interface UIToolElements {
|
|
2
5
|
avg: HTMLElement | null;
|
|
3
6
|
max: HTMLElement | null;
|
|
@@ -10,88 +13,124 @@ export interface UIToolElements {
|
|
|
10
13
|
export class RatonManager {
|
|
11
14
|
private el: UIToolElements;
|
|
12
15
|
private ctx: CanvasRenderingContext2D | null;
|
|
13
|
-
private worker: Worker;
|
|
14
16
|
private history: number[] = [];
|
|
15
|
-
private readonly MAX_HISTORY = 100;
|
|
17
|
+
private readonly MAX_HISTORY: number = 100;
|
|
16
18
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
19
|
private lastMouseEvent: MouseEvent | null = null;
|
|
20
|
+
private lastTime: number = 0;
|
|
21
|
+
private maxHz: number = 0;
|
|
22
|
+
private totalHistory: number[] = [];
|
|
23
|
+
private filter: StatisticalFilter = new StatisticalFilter();
|
|
24
|
+
private smoother: MovingAverage = new MovingAverage(20);
|
|
18
25
|
|
|
19
|
-
constructor(el: UIToolElements
|
|
26
|
+
constructor(el: UIToolElements) {
|
|
20
27
|
this.el = el;
|
|
21
28
|
this.ctx = el.canvas?.getContext('2d') ?? null;
|
|
22
|
-
this.worker = new Worker(workerUrl, { type: 'module' });
|
|
23
29
|
this.init();
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
private init(): void {
|
|
27
33
|
window.addEventListener('resize', () => this.resize());
|
|
28
34
|
this.resize();
|
|
29
|
-
|
|
30
|
-
this.worker.onmessage = (event) => this.handleWorkerMessage(event.data);
|
|
31
|
-
|
|
32
|
-
this.el.area?.addEventListener('mousemove', (e) => this.processMove(e));
|
|
35
|
+
this.el.area?.addEventListener('mousemove', (e: MouseEvent) => this.processMove(e));
|
|
33
36
|
this.el.area?.addEventListener('mouseleave', () => this.handleMouseLeave());
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
private resize(): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const { area, canvas } = this.el;
|
|
41
|
+
if (!area || !canvas) return;
|
|
42
|
+
const rect = area.getBoundingClientRect();
|
|
43
|
+
canvas.width = rect.width;
|
|
44
|
+
canvas.height = rect.height;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
+
private processMove(e: MouseEvent): void {
|
|
48
|
+
this.lastMouseEvent = e;
|
|
49
|
+
const now: number = performance.now();
|
|
50
|
+
this.calculateHz(now);
|
|
51
|
+
this.lastTime = now;
|
|
52
|
+
this.refreshIdleTimer();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private calculateHz(now: number): void {
|
|
56
|
+
if (this.lastTime <= 0) return;
|
|
57
|
+
const delta: number = now - this.lastTime;
|
|
58
|
+
if (delta <= 0.5 || delta >= 200) return;
|
|
59
|
+
|
|
60
|
+
const hz: number = 1000 / delta;
|
|
61
|
+
if (this.filter.isOutlier(hz)) return;
|
|
62
|
+
|
|
63
|
+
this.processValidMove(hz);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private processValidMove(hz: number): void {
|
|
67
|
+
const smoothed: number = this.smoother.next(hz);
|
|
68
|
+
this.history.push(smoothed);
|
|
47
69
|
if (this.history.length > this.MAX_HISTORY) this.history.shift();
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
if (this.
|
|
71
|
+
this.totalHistory.push(hz);
|
|
72
|
+
if (this.totalHistory.length > 200) this.totalHistory.shift();
|
|
73
|
+
|
|
74
|
+
if (hz > this.maxHz) this.maxHz = hz;
|
|
75
|
+
const avg: number = this.totalHistory.reduce((acc: number, val: number) => acc + val, 0) / this.totalHistory.length;
|
|
51
76
|
|
|
77
|
+
this.updateUI(hz, avg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private refreshIdleTimer(): void {
|
|
81
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
82
|
+
this.idleTimer = setTimeout(() => this.reset(), 100);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private updateUI(hz: number, avg: number): void {
|
|
86
|
+
const { max, avg: avgEl, placeholder } = this.el;
|
|
87
|
+
if (max) max.textContent = Math.round(this.maxHz).toString();
|
|
88
|
+
if (avgEl) avgEl.textContent = Math.round(avg).toString();
|
|
89
|
+
if (placeholder) placeholder.style.opacity = '0';
|
|
90
|
+
this.updateFollower(hz);
|
|
52
91
|
this.updateGraph();
|
|
92
|
+
}
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
private updateFollower(hz: number): void {
|
|
95
|
+
const { follower, area } = this.el;
|
|
96
|
+
if (!follower || !this.lastMouseEvent || !area) return;
|
|
97
|
+
follower.textContent = `${Math.round(hz)} Hz`;
|
|
98
|
+
const rect = area.getBoundingClientRect();
|
|
99
|
+
const x: number = this.lastMouseEvent.clientX - rect.left + 15;
|
|
100
|
+
const y: number = this.lastMouseEvent.clientY - rect.top + 15;
|
|
101
|
+
follower.style.transform = `translate(${x}px, ${y}px)`;
|
|
102
|
+
follower.style.opacity = '1';
|
|
62
103
|
}
|
|
63
104
|
|
|
64
105
|
private updateGraph(): void {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
const ctx = this.ctx;
|
|
107
|
+
const canvas = this.el.canvas;
|
|
108
|
+
if (!ctx || !canvas) return;
|
|
109
|
+
const { width: w, height: h } = canvas;
|
|
110
|
+
ctx.clearRect(0, 0, w, h);
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.strokeStyle = '#10b981';
|
|
113
|
+
ctx.lineWidth = 2;
|
|
114
|
+
const currentMax: number = Math.max(1000, ...this.history);
|
|
115
|
+
const stepX: number = w / this.MAX_HISTORY;
|
|
116
|
+
this.history.forEach((hz: number, i: number) => {
|
|
117
|
+
const x: number = i * stepX;
|
|
118
|
+
const y: number = h - (hz / currentMax) * (h * 0.8);
|
|
119
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
120
|
+
else ctx.lineTo(x, y);
|
|
78
121
|
});
|
|
79
|
-
|
|
122
|
+
ctx.stroke();
|
|
80
123
|
}
|
|
81
124
|
|
|
82
|
-
private
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
if (this.el.follower) this.el.follower.style.opacity = '0';
|
|
89
|
-
this.worker.postMessage('reset');
|
|
90
|
-
}, 100);
|
|
125
|
+
private reset(): void {
|
|
126
|
+
this.lastTime = 0;
|
|
127
|
+
this.filter.reset();
|
|
128
|
+
this.smoother.reset();
|
|
129
|
+
this.totalHistory = [];
|
|
130
|
+
if (this.el.follower) this.el.follower.style.opacity = '0';
|
|
91
131
|
}
|
|
92
132
|
|
|
93
133
|
private handleMouseLeave(): void {
|
|
94
|
-
|
|
95
|
-
this.worker.postMessage('reset');
|
|
134
|
+
this.reset();
|
|
96
135
|
}
|
|
97
136
|
}
|
|
@@ -1,38 +1,30 @@
|
|
|
1
|
-
export interface FilterConfig {
|
|
2
|
-
maxThreshold?: number;
|
|
3
|
-
stdDevMultiplier?: number;
|
|
4
|
-
minSamples?: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
export class StatisticalFilter {
|
|
8
2
|
private samples: number[] = [];
|
|
9
|
-
private readonly
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.config = {
|
|
13
|
-
maxThreshold: 1100,
|
|
14
|
-
stdDevMultiplier: 2,
|
|
15
|
-
minSamples: 20,
|
|
16
|
-
...config,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
3
|
+
private readonly MAX_THRESHOLD: number = 10000;
|
|
4
|
+
private readonly STD_DEV_MULTIPLIER: number = 4;
|
|
5
|
+
private readonly MIN_SAMPLES: number = 20;
|
|
19
6
|
|
|
20
7
|
public isOutlier(value: number): boolean {
|
|
21
|
-
if (
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
8
|
+
if (value > this.MAX_THRESHOLD) return true;
|
|
24
9
|
|
|
25
|
-
if (this.samples.length <
|
|
10
|
+
if (this.samples.length < this.MIN_SAMPLES) {
|
|
26
11
|
this.samples.push(value);
|
|
27
12
|
return false;
|
|
28
13
|
}
|
|
29
14
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
)
|
|
15
|
+
const total: number = this.samples.reduce((acc: number, val: number) => acc + val, 0);
|
|
16
|
+
const avg: number = total / this.samples.length;
|
|
17
|
+
|
|
18
|
+
const squareDiffs: number = this.samples.reduce((acc: number, val: number) => {
|
|
19
|
+
const diff: number = val - avg;
|
|
20
|
+
return acc + (diff * diff);
|
|
21
|
+
}, 0);
|
|
22
|
+
|
|
23
|
+
const stdDev: number = Math.sqrt(squareDiffs / this.samples.length);
|
|
24
|
+
|
|
25
|
+
if (stdDev === 0) return false;
|
|
34
26
|
|
|
35
|
-
const isFiltered = Math.abs(value - avg) > stdDev *
|
|
27
|
+
const isFiltered: boolean = Math.abs(value - avg) > stdDev * this.STD_DEV_MULTIPLIER;
|
|
36
28
|
|
|
37
29
|
if (!isFiltered) {
|
|
38
30
|
this.samples.push(value);
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { StatisticalFilter } from '../logic/StatisticalFilter';
|
|
2
|
-
import { MovingAverage } from '../logic/MovingAverage';
|
|
3
|
-
|
|
4
|
-
const statisticalFilter = new StatisticalFilter();
|
|
5
|
-
const smoothingFilter = new MovingAverage(20);
|
|
6
|
-
|
|
7
|
-
let lastTime = 0;
|
|
8
|
-
let maxHz = 0;
|
|
9
|
-
const history: number[] = [];
|
|
10
|
-
|
|
11
|
-
self.onmessage = (event: MessageEvent<{ time: number } | 'reset'>) => {
|
|
12
|
-
if (event.data === 'reset') {
|
|
13
|
-
lastTime = 0;
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const { time } = event.data;
|
|
18
|
-
|
|
19
|
-
if (lastTime === 0) {
|
|
20
|
-
lastTime = time;
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const delta = time - lastTime;
|
|
25
|
-
lastTime = time;
|
|
26
|
-
|
|
27
|
-
if (delta <= 0 || delta > 100) return;
|
|
28
|
-
|
|
29
|
-
const hz = 1000 / delta;
|
|
30
|
-
|
|
31
|
-
if (statisticalFilter.isOutlier(hz)) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const smoothedHz = smoothingFilter.next(hz);
|
|
36
|
-
|
|
37
|
-
history.push(hz);
|
|
38
|
-
if (history.length > 200) history.shift();
|
|
39
|
-
|
|
40
|
-
if (hz > maxHz) maxHz = hz;
|
|
41
|
-
|
|
42
|
-
const avgHz = history.reduce((a, b) => a + b, 0) / history.length;
|
|
43
|
-
|
|
44
|
-
self.postMessage({
|
|
45
|
-
hz,
|
|
46
|
-
smoothedHz,
|
|
47
|
-
avgHz,
|
|
48
|
-
maxHz
|
|
49
|
-
});
|
|
50
|
-
};
|