tracky-mouse 2.5.0 → 2.7.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/README.md +2 -1
- package/audio/click-press.wav +0 -0
- package/audio/click-release.wav +0 -0
- package/audio/middle-click-press.wav +0 -0
- package/audio/middle-click-release.wav +0 -0
- package/audio/pause.wav +0 -0
- package/audio/unpause.wav +0 -0
- package/audio.js +145 -0
- package/images/head-not-found.svg +135 -0
- package/images/manual-takeback.svg +127 -0
- package/locales/ar/translation.json +199 -202
- package/locales/ar-EG/translation.json +199 -202
- package/locales/bg/translation.json +199 -202
- package/locales/bn/translation.json +199 -202
- package/locales/ca/translation.json +199 -202
- package/locales/ce/translation.json +199 -202
- package/locales/ceb/translation.json +199 -202
- package/locales/cs/translation.json +199 -202
- package/locales/da/translation.json +199 -202
- package/locales/de/translation.json +199 -202
- package/locales/el/translation.json +199 -202
- package/locales/emoji/translation.json +199 -202
- package/locales/en/translation.json +199 -202
- package/locales/eo/translation.json +199 -202
- package/locales/es/translation.json +199 -202
- package/locales/eu/translation.json +199 -202
- package/locales/fa/translation.json +199 -202
- package/locales/fi/translation.json +199 -202
- package/locales/fr/translation.json +199 -202
- package/locales/gu/translation.json +199 -202
- package/locales/ha/translation.json +199 -202
- package/locales/he/translation.json +199 -202
- package/locales/hi/translation.json +199 -202
- package/locales/hr/translation.json +199 -202
- package/locales/hu/translation.json +199 -202
- package/locales/hy/translation.json +199 -202
- package/locales/id/translation.json +199 -202
- package/locales/it/translation.json +199 -202
- package/locales/ja/translation.json +199 -202
- package/locales/jv/translation.json +199 -202
- package/locales/ko/translation.json +199 -202
- package/locales/mr/translation.json +199 -202
- package/locales/ms/translation.json +199 -202
- package/locales/nan/translation.json +199 -202
- package/locales/nb/translation.json +199 -202
- package/locales/nl/translation.json +199 -202
- package/locales/pa/translation.json +199 -202
- package/locales/pl/translation.json +199 -202
- package/locales/pt/translation.json +199 -202
- package/locales/pt-BR/translation.json +199 -202
- package/locales/ro/translation.json +199 -202
- package/locales/ru/translation.json +199 -202
- package/locales/sk/translation.json +199 -202
- package/locales/sl/translation.json +199 -202
- package/locales/sr/translation.json +199 -202
- package/locales/sv/translation.json +199 -202
- package/locales/sw/translation.json +199 -202
- package/locales/ta/translation.json +199 -202
- package/locales/te/translation.json +199 -202
- package/locales/th/translation.json +199 -202
- package/locales/tl/translation.json +199 -202
- package/locales/tr/translation.json +199 -202
- package/locales/tt/translation.json +199 -202
- package/locales/uk/translation.json +199 -202
- package/locales/ur/translation.json +199 -202
- package/locales/uz/translation.json +199 -202
- package/locales/vi/translation.json +199 -202
- package/locales/war/translation.json +199 -202
- package/locales/zh/translation.json +199 -202
- package/locales/zh-simplified/translation.json +200 -203
- package/package.json +4 -1
- package/tracky-mouse.css +73 -7
- package/tracky-mouse.js +590 -343
package/tracky-mouse.js
CHANGED
|
@@ -52,103 +52,114 @@ const isSelectorValid = ((dummyElement) =>
|
|
|
52
52
|
|
|
53
53
|
const dwellClickers = [];
|
|
54
54
|
|
|
55
|
+
let playSound = () => { console.log("audio module not loaded yet; can't play sound effect"); };
|
|
56
|
+
let initialAudioEnabled = false;
|
|
57
|
+
let setAudioEnabled = (enabled) => { initialAudioEnabled = enabled; };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Object} config
|
|
61
|
+
* @param {string} config.targets - a CSS selector for the elements to click. Anything else will be ignored (except as an occluder).
|
|
62
|
+
* @param {(el: Element) => boolean} [config.shouldDrag] - a function that returns true if the element should be dragged rather than simply clicked.
|
|
63
|
+
* @param {(el: Element) => boolean} [config.noCenter] - a function that returns true if the element should be clicked anywhere on the element, rather than always at the center.
|
|
64
|
+
* @param {Array<{
|
|
65
|
+
* from: string | Element | ((el: Element) => boolean), // - an array of `{ from, to, withinMargin }` objects, which define rules for dynamically changing what is hovered/clicked when the mouse is over a different element.
|
|
66
|
+
* to: string | Element | ((el: Element) => Element | null), // - the element to retarget from. Can be a CSS selector, an element, or a function taking the element under the mouse and returning whether it should be retargeted.
|
|
67
|
+
* withinMargin?: number // - the element to retarget to. Can be a CSS selector for an element which is an ancestor or descendant of the `from` element, or an element, or a function taking the element under the mouse and returning an element to retarget to, or null to ignore the element.
|
|
68
|
+
* }>} [config.retarget] - a number of pixels within which to consider the mouse over the `to` element. Default to infinity.
|
|
69
|
+
* @param {(el1: Element, el2: Element) => boolean} [config.isEquivalentTarget] - a function that returns true if two elements should be considered part of the same control, i.e. if clicking either should do the same thing. Elements that are equal are always considered equivalent even if you return false. This option is used for preventing the system from detecting occluding elements as separate controls, and rejecting the click. (When an occlusion is detected, it flashes a red box.)
|
|
70
|
+
* @param {(el: Element) => boolean} [config.dwellClickEvenIfPaused] - a function that returns true if the element should be clicked even while dwell clicking is otherwise paused. Use this for a dwell clicking toggle button, so it's possible to resume dwell clicking. With dwell clicking it's important to let users take a break, since otherwise you have to constantly move the cursor in order to not click on things!
|
|
71
|
+
* @param {(el: Element) => boolean} [config.shouldClickThrough] - a function that returns true if the element should be totally ignored, allowing clicking on content behind it. Prefer `pointer-events: none` when possible, which will work for all input methods. Use this only if you need to differentiate input methods. Default: `(el) => el.matches(".tracky-mouse-click-through, .tracky-mouse-click-through *")`
|
|
72
|
+
* @param {(args: {x: number, y: number, target: Element}) => void} config.click - a function to trigger a click on the given target element.
|
|
73
|
+
* @param {() => void} [config.beforeDispatch] - a function to call before a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
|
|
74
|
+
* @param {() => void} [config.afterDispatch] - a function to call after a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
|
|
75
|
+
* @param {() => void} [config.beforePointerDownDispatch] - a function to call before a `pointerdown` event is dispatched. Likely to be merged with `config.beforeDispatch()` in the future.
|
|
76
|
+
* @param {() => boolean} [config.isHeld] - a function that returns true if the next dwell should be a release (triggering `pointerup`).
|
|
77
|
+
*/
|
|
55
78
|
const initDwellClicking = (config) => {
|
|
56
|
-
/*
|
|
57
|
-
Arguments:
|
|
58
|
-
- `config.targets` (required): a CSS selector for the elements to click. Anything else will be ignored.
|
|
59
|
-
- `config.shouldDrag(el)` (optional): a function that returns true if the element should be dragged rather than simply clicked.
|
|
60
|
-
- `config.noCenter(el)` (optional): a function that returns true if the element should be clicked anywhere on the element, rather than always at the center.
|
|
61
|
-
- `config.retarget` (optional): an array of `{ from, to, withinMargin }` objects, which define rules for dynamically changing what is hovered/clicked when the mouse is over a different element.
|
|
62
|
-
- `from` (required): the element to retarget from. Can be a CSS selector, an element, or a function taking the element under the mouse and returning whether it should be retargeted.
|
|
63
|
-
- `to` (required): the element to retarget to. Can be a CSS selector for an element which is an ancestor or descendant of the `from` element, or an element, or a function taking the element under the mouse and returning an element to retarget to, or null to ignore the element.
|
|
64
|
-
- `withinMargin` (optional): a number of pixels within which to consider the mouse over the `to` element. Default to infinity.
|
|
65
|
-
- `config.isEquivalentTarget(el1, el2)` (optional): a function that returns true if two elements should be considered part of the same control, i.e. if clicking either should do the same thing. Elements that are equal are always considered equivalent even if you return false. This option is used for preventing the system from detecting occluding elements as separate controls, and rejecting the click. (When an occlusion is detected, it flashes a red box.)
|
|
66
|
-
- `config.dwellClickEvenIfPaused(el)` (optional): a function that returns true if the element should be clicked even while dwell clicking is otherwise paused. Use this for a dwell clicking toggle button, so it's possible to resume dwell clicking. With dwell clicking it's important to let users take a break, since otherwise you have to constantly move the cursor in order to not click on things!
|
|
67
|
-
- `config.click({x, y, target})` (required): a function to trigger a click on the given target element.
|
|
68
|
-
- `config.beforeDispatch()` (optional): a function to call before a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
|
|
69
|
-
- `config.afterDispatch()` (optional): a function to call after a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
|
|
70
|
-
- `config.beforePointerDownDispatch()` (optional): a function to call before a `pointerdown` event is dispatched. Likely to be merged with `config.beforeDispatch()` in the future.
|
|
71
|
-
- `config.isHeld()` (optional): a function that returns true if the next dwell should be a release (triggering `pointerup`).
|
|
72
|
-
*/
|
|
73
79
|
|
|
74
80
|
/** translation placeholder */
|
|
75
|
-
const t = (
|
|
81
|
+
const t = (key, options = {}) => options.defaultValue ?? key;
|
|
76
82
|
|
|
77
83
|
if (typeof config !== "object") {
|
|
78
|
-
throw new Error(t("configuration object required for initDwellClicking"));
|
|
84
|
+
throw new Error(t("api.errors.configRequired", { defaultValue: "configuration object required for initDwellClicking" }));
|
|
79
85
|
}
|
|
80
86
|
if (config.targets === undefined) {
|
|
81
|
-
throw new Error(t("config.targets is required (must be a CSS selector)"));
|
|
87
|
+
throw new Error(t("api.errors.targetsRequired", { defaultValue: "config.targets is required (must be a CSS selector)" }));
|
|
82
88
|
}
|
|
83
89
|
if (typeof config.targets !== "string") {
|
|
84
|
-
throw new Error(t("config.targets must be a string (a CSS selector)"));
|
|
90
|
+
throw new Error(t("api.errors.targetsMustBeSelectorString", { defaultValue: "config.targets must be a string (a CSS selector)" }));
|
|
85
91
|
}
|
|
86
92
|
if (!isSelectorValid(config.targets)) {
|
|
87
|
-
throw new Error(t("config.targets is not a valid CSS selector"));
|
|
93
|
+
throw new Error(t("api.errors.targetsInvalidSelector", { defaultValue: "config.targets is not a valid CSS selector" }));
|
|
88
94
|
}
|
|
89
95
|
if (config.click === undefined) {
|
|
90
|
-
throw new Error(t("config.click is required"));
|
|
96
|
+
throw new Error(t("api.errors.clickRequired", { defaultValue: "config.click is required" }));
|
|
91
97
|
}
|
|
92
98
|
if (typeof config.click !== "function") {
|
|
93
|
-
throw new Error(t("
|
|
99
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.click"));
|
|
94
100
|
}
|
|
95
101
|
if (config.shouldDrag !== undefined && typeof config.shouldDrag !== "function") {
|
|
96
|
-
throw new Error(t("
|
|
102
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.shouldDrag"));
|
|
97
103
|
}
|
|
98
104
|
if (config.noCenter !== undefined && typeof config.noCenter !== "function") {
|
|
99
|
-
throw new Error(t("
|
|
105
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.noCenter"));
|
|
100
106
|
}
|
|
101
107
|
if (config.isEquivalentTarget !== undefined && typeof config.isEquivalentTarget !== "function") {
|
|
102
|
-
throw new Error(t("
|
|
108
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.isEquivalentTarget"));
|
|
103
109
|
}
|
|
104
110
|
if (config.dwellClickEvenIfPaused !== undefined && typeof config.dwellClickEvenIfPaused !== "function") {
|
|
105
|
-
throw new Error(t("
|
|
111
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.dwellClickEvenIfPaused"));
|
|
112
|
+
}
|
|
113
|
+
if (config.shouldClickThrough !== undefined && typeof config.shouldClickThrough !== "function") {
|
|
114
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.shouldClickThrough"));
|
|
106
115
|
}
|
|
107
116
|
if (config.beforeDispatch !== undefined && typeof config.beforeDispatch !== "function") {
|
|
108
|
-
throw new Error(t("
|
|
117
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.beforeDispatch"));
|
|
109
118
|
}
|
|
110
119
|
if (config.afterDispatch !== undefined && typeof config.afterDispatch !== "function") {
|
|
111
|
-
throw new Error(t("
|
|
120
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.afterDispatch"));
|
|
112
121
|
}
|
|
113
122
|
if (config.beforePointerDownDispatch !== undefined && typeof config.beforePointerDownDispatch !== "function") {
|
|
114
|
-
throw new Error(t("
|
|
123
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.beforePointerDownDispatch"));
|
|
115
124
|
}
|
|
116
125
|
if (config.isHeld !== undefined && typeof config.isHeld !== "function") {
|
|
117
|
-
throw new Error(t("
|
|
126
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.isHeld"));
|
|
118
127
|
}
|
|
119
128
|
if (config.retarget !== undefined) {
|
|
120
129
|
if (!Array.isArray(config.retarget)) {
|
|
121
|
-
throw new Error(t("config.retarget must be an array of objects"));
|
|
130
|
+
throw new Error(t("api.errors.retargetMustBeArray", { defaultValue: "config.retarget must be an array of objects" }));
|
|
122
131
|
}
|
|
123
132
|
for (let i = 0; i < config.retarget.length; i++) {
|
|
124
133
|
const rule = config.retarget[i];
|
|
125
134
|
if (typeof rule !== "object") {
|
|
126
|
-
throw new Error(t("config.retarget must be an array of objects"));
|
|
135
|
+
throw new Error(t("api.errors.retargetMustBeArray", { defaultValue: "config.retarget must be an array of objects" }));
|
|
127
136
|
}
|
|
128
137
|
if (rule.from === undefined) {
|
|
129
|
-
throw new Error(t("config.retarget[%0].from is required").replace("%0", i));
|
|
138
|
+
throw new Error(t("api.errors.retargetFromRequired", { defaultValue: "config.retarget[%0].from is required" }).replace("%0", i));
|
|
130
139
|
}
|
|
131
140
|
if (rule.to === undefined) {
|
|
132
|
-
throw new Error(t("config.retarget[%0].to is required (although can be null to ignore the element)").replace("%0", i));
|
|
141
|
+
throw new Error(t("api.errors.retargetToRequired", { defaultValue: "config.retarget[%0].to is required (although can be null to ignore the element)" }).replace("%0", i));
|
|
133
142
|
}
|
|
134
143
|
if (rule.withinMargin !== undefined && typeof rule.withinMargin !== "number") {
|
|
135
|
-
throw new Error(t("
|
|
144
|
+
throw new Error(t("api.errors.numberRequired", { defaultValue: "%0 must be a number" }).replace("%0", `config.retarget[${i}].withinMargin`));
|
|
136
145
|
}
|
|
137
146
|
if (typeof rule.from !== "string" && typeof rule.from !== "function" && !(rule.from instanceof Element)) {
|
|
138
|
-
throw new Error(t("config.retarget[%0].from must be a CSS selector string, an Element, or a function").replace("%0", i));
|
|
147
|
+
throw new Error(t("api.errors.retargetFromInvalidType", { defaultValue: "config.retarget[%0].from must be a CSS selector string, an Element, or a function" }).replace("%0", i));
|
|
139
148
|
}
|
|
140
149
|
if (typeof rule.to !== "string" && typeof rule.to !== "function" && !(rule.to instanceof Element) && rule.to !== null) {
|
|
141
|
-
throw new Error(t("config.retarget[%0].to must be a CSS selector string, an Element, a function, or null").replace("%0", i));
|
|
150
|
+
throw new Error(t("api.errors.retargetToInvalidType", { defaultValue: "config.retarget[%0].to must be a CSS selector string, an Element, a function, or null" }).replace("%0", i));
|
|
142
151
|
}
|
|
143
152
|
if (typeof rule.from === "string" && !isSelectorValid(rule.from)) {
|
|
144
|
-
throw new Error(t("config.retarget[%0].from is not a valid CSS selector").replace("%0", i));
|
|
153
|
+
throw new Error(t("api.errors.retargetFromInvalidSelector", { defaultValue: "config.retarget[%0].from is not a valid CSS selector" }).replace("%0", i));
|
|
145
154
|
}
|
|
146
155
|
if (typeof rule.to === "string" && !isSelectorValid(rule.to)) {
|
|
147
|
-
throw new Error(t("config.retarget[%0].to is not a valid CSS selector").replace("%0", i));
|
|
156
|
+
throw new Error(t("api.errors.retargetToInvalidSelector", { defaultValue: "config.retarget[%0].to is not a valid CSS selector" }).replace("%0", i));
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
160
|
|
|
161
|
+
const shouldClickThrough = config.shouldClickThrough ?? ((el) => el.matches(".tracky-mouse-click-through, .tracky-mouse-click-through *"));
|
|
162
|
+
|
|
152
163
|
// trackyMouseContainer.querySelector(".tracky-mouse-canvas").classList.add("inset-deep");
|
|
153
164
|
|
|
154
165
|
const circleRadiusMax = 50; // dwell indicator size in pixels
|
|
@@ -222,6 +233,14 @@ const initDwellClicking = (config) => {
|
|
|
222
233
|
return null;
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
if (shouldClickThrough(target)) {
|
|
237
|
+
const elements = document.elementsFromPoint(clientX, clientY);
|
|
238
|
+
target = elements.find(el => !shouldClickThrough(el));
|
|
239
|
+
if (!target) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
225
244
|
let hoverCandidate = {
|
|
226
245
|
x: clientX,
|
|
227
246
|
y: clientY,
|
|
@@ -365,6 +384,9 @@ const initDwellClicking = (config) => {
|
|
|
365
384
|
showOccluderIndicator(apparentHoverCandidate.target);
|
|
366
385
|
}
|
|
367
386
|
} else {
|
|
387
|
+
// TODO: ignore .tracky-mouse-click-through elements here as well
|
|
388
|
+
// TODO: distinguish occlusion vs moved element (i.e. element is no longer in the elementsFromPoint list)
|
|
389
|
+
// for example for the archery targets in the demo on the website, which animate
|
|
368
390
|
let occluder = document.elementFromPoint(hoverCandidate.x, hoverCandidate.y);
|
|
369
391
|
hoverCandidate = null;
|
|
370
392
|
deactivateForAtLeast(inactiveAfterInvalidTimespan);
|
|
@@ -391,6 +413,7 @@ const initDwellClicking = (config) => {
|
|
|
391
413
|
})
|
|
392
414
|
));
|
|
393
415
|
config.afterDispatch?.();
|
|
416
|
+
playSound("clickRelease");
|
|
394
417
|
} else {
|
|
395
418
|
config.beforePointerDownDispatch?.();
|
|
396
419
|
config.beforeDispatch?.();
|
|
@@ -403,6 +426,7 @@ const initDwellClicking = (config) => {
|
|
|
403
426
|
config.afterDispatch?.();
|
|
404
427
|
if (config.shouldDrag?.(hoverCandidate.target)) {
|
|
405
428
|
dwellDragging = hoverCandidate.target;
|
|
429
|
+
playSound("clickPress");
|
|
406
430
|
} else {
|
|
407
431
|
config.beforeDispatch?.();
|
|
408
432
|
hoverCandidate.target.dispatchEvent(new PointerEvent("pointerup",
|
|
@@ -413,6 +437,8 @@ const initDwellClicking = (config) => {
|
|
|
413
437
|
));
|
|
414
438
|
config.click(hoverCandidate);
|
|
415
439
|
config.afterDispatch?.();
|
|
440
|
+
playSound("clickPress");
|
|
441
|
+
playSound("clickRelease", { delay: 0.03 }); // fully separating the sounds sounded worse
|
|
416
442
|
}
|
|
417
443
|
}
|
|
418
444
|
hoverCandidate = null;
|
|
@@ -571,7 +597,57 @@ TrackyMouse.cleanupDwellClicking = function () {
|
|
|
571
597
|
}
|
|
572
598
|
};
|
|
573
599
|
|
|
574
|
-
TrackyMouse.
|
|
600
|
+
TrackyMouse._initAudio = async function () {
|
|
601
|
+
let module;
|
|
602
|
+
try {
|
|
603
|
+
// console.log("Loading audio support...");
|
|
604
|
+
module = await import("./audio.js");
|
|
605
|
+
} catch (e) {
|
|
606
|
+
console.warn("Failed to load audio module, click sounds will be disabled:", e);
|
|
607
|
+
}
|
|
608
|
+
// console.log("Audio module loaded.");
|
|
609
|
+
try {
|
|
610
|
+
const { initAudio } = module;
|
|
611
|
+
initAudio();
|
|
612
|
+
playSound = module.playSound;
|
|
613
|
+
setAudioEnabled = module.setAudioEnabled;
|
|
614
|
+
setAudioEnabled(initialAudioEnabled);
|
|
615
|
+
// console.log("Audio is initially " + (initialAudioEnabled ? "enabled" : "disabled"));
|
|
616
|
+
} catch (e) {
|
|
617
|
+
console.warn("Failed to initialize audio support, click sounds will be disabled:", e);
|
|
618
|
+
}
|
|
619
|
+
return module;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
623
|
+
|
|
624
|
+
const {
|
|
625
|
+
statsJs = false,
|
|
626
|
+
// Unstable
|
|
627
|
+
updateInputFeedback = window.electronAPI?.updateInputFeedback,
|
|
628
|
+
// Unstable
|
|
629
|
+
setMouseButtonState = window.electronAPI?.setMouseButtonState,
|
|
630
|
+
// Unstable
|
|
631
|
+
notifyToggleState = window.electronAPI?.notifyToggleState,
|
|
632
|
+
// Unstable
|
|
633
|
+
handleSettingsUpdate,
|
|
634
|
+
// Unstable
|
|
635
|
+
clickingModeSupported = false,
|
|
636
|
+
// TODO: manage all of electronAPI similarly? well, setOptions is already a function in scope here,
|
|
637
|
+
// and it's not like we want to expose all electronAPI as part of the public API necessarily
|
|
638
|
+
// Could group things under an "unstable" object, or ideally, design nice APIs for everything.
|
|
639
|
+
} = initOptions;
|
|
640
|
+
|
|
641
|
+
/** @type {SleepSweep | null} */
|
|
642
|
+
let sleepSweep = null;
|
|
643
|
+
|
|
644
|
+
TrackyMouse._initAudio().then((module) => {
|
|
645
|
+
// _initAudio warns in the console and resolves to undefined if it fails to load audio support
|
|
646
|
+
if (module) {
|
|
647
|
+
const { SleepSweep } = module;
|
|
648
|
+
sleepSweep = new SleepSweep();
|
|
649
|
+
}
|
|
650
|
+
});
|
|
575
651
|
|
|
576
652
|
const isDesktopApp = !!window.electronAPI;
|
|
577
653
|
|
|
@@ -617,7 +693,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
617
693
|
}
|
|
618
694
|
const rtlLanguages = ["ar", "he", "fa", "ur"]; // Right-to-left languages (current and future)
|
|
619
695
|
const isRTL = rtlLanguages.includes(locale.split("-")[0]);
|
|
620
|
-
const t = (
|
|
696
|
+
const t = (key, options = {}) => translations[key] ?? options.defaultValue ?? key;
|
|
621
697
|
// console.trace("Initializing UI with locale", locale);
|
|
622
698
|
|
|
623
699
|
// language name mappings marked with * may not be ISO 639-1
|
|
@@ -826,7 +902,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
826
902
|
};
|
|
827
903
|
|
|
828
904
|
|
|
829
|
-
|
|
905
|
+
let languageToDefaultRegion = {
|
|
830
906
|
aa: "ET",
|
|
831
907
|
ab: "GE",
|
|
832
908
|
abr: "GH",
|
|
@@ -1571,9 +1647,9 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1571
1647
|
// </svg>`;
|
|
1572
1648
|
}
|
|
1573
1649
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1650
|
+
let split = locale.toUpperCase().split(/-|_/);
|
|
1651
|
+
let lang = split.shift();
|
|
1652
|
+
let code = split.pop();
|
|
1577
1653
|
|
|
1578
1654
|
if (!/^[A-Z]{2}$/.test(code)) {
|
|
1579
1655
|
code = languageToDefaultRegion[lang.toLowerCase()];
|
|
@@ -1588,38 +1664,38 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1588
1664
|
return a + b;
|
|
1589
1665
|
}
|
|
1590
1666
|
|
|
1591
|
-
|
|
1667
|
+
let uiContainer = div || document.createElement("div");
|
|
1592
1668
|
uiContainer.classList.add("tracky-mouse-ui");
|
|
1593
1669
|
uiContainer.classList.toggle("tracky-mouse-rtl", isRTL);
|
|
1594
1670
|
uiContainer.dir = isRTL ? "rtl" : "ltr";
|
|
1595
1671
|
uiContainer.innerHTML = `
|
|
1596
1672
|
<div class="tracky-mouse-controls">
|
|
1597
|
-
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("Start")}</button>
|
|
1673
|
+
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("ui.startStopButton.start", { defaultValue: "Start" })}</button>
|
|
1598
1674
|
</div>
|
|
1599
1675
|
<div class="tracky-mouse-canvas-container-container">
|
|
1600
1676
|
<div class="tracky-mouse-canvas-container">
|
|
1601
1677
|
<div class="tracky-mouse-canvas-overlay">
|
|
1602
|
-
<button class="tracky-mouse-use-camera-button">${t("Allow Camera Access")}</button>
|
|
1603
|
-
<!--<button class="tracky-mouse-use-camera-button">${t("Use my camera")}</button>-->
|
|
1604
|
-
<button class="tracky-mouse-use-demo-footage-button" hidden>${t("Use demo footage")}</button>
|
|
1678
|
+
<button class="tracky-mouse-use-camera-button">${t("ui.camera.allowAccess", { defaultValue: "Allow Camera Access" })}</button>
|
|
1679
|
+
<!--<button class="tracky-mouse-use-camera-button">${t("ui.camera.useMyCamera", { defaultValue: "Use my camera" })}</button>-->
|
|
1680
|
+
<button class="tracky-mouse-use-demo-footage-button" hidden>${t("ui.camera.useDemoFootage", { defaultValue: "Use demo footage" })}</button>
|
|
1605
1681
|
<div class="tracky-mouse-error-message" role="alert" hidden></div>
|
|
1606
1682
|
</div>
|
|
1607
1683
|
<canvas class="tracky-mouse-canvas"></canvas>
|
|
1608
1684
|
</div>
|
|
1609
1685
|
</div>
|
|
1610
1686
|
<p class="tracky-mouse-desktop-app-download-message">
|
|
1611
|
-
${t('You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.')}
|
|
1687
|
+
${t("ui.desktopAppPromo.message", { defaultValue: 'You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.' })}
|
|
1612
1688
|
</p>
|
|
1613
1689
|
`;
|
|
1614
1690
|
if (!div) {
|
|
1615
1691
|
document.body.appendChild(uiContainer);
|
|
1616
1692
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1693
|
+
let startStopButton = uiContainer.querySelector(".tracky-mouse-start-stop-button");
|
|
1694
|
+
let useCameraButton = uiContainer.querySelector(".tracky-mouse-use-camera-button");
|
|
1695
|
+
let useDemoFootageButton = uiContainer.querySelector(".tracky-mouse-use-demo-footage-button");
|
|
1696
|
+
let errorMessage = uiContainer.querySelector(".tracky-mouse-error-message");
|
|
1697
|
+
let canvasContainer = uiContainer.querySelector('.tracky-mouse-canvas-container');
|
|
1698
|
+
let desktopAppDownloadMessage = uiContainer.querySelector('.tracky-mouse-desktop-app-download-message');
|
|
1623
1699
|
|
|
1624
1700
|
// Settings (initialized later; defaults are defined in settingsCategories)
|
|
1625
1701
|
const s = {};
|
|
@@ -1631,10 +1707,10 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1631
1707
|
const settingsCategories = [
|
|
1632
1708
|
{
|
|
1633
1709
|
type: "group",
|
|
1634
|
-
label: t("Cursor Movement"),
|
|
1710
|
+
label: t("settings.sections.cursorMovement.label", { defaultValue: "Cursor Movement" }),
|
|
1635
1711
|
settings: [
|
|
1636
1712
|
{
|
|
1637
|
-
label: t("Tilt influence"),
|
|
1713
|
+
label: t("settings.tiltInfluence.label", { defaultValue: "Tilt influence" }),
|
|
1638
1714
|
className: "tracky-mouse-tilt-influence",
|
|
1639
1715
|
key: "headTrackingTiltInfluence",
|
|
1640
1716
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -1644,20 +1720,21 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1644
1720
|
max: 100,
|
|
1645
1721
|
default: 0,
|
|
1646
1722
|
labels: {
|
|
1647
|
-
// min: t("Optical flow"), // too technical
|
|
1648
|
-
// min: t("Point tracking"), // still technical but at least it's terminology we're already using
|
|
1649
|
-
min: t("Point tracking (2D)"),
|
|
1650
|
-
// max: t("Head tilt"),
|
|
1651
|
-
max: t("Head tilt (3D)"),
|
|
1723
|
+
// min: t("settings.tiltInfluence.sliderMin.alt1", { defaultValue: "Optical flow" }), // too technical
|
|
1724
|
+
// min: t("settings.tiltInfluence.sliderMin.alt2", { defaultValue: "Point tracking" }), // still technical but at least it's terminology we're already using
|
|
1725
|
+
min: t("settings.tiltInfluence.sliderMin", { defaultValue: "Point tracking (2D)" }),
|
|
1726
|
+
// max: t("settings.tiltInfluence.sliderMax.alt1", { defaultValue: "Head tilt" }),
|
|
1727
|
+
max: t("settings.tiltInfluence.sliderMax", { defaultValue: "Head tilt (3D)" }),
|
|
1652
1728
|
},
|
|
1653
|
-
// description: t("Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed."),
|
|
1654
|
-
description: t(
|
|
1729
|
+
// description: t("settings.tiltInfluence.description.alt1", { defaultValue: "Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed." }),
|
|
1730
|
+
description: t("settings.tiltInfluence.description", {
|
|
1731
|
+
defaultValue: `Blends between using point tracking (2D) and detected head tilt (3D).
|
|
1655
1732
|
- At 0% it will use only point tracking. This moves the cursor according to visible movement of 2D points on your face within the camera's view, so it responds to both head rotation and translation.
|
|
1656
1733
|
- At 100% it will use only head tilt. This uses an estimate of your face's orientation in 3D space, and ignores head translation. Note that this is smoothed, so it's not as responsive as point tracking. In this mode you never need to recenter by pushing the cursor to the edge of the screen.
|
|
1657
|
-
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it.`),
|
|
1734
|
+
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it.` }),
|
|
1658
1735
|
},
|
|
1659
1736
|
{
|
|
1660
|
-
label: t("Motion threshold"),
|
|
1737
|
+
label: t("settings.motionThreshold.label", { defaultValue: "Motion threshold" }),
|
|
1661
1738
|
className: "tracky-mouse-min-distance",
|
|
1662
1739
|
key: "headTrackingMinDistance",
|
|
1663
1740
|
type: "slider",
|
|
@@ -1665,20 +1742,20 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1665
1742
|
max: 10,
|
|
1666
1743
|
default: 0,
|
|
1667
1744
|
labels: {
|
|
1668
|
-
min: t("Free"),
|
|
1669
|
-
max: t("Steady"),
|
|
1745
|
+
min: t("settings.motionThreshold.sliderMin", { defaultValue: "Free" }),
|
|
1746
|
+
max: t("settings.motionThreshold.sliderMax", { defaultValue: "Steady" }),
|
|
1670
1747
|
},
|
|
1671
|
-
description: t("Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor."),
|
|
1672
|
-
// description: t("Movement less than this distance in pixels will be ignored."),
|
|
1673
|
-
// description: t("Speed in pixels/frame required to move the cursor."),
|
|
1748
|
+
description: t("settings.motionThreshold.description", { defaultValue: "Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor." }),
|
|
1749
|
+
// description: t("settings.motionThreshold.description.alt1", { defaultValue: "Movement less than this distance in pixels will be ignored." }),
|
|
1750
|
+
// description: t("settings.motionThreshold.description.alt2", { defaultValue: "Speed in pixels/frame required to move the cursor." }),
|
|
1674
1751
|
},
|
|
1675
1752
|
{
|
|
1676
1753
|
type: "group",
|
|
1677
|
-
label: t("Point tracking"),
|
|
1754
|
+
label: t("settings.sections.pointTracking.label", { defaultValue: "Point tracking" }),
|
|
1678
1755
|
disabled: () => s.headTrackingTiltInfluence === 1,
|
|
1679
1756
|
settings: [
|
|
1680
1757
|
{
|
|
1681
|
-
label: t("Horizontal sensitivity"),
|
|
1758
|
+
label: t("settings.pointTracking.horizontalSensitivity.label", { defaultValue: "Horizontal sensitivity" }),
|
|
1682
1759
|
className: "tracky-mouse-sensitivity-x",
|
|
1683
1760
|
key: "headTrackingSensitivityX",
|
|
1684
1761
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -1688,13 +1765,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1688
1765
|
max: 100,
|
|
1689
1766
|
default: 25,
|
|
1690
1767
|
labels: {
|
|
1691
|
-
min: t("Slow"),
|
|
1692
|
-
max: t("Fast"),
|
|
1768
|
+
min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
|
|
1769
|
+
max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
|
|
1693
1770
|
},
|
|
1694
|
-
description: t("Speed of cursor movement in response to horizontal head movement."),
|
|
1771
|
+
description: t("settings.pointTracking.horizontalSensitivity.description", { defaultValue: "Speed of cursor movement in response to horizontal head movement." }),
|
|
1695
1772
|
},
|
|
1696
1773
|
{
|
|
1697
|
-
label: t("Vertical sensitivity"),
|
|
1774
|
+
label: t("settings.pointTracking.verticalSensitivity.label", { defaultValue: "Vertical sensitivity" }),
|
|
1698
1775
|
className: "tracky-mouse-sensitivity-y",
|
|
1699
1776
|
key: "headTrackingSensitivityY",
|
|
1700
1777
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -1704,13 +1781,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1704
1781
|
max: 100,
|
|
1705
1782
|
default: 50,
|
|
1706
1783
|
labels: {
|
|
1707
|
-
min: t("Slow"),
|
|
1708
|
-
max: t("Fast"),
|
|
1784
|
+
min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
|
|
1785
|
+
max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
|
|
1709
1786
|
},
|
|
1710
|
-
description: t("Speed of cursor movement in response to vertical head movement."),
|
|
1787
|
+
description: t("settings.pointTracking.verticalSensitivity.description", { defaultValue: "Speed of cursor movement in response to vertical head movement." }),
|
|
1711
1788
|
},
|
|
1712
1789
|
// {
|
|
1713
|
-
// label: t("Smoothing"),
|
|
1790
|
+
// label: t("settings.pointTracking.smoothing.label", { defaultValue: "Smoothing" }),
|
|
1714
1791
|
// className: "tracky-mouse-smoothing",
|
|
1715
1792
|
// key: "headTrackingSmoothing",
|
|
1716
1793
|
// type: "slider",
|
|
@@ -1718,8 +1795,8 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1718
1795
|
// max: 100,
|
|
1719
1796
|
// default: 50,
|
|
1720
1797
|
// labels: {
|
|
1721
|
-
// min: t("Linear"), // or "Direct", "Raw", "None"
|
|
1722
|
-
// max: t("Smooth"), // or "Smoothed"
|
|
1798
|
+
// min: t("settings.shared.sliderMinLinear", { defaultValue: "Linear" }), // or "Direct", "Raw", "None"
|
|
1799
|
+
// max: t("settings.shared.sliderMaxSmooth", { defaultValue: "Smooth" }), // or "Smoothed"
|
|
1723
1800
|
// },
|
|
1724
1801
|
// },
|
|
1725
1802
|
|
|
@@ -1732,7 +1809,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1732
1809
|
// Should it be swapped? What does other software with acceleration control look like?
|
|
1733
1810
|
// In Windows it's just a checkbox apparently, but it could go as far as a custom curve editor.
|
|
1734
1811
|
{
|
|
1735
|
-
label: t("Acceleration"),
|
|
1812
|
+
label: t("settings.pointTracking.acceleration.label", { defaultValue: "Acceleration" }),
|
|
1736
1813
|
className: "tracky-mouse-acceleration",
|
|
1737
1814
|
key: "headTrackingAcceleration",
|
|
1738
1815
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -1742,23 +1819,24 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1742
1819
|
max: 100,
|
|
1743
1820
|
default: 50,
|
|
1744
1821
|
labels: {
|
|
1745
|
-
min: t("Linear"), // or "Direct", "Raw"
|
|
1746
|
-
max: t("Smooth"),
|
|
1822
|
+
min: t("settings.shared.sliderMinLinear", { defaultValue: "Linear" }), // or "Direct", "Raw"
|
|
1823
|
+
max: t("settings.shared.sliderMaxSmooth", { defaultValue: "Smooth" }),
|
|
1747
1824
|
},
|
|
1748
|
-
// description: t("Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly."),
|
|
1749
|
-
// description: t("Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor."),
|
|
1750
|
-
description: t(
|
|
1751
|
-
|
|
1825
|
+
// description: t("settings.pointTracking.acceleration.description.alt1", { defaultValue: "Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly." }),
|
|
1826
|
+
// description: t("settings.pointTracking.acceleration.description.alt2", { defaultValue: "Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor." }),
|
|
1827
|
+
description: t("settings.pointTracking.acceleration.description", {
|
|
1828
|
+
defaultValue: `Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
|
|
1829
|
+
Helps to stabilize the cursor. However, when using point tracking in combination with head tilt, a lower value may work better since head tilt is linear, and you want the point tracking to roughly match the head tracking for it to act as a seamless auto-calibration.` }),
|
|
1752
1830
|
},
|
|
1753
1831
|
],
|
|
1754
1832
|
},
|
|
1755
1833
|
{
|
|
1756
1834
|
type: "group",
|
|
1757
|
-
label: t("Head tilt calibration"),
|
|
1835
|
+
label: t("settings.sections.headTiltCalibration.label", { defaultValue: "Head tilt calibration" }),
|
|
1758
1836
|
disabled: () => s.headTrackingTiltInfluence === 0,
|
|
1759
1837
|
settings: [
|
|
1760
1838
|
{
|
|
1761
|
-
label: t("Horizontal tilt range"),
|
|
1839
|
+
label: t("settings.headTilt.horizontalRange.label", { defaultValue: "Horizontal tilt range" }),
|
|
1762
1840
|
className: "tracky-mouse-head-tilt-yaw-range",
|
|
1763
1841
|
key: "headTiltYawRange",
|
|
1764
1842
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1768,16 +1846,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1768
1846
|
max: 90,
|
|
1769
1847
|
default: 60,
|
|
1770
1848
|
labels: {
|
|
1771
|
-
min: t("Little neck movement"),
|
|
1772
|
-
max: t("Large neck movement"),
|
|
1849
|
+
min: t("settings.headTilt.range.sliderMinLittleNeckMovement", { defaultValue: "Little neck movement" }),
|
|
1850
|
+
max: t("settings.headTilt.range.sliderMaxLargeNeckMovement", { defaultValue: "Large neck movement" }),
|
|
1773
1851
|
},
|
|
1774
|
-
// description: t("Range of horizontal head tilt that moves the cursor from one side of the screen to the other."),
|
|
1775
|
-
// description: t("How much you need to tilt your head left and right to reach the edges of the screen."),
|
|
1776
|
-
// description: t("How much you need to tilt your head left or right to reach the edge of the screen."),
|
|
1777
|
-
description: t("Controls how much you need to tilt your head left or right to reach the edge of the screen."),
|
|
1852
|
+
// description: t("settings.headTilt.horizontalRange.description.alt1", { defaultValue: "Range of horizontal head tilt that moves the cursor from one side of the screen to the other." }),
|
|
1853
|
+
// description: t("settings.headTilt.horizontalRange.description.alt2", { defaultValue: "How much you need to tilt your head left and right to reach the edges of the screen." }),
|
|
1854
|
+
// description: t("settings.headTilt.horizontalRange.description.alt3", { defaultValue: "How much you need to tilt your head left or right to reach the edge of the screen." }),
|
|
1855
|
+
description: t("settings.headTilt.horizontalRange.description", { defaultValue: "Controls how much you need to tilt your head left or right to reach the edge of the screen." }),
|
|
1778
1856
|
},
|
|
1779
1857
|
{
|
|
1780
|
-
label: t("Vertical tilt range"),
|
|
1858
|
+
label: t("settings.headTilt.verticalRange.label", { defaultValue: "Vertical tilt range" }),
|
|
1781
1859
|
className: "tracky-mouse-head-tilt-pitch-range",
|
|
1782
1860
|
key: "headTiltPitchRange",
|
|
1783
1861
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1787,17 +1865,17 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1787
1865
|
max: 60,
|
|
1788
1866
|
default: 25,
|
|
1789
1867
|
labels: {
|
|
1790
|
-
min: t("Little neck movement"),
|
|
1791
|
-
max: t("Large neck movement"),
|
|
1868
|
+
min: t("settings.headTilt.range.sliderMinLittleNeckMovement", { defaultValue: "Little neck movement" }),
|
|
1869
|
+
max: t("settings.headTilt.range.sliderMaxLargeNeckMovement", { defaultValue: "Large neck movement" }),
|
|
1792
1870
|
},
|
|
1793
|
-
// description: t("Range of vertical head tilt required to move the cursor from the top to the bottom of the screen."),
|
|
1794
|
-
// description: t("How much you need to tilt your head up and down to reach the edges of the screen."),
|
|
1795
|
-
// description: t("How much you need to tilt your head up or down to reach the edge of the screen."),
|
|
1796
|
-
description: t("Controls how much you need to tilt your head up or down to reach the edge of the screen."),
|
|
1871
|
+
// description: t("settings.headTilt.verticalRange.description.alt1", { defaultValue: "Range of vertical head tilt required to move the cursor from the top to the bottom of the screen." }),
|
|
1872
|
+
// description: t("settings.headTilt.verticalRange.description.alt2", { defaultValue: "How much you need to tilt your head up and down to reach the edges of the screen." }),
|
|
1873
|
+
// description: t("settings.headTilt.verticalRange.description.alt3", { defaultValue: "How much you need to tilt your head up or down to reach the edge of the screen." }),
|
|
1874
|
+
description: t("settings.headTilt.verticalRange.description", { defaultValue: "Controls how much you need to tilt your head up or down to reach the edge of the screen." }),
|
|
1797
1875
|
},
|
|
1798
1876
|
{
|
|
1799
1877
|
// label: "Horizontal tilt offset",
|
|
1800
|
-
label: t("Horizontal cursor offset"),
|
|
1878
|
+
label: t("settings.headTilt.horizontalOffset.label", { defaultValue: "Horizontal cursor offset" }),
|
|
1801
1879
|
className: "tracky-mouse-head-tilt-yaw-offset",
|
|
1802
1880
|
key: "headTiltYawOffset",
|
|
1803
1881
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1807,8 +1885,8 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1807
1885
|
max: 45,
|
|
1808
1886
|
default: 0,
|
|
1809
1887
|
labels: {
|
|
1810
|
-
min: t("Left"),
|
|
1811
|
-
max: t("Right"),
|
|
1888
|
+
min: t("settings.shared.directionLeft", { defaultValue: "Left" }),
|
|
1889
|
+
max: t("settings.shared.directionRight", { defaultValue: "Right" }),
|
|
1812
1890
|
},
|
|
1813
1891
|
// TODO: how to describe this??
|
|
1814
1892
|
// Specifically, how to disambiguate which direction is which / which way to adjust it?
|
|
@@ -1816,15 +1894,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1816
1894
|
// Since it's opposite, even though it's technically yaw (angle units), it's easier to think of as moving the cursor.
|
|
1817
1895
|
// Hence I've renamed the setting.
|
|
1818
1896
|
// A later update might change the definitions and include a settings file format upgrade step.
|
|
1819
|
-
// description: t("Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible."),
|
|
1820
|
-
// description: t("Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible."),
|
|
1897
|
+
// description: t("settings.headTilt.horizontalOffset.description.alt1", { defaultValue: "Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible." }),
|
|
1898
|
+
// description: t("settings.headTilt.horizontalOffset.description.alt2", { defaultValue: "Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible." }),
|
|
1821
1899
|
// TODO: should this say "horizontal" in the (main part of the) description?
|
|
1822
|
-
description: t(
|
|
1823
|
-
|
|
1900
|
+
description: t("settings.headTilt.horizontalOffset.description", {
|
|
1901
|
+
defaultValue: `Adjusts the position of the cursor when the camera sees the head facing straight ahead.
|
|
1902
|
+
⚠️ This horizontal offset is not recommended. Move the camera instead if possible. 📷` }),
|
|
1824
1903
|
},
|
|
1825
1904
|
{
|
|
1826
1905
|
// label: "Vertical tilt offset",
|
|
1827
|
-
label: t("Vertical cursor offset"),
|
|
1906
|
+
label: t("settings.headTilt.verticalOffset.label", { defaultValue: "Vertical cursor offset" }),
|
|
1828
1907
|
className: "tracky-mouse-head-tilt-pitch-offset",
|
|
1829
1908
|
key: "headTiltPitchOffset",
|
|
1830
1909
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1834,11 +1913,11 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1834
1913
|
max: 30,
|
|
1835
1914
|
default: 2.5,
|
|
1836
1915
|
labels: {
|
|
1837
|
-
min: t("Down"),
|
|
1838
|
-
max: t("Up"),
|
|
1916
|
+
min: t("settings.shared.directionDown", { defaultValue: "Down" }),
|
|
1917
|
+
max: t("settings.shared.directionUp", { defaultValue: "Up" }),
|
|
1839
1918
|
},
|
|
1840
|
-
// description: t("Adjusts the center position of vertical head tilt."),
|
|
1841
|
-
description: t("Adjusts the position of the cursor when the camera sees the head facing straight ahead."),
|
|
1919
|
+
// description: t("settings.headTilt.verticalOffset.description.alt1", { defaultValue: "Adjusts the center position of vertical head tilt." }),
|
|
1920
|
+
description: t("settings.headTilt.verticalOffset.description", { defaultValue: "Adjusts the position of the cursor when the camera sees the head facing straight ahead." }),
|
|
1842
1921
|
},
|
|
1843
1922
|
],
|
|
1844
1923
|
},
|
|
@@ -1858,42 +1937,43 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1858
1937
|
// which awkwardly affects what mouse button serenade-driver sends; this doesn't affect the web version.
|
|
1859
1938
|
{
|
|
1860
1939
|
type: "group",
|
|
1861
|
-
label: t("Clicking"),
|
|
1940
|
+
label: t("settings.sections.clicking.label", { defaultValue: "Clicking" }),
|
|
1862
1941
|
settings: [
|
|
1863
1942
|
{
|
|
1864
|
-
label: t("Clicking mode:"), // TODO: ":"?
|
|
1943
|
+
label: t("settings.clickingMode.label", { defaultValue: "Clicking mode:" }), // TODO: ":"?
|
|
1865
1944
|
className: "tracky-mouse-clicking-mode",
|
|
1866
1945
|
key: "clickingMode",
|
|
1867
1946
|
type: "dropdown",
|
|
1868
1947
|
options: [
|
|
1869
|
-
{ value: "dwell", label: t("Dwell to click"), description: t("Hold the cursor in place for a short time to click.") },
|
|
1870
|
-
{ value: "blink", label: t("Wink to click"), description: t("Close one eye to click. Left eye for left click, right eye for right click.") },
|
|
1948
|
+
{ value: "dwell", label: t("settings.clickingMode.dwell.label", { defaultValue: "Dwell to click" }), description: t("settings.clickingMode.dwell.description", { defaultValue: "Hold the cursor in place for a short time to click." }) },
|
|
1949
|
+
{ value: "blink", label: t("settings.clickingMode.wink.label", { defaultValue: "Wink to click" }), description: t("settings.clickingMode.wink.description", { defaultValue: "Close one eye to click. Left eye for left click, right eye for right click." }) },
|
|
1871
1950
|
// TODO: clarify that ooh works better than ah
|
|
1872
1951
|
// "open wide" refers to height, but could be misinterpreted as opposite advice - a wide mouth shape when narrow works better
|
|
1873
1952
|
// "open wide" is also perhaps unnecessary considering detection is improved... but who knows. maybe someone will try opening their mouth only slightly and expect it to work
|
|
1874
1953
|
// Some people may understand "tall and narrow" better than "ooh rather than ah" and visa-versa
|
|
1875
|
-
{ value: "open-mouth-simple", label: t("Open mouth to click (simple)"), description: t("Open your mouth wide to click. At least one eye must be open to click.") },
|
|
1876
|
-
{ value: "open-mouth-ignoring-eyes", label: t("Open mouth to click (ignoring eyes)"), description: t("Open your mouth wide to click. Eye state is ignored.") },
|
|
1877
|
-
{ value: "open-mouth", label: t("Open mouth to click (with eye modifiers)"), description: t("Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click.") },
|
|
1878
|
-
{ value: "off", label: t("Off"), description: t("Disable clicking. Use with an external switch or programs that provide their own dwell clicking.") },
|
|
1954
|
+
{ value: "open-mouth-simple", label: t("settings.clickingMode.openMouthSimple.label", { defaultValue: "Open mouth to click (simple)" }), description: t("settings.clickingMode.openMouthSimple.description", { defaultValue: "Open your mouth wide to click. At least one eye must be open to click." }) },
|
|
1955
|
+
{ value: "open-mouth-ignoring-eyes", label: t("settings.clickingMode.openMouthIgnoringEyes.label", { defaultValue: "Open mouth to click (ignoring eyes)" }), description: t("settings.clickingMode.openMouthIgnoringEyes.description", { defaultValue: "Open your mouth wide to click. Eye state is ignored." }) },
|
|
1956
|
+
{ value: "open-mouth", label: t("settings.clickingMode.openMouthWithEyeModifiers.label", { defaultValue: "Open mouth to click (with eye modifiers)" }), description: t("settings.clickingMode.openMouthWithEyeModifiers.description", { defaultValue: "Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click." }) },
|
|
1957
|
+
{ value: "off", label: t("settings.clickingMode.off.label", { defaultValue: "Off" }), description: t("settings.clickingMode.off.description", { defaultValue: "Disable clicking. Use with an external switch or programs that provide their own dwell clicking." }) },
|
|
1879
1958
|
],
|
|
1880
1959
|
default: "dwell",
|
|
1881
|
-
visible: () => isDesktopApp,
|
|
1882
|
-
description: t("Choose how to perform mouse clicks."),
|
|
1960
|
+
visible: () => isDesktopApp || clickingModeSupported,
|
|
1961
|
+
description: t("settings.clickingMode.description", { defaultValue: "Choose how to perform mouse clicks." }),
|
|
1883
1962
|
},
|
|
1884
1963
|
{
|
|
1885
1964
|
// on Windows, currently, when buttons are swapped at the system level, it affects serenade-driver's click()
|
|
1886
1965
|
// "swap" is purposefully generic language so we don't have to know what system-level setting is
|
|
1887
1966
|
// (also this may be seen as a weirdly named/designed option for right-clicking with the dwell clicker)
|
|
1888
|
-
label: t("Swap mouse buttons"),
|
|
1967
|
+
label: t("settings.swapMouseButtons.label", { defaultValue: "Swap mouse buttons" }),
|
|
1889
1968
|
className: "tracky-mouse-swap-mouse-buttons",
|
|
1890
1969
|
key: "swapMouseButtons",
|
|
1891
1970
|
type: "checkbox",
|
|
1892
1971
|
default: false,
|
|
1893
1972
|
visible: () => isDesktopApp,
|
|
1894
|
-
description: t(
|
|
1973
|
+
description: t("settings.swapMouseButtons.description", {
|
|
1974
|
+
defaultValue: `Switches the left and right mouse buttons.
|
|
1895
1975
|
Useful if your system's mouse buttons are swapped.
|
|
1896
|
-
Could also be used to right click with the dwell clicker in a pinch.`),
|
|
1976
|
+
Could also be used to right click with the dwell clicker in a pinch.` }),
|
|
1897
1977
|
},
|
|
1898
1978
|
|
|
1899
1979
|
// This setting could called "click stabilization", "drag delay", "delay before dragging", "click drag delay", "drag prevention", etc.
|
|
@@ -1902,33 +1982,37 @@ Could also be used to right click with the dwell clicker in a pinch.`),
|
|
|
1902
1982
|
// at the end of the slider, although you shouldn't need to do that to effectively avoid dragging when trying to click,
|
|
1903
1983
|
// and it might complicate the design of the slider labeling.
|
|
1904
1984
|
{
|
|
1905
|
-
label: t("
|
|
1985
|
+
label: t("settings.delayBeforeDragging.label", { defaultValue: "Delay before dragging" }),
|
|
1906
1986
|
className: "tracky-mouse-delay-before-dragging",
|
|
1907
1987
|
key: "delayBeforeDragging",
|
|
1908
1988
|
type: "slider",
|
|
1909
1989
|
min: 0,
|
|
1910
1990
|
max: 1000,
|
|
1911
1991
|
labels: {
|
|
1912
|
-
min: t("Easy to drag"),
|
|
1913
|
-
max: t("Easy to click"),
|
|
1992
|
+
min: t("settings.delayBeforeDragging.sliderMin", { defaultValue: "Easy to drag" }),
|
|
1993
|
+
max: t("settings.delayBeforeDragging.sliderMax", { defaultValue: "Easy to click" }),
|
|
1914
1994
|
},
|
|
1915
1995
|
default: 800,
|
|
1916
|
-
visible: () => isDesktopApp,
|
|
1996
|
+
visible: () => isDesktopApp || clickingModeSupported,
|
|
1917
1997
|
disabled: () => s.clickingMode === "off" || s.clickingMode === "dwell",
|
|
1918
|
-
// description: t("Locks mouse movement during the start of a click to prevent accidental dragging."),
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1998
|
+
// description: t("settings.delayBeforeDragging.description.alt1", { defaultValue: "Locks mouse movement during the start of a click to prevent accidental dragging." }),
|
|
1999
|
+
// Throwing a // in here so it's not detected by i18next-cli, whereas the others are allowed
|
|
2000
|
+
// simply because it wasn't previously detected and translated
|
|
2001
|
+
// due to being both commented out and multiline (though multiline and commented out t() calls are separately supported)
|
|
2002
|
+
// description: t//("settings.delayBeforeDragging.description.alt2", { defaultValue: `Prevents mouse movement for the specified time after a click starts.
|
|
2003
|
+
// You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.` }),
|
|
2004
|
+
description: t("settings.delayBeforeDragging.description", {
|
|
2005
|
+
defaultValue: `Locks mouse movement for the given duration during the start of a click.
|
|
2006
|
+
You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.` }),
|
|
1923
2007
|
},
|
|
1924
2008
|
],
|
|
1925
2009
|
},
|
|
1926
2010
|
{
|
|
1927
2011
|
type: "group",
|
|
1928
|
-
label: t("Video"),
|
|
2012
|
+
label: t("settings.sections.video.label", { defaultValue: "Video" }),
|
|
1929
2013
|
settings: [
|
|
1930
2014
|
{
|
|
1931
|
-
label: t("Camera source"),
|
|
2015
|
+
label: t("settings.cameraSource.label", { defaultValue: "Camera source" }),
|
|
1932
2016
|
className: "tracky-mouse-camera-select",
|
|
1933
2017
|
key: "cameraDeviceId",
|
|
1934
2018
|
handleSettingChange: () => {
|
|
@@ -1936,15 +2020,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1936
2020
|
},
|
|
1937
2021
|
type: "dropdown",
|
|
1938
2022
|
options: [
|
|
1939
|
-
{ value: "", label: t("Default") },
|
|
2023
|
+
{ value: "", label: t("settings.cameraSource.defaultCamera", { defaultValue: "Default" }) },
|
|
1940
2024
|
],
|
|
1941
2025
|
default: "",
|
|
1942
|
-
// description: t("Select which camera to use for head tracking."),
|
|
1943
|
-
description: t("Selects which camera is used for head tracking."),
|
|
2026
|
+
// description: t("settings.cameraSource.description.alt1", { defaultValue: "Select which camera to use for head tracking." }),
|
|
2027
|
+
description: t("settings.cameraSource.description", { defaultValue: "Selects which camera is used for head tracking." }),
|
|
1944
2028
|
},
|
|
1945
2029
|
// TODO: move this inline with the camera source dropdown?
|
|
1946
2030
|
{
|
|
1947
|
-
label: t("Open Camera Settings"),
|
|
2031
|
+
label: t("settings.openCameraSettings.label", { defaultValue: "Open Camera Settings" }),
|
|
1948
2032
|
className: "tracky-mouse-open-camera-settings",
|
|
1949
2033
|
key: "openCameraSettings",
|
|
1950
2034
|
type: "button",
|
|
@@ -1954,45 +2038,59 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1954
2038
|
try {
|
|
1955
2039
|
knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
|
|
1956
2040
|
} catch (error) {
|
|
1957
|
-
alert(t("Failed to open camera settings
|
|
2041
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + t("openCameraSettings.errors.parseKnownCameras", { defaultValue: "Failed to parse known cameras from localStorage:" }) + "\n" + error.message);
|
|
1958
2042
|
return;
|
|
1959
2043
|
}
|
|
1960
2044
|
|
|
1961
2045
|
const activeStream = cameraVideo.srcObject;
|
|
1962
2046
|
const activeDeviceId = activeStream?.getVideoTracks()[0]?.getSettings()?.deviceId;
|
|
1963
|
-
const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("Default");
|
|
2047
|
+
const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
|
|
1964
2048
|
|
|
1965
2049
|
try {
|
|
1966
2050
|
const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
|
|
1967
2051
|
if (result?.error) {
|
|
1968
|
-
alert(t("Failed to open camera settings
|
|
2052
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + result.error);
|
|
1969
2053
|
}
|
|
1970
2054
|
} catch (error) {
|
|
1971
|
-
alert(t("Failed to open camera settings
|
|
2055
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + error.message);
|
|
1972
2056
|
}
|
|
1973
2057
|
},
|
|
1974
|
-
// description: t("Open your camera's system settings window to adjust properties like brightness and contrast."),
|
|
1975
|
-
// description: t("Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure."),
|
|
1976
|
-
description: t("Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure."),
|
|
2058
|
+
// description: t("settings.openCameraSettings.description.alt1", { defaultValue: "Open your camera's system settings window to adjust properties like brightness and contrast." }),
|
|
2059
|
+
// description: t("settings.openCameraSettings.description.alt2", { defaultValue: "Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure." }),
|
|
2060
|
+
description: t("settings.openCameraSettings.description", { defaultValue: "Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure." }),
|
|
1977
2061
|
},
|
|
1978
2062
|
// TODO: try moving this to the corner of the camera view, so it's clearer it applies only to the camera view
|
|
1979
2063
|
{
|
|
1980
|
-
label: t("Mirror"),
|
|
2064
|
+
label: t("settings.mirror.label", { defaultValue: "Mirror" }),
|
|
1981
2065
|
className: "tracky-mouse-mirror",
|
|
1982
2066
|
key: "mirror",
|
|
1983
2067
|
type: "checkbox",
|
|
1984
2068
|
default: true,
|
|
1985
|
-
description: t("Mirrors the camera view horizontally."),
|
|
2069
|
+
description: t("settings.mirror.description", { defaultValue: "Mirrors the camera view horizontally." }),
|
|
1986
2070
|
},
|
|
1987
2071
|
]
|
|
1988
2072
|
},
|
|
1989
2073
|
{
|
|
1990
2074
|
type: "group",
|
|
1991
|
-
label: t("General"),
|
|
2075
|
+
label: t("settings.sections.general.label", { defaultValue: "General" }),
|
|
1992
2076
|
settings: [
|
|
2077
|
+
{
|
|
2078
|
+
label: t("settings.soundEffects.label", { defaultValue: "Sound effects" }),
|
|
2079
|
+
className: "tracky-mouse-sound-effects",
|
|
2080
|
+
key: "soundEffects",
|
|
2081
|
+
type: "checkbox",
|
|
2082
|
+
default: true,
|
|
2083
|
+
afterInitialLoad: () => {
|
|
2084
|
+
setAudioEnabled(s.soundEffects);
|
|
2085
|
+
},
|
|
2086
|
+
handleSettingChange: () => {
|
|
2087
|
+
setAudioEnabled(s.soundEffects);
|
|
2088
|
+
},
|
|
2089
|
+
description: t("settings.soundEffects.description", { defaultValue: "Plays sounds when you click." }),
|
|
2090
|
+
},
|
|
1993
2091
|
// opposite, "Start paused", might be clearer, especially if I add a "pause" button
|
|
1994
2092
|
{
|
|
1995
|
-
label: t("Start enabled"),
|
|
2093
|
+
label: t("settings.startEnabled.label", { defaultValue: "Start enabled" }),
|
|
1996
2094
|
className: "tracky-mouse-start-enabled",
|
|
1997
2095
|
key: "startEnabled",
|
|
1998
2096
|
afterInitialLoad: () => { // TODO: does this hook make sense? right now it's the only usage. could this code not just be called later?
|
|
@@ -2000,10 +2098,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2000
2098
|
},
|
|
2001
2099
|
type: "checkbox",
|
|
2002
2100
|
default: false,
|
|
2003
|
-
description: t("If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched."),
|
|
2004
|
-
// description: t("Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready."),
|
|
2005
|
-
// description: t("Makes Tracky Mouse active as soon as it's launched."),
|
|
2006
|
-
// description: t("Automatically starts Tracky Mouse as soon as it's run."),
|
|
2101
|
+
description: t("settings.startEnabled.description", { defaultValue: "If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched." }),
|
|
2102
|
+
// description: t("settings.startEnabled.description.alt1", { defaultValue: "Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready." }),
|
|
2103
|
+
// description: t("settings.startEnabled.description.alt2", { defaultValue: "Makes Tracky Mouse active as soon as it's launched." }),
|
|
2104
|
+
// description: t("settings.startEnabled.description.alt3", { defaultValue: "Automatically starts Tracky Mouse as soon as it's run." }),
|
|
2007
2105
|
},
|
|
2008
2106
|
{
|
|
2009
2107
|
// For "experimental" label:
|
|
@@ -2011,36 +2109,37 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2011
2109
|
// - I considered adding "⚠︎" but it feels a little too alarming
|
|
2012
2110
|
// label: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"Planned refinements include: visual and auditory feedback, improved detection accuracy, and separate settings for durations to toggle on and off.\">experimental</span>)",
|
|
2013
2111
|
// label: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• Missing visual and auditory feedback.\n• Missing settings for duration(s) to toggle on and off.\n• Affected by false positive blink detections, especially when looking downward.\">Experimental</span>)",
|
|
2014
|
-
label: t("Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• There is currently no visual or auditory feedback.\n• There are no settings for duration(s) to toggle on and off.\n• It is affected by false positive blink detections, especially when looking downward.\">Experimental</span>)"),
|
|
2112
|
+
// label: t("settings.closeEyesToToggle.label", { defaultValue: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• There is currently no visual or auditory feedback.\n• There are no settings for duration(s) to toggle on and off.\n• It is affected by false positive blink detections, especially when looking downward.\">Experimental</span>)" }),
|
|
2113
|
+
label: t("settings.closeEyesToToggle.label", { defaultValue: "Close eyes to start/stop" }),
|
|
2015
2114
|
className: "tracky-mouse-close-eyes-to-toggle",
|
|
2016
2115
|
key: "closeEyesToToggle",
|
|
2017
2116
|
type: "checkbox",
|
|
2018
2117
|
default: false,
|
|
2019
|
-
description: t("If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds."),
|
|
2118
|
+
description: t("settings.closeEyesToToggle.description", { defaultValue: "If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds." }),
|
|
2020
2119
|
},
|
|
2021
2120
|
{
|
|
2022
|
-
label: t("Run at login"),
|
|
2121
|
+
label: t("settings.runAtLogin.label", { defaultValue: "Run at login" }),
|
|
2023
2122
|
className: "tracky-mouse-run-at-login",
|
|
2024
2123
|
key: "runAtLogin",
|
|
2025
2124
|
type: "checkbox",
|
|
2026
2125
|
default: false,
|
|
2027
2126
|
visible: () => isDesktopApp,
|
|
2028
|
-
description: t("If enabled, Tracky Mouse will automatically start when you log into your computer."),
|
|
2029
|
-
// description: t("Makes Tracky Mouse start automatically when you log into your computer."),
|
|
2127
|
+
description: t("settings.runAtLogin.description", { defaultValue: "If enabled, Tracky Mouse will automatically start when you log into your computer." }),
|
|
2128
|
+
// description: t("settings.runAtLogin.description.alt1", { defaultValue: "Makes Tracky Mouse start automatically when you log into your computer." }),
|
|
2030
2129
|
},
|
|
2031
2130
|
{
|
|
2032
|
-
label: t("Check for updates"),
|
|
2131
|
+
label: t("settings.checkForUpdates.label", { defaultValue: "Check for updates" }),
|
|
2033
2132
|
className: "tracky-mouse-check-for-updates",
|
|
2034
2133
|
key: "checkForUpdates",
|
|
2035
2134
|
type: "checkbox",
|
|
2036
2135
|
default: true,
|
|
2037
2136
|
visible: () => isDesktopApp,
|
|
2038
|
-
description: t("If enabled, Tracky Mouse will automatically check for updates when it starts."),
|
|
2039
|
-
// description: t("Notifies you of new versions of Tracky Mouse."),
|
|
2040
|
-
// description: t("Notifies you when a new version of Tracky Mouse is available."),
|
|
2137
|
+
description: t("settings.checkForUpdates.description", { defaultValue: "If enabled, Tracky Mouse will automatically check for updates when it starts." }),
|
|
2138
|
+
// description: t("settings.checkForUpdates.description.alt1", { defaultValue: "Notifies you of new versions of Tracky Mouse." }),
|
|
2139
|
+
// description: t("settings.checkForUpdates.description.alt2", { defaultValue: "Notifies you when a new version of Tracky Mouse is available." }),
|
|
2041
2140
|
},
|
|
2042
2141
|
{
|
|
2043
|
-
label: t("Language"),
|
|
2142
|
+
label: t("settings.language.label", { defaultValue: "Language" }),
|
|
2044
2143
|
className: "tracky-mouse-language",
|
|
2045
2144
|
key: "language",
|
|
2046
2145
|
type: "dropdown",
|
|
@@ -2058,8 +2157,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2058
2157
|
}
|
|
2059
2158
|
reinit();
|
|
2060
2159
|
},
|
|
2061
|
-
description: t("Select the language for the Tracky Mouse interface."),
|
|
2062
|
-
// description: t("Changes the language Tracky Mouse is displayed in."),
|
|
2160
|
+
description: t("settings.language.description", { defaultValue: "Select the language for the Tracky Mouse interface." }),
|
|
2161
|
+
// description: t("settings.language.description.alt1", { defaultValue: "Changes the language Tracky Mouse is displayed in." }),
|
|
2063
2162
|
},
|
|
2064
2163
|
],
|
|
2065
2164
|
},
|
|
@@ -2181,7 +2280,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2181
2280
|
</select>
|
|
2182
2281
|
`;
|
|
2183
2282
|
if (setting.options.some(option => option.description)) {
|
|
2184
|
-
setting.description +=
|
|
2283
|
+
setting.description += "\n\n" + t("settings.dropdownDescriptionOptionsListHeading", { defaultValue: "Options:" }) + "\n" + setting.options.map(option => `• ${option.label}${option.description ? `: ${option.description}` : ''}`).join("\n");
|
|
2185
2284
|
}
|
|
2186
2285
|
} else if (setting.type === "button") {
|
|
2187
2286
|
rowEl.innerHTML = `
|
|
@@ -2284,26 +2383,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2284
2383
|
});
|
|
2285
2384
|
}
|
|
2286
2385
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2386
|
+
let canvas = uiContainer.querySelector(".tracky-mouse-canvas");
|
|
2387
|
+
let ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
2289
2388
|
|
|
2290
|
-
|
|
2389
|
+
let debugEyeCanvas = document.createElement("canvas");
|
|
2291
2390
|
debugEyeCanvas.className = "tracky-mouse-debug-eye-canvas";
|
|
2292
2391
|
debugEyeCanvas.style.display = "none";
|
|
2293
2392
|
uiContainer.querySelector(".tracky-mouse-canvas-container-container").appendChild(debugEyeCanvas);
|
|
2294
|
-
|
|
2393
|
+
let debugEyeCtx = debugEyeCanvas.getContext('2d');
|
|
2295
2394
|
|
|
2296
|
-
|
|
2395
|
+
let pointerEl = document.createElement('div');
|
|
2297
2396
|
pointerEl.className = "tracky-mouse-pointer";
|
|
2298
2397
|
pointerEl.style.display = "none";
|
|
2299
2398
|
document.body.appendChild(pointerEl);
|
|
2300
2399
|
|
|
2301
|
-
|
|
2400
|
+
let cameraVideo = document.createElement('video');
|
|
2302
2401
|
// required to work in iOS 11 & up:
|
|
2303
2402
|
cameraVideo.setAttribute('playsinline', '');
|
|
2304
2403
|
|
|
2404
|
+
let stats;
|
|
2305
2405
|
if (statsJs) {
|
|
2306
|
-
|
|
2406
|
+
stats = new Stats();
|
|
2307
2407
|
stats.domElement.style.position = 'fixed';
|
|
2308
2408
|
stats.domElement.style.top = '0px';
|
|
2309
2409
|
stats.domElement.style.right = '0px';
|
|
@@ -2312,72 +2412,74 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2312
2412
|
}
|
|
2313
2413
|
|
|
2314
2414
|
// Debug flags (not shown in the UI; could become Advanced Settings in the future)
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2415
|
+
let debugAcceleration = false;
|
|
2416
|
+
let showDebugText = false;
|
|
2417
|
+
let showDebugEyeZoom = false;
|
|
2418
|
+
let showDebugHeadTilt = false;
|
|
2419
|
+
let showDebugRegionFilter = false;
|
|
2319
2420
|
|
|
2320
2421
|
// Constants (could become Advanced Settings in the future)
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2422
|
+
let defaultWidth = 640;
|
|
2423
|
+
let defaultHeight = 480;
|
|
2424
|
+
let maxPoints = 1000;
|
|
2425
|
+
let faceScoreThreshold = 0.5;
|
|
2426
|
+
let facemeshOptions = {
|
|
2326
2427
|
maxContinuousChecks: 5,
|
|
2327
2428
|
detectionConfidence: 0.9,
|
|
2328
2429
|
maxFaces: 1,
|
|
2329
2430
|
iouThreshold: 0.3,
|
|
2330
2431
|
scoreThreshold: 0.75
|
|
2331
2432
|
};
|
|
2332
|
-
|
|
2433
|
+
let useFacemesh = true;
|
|
2434
|
+
let sleepGestureEyesClosedDuration = 2000;
|
|
2333
2435
|
// maybe should be based on size of head in view?
|
|
2334
2436
|
const pruningGridSize = 5;
|
|
2335
2437
|
const minDistanceToAddPoint = pruningGridSize * 1.5;
|
|
2336
2438
|
|
|
2337
2439
|
// Head tracking and facial gesture state
|
|
2338
2440
|
// ## Clmtrackr state
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
//
|
|
2343
|
-
|
|
2441
|
+
let face;
|
|
2442
|
+
let faceScore = 0;
|
|
2443
|
+
let faceConvergence = 0;
|
|
2444
|
+
// let faceConvergenceThreshold = 50;
|
|
2445
|
+
let pointsBasedOnFaceScore = 0;
|
|
2344
2446
|
// ## Facemesh state
|
|
2345
2447
|
let detector;
|
|
2346
2448
|
let currentCameraImageData;
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2449
|
+
let facemeshLoaded = false;
|
|
2450
|
+
let facemeshFirstEstimation = true;
|
|
2451
|
+
let facemeshEstimating = false;
|
|
2452
|
+
let facemeshRejectNext = 0;
|
|
2453
|
+
let facemeshPrediction;
|
|
2454
|
+
let facemeshEstimateFaces;
|
|
2455
|
+
let faceInViewConfidenceThreshold = 0.7;
|
|
2456
|
+
let pointsBasedOnFaceInViewConfidence = 0;
|
|
2457
|
+
let cameraFramesSinceFacemeshUpdate = [];
|
|
2458
|
+
let blinkInfo;
|
|
2459
|
+
let mouthInfo;
|
|
2460
|
+
let headTilt = { pitch: 0, yaw: 0, roll: 0 };
|
|
2461
|
+
let headTiltFilters = { pitch: null, yaw: null, roll: null };
|
|
2462
|
+
let sleepGestureProgress = 0;
|
|
2361
2463
|
// ## State related to switching between head trackers
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2464
|
+
let useClmTracking = true;
|
|
2465
|
+
let showClmTracking = useClmTracking;
|
|
2466
|
+
let fallbackTimeoutID;
|
|
2365
2467
|
|
|
2366
2468
|
// Mouse state
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2469
|
+
let mouseX = 0;
|
|
2470
|
+
let mouseY = 0;
|
|
2471
|
+
let buttonStates = {
|
|
2370
2472
|
left: false,
|
|
2371
2473
|
right: false,
|
|
2372
2474
|
middle: false,
|
|
2373
2475
|
};
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2476
|
+
let mouseButtonUntilMouthCloses = -1;
|
|
2477
|
+
let lastMouseDownTime = -Infinity;
|
|
2478
|
+
let mouseNeedsInitPos = true;
|
|
2377
2479
|
|
|
2378
2480
|
// Other state
|
|
2379
|
-
|
|
2380
|
-
|
|
2481
|
+
let paused = true;
|
|
2482
|
+
let pointTracker;
|
|
2381
2483
|
|
|
2382
2484
|
// Named lists of facemesh landmark indices
|
|
2383
2485
|
const MESH_ANNOTATIONS = {
|
|
@@ -2489,12 +2591,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2489
2591
|
setting._load?.(settings, initialLoad);
|
|
2490
2592
|
});
|
|
2491
2593
|
}
|
|
2594
|
+
setAudioEnabled(s.soundEffects);
|
|
2492
2595
|
|
|
2493
2596
|
// Now that all settings are loaded, update disabled states
|
|
2494
2597
|
for (const func of functionsToUpdateDisabledStates) {
|
|
2495
2598
|
func();
|
|
2496
2599
|
}
|
|
2497
2600
|
|
|
2601
|
+
// Unstable hook
|
|
2602
|
+
handleSettingsUpdate?.(settings);
|
|
2498
2603
|
}
|
|
2499
2604
|
const formatVersion = 1;
|
|
2500
2605
|
const formatName = "tracky-mouse-settings";
|
|
@@ -2523,25 +2628,49 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2523
2628
|
console.error(e);
|
|
2524
2629
|
}
|
|
2525
2630
|
}
|
|
2631
|
+
// Unstable hook
|
|
2632
|
+
handleSettingsUpdate?.(options);
|
|
2526
2633
|
};
|
|
2527
2634
|
const loadOptions = async (initialLoad = false) => {
|
|
2635
|
+
// Desktop app: start from any saved settings in the main process,
|
|
2636
|
+
// then, on first load, push the renderer's canonical defaults back
|
|
2637
|
+
// so the main process has the same effective settings (and can
|
|
2638
|
+
// correctly drive features like dwell clicking on first run).
|
|
2639
|
+
// Web demo: similarly needs canonical defaults pushed to
|
|
2640
|
+
// correctly enable dwell clicking on first run,
|
|
2641
|
+
// now that it supports multiple clicking modes.
|
|
2642
|
+
// General API usage: does not yet support multiple clicking modes
|
|
2643
|
+
// (there's a lot of glue code in the demo)
|
|
2644
|
+
// but we only call handleSettingsUpdate if it exists.
|
|
2645
|
+
let stored;
|
|
2528
2646
|
if (window.electronAPI) {
|
|
2529
|
-
|
|
2530
|
-
// then, on first load, push the renderer's canonical defaults back
|
|
2531
|
-
// so the main process has the same effective settings (and can
|
|
2532
|
-
// correctly drive features like dwell clicking on first run).
|
|
2533
|
-
const stored = await window.electronAPI.getOptions();
|
|
2534
|
-
deserializeSettings(stored, initialLoad);
|
|
2535
|
-
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2536
|
-
setOptions(serializeSettings());
|
|
2537
|
-
}
|
|
2647
|
+
stored = await window.electronAPI.getOptions();
|
|
2538
2648
|
} else {
|
|
2539
2649
|
try {
|
|
2540
2650
|
if (localStorage.getItem("tracky-mouse-settings")) {
|
|
2541
|
-
|
|
2651
|
+
stored = JSON.parse(localStorage.getItem("tracky-mouse-settings"));
|
|
2542
2652
|
}
|
|
2543
2653
|
} catch (e) {
|
|
2544
2654
|
console.error(e);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (stored) {
|
|
2659
|
+
deserializeSettings(stored, initialLoad);
|
|
2660
|
+
} else {
|
|
2661
|
+
// HACK: ensure handleInitialLoad is called even for first run
|
|
2662
|
+
// Combined with the below, this feels very redundant, and I'd like to
|
|
2663
|
+
// move to a subscription-based pattern, more of a formal "settings store", something like that.
|
|
2664
|
+
// This is currently necessary for sound effects to work on the first run of the web demo.
|
|
2665
|
+
deserializeSettings(serializeSettings(), initialLoad);
|
|
2666
|
+
}
|
|
2667
|
+
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2668
|
+
// We could just call setOptions in both cases,
|
|
2669
|
+
// but do we want to save to localStorage initially? Maybe not.
|
|
2670
|
+
if (window.electronAPI) {
|
|
2671
|
+
setOptions(serializeSettings()); // (includes handleSettingsUpdate)
|
|
2672
|
+
} else {
|
|
2673
|
+
handleSettingsUpdate?.(serializeSettings());
|
|
2545
2674
|
}
|
|
2546
2675
|
}
|
|
2547
2676
|
};
|
|
@@ -2592,14 +2721,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2592
2721
|
|
|
2593
2722
|
const defaultOption = document.createElement("option");
|
|
2594
2723
|
defaultOption.value = "";
|
|
2595
|
-
defaultOption.text = t("Default");
|
|
2724
|
+
defaultOption.text = t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
|
|
2596
2725
|
cameraSelect.appendChild(defaultOption);
|
|
2597
2726
|
|
|
2598
2727
|
let matchingDeviceId = "";
|
|
2599
2728
|
for (const device of videoDevices) {
|
|
2600
2729
|
const option = document.createElement('option');
|
|
2601
2730
|
option.value = device.deviceId;
|
|
2602
|
-
option.text = device.label || t("Camera %0").replace("%0", cameraSelect.length);
|
|
2731
|
+
option.text = device.label || t("settings.cameraSource.numberedCamera", { defaultValue: "Camera %0" }).replace("%0", cameraSelect.length);
|
|
2603
2732
|
cameraSelect.appendChild(option);
|
|
2604
2733
|
if (device.deviceId === s.cameraDeviceId) {
|
|
2605
2734
|
matchingDeviceId = device.deviceId;
|
|
@@ -2617,7 +2746,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2617
2746
|
const option = document.createElement("option");
|
|
2618
2747
|
option.value = s.cameraDeviceId;
|
|
2619
2748
|
const knownInfo = knownCameras[s.cameraDeviceId];
|
|
2620
|
-
option.text = knownInfo ? `${knownInfo.name} (${t("Unavailable")})` : t("Unavailable camera");
|
|
2749
|
+
option.text = knownInfo ? `${knownInfo.name} (${t("settings.cameraSource.unavailableCameraAdjective", { defaultValue: "Unavailable" })})` : t("settings.cameraSource.unavailableCamera", { defaultValue: "Unavailable camera" });
|
|
2621
2750
|
cameraSelect.appendChild(option);
|
|
2622
2751
|
cameraSelect.value = s.cameraDeviceId;
|
|
2623
2752
|
} else {
|
|
@@ -2647,9 +2776,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2647
2776
|
const settingsLoadedPromise = loadOptions(true);
|
|
2648
2777
|
|
|
2649
2778
|
// Don't use WebGL because clmTracker is our fallback! It's also not much slower than with WebGL.
|
|
2650
|
-
|
|
2779
|
+
let clmTracker = new clm.tracker({ useWebGL: false });
|
|
2651
2780
|
clmTracker.init();
|
|
2652
|
-
|
|
2781
|
+
let clmTrackingStarted = false;
|
|
2653
2782
|
|
|
2654
2783
|
const stopCameraStream = () => {
|
|
2655
2784
|
if (cameraVideo.srcObject) {
|
|
@@ -2675,10 +2804,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2675
2804
|
pointsBasedOnFaceScore = 0;
|
|
2676
2805
|
faceScore = 0;
|
|
2677
2806
|
faceConvergence = 0;
|
|
2678
|
-
|
|
2807
|
+
sleepGestureProgress = 0;
|
|
2679
2808
|
updateStartStopButton();
|
|
2680
2809
|
};
|
|
2681
2810
|
|
|
2811
|
+
let showedCameraError = false;
|
|
2682
2812
|
useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
|
|
2683
2813
|
// Phases:
|
|
2684
2814
|
// 1. "tryPreferredCamera"
|
|
@@ -2803,7 +2933,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2803
2933
|
}
|
|
2804
2934
|
if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
|
|
2805
2935
|
// required track is missing
|
|
2806
|
-
errorMessage.textContent = t("No camera found. Please make sure you have a camera connected and enabled.");
|
|
2936
|
+
errorMessage.textContent = t("video.errors.noCameraFound", { defaultValue: "No camera found. Please make sure you have a camera connected and enabled." });
|
|
2807
2937
|
} else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
|
|
2808
2938
|
// webcam is already in use
|
|
2809
2939
|
// or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
|
|
@@ -2811,7 +2941,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2811
2941
|
// (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
|
|
2812
2942
|
// though care should be given to make it clear it's a list with one item, with something like "(no more cameras detected)" following the list
|
|
2813
2943
|
// or "1 camera source detected" preceding it)
|
|
2814
|
-
errorMessage.textContent = t("Webcam is already in use. Please make sure you have no other programs using the camera.");
|
|
2944
|
+
errorMessage.textContent = t("video.errors.cameraInUse", { defaultValue: "Webcam is already in use. Please make sure you have no other programs using the camera." });
|
|
2815
2945
|
} else if (error.name === "AbortError") {
|
|
2816
2946
|
// webcam is likely already in use
|
|
2817
2947
|
// I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
|
|
@@ -2819,7 +2949,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2819
2949
|
// Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
|
|
2820
2950
|
// I really hope that isn't the problem.
|
|
2821
2951
|
// errorMessage.textContent = "Webcam may already be in use. Please make sure you have no other programs using the camera.";
|
|
2822
|
-
errorMessage.textContent = t("Please make sure no other programs are using the camera and try again.");
|
|
2952
|
+
errorMessage.textContent = t("video.errors.retryAfterClosingOtherPrograms", { defaultValue: "Please make sure no other programs are using the camera and try again." });
|
|
2823
2953
|
// A more honest/helpful message might be:
|
|
2824
2954
|
// errorMessage.textContent = "Please try again and then make sure no other programs are using the camera and try again again.";
|
|
2825
2955
|
// errorMessage.textContent = "Please try again before/after making sure no other programs are using the camera.";
|
|
@@ -2837,25 +2967,32 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2837
2967
|
// errorMessage.textContent = "The previously selected camera is not available. Please mess around with Video > Camera source.";
|
|
2838
2968
|
// errorMessage.textContent = "The previously selected camera is not available. Try changing Video > Camera source.";
|
|
2839
2969
|
// errorMessage.textContent = "The previously selected camera is not available. Please select a camera from the \"Camera source\" dropdown in the Video settings and if it doesn't show up, it might after you select Default.";
|
|
2840
|
-
errorMessage.textContent = t("The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to.");
|
|
2970
|
+
errorMessage.textContent = t("video.errors.previouslySelectedUnavailable", { defaultValue: "The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to." });
|
|
2841
2971
|
// It's awkward but that's my best attempt at conveying how you may need to proceed
|
|
2842
2972
|
// without complicated description of how/why the dropdown might be populated with
|
|
2843
2973
|
// fake information until a camera stream is successfully opened.
|
|
2844
2974
|
} else {
|
|
2845
|
-
errorMessage.textContent = t("Webcam does not support the required resolution. Please change your settings.");
|
|
2975
|
+
errorMessage.textContent = t("video.errors.unsupportedResolution", { defaultValue: "Webcam does not support the required resolution. Please change your settings." });
|
|
2846
2976
|
}
|
|
2847
2977
|
} else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
|
|
2848
2978
|
// permission denied in browser
|
|
2849
|
-
errorMessage.textContent = t("Permission denied. Please enable access to the camera.");
|
|
2979
|
+
errorMessage.textContent = t("video.errors.permissionDenied", { defaultValue: "Permission denied. Please enable access to the camera." });
|
|
2850
2980
|
} else if (error.name == "TypeError") {
|
|
2851
2981
|
// empty constraints object
|
|
2852
|
-
errorMessage.textContent = `${t("Something went wrong accessing the camera.")} (${error.name}: ${error.message})`;
|
|
2982
|
+
errorMessage.textContent = `${t("video.errors.accessFailed", { defaultValue: "Something went wrong accessing the camera." })} (${error.name}: ${error.message})`;
|
|
2853
2983
|
} else {
|
|
2854
2984
|
// other errors
|
|
2855
|
-
errorMessage.textContent = `${t("Something went wrong accessing the camera. Please try again.")} (${error.name}: ${error.message})`;
|
|
2985
|
+
errorMessage.textContent = `${t("video.errors.accessFailedRetry", { defaultValue: "Something went wrong accessing the camera. Please try again." })} (${error.name}: ${error.message})`;
|
|
2856
2986
|
}
|
|
2857
|
-
errorMessage.textContent = `${t("⚠️
|
|
2987
|
+
errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${errorMessage.textContent}`;
|
|
2858
2988
|
errorMessage.hidden = false;
|
|
2989
|
+
// Play CSS animation only on retries
|
|
2990
|
+
errorMessage.style.animation = "none";
|
|
2991
|
+
if (showedCameraError) {
|
|
2992
|
+
void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
|
|
2993
|
+
errorMessage.style.animation = "";
|
|
2994
|
+
}
|
|
2995
|
+
showedCameraError = true;
|
|
2859
2996
|
});
|
|
2860
2997
|
};
|
|
2861
2998
|
useDemoFootageButton.onclick = TrackyMouse.useDemoFootage = () => {
|
|
@@ -2955,7 +3092,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2955
3092
|
}
|
|
2956
3093
|
addPoint(x, y) {
|
|
2957
3094
|
if (this.pointCount < maxPoints) {
|
|
2958
|
-
|
|
3095
|
+
let pointIndex = this.pointCount * 2;
|
|
2959
3096
|
this.curXY[pointIndex] = x;
|
|
2960
3097
|
this.curXY[pointIndex + 1] = y;
|
|
2961
3098
|
this.prevXY[pointIndex] = x;
|
|
@@ -2964,8 +3101,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2964
3101
|
}
|
|
2965
3102
|
}
|
|
2966
3103
|
filterPoints(condition) {
|
|
2967
|
-
|
|
2968
|
-
for (
|
|
3104
|
+
let outputPointIndex = 0;
|
|
3105
|
+
for (let inputPointIndex = 0; inputPointIndex < this.pointCount; inputPointIndex++) {
|
|
2969
3106
|
if (condition(inputPointIndex)) {
|
|
2970
3107
|
if (outputPointIndex < inputPointIndex) {
|
|
2971
3108
|
const inputOffset = inputPointIndex * 2;
|
|
@@ -3013,10 +3150,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3013
3150
|
[this.prevPyramid, this.curPyramid] = [this.curPyramid, this.prevPyramid];
|
|
3014
3151
|
|
|
3015
3152
|
// these are options worth breaking out and exploring
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3153
|
+
let winSize = 20;
|
|
3154
|
+
let maxIterations = 30;
|
|
3155
|
+
let epsilon = 0.01;
|
|
3156
|
+
let minEigen = 0.001;
|
|
3020
3157
|
|
|
3021
3158
|
jsfeat.imgproc.grayscale(imageData.data, imageData.width, imageData.height, this.curPyramid.data[0]);
|
|
3022
3159
|
this.curPyramid.build(this.curPyramid.data[0], true);
|
|
@@ -3030,9 +3167,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3030
3167
|
this.prunePoints();
|
|
3031
3168
|
}
|
|
3032
3169
|
draw(ctx) {
|
|
3033
|
-
for (
|
|
3034
|
-
|
|
3035
|
-
//
|
|
3170
|
+
for (let i = 0; i < this.pointCount; i++) {
|
|
3171
|
+
let pointOffset = i * 2;
|
|
3172
|
+
// let distMoved = Math.hypot(
|
|
3036
3173
|
// this.prevXY[pointOffset] - this.curXY[pointOffset],
|
|
3037
3174
|
// this.prevXY[pointOffset + 1] - this.curXY[pointOffset + 1]
|
|
3038
3175
|
// );
|
|
@@ -3050,11 +3187,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3050
3187
|
}
|
|
3051
3188
|
}
|
|
3052
3189
|
getMovement() {
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
for (
|
|
3057
|
-
|
|
3190
|
+
let movementX = 0;
|
|
3191
|
+
let movementY = 0;
|
|
3192
|
+
let numMovements = 0;
|
|
3193
|
+
for (let i = 0; i < this.pointCount; i++) {
|
|
3194
|
+
let pointOffset = i * 2;
|
|
3058
3195
|
movementX += this.curXY[pointOffset] - this.prevXY[pointOffset];
|
|
3059
3196
|
movementY += this.curXY[pointOffset + 1] - this.prevXY[pointOffset + 1];
|
|
3060
3197
|
numMovements += 1;
|
|
@@ -3091,9 +3228,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3091
3228
|
// in order to keep a smooth overall tracking calculation,
|
|
3092
3229
|
// don't add points if they're close to an existing point.
|
|
3093
3230
|
// Otherwise, it would not just be redundant, but often remove the older points, in the pruning.
|
|
3094
|
-
for (
|
|
3095
|
-
|
|
3096
|
-
//
|
|
3231
|
+
for (let pointIndex = 0; pointIndex < oops.pointCount; pointIndex++) {
|
|
3232
|
+
let pointOffset = pointIndex * 2;
|
|
3233
|
+
// let distance = Math.hypot(
|
|
3097
3234
|
// x - oops.curXY[pointOffset],
|
|
3098
3235
|
// y - oops.curXY[pointOffset + 1]
|
|
3099
3236
|
// );
|
|
@@ -3130,6 +3267,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3130
3267
|
return ((px - x1) * nx + (py - y1) * ny) / Math.hypot(nx, ny);
|
|
3131
3268
|
}
|
|
3132
3269
|
|
|
3270
|
+
let lastTimestamp = -Infinity;
|
|
3133
3271
|
function draw(update = true) {
|
|
3134
3272
|
ctx.resetTransform(); // in case there is an error, don't flip constantly back and forth due to mirroring
|
|
3135
3273
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // in case there's no footage
|
|
@@ -3144,6 +3282,13 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3144
3282
|
ctx.drawImage(cameraVideo, 0, 0, canvas.width, canvas.height);
|
|
3145
3283
|
}
|
|
3146
3284
|
|
|
3285
|
+
const timestamp = performance.now();
|
|
3286
|
+
const deltaTime = Math.min(timestamp - lastTimestamp, 100);
|
|
3287
|
+
lastTimestamp = timestamp;
|
|
3288
|
+
|
|
3289
|
+
sleepSweep?.setEnabled(s.closeEyesToToggle);
|
|
3290
|
+
sleepSweep?.update(sleepGestureProgress);
|
|
3291
|
+
|
|
3147
3292
|
if (!pointTracker) {
|
|
3148
3293
|
return;
|
|
3149
3294
|
}
|
|
@@ -3279,39 +3424,77 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3279
3424
|
|
|
3280
3425
|
// TODO: separate confidence threshold for removing vs adding points?
|
|
3281
3426
|
|
|
3427
|
+
|
|
3282
3428
|
// cull points to those within useful facial region
|
|
3283
|
-
|
|
3284
|
-
|
|
3429
|
+
function regionFilter([x, y]) {
|
|
3430
|
+
|
|
3285
3431
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3286
|
-
|
|
3287
|
-
(annotations.noseTip[0][0] -
|
|
3288
|
-
annotations.noseTip[0][1] -
|
|
3432
|
+
let distance = Math.hypot(
|
|
3433
|
+
(annotations.noseTip[0][0] - x) * 1.4,
|
|
3434
|
+
annotations.noseTip[0][1] - y
|
|
3289
3435
|
);
|
|
3290
|
-
|
|
3436
|
+
let headSize = Math.hypot(
|
|
3291
3437
|
annotations.leftCheek[0][0] - annotations.rightCheek[0][0],
|
|
3292
3438
|
annotations.leftCheek[0][1] - annotations.rightCheek[0][1]
|
|
3293
3439
|
);
|
|
3294
3440
|
if (distance > headSize) {
|
|
3295
3441
|
return false;
|
|
3296
3442
|
}
|
|
3443
|
+
// Avoid mouth affecting pointer position.
|
|
3444
|
+
distance = annotations.lipsLowerInner.map((lipPoint) =>
|
|
3445
|
+
Math.min(
|
|
3446
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] - y),
|
|
3447
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.1 - y), // a bit below too
|
|
3448
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.2 - y), // a bit below too
|
|
3449
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.3 - y), // a bit below too
|
|
3450
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.4 - y), // a bit below too (yeah I'm being a little lazy here)
|
|
3451
|
+
)
|
|
3452
|
+
).reduce((a, b) => Math.min(a, b), Infinity);
|
|
3453
|
+
if (distance < headSize * 0.1) {
|
|
3454
|
+
return false;
|
|
3455
|
+
}
|
|
3297
3456
|
// Avoid blinking eyes affecting pointer position.
|
|
3298
3457
|
// distance to outer corners of eyes
|
|
3299
3458
|
distance = Math.min(
|
|
3300
3459
|
Math.hypot(
|
|
3301
|
-
annotations.leftEyeLower0[0][0] -
|
|
3302
|
-
annotations.leftEyeLower0[0][1] -
|
|
3460
|
+
annotations.leftEyeLower0[0][0] - x,
|
|
3461
|
+
annotations.leftEyeLower0[0][1] - y
|
|
3303
3462
|
),
|
|
3304
3463
|
Math.hypot(
|
|
3305
|
-
annotations.rightEyeLower0[0][0] -
|
|
3306
|
-
annotations.rightEyeLower0[0][1] -
|
|
3464
|
+
annotations.rightEyeLower0[0][0] - x,
|
|
3465
|
+
annotations.rightEyeLower0[0][1] - y
|
|
3307
3466
|
),
|
|
3308
3467
|
);
|
|
3309
3468
|
if (distance < headSize * 0.42) {
|
|
3310
3469
|
return false;
|
|
3311
3470
|
}
|
|
3312
3471
|
return true;
|
|
3472
|
+
}
|
|
3473
|
+
pointTracker.filterPoints((pointIndex) => {
|
|
3474
|
+
let pointOffset = pointIndex * 2;
|
|
3475
|
+
const point = [pointTracker.curXY[pointOffset], pointTracker.curXY[pointOffset + 1]];
|
|
3476
|
+
return regionFilter(point);
|
|
3313
3477
|
});
|
|
3314
3478
|
|
|
3479
|
+
// Debug visualization for region filter (a sort of heatmap of where points will be culled)
|
|
3480
|
+
if (showDebugRegionFilter) {
|
|
3481
|
+
ctx.save();
|
|
3482
|
+
if (s.mirror) {
|
|
3483
|
+
ctx.translate(canvas.width, 0);
|
|
3484
|
+
ctx.scale(-1, 1);
|
|
3485
|
+
}
|
|
3486
|
+
ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
|
|
3487
|
+
const vizStep = 4;
|
|
3488
|
+
for (let x = 0; x < canvas.width; x += vizStep) {
|
|
3489
|
+
for (let y = 0; y < canvas.height; y += vizStep) {
|
|
3490
|
+
if (!regionFilter([x, y])) {
|
|
3491
|
+
ctx.fillRect(x - 5, y - 5, vizStep, vizStep);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
ctx.restore();
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3315
3498
|
const keypoints = facemeshPrediction.keypoints;
|
|
3316
3499
|
if (keypoints) {
|
|
3317
3500
|
const top = keypoints[10]; // Top of forehead
|
|
@@ -3493,16 +3676,19 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3493
3676
|
|
|
3494
3677
|
blinkInfo = detectBlinks();
|
|
3495
3678
|
mouthInfo = detectMouthOpen();
|
|
3496
|
-
if (blinkInfo.rightEye.open
|
|
3497
|
-
|
|
3679
|
+
if (!blinkInfo.rightEye.open && !blinkInfo.leftEye.open) {
|
|
3680
|
+
sleepGestureProgress += deltaTime / sleepGestureEyesClosedDuration;
|
|
3681
|
+
sleepGestureProgress = Math.min(sleepGestureProgress, 1);
|
|
3682
|
+
} else {
|
|
3683
|
+
sleepGestureProgress -= deltaTime / sleepGestureEyesClosedDuration;
|
|
3684
|
+
sleepGestureProgress = Math.max(sleepGestureProgress, 0);
|
|
3498
3685
|
}
|
|
3499
|
-
if (
|
|
3686
|
+
if (sleepGestureProgress >= 1) {
|
|
3687
|
+
sleepGestureProgress = 0;
|
|
3500
3688
|
if (s.closeEyesToToggle) {
|
|
3501
3689
|
paused = !paused;
|
|
3502
3690
|
updatePaused();
|
|
3503
|
-
|
|
3504
|
-
// TODO: try to keep variable names meaningful
|
|
3505
|
-
lastTimeWhenAnEyeWasOpen = Infinity;
|
|
3691
|
+
sleepSweep?.sleepModeWasToggled(paused);
|
|
3506
3692
|
}
|
|
3507
3693
|
}
|
|
3508
3694
|
|
|
@@ -3562,15 +3748,43 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3562
3748
|
}
|
|
3563
3749
|
}
|
|
3564
3750
|
|
|
3565
|
-
// TODO: implement these clicking modes for the web library version
|
|
3566
|
-
// and unhide the "Clicking mode" setting in the UI
|
|
3567
|
-
// https://github.com/1j01/tracky-mouse/issues/72
|
|
3568
3751
|
const buttonNames = ["left", "middle", "right"];
|
|
3569
3752
|
for (let buttonIndex = 0; buttonIndex < 3; buttonIndex++) {
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3753
|
+
const buttonIsActive = clickButton === buttonIndex;
|
|
3754
|
+
if (buttonIsActive !== buttonStates[buttonNames[buttonIndex]]) {
|
|
3755
|
+
// Wait for confirmation of the button state change before playing SFX
|
|
3756
|
+
// but not before updating buttonStates, since we check that in this loop
|
|
3757
|
+
// to decide whether to call setMouseButtonState.
|
|
3758
|
+
// We don't want to send extraneous mouse button changes to the main process,
|
|
3759
|
+
// even if it does track button states itself. If nothing else it's wasted IPC.
|
|
3760
|
+
// That said, an argument could be made for updating lastMouseDownTime later
|
|
3761
|
+
// if the IPC is slow, to extend the time frame for making a simple click
|
|
3762
|
+
// rather than a drag.
|
|
3763
|
+
if (!setMouseButtonState) {
|
|
3764
|
+
console.warn("setMouseButtonState function not provided");
|
|
3765
|
+
} else {
|
|
3766
|
+
const maybeAPromise = setMouseButtonState(buttonIndex, buttonIsActive);
|
|
3767
|
+
const playSoundForButton = (changedButtonState) => {
|
|
3768
|
+
if (changedButtonState) {
|
|
3769
|
+
if (buttonIndex === 1) {
|
|
3770
|
+
playSound(buttonIsActive ? "middleClickPress" : "middleClickRelease", {
|
|
3771
|
+
volume: 4,
|
|
3772
|
+
});
|
|
3773
|
+
} else {
|
|
3774
|
+
playSound(buttonIsActive ? "clickPress" : "clickRelease", {
|
|
3775
|
+
playbackRate: buttonIndex === 0 ? 1 : buttonIndex === 2 ? 1.2 : 1.5,
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
if (maybeAPromise instanceof Promise) {
|
|
3781
|
+
maybeAPromise.then(playSoundForButton);
|
|
3782
|
+
} else {
|
|
3783
|
+
playSoundForButton(maybeAPromise);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
buttonStates[buttonNames[buttonIndex]] = buttonIsActive;
|
|
3787
|
+
if (buttonIsActive) {
|
|
3574
3788
|
lastMouseDownTime = performance.now();
|
|
3575
3789
|
} else {
|
|
3576
3790
|
// Limit "Delay Before Dragging" effect to the duration of a click.
|
|
@@ -3589,13 +3803,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3589
3803
|
pointTracker.update(imageData);
|
|
3590
3804
|
}
|
|
3591
3805
|
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3806
|
+
updateInputFeedback?.({
|
|
3807
|
+
headNotFound: !face && !facemeshPrediction,
|
|
3808
|
+
blinkInfo,
|
|
3809
|
+
mouthInfo,
|
|
3810
|
+
});
|
|
3599
3811
|
|
|
3600
3812
|
if (facemeshPrediction) {
|
|
3601
3813
|
ctx.fillStyle = "red";
|
|
@@ -3640,17 +3852,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3640
3852
|
const textYStart = -10;
|
|
3641
3853
|
|
|
3642
3854
|
|
|
3643
|
-
const
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
);
|
|
3652
|
-
const
|
|
3653
|
-
const
|
|
3855
|
+
const headTiltRows = [
|
|
3856
|
+
{ label: t("debug.headTilt.pitch", { defaultValue: "Pitch:" }), value: `${(headTilt.pitch * 180 / Math.PI).toFixed(1)}°` },
|
|
3857
|
+
{ label: t("debug.headTilt.yaw", { defaultValue: "Yaw:" }), value: `${(headTilt.yaw * 180 / Math.PI).toFixed(1)}°` },
|
|
3858
|
+
{ label: t("debug.headTilt.roll", { defaultValue: "Roll:" }), value: `${(headTilt.roll * 180 / Math.PI).toFixed(1)}°` },
|
|
3859
|
+
];
|
|
3860
|
+
const labelWidths = headTiltRows.map(row => ctx.measureText(row.label).width);
|
|
3861
|
+
const maxLabelWidth = Math.max(...labelWidths);
|
|
3862
|
+
const valueColumnTemplate = "-180.0°";
|
|
3863
|
+
const maxValueWidth = ctx.measureText(valueColumnTemplate).width;
|
|
3864
|
+
const labelToValueGap = 10;
|
|
3865
|
+
const boxPadding = 10;
|
|
3866
|
+
const boxWidth = boxPadding * 2 + maxLabelWidth + labelToValueGap + maxValueWidth;
|
|
3867
|
+
const boxHeight = textLineHeight * headTiltRows.length;
|
|
3868
|
+
const valueColumnRightOffset = boxPadding + maxLabelWidth + labelToValueGap + maxValueWidth;
|
|
3654
3869
|
|
|
3655
3870
|
// Calculate screen coordinates for the text box
|
|
3656
3871
|
let screenX = s.mirror ? canvas.width - cx : cx;
|
|
@@ -3661,7 +3876,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3661
3876
|
let textScreenY = screenY + textYStart;
|
|
3662
3877
|
|
|
3663
3878
|
// Clamp to canvas bounds
|
|
3664
|
-
textScreenX = Math.max(
|
|
3879
|
+
textScreenX = Math.max(boxPadding, Math.min(canvas.width - boxWidth - boxPadding, textScreenX));
|
|
3665
3880
|
textScreenY = Math.max(textLineHeight, Math.min(canvas.height - boxHeight + textLineHeight, textScreenY));
|
|
3666
3881
|
|
|
3667
3882
|
ctx.save();
|
|
@@ -3675,12 +3890,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3675
3890
|
const dx = textScreenX - screenNoseX;
|
|
3676
3891
|
const dy = textScreenY - screenNoseY;
|
|
3677
3892
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3893
|
+
for (let i = 0; i < headTiltRows.length; i++) {
|
|
3894
|
+
const row = headTiltRows[i];
|
|
3895
|
+
const baselineY = dy + textLineHeight * (i + 1);
|
|
3896
|
+
const labelX = dx + boxPadding;
|
|
3897
|
+
const valueTextWidth = ctx.measureText(row.value).width;
|
|
3898
|
+
const valueRightX = dx + valueColumnRightOffset;
|
|
3899
|
+
const valueX = valueRightX - valueTextWidth;
|
|
3900
|
+
ctx.strokeText(row.label, labelX, baselineY);
|
|
3901
|
+
ctx.fillText(row.label, labelX, baselineY);
|
|
3902
|
+
ctx.strokeText(row.value, valueX, baselineY);
|
|
3903
|
+
ctx.fillText(row.value, valueX, baselineY);
|
|
3904
|
+
}
|
|
3684
3905
|
|
|
3685
3906
|
ctx.restore();
|
|
3686
3907
|
|
|
@@ -3836,14 +4057,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3836
4057
|
|
|
3837
4058
|
// cull points to those within useful facial region
|
|
3838
4059
|
pointTracker.filterPoints((pointIndex) => {
|
|
3839
|
-
|
|
4060
|
+
let pointOffset = pointIndex * 2;
|
|
3840
4061
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3841
|
-
|
|
4062
|
+
let distance = Math.hypot(
|
|
3842
4063
|
(face[62][0] - pointTracker.curXY[pointOffset]) * 1.4,
|
|
3843
4064
|
face[62][1] - pointTracker.curXY[pointOffset + 1]
|
|
3844
4065
|
);
|
|
3845
4066
|
// distance based on outer eye corners
|
|
3846
|
-
|
|
4067
|
+
let headSize = Math.hypot(
|
|
3847
4068
|
face[23][0] - face[28][0],
|
|
3848
4069
|
face[23][1] - face[28][1]
|
|
3849
4070
|
);
|
|
@@ -3871,18 +4092,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3871
4092
|
const screenWidth = window.electronAPI ? screen.width : innerWidth;
|
|
3872
4093
|
const screenHeight = window.electronAPI ? screen.height : innerHeight;
|
|
3873
4094
|
|
|
3874
|
-
|
|
4095
|
+
let [movementX, movementY] = pointTracker.getMovement();
|
|
3875
4096
|
|
|
3876
4097
|
// Acceleration curves add a lot of stability,
|
|
3877
4098
|
// letting you focus on a specific point without jitter, but still move quickly.
|
|
3878
4099
|
|
|
3879
|
-
//
|
|
3880
|
-
//
|
|
3881
|
-
|
|
4100
|
+
// let accelerate = (delta, distance) => (delta / 10) * (distance ** 0.8);
|
|
4101
|
+
// let accelerate = (delta, distance) => (delta / 1) * (Math.abs(delta) ** 0.8);
|
|
4102
|
+
let accelerate = (delta, _distance) => (delta / 1) * (Math.abs(delta * 5) ** s.headTrackingAcceleration);
|
|
3882
4103
|
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
4104
|
+
let distance = Math.hypot(movementX, movementY);
|
|
4105
|
+
let deltaX = accelerate(movementX * s.headTrackingSensitivityX, distance);
|
|
4106
|
+
let deltaY = accelerate(movementY * s.headTrackingSensitivityY, distance);
|
|
3886
4107
|
|
|
3887
4108
|
if (s.headTrackingTiltInfluence > 0) {
|
|
3888
4109
|
const yawRange = [
|
|
@@ -4013,8 +4234,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4013
4234
|
pointerEl.style.display = "none";
|
|
4014
4235
|
} else {
|
|
4015
4236
|
pointerEl.style.display = "";
|
|
4016
|
-
pointerEl.style.left = `${mouseX}px`;
|
|
4017
|
-
pointerEl.style.top = `${mouseY}px`;
|
|
4237
|
+
pointerEl.style.left = `${Math.floor(mouseX)}px`;
|
|
4238
|
+
pointerEl.style.top = `${Math.floor(mouseY)}px`;
|
|
4018
4239
|
}
|
|
4019
4240
|
if (TrackyMouse.onPointerMove) {
|
|
4020
4241
|
TrackyMouse.onPointerMove(mouseX, mouseY);
|
|
@@ -4030,9 +4251,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4030
4251
|
ctx.lineWidth = 3;
|
|
4031
4252
|
ctx.font = "20px sans-serif";
|
|
4032
4253
|
ctx.beginPath();
|
|
4033
|
-
const text3 = t("Face convergence score:
|
|
4034
|
-
const text1 = t("Face tracking score:
|
|
4035
|
-
const text2 = t("Points based on score:
|
|
4254
|
+
const text3 = `${t("debug.faceConvergenceScore", { defaultValue: "Face convergence score:" })} ${((useFacemesh && facemeshPrediction) ? t("common.notApplicable", { defaultValue: "N/A" }) : faceConvergence.toFixed(4))}`;
|
|
4255
|
+
const text1 = `${t("debug.faceTrackingScore", { defaultValue: "Face tracking score:" })} ${((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4)}`;
|
|
4256
|
+
const text2 = `${t("debug.pointsBasedOnScore", { defaultValue: "Points based on score:" })} ${((useFacemesh && facemeshPrediction) ? pointsBasedOnFaceInViewConfidence : pointsBasedOnFaceScore).toFixed(4)}`;
|
|
4036
4257
|
ctx.strokeText(text1, 50, 50);
|
|
4037
4258
|
ctx.fillText(text1, 50, 50);
|
|
4038
4259
|
ctx.strokeText(text2, 50, 70);
|
|
@@ -4055,7 +4276,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4055
4276
|
|
|
4056
4277
|
// Can't use requestAnimationFrame, doesn't work with webPreferences.backgroundThrottling: false (at least in some version of Electron (v12 I think, when I tested it), on Ubuntu, with XFCE)
|
|
4057
4278
|
const iid = setInterval(function animationLoop() {
|
|
4058
|
-
draw(!paused || document.visibilityState === "visible");
|
|
4279
|
+
draw(!paused || document.visibilityState === "visible" || isDesktopApp);
|
|
4059
4280
|
}, 15);
|
|
4060
4281
|
|
|
4061
4282
|
let autoDemo = false;
|
|
@@ -4068,14 +4289,26 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4068
4289
|
TrackyMouse.useDemoFootage();
|
|
4069
4290
|
} else if (window.electronAPI) {
|
|
4070
4291
|
TrackyMouse.useCamera();
|
|
4292
|
+
} else {
|
|
4293
|
+
// Passively querying the camera permission isn't supported in all browsers,
|
|
4294
|
+
// hence some of the complex logic in useCamera, but when it is,
|
|
4295
|
+
// we can connect to the camera right away if the permission is already granted.
|
|
4296
|
+
// This speeds up the development cycle, at the very least.
|
|
4297
|
+
navigator.permissions?.query?.({ name: "camera" }).then((status) => {
|
|
4298
|
+
if (status.state === "granted") {
|
|
4299
|
+
TrackyMouse.useCamera();
|
|
4300
|
+
}
|
|
4301
|
+
}, (error) => {
|
|
4302
|
+
console.log("Error querying permissions:", error);
|
|
4303
|
+
});
|
|
4071
4304
|
}
|
|
4072
4305
|
|
|
4073
4306
|
const updateStartStopButton = () => {
|
|
4074
4307
|
if (paused) {
|
|
4075
|
-
startStopButton.textContent = t("Start");
|
|
4308
|
+
startStopButton.textContent = t("ui.startStopButton.start", { defaultValue: "Start" });
|
|
4076
4309
|
startStopButton.setAttribute("aria-pressed", "false");
|
|
4077
4310
|
} else {
|
|
4078
|
-
startStopButton.textContent = t("Stop");
|
|
4311
|
+
startStopButton.textContent = t("ui.startStopButton.stop", { defaultValue: "Stop" });
|
|
4079
4312
|
startStopButton.setAttribute("aria-pressed", "true");
|
|
4080
4313
|
}
|
|
4081
4314
|
};
|
|
@@ -4085,9 +4318,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4085
4318
|
pointerEl.style.display = "none";
|
|
4086
4319
|
}
|
|
4087
4320
|
updateStartStopButton();
|
|
4088
|
-
|
|
4089
|
-
window.electronAPI.notifyToggleState(!paused);
|
|
4090
|
-
}
|
|
4321
|
+
notifyToggleState?.(!paused);
|
|
4091
4322
|
};
|
|
4092
4323
|
const handleShortcut = (shortcutType) => {
|
|
4093
4324
|
if (shortcutType === "toggle-tracking") {
|
|
@@ -4229,15 +4460,17 @@ TrackyMouse.init = function (div, opts = {}) {
|
|
|
4229
4460
|
TrackyMouse.initScreenOverlay = () => {
|
|
4230
4461
|
|
|
4231
4462
|
const template = `
|
|
4463
|
+
<div class="tracky-mouse-hide-near-cursor">
|
|
4232
4464
|
<div class="tracky-mouse-absolute-center">
|
|
4233
4465
|
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
|
|
4234
|
-
<img src="
|
|
4466
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
|
|
4235
4467
|
</div>
|
|
4236
4468
|
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
|
|
4237
|
-
<img src="
|
|
4469
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/head-not-found.svg" alt="head not found" width="128" height="128">
|
|
4238
4470
|
</div>
|
|
4239
4471
|
</div>
|
|
4240
4472
|
<div id="tracky-mouse-screen-overlay-message"></div>
|
|
4473
|
+
</div>
|
|
4241
4474
|
`;
|
|
4242
4475
|
const fragment = document.createRange().createContextualFragment(template);
|
|
4243
4476
|
document.body.appendChild(fragment);
|
|
@@ -4245,8 +4478,11 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4245
4478
|
const message = document.getElementById("tracky-mouse-screen-overlay-message");
|
|
4246
4479
|
message.dir = "auto";
|
|
4247
4480
|
|
|
4481
|
+
const hideNearCursorEls = document.querySelectorAll(".tracky-mouse-hide-near-cursor");
|
|
4482
|
+
|
|
4248
4483
|
const inputFeedbackCanvas = document.createElement("canvas");
|
|
4249
|
-
inputFeedbackCanvas.style.position = "
|
|
4484
|
+
inputFeedbackCanvas.style.position = "fixed";
|
|
4485
|
+
inputFeedbackCanvas.style.zIndex = "899990"; // just below .tracky-mouse-pointer
|
|
4250
4486
|
inputFeedbackCanvas.style.top = "0";
|
|
4251
4487
|
inputFeedbackCanvas.style.left = "0";
|
|
4252
4488
|
inputFeedbackCanvas.style.pointerEvents = "none";
|
|
@@ -4282,10 +4518,11 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4282
4518
|
// inputFeedbackCanvas.style.transform = `translate(${x - inputFeedbackCanvas.width / 2}px, ${y - inputFeedbackCanvas.height / 2}px)`;
|
|
4283
4519
|
// inputFeedbackCanvas.style.transform = `translate(${x}px, ${y}px)`;
|
|
4284
4520
|
inputFeedbackCanvas.style.transform = `translate(${Math.min(x, window.innerWidth - inputFeedbackCanvas.width)}px, ${Math.min(y, window.innerHeight - inputFeedbackCanvas.height)}px)`;
|
|
4521
|
+
|
|
4285
4522
|
}
|
|
4286
4523
|
|
|
4287
4524
|
function update(data) {
|
|
4288
|
-
const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset } = data;
|
|
4525
|
+
const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset, systemMousePosition } = data;
|
|
4289
4526
|
|
|
4290
4527
|
message.style.bottom = `${bottomOffset}px`;
|
|
4291
4528
|
|
|
@@ -4294,21 +4531,31 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4294
4531
|
// - bad lighting conditions
|
|
4295
4532
|
// see: https://github.com/1j01/tracky-mouse/issues/26
|
|
4296
4533
|
|
|
4297
|
-
document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback);
|
|
4298
|
-
document.body.classList.toggle("tracky-mouse-head-not-found", inputFeedback.headNotFound);
|
|
4534
|
+
document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback ?? false);
|
|
4535
|
+
document.body.classList.toggle("tracky-mouse-head-not-found", inputFeedback.headNotFound ?? false);
|
|
4299
4536
|
|
|
4300
4537
|
message.innerText = messageText;
|
|
4301
4538
|
|
|
4302
4539
|
if (!isEnabled && !isManualTakeback) {
|
|
4303
4540
|
// Fade out the message after a little while so it doesn't get in the way.
|
|
4304
4541
|
// TODO: make sure animation isn't interrupted by inputFeedback updates.
|
|
4305
|
-
message.style.animation = "tracky-mouse-
|
|
4542
|
+
message.style.animation = "tracky-mouse-fade-out 2s ease-in-out forwards 10s";
|
|
4306
4543
|
} else {
|
|
4307
4544
|
message.style.animation = "";
|
|
4308
4545
|
message.style.opacity = "1";
|
|
4309
4546
|
}
|
|
4310
4547
|
|
|
4311
4548
|
drawInputFeedback(data);
|
|
4549
|
+
|
|
4550
|
+
if (systemMousePosition) {
|
|
4551
|
+
const { x, y } = systemMousePosition;
|
|
4552
|
+
// TODO: optimize CSS parsing by using CSS variables?
|
|
4553
|
+
const maskImage = `radial-gradient(circle at ${x}px ${y}px, transparent 0, transparent 50px, rgba(0, 0, 0, 0.85) 140px, rgba(0, 0, 0, 1) 200px, rgba(0, 0, 0, 1) 100%)`;
|
|
4554
|
+
for (const el of hideNearCursorEls) {
|
|
4555
|
+
el.style.webkitMaskImage = maskImage;
|
|
4556
|
+
el.style.maskImage = maskImage;
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4312
4559
|
}
|
|
4313
4560
|
|
|
4314
4561
|
return {
|