tracky-mouse 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/lib/face-landmarks-detection.min.js +1 -1
- package/lib/face_mesh/face_mesh.js +1 -1
- package/locales/ar/translation.json +8 -2
- package/locales/ar-EG/translation.json +8 -2
- package/locales/bg/translation.json +8 -2
- package/locales/bn/translation.json +8 -2
- package/locales/ca/translation.json +8 -2
- package/locales/ce/translation.json +8 -2
- package/locales/ceb/translation.json +9 -3
- package/locales/cs/translation.json +8 -2
- package/locales/da/translation.json +8 -2
- package/locales/de/translation.json +8 -2
- package/locales/el/translation.json +8 -2
- package/locales/emoji/emoji-translation-notes.md +1 -0
- package/locales/emoji/translation.json +12 -6
- package/locales/en/translation.json +8 -2
- package/locales/eo/translation.json +8 -2
- package/locales/es/translation.json +8 -2
- package/locales/eu/translation.json +8 -2
- package/locales/fa/translation.json +8 -2
- package/locales/fi/translation.json +8 -2
- package/locales/fr/translation.json +8 -2
- package/locales/gu/translation.json +8 -2
- package/locales/ha/translation.json +8 -2
- package/locales/he/translation.json +8 -2
- package/locales/hi/translation.json +8 -2
- package/locales/hr/translation.json +8 -2
- package/locales/hu/translation.json +8 -2
- package/locales/hy/translation.json +8 -2
- package/locales/id/translation.json +8 -2
- package/locales/it/translation.json +8 -2
- package/locales/ja/translation.json +8 -2
- package/locales/jv/translation.json +9 -3
- package/locales/ko/translation.json +8 -2
- package/locales/mr/translation.json +8 -2
- package/locales/ms/translation.json +8 -2
- package/locales/nan/translation.json +8 -2
- package/locales/nb/translation.json +8 -2
- package/locales/nl/translation.json +8 -2
- package/locales/pa/translation.json +8 -2
- package/locales/pl/translation.json +8 -2
- package/locales/pt/translation.json +8 -2
- package/locales/pt-BR/translation.json +8 -2
- package/locales/ro/translation.json +8 -2
- package/locales/ru/translation.json +8 -2
- package/locales/sk/translation.json +8 -2
- package/locales/sl/translation.json +8 -2
- package/locales/sr/translation.json +8 -2
- package/locales/sv/translation.json +8 -2
- package/locales/sw/translation.json +8 -2
- package/locales/ta/translation.json +8 -2
- package/locales/te/translation.json +8 -2
- package/locales/th/translation.json +8 -2
- package/locales/tl/translation.json +9 -3
- package/locales/tr/translation.json +8 -2
- package/locales/tt/translation.json +8 -2
- package/locales/uk/translation.json +8 -2
- package/locales/ur/translation.json +8 -2
- package/locales/uz/translation.json +8 -2
- package/locales/vi/translation.json +8 -2
- package/locales/war/translation.json +9 -3
- package/locales/zh/translation.json +8 -2
- package/locales/zh-simplified/translation.json +8 -2
- package/package.json +3 -1
- package/src/audio.js +145 -0
- package/src/autoscroll.js +189 -0
- package/src/input-simulator.js +518 -0
- package/tracky-mouse.css +32 -1
- package/tracky-mouse.js +457 -185
package/tracky-mouse.js
CHANGED
|
@@ -52,24 +52,30 @@ 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
81
|
const t = (key, options = {}) => options.defaultValue ?? key;
|
|
@@ -104,6 +110,9 @@ const initDwellClicking = (config) => {
|
|
|
104
110
|
if (config.dwellClickEvenIfPaused !== undefined && typeof config.dwellClickEvenIfPaused !== "function") {
|
|
105
111
|
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.dwellClickEvenIfPaused"));
|
|
106
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"));
|
|
115
|
+
}
|
|
107
116
|
if (config.beforeDispatch !== undefined && typeof config.beforeDispatch !== "function") {
|
|
108
117
|
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.beforeDispatch"));
|
|
109
118
|
}
|
|
@@ -149,6 +158,8 @@ const initDwellClicking = (config) => {
|
|
|
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,6 +597,28 @@ TrackyMouse.cleanupDwellClicking = function () {
|
|
|
571
597
|
}
|
|
572
598
|
};
|
|
573
599
|
|
|
600
|
+
TrackyMouse._initAudio = async function () {
|
|
601
|
+
let module;
|
|
602
|
+
try {
|
|
603
|
+
// console.log("Loading audio support...");
|
|
604
|
+
module = await import("./src/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
|
+
|
|
574
622
|
TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
575
623
|
|
|
576
624
|
const {
|
|
@@ -590,6 +638,17 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
590
638
|
// Could group things under an "unstable" object, or ideally, design nice APIs for everything.
|
|
591
639
|
} = initOptions;
|
|
592
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
|
+
});
|
|
651
|
+
|
|
593
652
|
const isDesktopApp = !!window.electronAPI;
|
|
594
653
|
|
|
595
654
|
let translations = {};
|
|
@@ -843,7 +902,7 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
843
902
|
};
|
|
844
903
|
|
|
845
904
|
|
|
846
|
-
|
|
905
|
+
let languageToDefaultRegion = {
|
|
847
906
|
aa: "ET",
|
|
848
907
|
ab: "GE",
|
|
849
908
|
abr: "GH",
|
|
@@ -1588,9 +1647,9 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
1588
1647
|
// </svg>`;
|
|
1589
1648
|
}
|
|
1590
1649
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1650
|
+
let split = locale.toUpperCase().split(/-|_/);
|
|
1651
|
+
let lang = split.shift();
|
|
1652
|
+
let code = split.pop();
|
|
1594
1653
|
|
|
1595
1654
|
if (!/^[A-Z]{2}$/.test(code)) {
|
|
1596
1655
|
code = languageToDefaultRegion[lang.toLowerCase()];
|
|
@@ -1605,7 +1664,7 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
1605
1664
|
return a + b;
|
|
1606
1665
|
}
|
|
1607
1666
|
|
|
1608
|
-
|
|
1667
|
+
let uiContainer = div || document.createElement("div");
|
|
1609
1668
|
uiContainer.classList.add("tracky-mouse-ui");
|
|
1610
1669
|
uiContainer.classList.toggle("tracky-mouse-rtl", isRTL);
|
|
1611
1670
|
uiContainer.dir = isRTL ? "rtl" : "ltr";
|
|
@@ -1613,7 +1672,7 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
1613
1672
|
<div class="tracky-mouse-controls">
|
|
1614
1673
|
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("ui.startStopButton.start", { defaultValue: "Start" })}</button>
|
|
1615
1674
|
</div>
|
|
1616
|
-
<div class="tracky-mouse-
|
|
1675
|
+
<div class="tracky-mouse-camera-area">
|
|
1617
1676
|
<div class="tracky-mouse-canvas-container">
|
|
1618
1677
|
<div class="tracky-mouse-canvas-overlay">
|
|
1619
1678
|
<button class="tracky-mouse-use-camera-button">${t("ui.camera.allowAccess", { defaultValue: "Allow Camera Access" })}</button>
|
|
@@ -1631,12 +1690,43 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
|
1631
1690
|
if (!div) {
|
|
1632
1691
|
document.body.appendChild(uiContainer);
|
|
1633
1692
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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');
|
|
1699
|
+
|
|
1700
|
+
let lastShownErrorDetails = null;
|
|
1701
|
+
function showError(message, error, { warningIcon = true, errorClass = "other" } = {}) {
|
|
1702
|
+
const alreadyShown = !errorMessage.hidden && lastShownErrorDetails?.message === message && lastShownErrorDetails?.error?.name === error?.name && lastShownErrorDetails?.error?.message === error?.message;
|
|
1703
|
+
if (alreadyShown) {
|
|
1704
|
+
// Play CSS animation to indicate repeated errors
|
|
1705
|
+
// but not if they're occurring constantly
|
|
1706
|
+
// Note: for constant errors, with this scheme, it may animate
|
|
1707
|
+
// when returning to the tab due to timer throttling, or due to lag.
|
|
1708
|
+
if (performance.now() > lastShownErrorDetails.time + 100) {
|
|
1709
|
+
errorMessage.style.animation = "none";
|
|
1710
|
+
if (alreadyShown) {
|
|
1711
|
+
void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
|
|
1712
|
+
errorMessage.style.animation = "";
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
if (warningIcon) {
|
|
1717
|
+
errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${message}`;
|
|
1718
|
+
} else {
|
|
1719
|
+
errorMessage.textContent = message;
|
|
1720
|
+
}
|
|
1721
|
+
if (error) {
|
|
1722
|
+
const pre = document.createElement("pre");
|
|
1723
|
+
pre.textContent = error.name + ": " + error.message;
|
|
1724
|
+
errorMessage.appendChild(pre);
|
|
1725
|
+
}
|
|
1726
|
+
errorMessage.hidden = false;
|
|
1727
|
+
}
|
|
1728
|
+
lastShownErrorDetails = { message, error, time: performance.now(), errorClass };
|
|
1729
|
+
}
|
|
1640
1730
|
|
|
1641
1731
|
// Settings (initialized later; defaults are defined in settingsCategories)
|
|
1642
1732
|
const s = {};
|
|
@@ -1975,11 +2065,21 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1975
2065
|
type: "button",
|
|
1976
2066
|
visible: () => isDesktopApp,
|
|
1977
2067
|
onClick: async () => {
|
|
2068
|
+
function showToast(message) {
|
|
2069
|
+
const toast = document.createElement("div");
|
|
2070
|
+
toast.className = "tracky-mouse-toast";
|
|
2071
|
+
toast.textContent = message;
|
|
2072
|
+
document.body.appendChild(toast);
|
|
2073
|
+
setTimeout(() => {
|
|
2074
|
+
toast.remove();
|
|
2075
|
+
}, 5000);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1978
2078
|
let knownCameras = {};
|
|
1979
2079
|
try {
|
|
1980
2080
|
knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
|
|
1981
2081
|
} catch (error) {
|
|
1982
|
-
|
|
2082
|
+
showToast(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.name + ": " + error.message);
|
|
1983
2083
|
return;
|
|
1984
2084
|
}
|
|
1985
2085
|
|
|
@@ -1990,10 +2090,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1990
2090
|
try {
|
|
1991
2091
|
const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
|
|
1992
2092
|
if (result?.error) {
|
|
1993
|
-
|
|
2093
|
+
showToast(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + result.error);
|
|
1994
2094
|
}
|
|
1995
2095
|
} catch (error) {
|
|
1996
|
-
|
|
2096
|
+
showToast(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + error.name + ": " + error.message);
|
|
1997
2097
|
}
|
|
1998
2098
|
},
|
|
1999
2099
|
// description: t("settings.openCameraSettings.description.alt1", { defaultValue: "Open your camera's system settings window to adjust properties like brightness and contrast." }),
|
|
@@ -2015,6 +2115,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2015
2115
|
type: "group",
|
|
2016
2116
|
label: t("settings.sections.general.label", { defaultValue: "General" }),
|
|
2017
2117
|
settings: [
|
|
2118
|
+
{
|
|
2119
|
+
label: t("settings.soundEffects.label", { defaultValue: "Sound effects" }),
|
|
2120
|
+
className: "tracky-mouse-sound-effects",
|
|
2121
|
+
key: "soundEffects",
|
|
2122
|
+
type: "checkbox",
|
|
2123
|
+
default: true,
|
|
2124
|
+
afterInitialLoad: () => {
|
|
2125
|
+
setAudioEnabled(s.soundEffects);
|
|
2126
|
+
},
|
|
2127
|
+
handleSettingChange: () => {
|
|
2128
|
+
setAudioEnabled(s.soundEffects);
|
|
2129
|
+
},
|
|
2130
|
+
description: t("settings.soundEffects.description", { defaultValue: "Plays sounds when you click." }),
|
|
2131
|
+
},
|
|
2018
2132
|
// opposite, "Start paused", might be clearer, especially if I add a "pause" button
|
|
2019
2133
|
{
|
|
2020
2134
|
label: t("settings.startEnabled.label", { defaultValue: "Start enabled" }),
|
|
@@ -2036,7 +2150,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2036
2150
|
// - I considered adding "⚠︎" but it feels a little too alarming
|
|
2037
2151
|
// 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>)",
|
|
2038
2152
|
// 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>)",
|
|
2039
|
-
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>)" }),
|
|
2153
|
+
// 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>)" }),
|
|
2154
|
+
label: t("settings.closeEyesToToggle.label", { defaultValue: "Close eyes to start/stop" }),
|
|
2040
2155
|
className: "tracky-mouse-close-eyes-to-toggle",
|
|
2041
2156
|
key: "closeEyesToToggle",
|
|
2042
2157
|
type: "checkbox",
|
|
@@ -2309,26 +2424,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2309
2424
|
});
|
|
2310
2425
|
}
|
|
2311
2426
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2427
|
+
let canvas = uiContainer.querySelector(".tracky-mouse-canvas");
|
|
2428
|
+
let ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
2314
2429
|
|
|
2315
|
-
|
|
2430
|
+
let debugEyeCanvas = document.createElement("canvas");
|
|
2316
2431
|
debugEyeCanvas.className = "tracky-mouse-debug-eye-canvas";
|
|
2317
2432
|
debugEyeCanvas.style.display = "none";
|
|
2318
|
-
uiContainer.querySelector(".tracky-mouse-
|
|
2319
|
-
|
|
2433
|
+
uiContainer.querySelector(".tracky-mouse-camera-area").appendChild(debugEyeCanvas);
|
|
2434
|
+
let debugEyeCtx = debugEyeCanvas.getContext('2d');
|
|
2320
2435
|
|
|
2321
|
-
|
|
2436
|
+
let pointerEl = document.createElement('div');
|
|
2322
2437
|
pointerEl.className = "tracky-mouse-pointer";
|
|
2323
2438
|
pointerEl.style.display = "none";
|
|
2324
2439
|
document.body.appendChild(pointerEl);
|
|
2325
2440
|
|
|
2326
|
-
|
|
2441
|
+
let cameraVideo = document.createElement('video');
|
|
2327
2442
|
// required to work in iOS 11 & up:
|
|
2328
2443
|
cameraVideo.setAttribute('playsinline', '');
|
|
2329
2444
|
|
|
2445
|
+
let stats;
|
|
2330
2446
|
if (statsJs) {
|
|
2331
|
-
|
|
2447
|
+
stats = new Stats();
|
|
2332
2448
|
stats.domElement.style.position = 'fixed';
|
|
2333
2449
|
stats.domElement.style.top = '0px';
|
|
2334
2450
|
stats.domElement.style.right = '0px';
|
|
@@ -2337,72 +2453,86 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2337
2453
|
}
|
|
2338
2454
|
|
|
2339
2455
|
// Debug flags (not shown in the UI; could become Advanced Settings in the future)
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2456
|
+
let debugAcceleration = false;
|
|
2457
|
+
let showDebugText = false;
|
|
2458
|
+
let showDebugEyeZoom = false;
|
|
2459
|
+
let showDebugHeadTilt = false;
|
|
2460
|
+
let showDebugRegionFilter = false;
|
|
2344
2461
|
|
|
2345
2462
|
// Constants (could become Advanced Settings in the future)
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2463
|
+
let defaultWidth = 640;
|
|
2464
|
+
let defaultHeight = 480;
|
|
2465
|
+
let maxPoints = 1000;
|
|
2466
|
+
let faceScoreThreshold = 0.5;
|
|
2467
|
+
let facemeshOptions = {
|
|
2351
2468
|
maxContinuousChecks: 5,
|
|
2352
2469
|
detectionConfidence: 0.9,
|
|
2353
2470
|
maxFaces: 1,
|
|
2354
2471
|
iouThreshold: 0.3,
|
|
2355
2472
|
scoreThreshold: 0.75
|
|
2356
2473
|
};
|
|
2357
|
-
|
|
2474
|
+
let useFacemesh = true;
|
|
2475
|
+
let sleepGestureEyesClosedDuration = 2000;
|
|
2358
2476
|
// maybe should be based on size of head in view?
|
|
2359
2477
|
const pruningGridSize = 5;
|
|
2360
2478
|
const minDistanceToAddPoint = pruningGridSize * 1.5;
|
|
2361
2479
|
|
|
2362
2480
|
// Head tracking and facial gesture state
|
|
2363
2481
|
// ## Clmtrackr state
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
//
|
|
2368
|
-
|
|
2482
|
+
let face;
|
|
2483
|
+
let faceScore = 0;
|
|
2484
|
+
let faceConvergence = 0;
|
|
2485
|
+
// let faceConvergenceThreshold = 50;
|
|
2486
|
+
let pointsBasedOnFaceScore = 0;
|
|
2369
2487
|
// ## Facemesh state
|
|
2370
2488
|
let detector;
|
|
2371
2489
|
let currentCameraImageData;
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2490
|
+
let facemeshLoaded = false;
|
|
2491
|
+
let facemeshFirstEstimation = true;
|
|
2492
|
+
let facemeshEstimating = false;
|
|
2493
|
+
let facemeshRejectNext = 0;
|
|
2494
|
+
let facemeshPrediction;
|
|
2495
|
+
let facemeshEstimateFaces;
|
|
2496
|
+
let faceInViewConfidenceThreshold = 0.7;
|
|
2497
|
+
let pointsBasedOnFaceInViewConfidence = 0;
|
|
2498
|
+
let cameraFramesSinceFacemeshUpdate = [];
|
|
2499
|
+
let blinkInfo;
|
|
2500
|
+
let mouthInfo;
|
|
2501
|
+
let headTilt = { pitch: 0, yaw: 0, roll: 0 };
|
|
2502
|
+
let headTiltFilters = { pitch: null, yaw: null, roll: null };
|
|
2503
|
+
let sleepGestureProgress = 0;
|
|
2386
2504
|
// ## State related to switching between head trackers
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2505
|
+
let useClmTracking = true;
|
|
2506
|
+
let showClmTracking = useClmTracking;
|
|
2507
|
+
let fallbackTimeoutID;
|
|
2390
2508
|
|
|
2391
2509
|
// Mouse state
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2510
|
+
let mouseX = 0;
|
|
2511
|
+
let mouseY = 0;
|
|
2512
|
+
let buttonStates = {
|
|
2395
2513
|
left: false,
|
|
2396
2514
|
right: false,
|
|
2397
2515
|
middle: false,
|
|
2398
2516
|
};
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2517
|
+
let mouseButtonUntilMouthCloses = -1;
|
|
2518
|
+
let lastMouseDownTime = -Infinity;
|
|
2519
|
+
let mouseNeedsInitPos = true;
|
|
2520
|
+
|
|
2521
|
+
// Virtual display bounds cache (Electron only); covers all connected monitors.
|
|
2522
|
+
let virtualDisplayBounds = null;
|
|
2523
|
+
if (window.electronAPI?.getVirtualDisplayBounds) {
|
|
2524
|
+
window.electronAPI.getVirtualDisplayBounds().then((bounds) => {
|
|
2525
|
+
virtualDisplayBounds = bounds;
|
|
2526
|
+
});
|
|
2527
|
+
window.electronAPI.onVirtualDisplayBoundsChanged?.((bounds) => {
|
|
2528
|
+
virtualDisplayBounds = bounds;
|
|
2529
|
+
mouseNeedsInitPos = true;
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2402
2532
|
|
|
2403
2533
|
// Other state
|
|
2404
|
-
|
|
2405
|
-
|
|
2534
|
+
let paused = true;
|
|
2535
|
+
let pointTracker;
|
|
2406
2536
|
|
|
2407
2537
|
// Named lists of facemesh landmark indices
|
|
2408
2538
|
const MESH_ANNOTATIONS = {
|
|
@@ -2471,14 +2601,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2471
2601
|
|
|
2472
2602
|
try {
|
|
2473
2603
|
detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
|
|
2604
|
+
if (lastShownErrorDetails?.errorClass === "faceLandmarksDetection.createDetector") {
|
|
2605
|
+
errorMessage.hidden = true;
|
|
2606
|
+
}
|
|
2474
2607
|
} catch (error) {
|
|
2475
2608
|
detector = null;
|
|
2476
|
-
// TODO: avoid alert
|
|
2477
2609
|
console.error("Failed to create facemesh detector:", error);
|
|
2478
|
-
|
|
2610
|
+
showError(t("faceDetectorInitError", { defaultValue: "Failed to create face detector" }), error, { errorClass: "faceLandmarksDetection.createDetector" });
|
|
2479
2611
|
}
|
|
2480
2612
|
|
|
2481
2613
|
facemeshLoaded = true;
|
|
2614
|
+
let loggedDetectorError = false;
|
|
2482
2615
|
facemeshEstimateFaces = async () => {
|
|
2483
2616
|
const imageData = currentCameraImageData;//getCameraImageData();
|
|
2484
2617
|
if (!imageData) {
|
|
@@ -2492,11 +2625,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2492
2625
|
}
|
|
2493
2626
|
return faces;
|
|
2494
2627
|
} catch (error) {
|
|
2495
|
-
|
|
2628
|
+
if (!loggedDetectorError) {
|
|
2629
|
+
console.error("Facemesh estimation failed:", error);
|
|
2630
|
+
loggedDetectorError = true;
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
detector?.dispose();
|
|
2634
|
+
} catch (disposeError) {
|
|
2635
|
+
console.error("Failed to dispose facemesh detector after estimation error:", disposeError);
|
|
2636
|
+
}
|
|
2496
2637
|
detector = null;
|
|
2497
|
-
|
|
2498
|
-
console.error("Facemesh estimation failed:", error);
|
|
2499
|
-
alert(error);
|
|
2638
|
+
showError(t("faceDetectorError", { defaultValue: "Face detector error" }), error);
|
|
2500
2639
|
}
|
|
2501
2640
|
return [];
|
|
2502
2641
|
};
|
|
@@ -2514,6 +2653,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2514
2653
|
setting._load?.(settings, initialLoad);
|
|
2515
2654
|
});
|
|
2516
2655
|
}
|
|
2656
|
+
setAudioEnabled(s.soundEffects);
|
|
2517
2657
|
|
|
2518
2658
|
// Now that all settings are loaded, update disabled states
|
|
2519
2659
|
for (const func of functionsToUpdateDisabledStates) {
|
|
@@ -2579,6 +2719,12 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2579
2719
|
}
|
|
2580
2720
|
if (stored) {
|
|
2581
2721
|
deserializeSettings(stored, initialLoad);
|
|
2722
|
+
} else {
|
|
2723
|
+
// HACK: ensure handleInitialLoad is called even for first run
|
|
2724
|
+
// Combined with the below, this feels very redundant, and I'd like to
|
|
2725
|
+
// move to a subscription-based pattern, more of a formal "settings store", something like that.
|
|
2726
|
+
// This is currently necessary for sound effects to work on the first run of the web demo.
|
|
2727
|
+
deserializeSettings(serializeSettings(), initialLoad);
|
|
2582
2728
|
}
|
|
2583
2729
|
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2584
2730
|
// We could just call setOptions in both cases,
|
|
@@ -2692,9 +2838,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2692
2838
|
const settingsLoadedPromise = loadOptions(true);
|
|
2693
2839
|
|
|
2694
2840
|
// Don't use WebGL because clmTracker is our fallback! It's also not much slower than with WebGL.
|
|
2695
|
-
|
|
2841
|
+
let clmTracker = new clm.tracker({ useWebGL: false });
|
|
2696
2842
|
clmTracker.init();
|
|
2697
|
-
|
|
2843
|
+
let clmTrackingStarted = false;
|
|
2698
2844
|
|
|
2699
2845
|
const stopCameraStream = () => {
|
|
2700
2846
|
if (cameraVideo.srcObject) {
|
|
@@ -2720,11 +2866,21 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2720
2866
|
pointsBasedOnFaceScore = 0;
|
|
2721
2867
|
faceScore = 0;
|
|
2722
2868
|
faceConvergence = 0;
|
|
2723
|
-
|
|
2869
|
+
sleepGestureProgress = 0;
|
|
2724
2870
|
updateStartStopButton();
|
|
2725
2871
|
};
|
|
2726
2872
|
|
|
2727
|
-
|
|
2873
|
+
// Handle monkey-patched alert() replacement in face-landmarks-detection.min.js
|
|
2874
|
+
// (Hm, could make it throw instead. Then we wouldn't need this.)
|
|
2875
|
+
window._TrackyMouse_faceLandmarksDetectionAlert = (message) => {
|
|
2876
|
+
// TODO: i18n (it's just one message; we could check for the string (or not) and translate it)
|
|
2877
|
+
// const isContextCreationMessage = message === "Failed to create WebGL canvas context when passing video frame.";
|
|
2878
|
+
errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${message}`;
|
|
2879
|
+
errorMessage.hidden = false;
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2882
|
+
const cameraAccessSlowWarningDelayMS = 5000;
|
|
2883
|
+
let cameraAccessSlowWarningTimeoutID;
|
|
2728
2884
|
useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
|
|
2729
2885
|
// Phases:
|
|
2730
2886
|
// 1. "tryPreferredCamera"
|
|
@@ -2814,8 +2970,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2814
2970
|
delete constraints.video.facingMode;
|
|
2815
2971
|
constraints.video.deviceId = { exact: deviceIdToTry };
|
|
2816
2972
|
}
|
|
2973
|
+
clearTimeout(cameraAccessSlowWarningTimeoutID);
|
|
2974
|
+
errorMessage.hidden = true;
|
|
2975
|
+
cameraAccessSlowWarningTimeoutID = setTimeout(() => {
|
|
2976
|
+
errorMessage.textContent = t("video.status.accessTakingLongerThanExpected", { defaultValue: "Accessing the camera is taking longer than expected..." });
|
|
2977
|
+
errorMessage.hidden = false;
|
|
2978
|
+
}, cameraAccessSlowWarningDelayMS);
|
|
2817
2979
|
console.log("TrackyMouse.useCamera phase", phase, "constraints", constraints);
|
|
2818
2980
|
navigator.mediaDevices.getUserMedia(constraints).then(async (stream) => {
|
|
2981
|
+
clearTimeout(cameraAccessSlowWarningTimeoutID);
|
|
2819
2982
|
if (phase === "justGetPermission") {
|
|
2820
2983
|
for (const track of stream.getTracks()) {
|
|
2821
2984
|
track.stop();
|
|
@@ -2838,6 +3001,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2838
3001
|
useCameraButton.hidden = true;
|
|
2839
3002
|
errorMessage.hidden = true;
|
|
2840
3003
|
}, async (error) => {
|
|
3004
|
+
clearTimeout(cameraAccessSlowWarningTimeoutID);
|
|
2841
3005
|
console.log("TrackyMouse.useCamera phase", phase, "error", error);
|
|
2842
3006
|
if (
|
|
2843
3007
|
phase === "tryPreferredCamera" &&
|
|
@@ -2849,7 +3013,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2849
3013
|
}
|
|
2850
3014
|
if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
|
|
2851
3015
|
// required track is missing
|
|
2852
|
-
|
|
3016
|
+
showError(t("video.errors.noCameraFound", { defaultValue: "No camera found. Please make sure you have a camera connected and enabled." }));
|
|
2853
3017
|
} else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
|
|
2854
3018
|
// webcam is already in use
|
|
2855
3019
|
// or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
|
|
@@ -2857,18 +3021,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2857
3021
|
// (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
|
|
2858
3022
|
// 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
|
|
2859
3023
|
// or "1 camera source detected" preceding it)
|
|
2860
|
-
|
|
3024
|
+
showError(t("video.errors.cameraInUse", { defaultValue: "Webcam is already in use. Please make sure you have no other programs using the camera." }));
|
|
2861
3025
|
} else if (error.name === "AbortError") {
|
|
2862
3026
|
// webcam is likely already in use
|
|
2863
3027
|
// I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
|
|
2864
3028
|
// Update: it definitely isn't, but I can't say exactly what it means in other cases.
|
|
2865
3029
|
// Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
|
|
2866
3030
|
// I really hope that isn't the problem.
|
|
2867
|
-
//
|
|
2868
|
-
|
|
3031
|
+
// showError("Webcam may already be in use. Please make sure you have no other programs using the camera.");
|
|
3032
|
+
showError(t("video.errors.retryAfterClosingOtherPrograms", { defaultValue: "Please make sure no other programs are using the camera and try again." }));
|
|
2869
3033
|
// A more honest/helpful message might be:
|
|
2870
|
-
//
|
|
2871
|
-
//
|
|
3034
|
+
// showError("Please try again and then make sure no other programs are using the camera and try again again.");
|
|
3035
|
+
// showError("Please try again before/after making sure no other programs are using the camera.");
|
|
2872
3036
|
// if it were not to be confusing.
|
|
2873
3037
|
// That is, one could save some time by just hitting the button to try again before trying to figure out of another program is using the camera,
|
|
2874
3038
|
// because sometimes that's enough.
|
|
@@ -2879,36 +3043,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2879
3043
|
// either due to the device not being present, or the ID having changed (don't ask me why that can happen but it can)
|
|
2880
3044
|
// Note: OverconstrainedError has a `constraint` property but not in Firefox so it's not very helpful.
|
|
2881
3045
|
if (constraints.video.deviceId?.exact) {
|
|
2882
|
-
//
|
|
2883
|
-
//
|
|
2884
|
-
//
|
|
2885
|
-
//
|
|
2886
|
-
|
|
3046
|
+
// showError("The previously selected camera is not available. Please select a different camera from the dropdown and try again.");
|
|
3047
|
+
// showError("The previously selected camera is not available. Please mess around with Video > Camera source.");
|
|
3048
|
+
// showError("The previously selected camera is not available. Try changing Video > Camera source.");
|
|
3049
|
+
// showError("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.");
|
|
3050
|
+
showError(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." }));
|
|
2887
3051
|
// It's awkward but that's my best attempt at conveying how you may need to proceed
|
|
2888
3052
|
// without complicated description of how/why the dropdown might be populated with
|
|
2889
3053
|
// fake information until a camera stream is successfully opened.
|
|
2890
3054
|
} else {
|
|
2891
|
-
|
|
3055
|
+
showError(t("video.errors.unsupportedResolution", { defaultValue: "Webcam does not support the required resolution. Please change your settings." }));
|
|
2892
3056
|
}
|
|
2893
3057
|
} else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
|
|
2894
3058
|
// permission denied in browser
|
|
2895
|
-
|
|
3059
|
+
showError(t("video.errors.permissionDenied", { defaultValue: "Permission denied. Please enable access to the camera." }));
|
|
2896
3060
|
} else if (error.name == "TypeError") {
|
|
2897
3061
|
// empty constraints object
|
|
2898
|
-
|
|
3062
|
+
showError(t("video.errors.accessFailed", { defaultValue: "Something went wrong accessing the camera." }), error);
|
|
2899
3063
|
} else {
|
|
2900
3064
|
// other errors
|
|
2901
|
-
|
|
3065
|
+
showError(t("video.errors.accessFailedRetry", { defaultValue: "Something went wrong accessing the camera. Please try again." }), error);
|
|
2902
3066
|
}
|
|
2903
|
-
errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${errorMessage.textContent}`;
|
|
2904
|
-
errorMessage.hidden = false;
|
|
2905
|
-
// Play CSS animation only on retries
|
|
2906
|
-
errorMessage.style.animation = "none";
|
|
2907
|
-
if (showedCameraError) {
|
|
2908
|
-
void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
|
|
2909
|
-
errorMessage.style.animation = "";
|
|
2910
|
-
}
|
|
2911
|
-
showedCameraError = true;
|
|
2912
3067
|
});
|
|
2913
3068
|
};
|
|
2914
3069
|
useDemoFootageButton.onclick = TrackyMouse.useDemoFootage = () => {
|
|
@@ -3008,7 +3163,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3008
3163
|
}
|
|
3009
3164
|
addPoint(x, y) {
|
|
3010
3165
|
if (this.pointCount < maxPoints) {
|
|
3011
|
-
|
|
3166
|
+
let pointIndex = this.pointCount * 2;
|
|
3012
3167
|
this.curXY[pointIndex] = x;
|
|
3013
3168
|
this.curXY[pointIndex + 1] = y;
|
|
3014
3169
|
this.prevXY[pointIndex] = x;
|
|
@@ -3017,8 +3172,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3017
3172
|
}
|
|
3018
3173
|
}
|
|
3019
3174
|
filterPoints(condition) {
|
|
3020
|
-
|
|
3021
|
-
for (
|
|
3175
|
+
let outputPointIndex = 0;
|
|
3176
|
+
for (let inputPointIndex = 0; inputPointIndex < this.pointCount; inputPointIndex++) {
|
|
3022
3177
|
if (condition(inputPointIndex)) {
|
|
3023
3178
|
if (outputPointIndex < inputPointIndex) {
|
|
3024
3179
|
const inputOffset = inputPointIndex * 2;
|
|
@@ -3066,10 +3221,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3066
3221
|
[this.prevPyramid, this.curPyramid] = [this.curPyramid, this.prevPyramid];
|
|
3067
3222
|
|
|
3068
3223
|
// these are options worth breaking out and exploring
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3224
|
+
let winSize = 20;
|
|
3225
|
+
let maxIterations = 30;
|
|
3226
|
+
let epsilon = 0.01;
|
|
3227
|
+
let minEigen = 0.001;
|
|
3073
3228
|
|
|
3074
3229
|
jsfeat.imgproc.grayscale(imageData.data, imageData.width, imageData.height, this.curPyramid.data[0]);
|
|
3075
3230
|
this.curPyramid.build(this.curPyramid.data[0], true);
|
|
@@ -3083,9 +3238,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3083
3238
|
this.prunePoints();
|
|
3084
3239
|
}
|
|
3085
3240
|
draw(ctx) {
|
|
3086
|
-
for (
|
|
3087
|
-
|
|
3088
|
-
//
|
|
3241
|
+
for (let i = 0; i < this.pointCount; i++) {
|
|
3242
|
+
let pointOffset = i * 2;
|
|
3243
|
+
// let distMoved = Math.hypot(
|
|
3089
3244
|
// this.prevXY[pointOffset] - this.curXY[pointOffset],
|
|
3090
3245
|
// this.prevXY[pointOffset + 1] - this.curXY[pointOffset + 1]
|
|
3091
3246
|
// );
|
|
@@ -3103,11 +3258,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3103
3258
|
}
|
|
3104
3259
|
}
|
|
3105
3260
|
getMovement() {
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
for (
|
|
3110
|
-
|
|
3261
|
+
let movementX = 0;
|
|
3262
|
+
let movementY = 0;
|
|
3263
|
+
let numMovements = 0;
|
|
3264
|
+
for (let i = 0; i < this.pointCount; i++) {
|
|
3265
|
+
let pointOffset = i * 2;
|
|
3111
3266
|
movementX += this.curXY[pointOffset] - this.prevXY[pointOffset];
|
|
3112
3267
|
movementY += this.curXY[pointOffset + 1] - this.prevXY[pointOffset + 1];
|
|
3113
3268
|
numMovements += 1;
|
|
@@ -3144,9 +3299,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3144
3299
|
// in order to keep a smooth overall tracking calculation,
|
|
3145
3300
|
// don't add points if they're close to an existing point.
|
|
3146
3301
|
// Otherwise, it would not just be redundant, but often remove the older points, in the pruning.
|
|
3147
|
-
for (
|
|
3148
|
-
|
|
3149
|
-
//
|
|
3302
|
+
for (let pointIndex = 0; pointIndex < oops.pointCount; pointIndex++) {
|
|
3303
|
+
let pointOffset = pointIndex * 2;
|
|
3304
|
+
// let distance = Math.hypot(
|
|
3150
3305
|
// x - oops.curXY[pointOffset],
|
|
3151
3306
|
// y - oops.curXY[pointOffset + 1]
|
|
3152
3307
|
// );
|
|
@@ -3183,6 +3338,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3183
3338
|
return ((px - x1) * nx + (py - y1) * ny) / Math.hypot(nx, ny);
|
|
3184
3339
|
}
|
|
3185
3340
|
|
|
3341
|
+
let lastTimestamp = -Infinity;
|
|
3186
3342
|
function draw(update = true) {
|
|
3187
3343
|
ctx.resetTransform(); // in case there is an error, don't flip constantly back and forth due to mirroring
|
|
3188
3344
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // in case there's no footage
|
|
@@ -3197,6 +3353,13 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3197
3353
|
ctx.drawImage(cameraVideo, 0, 0, canvas.width, canvas.height);
|
|
3198
3354
|
}
|
|
3199
3355
|
|
|
3356
|
+
const timestamp = performance.now();
|
|
3357
|
+
const deltaTime = Math.min(timestamp - lastTimestamp, 100);
|
|
3358
|
+
lastTimestamp = timestamp;
|
|
3359
|
+
|
|
3360
|
+
sleepSweep?.setEnabled(s.closeEyesToToggle);
|
|
3361
|
+
sleepSweep?.update(sleepGestureProgress);
|
|
3362
|
+
|
|
3200
3363
|
if (!pointTracker) {
|
|
3201
3364
|
return;
|
|
3202
3365
|
}
|
|
@@ -3332,39 +3495,77 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3332
3495
|
|
|
3333
3496
|
// TODO: separate confidence threshold for removing vs adding points?
|
|
3334
3497
|
|
|
3498
|
+
|
|
3335
3499
|
// cull points to those within useful facial region
|
|
3336
|
-
|
|
3337
|
-
|
|
3500
|
+
function regionFilter([x, y]) {
|
|
3501
|
+
|
|
3338
3502
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3339
|
-
|
|
3340
|
-
(annotations.noseTip[0][0] -
|
|
3341
|
-
annotations.noseTip[0][1] -
|
|
3503
|
+
let distance = Math.hypot(
|
|
3504
|
+
(annotations.noseTip[0][0] - x) * 1.4,
|
|
3505
|
+
annotations.noseTip[0][1] - y
|
|
3342
3506
|
);
|
|
3343
|
-
|
|
3507
|
+
let headSize = Math.hypot(
|
|
3344
3508
|
annotations.leftCheek[0][0] - annotations.rightCheek[0][0],
|
|
3345
3509
|
annotations.leftCheek[0][1] - annotations.rightCheek[0][1]
|
|
3346
3510
|
);
|
|
3347
3511
|
if (distance > headSize) {
|
|
3348
3512
|
return false;
|
|
3349
3513
|
}
|
|
3514
|
+
// Avoid mouth affecting pointer position.
|
|
3515
|
+
distance = annotations.lipsLowerInner.map((lipPoint) =>
|
|
3516
|
+
Math.min(
|
|
3517
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] - y),
|
|
3518
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.1 - y), // a bit below too
|
|
3519
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.2 - y), // a bit below too
|
|
3520
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.3 - y), // a bit below too
|
|
3521
|
+
Math.hypot(lipPoint[0] - x, lipPoint[1] + headSize * 0.4 - y), // a bit below too (yeah I'm being a little lazy here)
|
|
3522
|
+
)
|
|
3523
|
+
).reduce((a, b) => Math.min(a, b), Infinity);
|
|
3524
|
+
if (distance < headSize * 0.1) {
|
|
3525
|
+
return false;
|
|
3526
|
+
}
|
|
3350
3527
|
// Avoid blinking eyes affecting pointer position.
|
|
3351
3528
|
// distance to outer corners of eyes
|
|
3352
3529
|
distance = Math.min(
|
|
3353
3530
|
Math.hypot(
|
|
3354
|
-
annotations.leftEyeLower0[0][0] -
|
|
3355
|
-
annotations.leftEyeLower0[0][1] -
|
|
3531
|
+
annotations.leftEyeLower0[0][0] - x,
|
|
3532
|
+
annotations.leftEyeLower0[0][1] - y
|
|
3356
3533
|
),
|
|
3357
3534
|
Math.hypot(
|
|
3358
|
-
annotations.rightEyeLower0[0][0] -
|
|
3359
|
-
annotations.rightEyeLower0[0][1] -
|
|
3535
|
+
annotations.rightEyeLower0[0][0] - x,
|
|
3536
|
+
annotations.rightEyeLower0[0][1] - y
|
|
3360
3537
|
),
|
|
3361
3538
|
);
|
|
3362
3539
|
if (distance < headSize * 0.42) {
|
|
3363
3540
|
return false;
|
|
3364
3541
|
}
|
|
3365
3542
|
return true;
|
|
3543
|
+
}
|
|
3544
|
+
pointTracker.filterPoints((pointIndex) => {
|
|
3545
|
+
let pointOffset = pointIndex * 2;
|
|
3546
|
+
const point = [pointTracker.curXY[pointOffset], pointTracker.curXY[pointOffset + 1]];
|
|
3547
|
+
return regionFilter(point);
|
|
3366
3548
|
});
|
|
3367
3549
|
|
|
3550
|
+
// Debug visualization for region filter (a sort of heatmap of where points will be culled)
|
|
3551
|
+
if (showDebugRegionFilter) {
|
|
3552
|
+
ctx.save();
|
|
3553
|
+
if (s.mirror) {
|
|
3554
|
+
ctx.translate(canvas.width, 0);
|
|
3555
|
+
ctx.scale(-1, 1);
|
|
3556
|
+
}
|
|
3557
|
+
ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
|
|
3558
|
+
const vizStep = 4;
|
|
3559
|
+
for (let x = 0; x < canvas.width; x += vizStep) {
|
|
3560
|
+
for (let y = 0; y < canvas.height; y += vizStep) {
|
|
3561
|
+
if (!regionFilter([x, y])) {
|
|
3562
|
+
ctx.fillRect(x - 5, y - 5, vizStep, vizStep);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
ctx.restore();
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3368
3569
|
const keypoints = facemeshPrediction.keypoints;
|
|
3369
3570
|
if (keypoints) {
|
|
3370
3571
|
const top = keypoints[10]; // Top of forehead
|
|
@@ -3546,16 +3747,19 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3546
3747
|
|
|
3547
3748
|
blinkInfo = detectBlinks();
|
|
3548
3749
|
mouthInfo = detectMouthOpen();
|
|
3549
|
-
if (blinkInfo.rightEye.open
|
|
3550
|
-
|
|
3750
|
+
if (!blinkInfo.rightEye.open && !blinkInfo.leftEye.open) {
|
|
3751
|
+
sleepGestureProgress += deltaTime / sleepGestureEyesClosedDuration;
|
|
3752
|
+
sleepGestureProgress = Math.min(sleepGestureProgress, 1);
|
|
3753
|
+
} else {
|
|
3754
|
+
sleepGestureProgress -= deltaTime / sleepGestureEyesClosedDuration;
|
|
3755
|
+
sleepGestureProgress = Math.max(sleepGestureProgress, 0);
|
|
3551
3756
|
}
|
|
3552
|
-
if (
|
|
3757
|
+
if (sleepGestureProgress >= 1) {
|
|
3758
|
+
sleepGestureProgress = 0;
|
|
3553
3759
|
if (s.closeEyesToToggle) {
|
|
3554
3760
|
paused = !paused;
|
|
3555
3761
|
updatePaused();
|
|
3556
|
-
|
|
3557
|
-
// TODO: try to keep variable names meaningful
|
|
3558
|
-
lastTimeWhenAnEyeWasOpen = Infinity;
|
|
3762
|
+
sleepSweep?.sleepModeWasToggled(paused);
|
|
3559
3763
|
}
|
|
3560
3764
|
}
|
|
3561
3765
|
|
|
@@ -3617,10 +3821,41 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3617
3821
|
|
|
3618
3822
|
const buttonNames = ["left", "middle", "right"];
|
|
3619
3823
|
for (let buttonIndex = 0; buttonIndex < 3; buttonIndex++) {
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3824
|
+
const buttonIsActive = clickButton === buttonIndex;
|
|
3825
|
+
if (buttonIsActive !== buttonStates[buttonNames[buttonIndex]]) {
|
|
3826
|
+
// Wait for confirmation of the button state change before playing SFX
|
|
3827
|
+
// but not before updating buttonStates, since we check that in this loop
|
|
3828
|
+
// to decide whether to call setMouseButtonState.
|
|
3829
|
+
// We don't want to send extraneous mouse button changes to the main process,
|
|
3830
|
+
// even if it does track button states itself. If nothing else it's wasted IPC.
|
|
3831
|
+
// That said, an argument could be made for updating lastMouseDownTime later
|
|
3832
|
+
// if the IPC is slow, to extend the time frame for making a simple click
|
|
3833
|
+
// rather than a drag.
|
|
3834
|
+
if (!setMouseButtonState) {
|
|
3835
|
+
console.warn("setMouseButtonState function not provided");
|
|
3836
|
+
} else {
|
|
3837
|
+
const maybeAPromise = setMouseButtonState(buttonIndex, buttonIsActive);
|
|
3838
|
+
const playSoundForButton = (changedButtonState) => {
|
|
3839
|
+
if (changedButtonState) {
|
|
3840
|
+
if (buttonIndex === 1) {
|
|
3841
|
+
playSound(buttonIsActive ? "middleClickPress" : "middleClickRelease", {
|
|
3842
|
+
volume: 4,
|
|
3843
|
+
});
|
|
3844
|
+
} else {
|
|
3845
|
+
playSound(buttonIsActive ? "clickPress" : "clickRelease", {
|
|
3846
|
+
playbackRate: buttonIndex === 0 ? 1 : buttonIndex === 2 ? 1.2 : 1.5,
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
if (maybeAPromise instanceof Promise) {
|
|
3852
|
+
maybeAPromise.then(playSoundForButton);
|
|
3853
|
+
} else {
|
|
3854
|
+
playSoundForButton(maybeAPromise);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
buttonStates[buttonNames[buttonIndex]] = buttonIsActive;
|
|
3858
|
+
if (buttonIsActive) {
|
|
3624
3859
|
lastMouseDownTime = performance.now();
|
|
3625
3860
|
} else {
|
|
3626
3861
|
// Limit "Delay Before Dragging" effect to the duration of a click.
|
|
@@ -3893,14 +4128,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3893
4128
|
|
|
3894
4129
|
// cull points to those within useful facial region
|
|
3895
4130
|
pointTracker.filterPoints((pointIndex) => {
|
|
3896
|
-
|
|
4131
|
+
let pointOffset = pointIndex * 2;
|
|
3897
4132
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3898
|
-
|
|
4133
|
+
let distance = Math.hypot(
|
|
3899
4134
|
(face[62][0] - pointTracker.curXY[pointOffset]) * 1.4,
|
|
3900
4135
|
face[62][1] - pointTracker.curXY[pointOffset + 1]
|
|
3901
4136
|
);
|
|
3902
4137
|
// distance based on outer eye corners
|
|
3903
|
-
|
|
4138
|
+
let headSize = Math.hypot(
|
|
3904
4139
|
face[23][0] - face[28][0],
|
|
3905
4140
|
face[23][1] - face[28][1]
|
|
3906
4141
|
);
|
|
@@ -3925,21 +4160,23 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3925
4160
|
pointTracker.draw(debugPointsCtx);
|
|
3926
4161
|
|
|
3927
4162
|
if (update) {
|
|
3928
|
-
const screenWidth = window.electronAPI ? screen.width : innerWidth;
|
|
3929
|
-
const screenHeight = window.electronAPI ? screen.height : innerHeight;
|
|
4163
|
+
const screenWidth = window.electronAPI ? (virtualDisplayBounds?.width ?? screen.width) : innerWidth;
|
|
4164
|
+
const screenHeight = window.electronAPI ? (virtualDisplayBounds?.height ?? screen.height) : innerHeight;
|
|
4165
|
+
const screenOffsetX = window.electronAPI ? (virtualDisplayBounds?.x ?? 0) : 0;
|
|
4166
|
+
const screenOffsetY = window.electronAPI ? (virtualDisplayBounds?.y ?? 0) : 0;
|
|
3930
4167
|
|
|
3931
|
-
|
|
4168
|
+
let [movementX, movementY] = pointTracker.getMovement();
|
|
3932
4169
|
|
|
3933
4170
|
// Acceleration curves add a lot of stability,
|
|
3934
4171
|
// letting you focus on a specific point without jitter, but still move quickly.
|
|
3935
4172
|
|
|
3936
|
-
//
|
|
3937
|
-
//
|
|
3938
|
-
|
|
4173
|
+
// let accelerate = (delta, distance) => (delta / 10) * (distance ** 0.8);
|
|
4174
|
+
// let accelerate = (delta, distance) => (delta / 1) * (Math.abs(delta) ** 0.8);
|
|
4175
|
+
let accelerate = (delta, _distance) => (delta / 1) * (Math.abs(delta * 5) ** s.headTrackingAcceleration);
|
|
3939
4176
|
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
4177
|
+
let distance = Math.hypot(movementX, movementY);
|
|
4178
|
+
let deltaX = accelerate(movementX * s.headTrackingSensitivityX, distance);
|
|
4179
|
+
let deltaY = accelerate(movementY * s.headTrackingSensitivityY, distance);
|
|
3943
4180
|
|
|
3944
4181
|
if (s.headTrackingTiltInfluence > 0) {
|
|
3945
4182
|
const yawRange = [
|
|
@@ -4056,13 +4293,13 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4056
4293
|
mouseX -= deltaX * screenWidth;
|
|
4057
4294
|
mouseY += deltaY * screenHeight;
|
|
4058
4295
|
|
|
4059
|
-
mouseX = Math.min(Math.max(
|
|
4060
|
-
mouseY = Math.min(Math.max(
|
|
4296
|
+
mouseX = Math.min(Math.max(screenOffsetX, mouseX), screenOffsetX + screenWidth);
|
|
4297
|
+
mouseY = Math.min(Math.max(screenOffsetY, mouseY), screenOffsetY + screenHeight);
|
|
4061
4298
|
|
|
4062
4299
|
if (mouseNeedsInitPos) {
|
|
4063
4300
|
// TODO: option to get preexisting mouse position instead of set it to center of screen
|
|
4064
|
-
mouseX = screenWidth / 2;
|
|
4065
|
-
mouseY = screenHeight / 2;
|
|
4301
|
+
mouseX = screenOffsetX + screenWidth / 2;
|
|
4302
|
+
mouseY = screenOffsetY + screenHeight / 2;
|
|
4066
4303
|
mouseNeedsInitPos = false;
|
|
4067
4304
|
}
|
|
4068
4305
|
if (window.electronAPI) {
|
|
@@ -4112,7 +4349,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4112
4349
|
|
|
4113
4350
|
// 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)
|
|
4114
4351
|
const iid = setInterval(function animationLoop() {
|
|
4115
|
-
draw(!paused || document.visibilityState === "visible");
|
|
4352
|
+
draw(!paused || document.visibilityState === "visible" || isDesktopApp);
|
|
4116
4353
|
}, 15);
|
|
4117
4354
|
|
|
4118
4355
|
let autoDemo = false;
|
|
@@ -4191,6 +4428,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4191
4428
|
_waitForSettingsLoaded() {
|
|
4192
4429
|
return settingsLoadedPromise;
|
|
4193
4430
|
},
|
|
4431
|
+
get _facemeshPrediction() {
|
|
4432
|
+
return facemeshPrediction;
|
|
4433
|
+
},
|
|
4434
|
+
get _headTilt() {
|
|
4435
|
+
return headTilt;
|
|
4436
|
+
},
|
|
4437
|
+
get _video() {
|
|
4438
|
+
return cameraVideo;
|
|
4439
|
+
},
|
|
4194
4440
|
dispose() {
|
|
4195
4441
|
// TODO: re-structure so that cleanup can succeed even if initialization fails
|
|
4196
4442
|
// OOP would help with this, by storing references in an object, but it doesn't necessarily
|
|
@@ -4285,11 +4531,13 @@ TrackyMouse.init = function (div, opts = {}) {
|
|
|
4285
4531
|
|
|
4286
4532
|
createInner();
|
|
4287
4533
|
|
|
4288
|
-
return {
|
|
4289
|
-
|
|
4290
|
-
inner
|
|
4291
|
-
|
|
4292
|
-
|
|
4534
|
+
return new Proxy({}, {
|
|
4535
|
+
get(_target, prop) {
|
|
4536
|
+
if (prop in inner) {
|
|
4537
|
+
return inner[prop];
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4293
4541
|
|
|
4294
4542
|
};
|
|
4295
4543
|
|
|
@@ -4297,21 +4545,24 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4297
4545
|
|
|
4298
4546
|
const template = `
|
|
4299
4547
|
<div class="tracky-mouse-hide-near-cursor">
|
|
4300
|
-
<div
|
|
4301
|
-
<div class="tracky-mouse-
|
|
4302
|
-
<
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
<
|
|
4548
|
+
<div id="tracky-mouse-screen-overlay-work-area">
|
|
4549
|
+
<div class="tracky-mouse-absolute-center">
|
|
4550
|
+
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
|
|
4551
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
|
|
4552
|
+
</div>
|
|
4553
|
+
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
|
|
4554
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/head-not-found.svg" alt="head not found" width="128" height="128">
|
|
4555
|
+
</div>
|
|
4306
4556
|
</div>
|
|
4557
|
+
<div id="tracky-mouse-screen-overlay-message"></div>
|
|
4307
4558
|
</div>
|
|
4308
|
-
<div id="tracky-mouse-screen-overlay-message"></div>
|
|
4309
4559
|
</div>
|
|
4310
4560
|
`;
|
|
4311
4561
|
const fragment = document.createRange().createContextualFragment(template);
|
|
4312
4562
|
document.body.appendChild(fragment);
|
|
4313
4563
|
|
|
4314
4564
|
const message = document.getElementById("tracky-mouse-screen-overlay-message");
|
|
4565
|
+
const workAreaContainer = document.getElementById("tracky-mouse-screen-overlay-work-area");
|
|
4315
4566
|
message.dir = "auto";
|
|
4316
4567
|
|
|
4317
4568
|
const hideNearCursorEls = document.querySelectorAll(".tracky-mouse-hide-near-cursor");
|
|
@@ -4358,9 +4609,30 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4358
4609
|
}
|
|
4359
4610
|
|
|
4360
4611
|
function update(data) {
|
|
4361
|
-
const {
|
|
4362
|
-
|
|
4363
|
-
|
|
4612
|
+
const {
|
|
4613
|
+
messageText,
|
|
4614
|
+
isEnabled,
|
|
4615
|
+
isManualTakeback,
|
|
4616
|
+
inputFeedback,
|
|
4617
|
+
workAreaContainerBounds,
|
|
4618
|
+
bottomOffset,
|
|
4619
|
+
systemMousePosition,
|
|
4620
|
+
} = data;
|
|
4621
|
+
|
|
4622
|
+
if (workAreaContainerBounds) {
|
|
4623
|
+
workAreaContainer.style.left = `${workAreaContainerBounds.x}px`;
|
|
4624
|
+
workAreaContainer.style.top = `${workAreaContainerBounds.y}px`;
|
|
4625
|
+
workAreaContainer.style.width = `${workAreaContainerBounds.width}px`;
|
|
4626
|
+
workAreaContainer.style.height = `${workAreaContainerBounds.height}px`;
|
|
4627
|
+
message.style.bottom = "0px";
|
|
4628
|
+
} else {
|
|
4629
|
+
// bottomOffset was a never-released part of an unstable API.
|
|
4630
|
+
// workAreaContainerBounds could be made required, just like bottomOffset was.
|
|
4631
|
+
workAreaContainer.style.left = "0px";
|
|
4632
|
+
workAreaContainer.style.top = "0px";
|
|
4633
|
+
workAreaContainer.style.width = "100%";
|
|
4634
|
+
workAreaContainer.style.height = `calc(100% - ${bottomOffset ?? 0}px)`;
|
|
4635
|
+
}
|
|
4364
4636
|
|
|
4365
4637
|
// Other diagnostics in the future would be stuff like:
|
|
4366
4638
|
// - head too far away (smaller than a certain size) https://github.com/1j01/tracky-mouse/issues/49
|