tracky-mouse 2.6.0 → 2.8.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.
Files changed (76) hide show
  1. package/README.md +2 -1
  2. package/audio/click-press.wav +0 -0
  3. package/audio/click-release.wav +0 -0
  4. package/audio/middle-click-press.wav +0 -0
  5. package/audio/middle-click-release.wav +0 -0
  6. package/audio/pause.wav +0 -0
  7. package/audio/unpause.wav +0 -0
  8. package/lib/face-landmarks-detection.min.js +1 -1
  9. package/lib/face_mesh/face_mesh.js +1 -1
  10. package/locales/ar/translation.json +8 -2
  11. package/locales/ar-EG/translation.json +8 -2
  12. package/locales/bg/translation.json +8 -2
  13. package/locales/bn/translation.json +8 -2
  14. package/locales/ca/translation.json +8 -2
  15. package/locales/ce/translation.json +8 -2
  16. package/locales/ceb/translation.json +9 -3
  17. package/locales/cs/translation.json +8 -2
  18. package/locales/da/translation.json +8 -2
  19. package/locales/de/translation.json +8 -2
  20. package/locales/el/translation.json +8 -2
  21. package/locales/emoji/emoji-translation-notes.md +1 -0
  22. package/locales/emoji/translation.json +12 -6
  23. package/locales/en/translation.json +8 -2
  24. package/locales/eo/translation.json +8 -2
  25. package/locales/es/translation.json +8 -2
  26. package/locales/eu/translation.json +8 -2
  27. package/locales/fa/translation.json +8 -2
  28. package/locales/fi/translation.json +8 -2
  29. package/locales/fr/translation.json +8 -2
  30. package/locales/gu/translation.json +8 -2
  31. package/locales/ha/translation.json +8 -2
  32. package/locales/he/translation.json +8 -2
  33. package/locales/hi/translation.json +8 -2
  34. package/locales/hr/translation.json +8 -2
  35. package/locales/hu/translation.json +8 -2
  36. package/locales/hy/translation.json +8 -2
  37. package/locales/id/translation.json +8 -2
  38. package/locales/it/translation.json +8 -2
  39. package/locales/ja/translation.json +8 -2
  40. package/locales/jv/translation.json +9 -3
  41. package/locales/ko/translation.json +8 -2
  42. package/locales/mr/translation.json +8 -2
  43. package/locales/ms/translation.json +8 -2
  44. package/locales/nan/translation.json +8 -2
  45. package/locales/nb/translation.json +8 -2
  46. package/locales/nl/translation.json +8 -2
  47. package/locales/pa/translation.json +8 -2
  48. package/locales/pl/translation.json +8 -2
  49. package/locales/pt/translation.json +8 -2
  50. package/locales/pt-BR/translation.json +8 -2
  51. package/locales/ro/translation.json +8 -2
  52. package/locales/ru/translation.json +8 -2
  53. package/locales/sk/translation.json +8 -2
  54. package/locales/sl/translation.json +8 -2
  55. package/locales/sr/translation.json +8 -2
  56. package/locales/sv/translation.json +8 -2
  57. package/locales/sw/translation.json +8 -2
  58. package/locales/ta/translation.json +8 -2
  59. package/locales/te/translation.json +8 -2
  60. package/locales/th/translation.json +8 -2
  61. package/locales/tl/translation.json +9 -3
  62. package/locales/tr/translation.json +8 -2
  63. package/locales/tt/translation.json +8 -2
  64. package/locales/uk/translation.json +8 -2
  65. package/locales/ur/translation.json +8 -2
  66. package/locales/uz/translation.json +8 -2
  67. package/locales/vi/translation.json +8 -2
  68. package/locales/war/translation.json +9 -3
  69. package/locales/zh/translation.json +8 -2
  70. package/locales/zh-simplified/translation.json +8 -2
  71. package/package.json +3 -1
  72. package/src/audio.js +145 -0
  73. package/src/autoscroll.js +189 -0
  74. package/src/input-simulator.js +518 -0
  75. package/tracky-mouse.css +32 -1
  76. package/tracky-mouse.js +457 -185
package/src/audio.js ADDED
@@ -0,0 +1,145 @@
1
+
2
+ /** @type {AudioContext | null} */
3
+ let actx = null;
4
+
5
+ const audioPath = new URL("../audio", import.meta.url).href;
6
+ const audioFiles = {
7
+ // https://opengameart.org/content/51-ui-sound-effects-buttons-switches-and-clicks (CC0)
8
+ clickPress: `${audioPath}/click-press.wav`,
9
+ clickRelease: `${audioPath}/click-release.wav`,
10
+ // https://opengameart.org/content/middle-mouse-click (original recordings for this project, released under CC0)
11
+ middleClickPress: `${audioPath}/middle-click-press.wav`,
12
+ middleClickRelease: `${audioPath}/middle-click-release.wav`,
13
+ // https://opengameart.org/content/9-sci-fi-computer-sounds-and-beeps (CC-BY 3.0)
14
+ pause: `${audioPath}/pause.wav`,
15
+ unpause: `${audioPath}/unpause.wav`,
16
+ };
17
+ const audioBuffers = {};
18
+
19
+ // Sound effects are disabled by default because the dwell clicker can be initialized without the UI,
20
+ // in which case there's no UI to disable the sound effects from.
21
+ // The actual default in the app is separate.
22
+ export let audioEnabled = false;
23
+
24
+ export function setAudioEnabled(enabled) {
25
+ audioEnabled = enabled;
26
+ };
27
+
28
+ export function initAudio() {
29
+ if (actx === null) {
30
+ actx = new AudioContext();
31
+
32
+ // "User gesture" requirements cripple this accessibility feature,
33
+ // but we have to at least try to work around it.
34
+ const unsuspend = (event) => {
35
+ if (actx.state === "suspended") {
36
+ console.log("Starting suspended audio context via", event.type);
37
+ actx.resume();
38
+ }
39
+ };
40
+ addEventListener("keydown", unsuspend);
41
+ addEventListener("pointerdown", unsuspend);
42
+
43
+ // Load audio files
44
+ for (const [key, url] of Object.entries(audioFiles)) {
45
+ fetch(url)
46
+ .then((response) => response.arrayBuffer())
47
+ .then((arrayBuffer) => actx.decodeAudioData(arrayBuffer))
48
+ .then((audioBuffer) => {
49
+ audioBuffers[key] = audioBuffer;
50
+ })
51
+ .catch((error) => {
52
+ console.error("Error loading audio file:", url, error);
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ export function playSound(soundId, { delay = 0, playbackRate = 1, volume = 1 } = {}) {
59
+ if (audioEnabled && actx && actx.state === "running" && audioBuffers[soundId]) {
60
+ const gain = actx.createGain();
61
+ const source = actx.createBufferSource();
62
+ source.buffer = audioBuffers[soundId];
63
+ source.connect(gain).connect(actx.destination);
64
+ gain.gain.value = volume;
65
+ source.playbackRate.value = playbackRate;
66
+ source.start(actx.currentTime + delay);
67
+ }
68
+ }
69
+
70
+ export class SleepSweep {
71
+ constructor(ctx = actx) {
72
+ this.ctx = ctx;
73
+
74
+ this.osc = this.ctx.createOscillator();
75
+ this.osc.type = "sine";
76
+
77
+ this.gain = this.ctx.createGain();
78
+ this.gain.gain.value = 0;
79
+
80
+ // Not sure how much this filter is actually doing
81
+ this.filter = this.ctx.createBiquadFilter();
82
+ this.filter.type = "lowpass";
83
+ this.filter.frequency.value = 800;
84
+
85
+ this.osc.connect(this.filter);
86
+ this.filter.connect(this.gain);
87
+ this.gain.connect(this.ctx.destination);
88
+
89
+ this.osc.start();
90
+
91
+ this.enabled = false;
92
+ this.active = false;
93
+ this.timeOfLastGestureTrigger = 0; // audio context time
94
+ this.maxEffectDurationAfterGestureTrigger = 2.0; // seconds
95
+ }
96
+
97
+ update(gestureProgress) {
98
+ const now = this.ctx.currentTime;
99
+ if (!this.enabled || !audioEnabled) {
100
+ this.gain.gain.setTargetAtTime(0, now, 0.05);
101
+ return;
102
+ }
103
+
104
+ const effectStartFraction = 0.5;
105
+
106
+ if (gestureProgress < effectStartFraction) {
107
+ if (this.timeOfLastGestureTrigger + this.maxEffectDurationAfterGestureTrigger < now) {
108
+ this.gain.gain.setTargetAtTime(0, now, 0.05);
109
+ }
110
+ return;
111
+ }
112
+
113
+ const effectProgress = (gestureProgress - effectStartFraction) / (1 - effectStartFraction);
114
+
115
+ const volume = effectProgress * effectProgress * 0.4;
116
+
117
+ const baseFreq = 120;
118
+ const freq = baseFreq + effectProgress * 40;
119
+
120
+ this.gain.gain.setTargetAtTime(volume, now, 0.05);
121
+ this.osc.frequency.setTargetAtTime(freq, now, 0.05);
122
+
123
+ this.filter.frequency.setTargetAtTime(800 + effectProgress * 1200, now, 0.05);
124
+ }
125
+
126
+ sleepModeWasToggled(nowInSleepMode) {
127
+ const now = this.ctx.currentTime;
128
+ const currentFreq = this.osc.frequency.value;
129
+ const targetFreq = currentFreq * (nowInSleepMode ? 0.5 : 2.0);
130
+
131
+ this.osc.frequency.cancelScheduledValues(now);
132
+ this.osc.frequency.setValueAtTime(currentFreq, now);
133
+ this.osc.frequency.exponentialRampToValueAtTime(targetFreq, now + (nowInSleepMode ? 1 : 0.15));
134
+
135
+ this.gain.gain.setTargetAtTime(0, now + 0.05, nowInSleepMode ? 0.2 : 0.1); // should be <= this.maxEffectDurationAfterGestureTrigger
136
+
137
+ playSound(nowInSleepMode ? "pause" : "unpause");
138
+
139
+ this.timeOfLastGestureTrigger = now;
140
+ }
141
+
142
+ setEnabled(enabled) {
143
+ this.enabled = enabled;
144
+ }
145
+ }
@@ -0,0 +1,189 @@
1
+
2
+ const indicator = document.createElement("div");
3
+ indicator.innerHTML = `
4
+ <svg width="32" height="32" viewBox="0 0 32 32">
5
+ <!-- base shape -->
6
+ <circle cx="16" cy="16" r="13" fill="rgb(230, 230, 230)" stroke="rgb(50, 50, 50)" stroke-width="1" />
7
+ <!-- middle dot -->
8
+ <circle cx="16" cy="16" r="2" fill="rgb(0, 0, 0)" />
9
+ <!-- triangle above -->
10
+ <polygon data-axis="y" points="16,5 13,10 19,10" fill="rgb(0, 0, 0)" />
11
+ <!-- triangle below -->
12
+ <polygon data-axis="y" points="16,27 13,22 19,22" fill="rgb(0, 0, 0)" />
13
+ <!-- triangle left -->
14
+ <polygon data-axis="x" points="5,16 10,13 10,19" fill="rgb(0, 0, 0)" />
15
+ <!-- triangle right -->
16
+ <polygon data-axis="x" points="27,16 22,13 22,19" fill="rgb(0, 0, 0)" />
17
+ </svg>
18
+ `;
19
+ indicator.style.position = "fixed";
20
+ indicator.style.pointerEvents = "none";
21
+ indicator.style.transform = "translate(-50%, -50%)";
22
+ indicator.style.zIndex = "800000"; // below .tracky-mouse-cursor and inputFeedbackCanvas
23
+
24
+ const lockingClickRadius = 10; // pixels
25
+ const scrollSpeed = 0.01;
26
+ const scrollExponent = 1.6;
27
+ const deadZone = 10; // pixels (taxicab distance)
28
+ const maxDeltaTime = 100; // milliseconds
29
+
30
+
31
+ // Block clicks with a full-page transparent element while autoscrolling,
32
+ // so it doesn't doubly-act when stopping locked autoscroll with a click.
33
+ const clickBlocker = document.createElement("div");
34
+ clickBlocker.style.position = "fixed";
35
+ clickBlocker.style.left = "0";
36
+ clickBlocker.style.top = "0";
37
+ clickBlocker.style.width = "100%";
38
+ clickBlocker.style.height = "100%";
39
+ clickBlocker.style.zIndex = "1000000";
40
+ clickBlocker.style.pointerEvents = "auto"; // default
41
+ clickBlocker.style.backgroundColor = "transparent"; // default (but could override weird CSS ig)
42
+ clickBlocker.addEventListener("pointerdown", (event) => {
43
+ event.stopPropagation();
44
+ event.preventDefault();
45
+ autoscroll.stopAutoscroll();
46
+ });
47
+
48
+ addEventListener("keydown", (event) => {
49
+ if (event.key === "Escape" && autoscroll._start) {
50
+ event.stopPropagation();
51
+ event.preventDefault();
52
+ autoscroll.stopAutoscroll();
53
+ }
54
+ }, { capture: true });
55
+
56
+ export const autoscroll = {
57
+ _start: null,
58
+ _currentScrollDelta: null,
59
+ _lastTimestamp: null,
60
+ _animationFrameRequest: null,
61
+ pointerDown(target, x, y, buttonIndex = 0, pointerId) {
62
+ if (buttonIndex !== 1) {
63
+ this.stopAutoscroll();
64
+ return;
65
+ }
66
+ if (target.closest("a")) return;
67
+ this.startAutoscroll(target, x, y, pointerId);
68
+ },
69
+ pointerUp(_target, x, y, buttonIndex = 0, _pointerId) {
70
+ // Allow any pointer to stop autoscroll, because it's more likely to be annoying if it gets stuck
71
+ if (buttonIndex !== 1 || !this._start) return;
72
+ if (Math.hypot(x - this._start.x, y - this._start.y) < lockingClickRadius) {
73
+ return; // lock autoscroll mode until next click
74
+ }
75
+ this.stopAutoscroll();
76
+ },
77
+ startAutoscroll(target, x, y, pointerId) {
78
+ indicator.style.left = `${x}px`;
79
+ indicator.style.top = `${y}px`;
80
+ document.body.appendChild(indicator);
81
+ document.body.appendChild(clickBlocker);
82
+ this._start = { x, y, target, pointerId };
83
+ this._currentScrollDelta = null;
84
+ this._lastTimestamp = performance.now();
85
+ // Update arrow visibility immediately, and start animation loop
86
+ this.updateAutoscroll();
87
+ },
88
+ stopAutoscroll() {
89
+ this._start = null;
90
+ this._currentScrollDelta = null;
91
+ this._lastTimestamp = null;
92
+ if (indicator.parentElement) {
93
+ document.body.removeChild(indicator);
94
+ }
95
+ if (clickBlocker.parentElement) {
96
+ document.body.removeChild(clickBlocker);
97
+ }
98
+ cancelAnimationFrame(this._animationFrameRequest);
99
+ this._animationFrameRequest = null;
100
+ },
101
+ pointerMove(_target, x, y, pointerId) {
102
+ // Don't allow other pointers to affect autoscroll started by a different pointer
103
+ if (!this._start || this._start.pointerId !== pointerId) return;
104
+ const diff = { x: x - this._start.x, y: y - this._start.y };
105
+ // Note: Don't return early if within deadzone,
106
+ // because we still want to update the indicator arrows.
107
+ if (Math.abs(diff.x) < deadZone) diff.x = 0;
108
+ if (Math.abs(diff.y) < deadZone) diff.y = 0;
109
+ diff.x -= Math.sign(diff.x) * deadZone;
110
+ diff.y -= Math.sign(diff.y) * deadZone;
111
+
112
+ // Note: there's a question of whether to apply the exponent or multiplier first.
113
+ // I think with exponent after multiplier, adjusting the exponent changes the
114
+ // average speed less for nominal values of input/exponent/multiplier,
115
+ // making it more intuitive to tweak the curvature,
116
+ // but tweaking the multiplier may be more intuitive, at least in a strict mathematical sense,
117
+ // with the exponent applied first.
118
+ // The set of curves expressible should be equal.
119
+ // As an aside, switching between the two orders could be easier if we used one variable
120
+ // instead of both `diff` and `scrollDelta`. I doubt it would harm clarity.
121
+ const scrollDelta = { x: diff.x * scrollSpeed, y: diff.y * scrollSpeed };
122
+ scrollDelta.x = Math.sign(scrollDelta.x) * Math.pow(Math.abs(scrollDelta.x), scrollExponent);
123
+ scrollDelta.y = Math.sign(scrollDelta.y) * Math.pow(Math.abs(scrollDelta.y), scrollExponent);
124
+ this._currentScrollDelta = scrollDelta;
125
+ },
126
+ getScrollable() {
127
+ let container = this._start.target;
128
+ let canScrollX = false;
129
+ let canScrollY = false;
130
+ while (container && container !== document.body) {
131
+ // This initial test gives a false positive on the demo section of the website
132
+ // Trying an actual scroll seems like a sure test, but could cause performance issues
133
+ canScrollX = container.scrollWidth > container.clientWidth;
134
+ canScrollY = container.scrollHeight > container.clientHeight;
135
+ if (canScrollX || canScrollY) {
136
+ if (canScrollX) {
137
+ const oldScrollLeft = container.scrollLeft;
138
+ container.scrollLeft = 1;
139
+ if (container.scrollLeft === 0) {
140
+ canScrollX = false;
141
+ }
142
+ container.scrollLeft = oldScrollLeft;
143
+ }
144
+ if (canScrollY) {
145
+ const oldScrollTop = container.scrollTop;
146
+ container.scrollTop = 1;
147
+ if (container.scrollTop === 0) {
148
+ canScrollY = false;
149
+ }
150
+ container.scrollTop = oldScrollTop;
151
+ }
152
+ }
153
+ if (canScrollX || canScrollY) {
154
+ break;
155
+ }
156
+ container = container.parentElement;
157
+ }
158
+ if (!container || container === document.body) {
159
+ // container = document.scrollingElement;
160
+ container = window;
161
+ canScrollX = document.scrollingElement.scrollWidth > document.scrollingElement.clientWidth;
162
+ canScrollY = document.scrollingElement.scrollHeight > document.scrollingElement.clientHeight;
163
+ }
164
+ return { container, canScrollX, canScrollY };
165
+ },
166
+ updateAutoscroll() {
167
+ const deltaTime = Math.min(maxDeltaTime, performance.now() - this._lastTimestamp);
168
+ // Note: we could optimize by not calling getScrollable every frame
169
+ const { container, canScrollX, canScrollY } = this.getScrollable();
170
+ const scrollDelta = this._currentScrollDelta;
171
+ if (scrollDelta) {
172
+ // Note: scrolling might be limited to integers, which could cause it to not scroll at low speeds,
173
+ // especially at high frame rates,
174
+ // and affect the accuracy of deltaTime-based movement. We could accumulate fractional scroll
175
+ // deltas and apply them when they reach a whole pixel.
176
+ container.scrollBy(scrollDelta.x * deltaTime, scrollDelta.y * deltaTime);
177
+ }
178
+
179
+ for (const arrow of indicator.querySelectorAll("[data-axis]")) {
180
+ const axis = arrow.dataset.axis;
181
+ arrow.style.display = (axis === "x" ? canScrollX : canScrollY) ? "" : "none";
182
+ }
183
+
184
+ this._lastTimestamp = performance.now();
185
+
186
+ cancelAnimationFrame(this._animationFrameRequest);
187
+ this._animationFrameRequest = requestAnimationFrame(() => this.updateAutoscroll());
188
+ },
189
+ };