tracky-mouse 2.6.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/locales/ar/translation.json +3 -1
- package/locales/ar-EG/translation.json +3 -1
- package/locales/bg/translation.json +3 -1
- package/locales/bn/translation.json +3 -1
- package/locales/ca/translation.json +3 -1
- package/locales/ce/translation.json +3 -1
- package/locales/ceb/translation.json +3 -1
- package/locales/cs/translation.json +3 -1
- package/locales/da/translation.json +3 -1
- package/locales/de/translation.json +3 -1
- package/locales/el/translation.json +3 -1
- package/locales/emoji/translation.json +3 -1
- package/locales/en/translation.json +3 -1
- package/locales/eo/translation.json +3 -1
- package/locales/es/translation.json +3 -1
- package/locales/eu/translation.json +3 -1
- package/locales/fa/translation.json +3 -1
- package/locales/fi/translation.json +3 -1
- package/locales/fr/translation.json +3 -1
- package/locales/gu/translation.json +3 -1
- package/locales/ha/translation.json +3 -1
- package/locales/he/translation.json +3 -1
- package/locales/hi/translation.json +3 -1
- package/locales/hr/translation.json +3 -1
- package/locales/hu/translation.json +3 -1
- package/locales/hy/translation.json +3 -1
- package/locales/id/translation.json +3 -1
- package/locales/it/translation.json +3 -1
- package/locales/ja/translation.json +3 -1
- package/locales/jv/translation.json +3 -1
- package/locales/ko/translation.json +3 -1
- package/locales/mr/translation.json +3 -1
- package/locales/ms/translation.json +3 -1
- package/locales/nan/translation.json +3 -1
- package/locales/nb/translation.json +3 -1
- package/locales/nl/translation.json +3 -1
- package/locales/pa/translation.json +3 -1
- package/locales/pl/translation.json +3 -1
- package/locales/pt/translation.json +3 -1
- package/locales/pt-BR/translation.json +3 -1
- package/locales/ro/translation.json +3 -1
- package/locales/ru/translation.json +3 -1
- package/locales/sk/translation.json +3 -1
- package/locales/sl/translation.json +3 -1
- package/locales/sr/translation.json +3 -1
- package/locales/sv/translation.json +3 -1
- package/locales/sw/translation.json +3 -1
- package/locales/ta/translation.json +3 -1
- package/locales/te/translation.json +3 -1
- package/locales/th/translation.json +3 -1
- package/locales/tl/translation.json +3 -1
- package/locales/tr/translation.json +3 -1
- package/locales/tt/translation.json +3 -1
- package/locales/uk/translation.json +3 -1
- package/locales/ur/translation.json +3 -1
- package/locales/uz/translation.json +3 -1
- package/locales/vi/translation.json +3 -1
- package/locales/war/translation.json +3 -1
- package/locales/zh/translation.json +3 -1
- package/locales/zh-simplified/translation.json +3 -1
- package/package.json +3 -1
- package/tracky-mouse.css +1 -1
- package/tracky-mouse.js +292 -128
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("./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";
|
|
@@ -1631,12 +1690,12 @@ 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');
|
|
1640
1699
|
|
|
1641
1700
|
// Settings (initialized later; defaults are defined in settingsCategories)
|
|
1642
1701
|
const s = {};
|
|
@@ -2015,6 +2074,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2015
2074
|
type: "group",
|
|
2016
2075
|
label: t("settings.sections.general.label", { defaultValue: "General" }),
|
|
2017
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
|
+
},
|
|
2018
2091
|
// opposite, "Start paused", might be clearer, especially if I add a "pause" button
|
|
2019
2092
|
{
|
|
2020
2093
|
label: t("settings.startEnabled.label", { defaultValue: "Start enabled" }),
|
|
@@ -2036,7 +2109,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2036
2109
|
// - I considered adding "⚠︎" but it feels a little too alarming
|
|
2037
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>)",
|
|
2038
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>)",
|
|
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>)" }),
|
|
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" }),
|
|
2040
2114
|
className: "tracky-mouse-close-eyes-to-toggle",
|
|
2041
2115
|
key: "closeEyesToToggle",
|
|
2042
2116
|
type: "checkbox",
|
|
@@ -2309,26 +2383,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2309
2383
|
});
|
|
2310
2384
|
}
|
|
2311
2385
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2386
|
+
let canvas = uiContainer.querySelector(".tracky-mouse-canvas");
|
|
2387
|
+
let ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
2314
2388
|
|
|
2315
|
-
|
|
2389
|
+
let debugEyeCanvas = document.createElement("canvas");
|
|
2316
2390
|
debugEyeCanvas.className = "tracky-mouse-debug-eye-canvas";
|
|
2317
2391
|
debugEyeCanvas.style.display = "none";
|
|
2318
2392
|
uiContainer.querySelector(".tracky-mouse-canvas-container-container").appendChild(debugEyeCanvas);
|
|
2319
|
-
|
|
2393
|
+
let debugEyeCtx = debugEyeCanvas.getContext('2d');
|
|
2320
2394
|
|
|
2321
|
-
|
|
2395
|
+
let pointerEl = document.createElement('div');
|
|
2322
2396
|
pointerEl.className = "tracky-mouse-pointer";
|
|
2323
2397
|
pointerEl.style.display = "none";
|
|
2324
2398
|
document.body.appendChild(pointerEl);
|
|
2325
2399
|
|
|
2326
|
-
|
|
2400
|
+
let cameraVideo = document.createElement('video');
|
|
2327
2401
|
// required to work in iOS 11 & up:
|
|
2328
2402
|
cameraVideo.setAttribute('playsinline', '');
|
|
2329
2403
|
|
|
2404
|
+
let stats;
|
|
2330
2405
|
if (statsJs) {
|
|
2331
|
-
|
|
2406
|
+
stats = new Stats();
|
|
2332
2407
|
stats.domElement.style.position = 'fixed';
|
|
2333
2408
|
stats.domElement.style.top = '0px';
|
|
2334
2409
|
stats.domElement.style.right = '0px';
|
|
@@ -2337,72 +2412,74 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2337
2412
|
}
|
|
2338
2413
|
|
|
2339
2414
|
// Debug flags (not shown in the UI; could become Advanced Settings in the future)
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2415
|
+
let debugAcceleration = false;
|
|
2416
|
+
let showDebugText = false;
|
|
2417
|
+
let showDebugEyeZoom = false;
|
|
2418
|
+
let showDebugHeadTilt = false;
|
|
2419
|
+
let showDebugRegionFilter = false;
|
|
2344
2420
|
|
|
2345
2421
|
// Constants (could become Advanced Settings in the future)
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2422
|
+
let defaultWidth = 640;
|
|
2423
|
+
let defaultHeight = 480;
|
|
2424
|
+
let maxPoints = 1000;
|
|
2425
|
+
let faceScoreThreshold = 0.5;
|
|
2426
|
+
let facemeshOptions = {
|
|
2351
2427
|
maxContinuousChecks: 5,
|
|
2352
2428
|
detectionConfidence: 0.9,
|
|
2353
2429
|
maxFaces: 1,
|
|
2354
2430
|
iouThreshold: 0.3,
|
|
2355
2431
|
scoreThreshold: 0.75
|
|
2356
2432
|
};
|
|
2357
|
-
|
|
2433
|
+
let useFacemesh = true;
|
|
2434
|
+
let sleepGestureEyesClosedDuration = 2000;
|
|
2358
2435
|
// maybe should be based on size of head in view?
|
|
2359
2436
|
const pruningGridSize = 5;
|
|
2360
2437
|
const minDistanceToAddPoint = pruningGridSize * 1.5;
|
|
2361
2438
|
|
|
2362
2439
|
// Head tracking and facial gesture state
|
|
2363
2440
|
// ## Clmtrackr state
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
//
|
|
2368
|
-
|
|
2441
|
+
let face;
|
|
2442
|
+
let faceScore = 0;
|
|
2443
|
+
let faceConvergence = 0;
|
|
2444
|
+
// let faceConvergenceThreshold = 50;
|
|
2445
|
+
let pointsBasedOnFaceScore = 0;
|
|
2369
2446
|
// ## Facemesh state
|
|
2370
2447
|
let detector;
|
|
2371
2448
|
let currentCameraImageData;
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
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;
|
|
2386
2463
|
// ## State related to switching between head trackers
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2464
|
+
let useClmTracking = true;
|
|
2465
|
+
let showClmTracking = useClmTracking;
|
|
2466
|
+
let fallbackTimeoutID;
|
|
2390
2467
|
|
|
2391
2468
|
// Mouse state
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2469
|
+
let mouseX = 0;
|
|
2470
|
+
let mouseY = 0;
|
|
2471
|
+
let buttonStates = {
|
|
2395
2472
|
left: false,
|
|
2396
2473
|
right: false,
|
|
2397
2474
|
middle: false,
|
|
2398
2475
|
};
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2476
|
+
let mouseButtonUntilMouthCloses = -1;
|
|
2477
|
+
let lastMouseDownTime = -Infinity;
|
|
2478
|
+
let mouseNeedsInitPos = true;
|
|
2402
2479
|
|
|
2403
2480
|
// Other state
|
|
2404
|
-
|
|
2405
|
-
|
|
2481
|
+
let paused = true;
|
|
2482
|
+
let pointTracker;
|
|
2406
2483
|
|
|
2407
2484
|
// Named lists of facemesh landmark indices
|
|
2408
2485
|
const MESH_ANNOTATIONS = {
|
|
@@ -2514,6 +2591,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2514
2591
|
setting._load?.(settings, initialLoad);
|
|
2515
2592
|
});
|
|
2516
2593
|
}
|
|
2594
|
+
setAudioEnabled(s.soundEffects);
|
|
2517
2595
|
|
|
2518
2596
|
// Now that all settings are loaded, update disabled states
|
|
2519
2597
|
for (const func of functionsToUpdateDisabledStates) {
|
|
@@ -2579,6 +2657,12 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2579
2657
|
}
|
|
2580
2658
|
if (stored) {
|
|
2581
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);
|
|
2582
2666
|
}
|
|
2583
2667
|
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2584
2668
|
// We could just call setOptions in both cases,
|
|
@@ -2692,9 +2776,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2692
2776
|
const settingsLoadedPromise = loadOptions(true);
|
|
2693
2777
|
|
|
2694
2778
|
// Don't use WebGL because clmTracker is our fallback! It's also not much slower than with WebGL.
|
|
2695
|
-
|
|
2779
|
+
let clmTracker = new clm.tracker({ useWebGL: false });
|
|
2696
2780
|
clmTracker.init();
|
|
2697
|
-
|
|
2781
|
+
let clmTrackingStarted = false;
|
|
2698
2782
|
|
|
2699
2783
|
const stopCameraStream = () => {
|
|
2700
2784
|
if (cameraVideo.srcObject) {
|
|
@@ -2720,7 +2804,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2720
2804
|
pointsBasedOnFaceScore = 0;
|
|
2721
2805
|
faceScore = 0;
|
|
2722
2806
|
faceConvergence = 0;
|
|
2723
|
-
|
|
2807
|
+
sleepGestureProgress = 0;
|
|
2724
2808
|
updateStartStopButton();
|
|
2725
2809
|
};
|
|
2726
2810
|
|
|
@@ -3008,7 +3092,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3008
3092
|
}
|
|
3009
3093
|
addPoint(x, y) {
|
|
3010
3094
|
if (this.pointCount < maxPoints) {
|
|
3011
|
-
|
|
3095
|
+
let pointIndex = this.pointCount * 2;
|
|
3012
3096
|
this.curXY[pointIndex] = x;
|
|
3013
3097
|
this.curXY[pointIndex + 1] = y;
|
|
3014
3098
|
this.prevXY[pointIndex] = x;
|
|
@@ -3017,8 +3101,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3017
3101
|
}
|
|
3018
3102
|
}
|
|
3019
3103
|
filterPoints(condition) {
|
|
3020
|
-
|
|
3021
|
-
for (
|
|
3104
|
+
let outputPointIndex = 0;
|
|
3105
|
+
for (let inputPointIndex = 0; inputPointIndex < this.pointCount; inputPointIndex++) {
|
|
3022
3106
|
if (condition(inputPointIndex)) {
|
|
3023
3107
|
if (outputPointIndex < inputPointIndex) {
|
|
3024
3108
|
const inputOffset = inputPointIndex * 2;
|
|
@@ -3066,10 +3150,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3066
3150
|
[this.prevPyramid, this.curPyramid] = [this.curPyramid, this.prevPyramid];
|
|
3067
3151
|
|
|
3068
3152
|
// these are options worth breaking out and exploring
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3153
|
+
let winSize = 20;
|
|
3154
|
+
let maxIterations = 30;
|
|
3155
|
+
let epsilon = 0.01;
|
|
3156
|
+
let minEigen = 0.001;
|
|
3073
3157
|
|
|
3074
3158
|
jsfeat.imgproc.grayscale(imageData.data, imageData.width, imageData.height, this.curPyramid.data[0]);
|
|
3075
3159
|
this.curPyramid.build(this.curPyramid.data[0], true);
|
|
@@ -3083,9 +3167,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3083
3167
|
this.prunePoints();
|
|
3084
3168
|
}
|
|
3085
3169
|
draw(ctx) {
|
|
3086
|
-
for (
|
|
3087
|
-
|
|
3088
|
-
//
|
|
3170
|
+
for (let i = 0; i < this.pointCount; i++) {
|
|
3171
|
+
let pointOffset = i * 2;
|
|
3172
|
+
// let distMoved = Math.hypot(
|
|
3089
3173
|
// this.prevXY[pointOffset] - this.curXY[pointOffset],
|
|
3090
3174
|
// this.prevXY[pointOffset + 1] - this.curXY[pointOffset + 1]
|
|
3091
3175
|
// );
|
|
@@ -3103,11 +3187,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3103
3187
|
}
|
|
3104
3188
|
}
|
|
3105
3189
|
getMovement() {
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
for (
|
|
3110
|
-
|
|
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;
|
|
3111
3195
|
movementX += this.curXY[pointOffset] - this.prevXY[pointOffset];
|
|
3112
3196
|
movementY += this.curXY[pointOffset + 1] - this.prevXY[pointOffset + 1];
|
|
3113
3197
|
numMovements += 1;
|
|
@@ -3144,9 +3228,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3144
3228
|
// in order to keep a smooth overall tracking calculation,
|
|
3145
3229
|
// don't add points if they're close to an existing point.
|
|
3146
3230
|
// Otherwise, it would not just be redundant, but often remove the older points, in the pruning.
|
|
3147
|
-
for (
|
|
3148
|
-
|
|
3149
|
-
//
|
|
3231
|
+
for (let pointIndex = 0; pointIndex < oops.pointCount; pointIndex++) {
|
|
3232
|
+
let pointOffset = pointIndex * 2;
|
|
3233
|
+
// let distance = Math.hypot(
|
|
3150
3234
|
// x - oops.curXY[pointOffset],
|
|
3151
3235
|
// y - oops.curXY[pointOffset + 1]
|
|
3152
3236
|
// );
|
|
@@ -3183,6 +3267,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3183
3267
|
return ((px - x1) * nx + (py - y1) * ny) / Math.hypot(nx, ny);
|
|
3184
3268
|
}
|
|
3185
3269
|
|
|
3270
|
+
let lastTimestamp = -Infinity;
|
|
3186
3271
|
function draw(update = true) {
|
|
3187
3272
|
ctx.resetTransform(); // in case there is an error, don't flip constantly back and forth due to mirroring
|
|
3188
3273
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // in case there's no footage
|
|
@@ -3197,6 +3282,13 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3197
3282
|
ctx.drawImage(cameraVideo, 0, 0, canvas.width, canvas.height);
|
|
3198
3283
|
}
|
|
3199
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
|
+
|
|
3200
3292
|
if (!pointTracker) {
|
|
3201
3293
|
return;
|
|
3202
3294
|
}
|
|
@@ -3332,39 +3424,77 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3332
3424
|
|
|
3333
3425
|
// TODO: separate confidence threshold for removing vs adding points?
|
|
3334
3426
|
|
|
3427
|
+
|
|
3335
3428
|
// cull points to those within useful facial region
|
|
3336
|
-
|
|
3337
|
-
|
|
3429
|
+
function regionFilter([x, y]) {
|
|
3430
|
+
|
|
3338
3431
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3339
|
-
|
|
3340
|
-
(annotations.noseTip[0][0] -
|
|
3341
|
-
annotations.noseTip[0][1] -
|
|
3432
|
+
let distance = Math.hypot(
|
|
3433
|
+
(annotations.noseTip[0][0] - x) * 1.4,
|
|
3434
|
+
annotations.noseTip[0][1] - y
|
|
3342
3435
|
);
|
|
3343
|
-
|
|
3436
|
+
let headSize = Math.hypot(
|
|
3344
3437
|
annotations.leftCheek[0][0] - annotations.rightCheek[0][0],
|
|
3345
3438
|
annotations.leftCheek[0][1] - annotations.rightCheek[0][1]
|
|
3346
3439
|
);
|
|
3347
3440
|
if (distance > headSize) {
|
|
3348
3441
|
return false;
|
|
3349
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
|
+
}
|
|
3350
3456
|
// Avoid blinking eyes affecting pointer position.
|
|
3351
3457
|
// distance to outer corners of eyes
|
|
3352
3458
|
distance = Math.min(
|
|
3353
3459
|
Math.hypot(
|
|
3354
|
-
annotations.leftEyeLower0[0][0] -
|
|
3355
|
-
annotations.leftEyeLower0[0][1] -
|
|
3460
|
+
annotations.leftEyeLower0[0][0] - x,
|
|
3461
|
+
annotations.leftEyeLower0[0][1] - y
|
|
3356
3462
|
),
|
|
3357
3463
|
Math.hypot(
|
|
3358
|
-
annotations.rightEyeLower0[0][0] -
|
|
3359
|
-
annotations.rightEyeLower0[0][1] -
|
|
3464
|
+
annotations.rightEyeLower0[0][0] - x,
|
|
3465
|
+
annotations.rightEyeLower0[0][1] - y
|
|
3360
3466
|
),
|
|
3361
3467
|
);
|
|
3362
3468
|
if (distance < headSize * 0.42) {
|
|
3363
3469
|
return false;
|
|
3364
3470
|
}
|
|
3365
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);
|
|
3366
3477
|
});
|
|
3367
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
|
+
|
|
3368
3498
|
const keypoints = facemeshPrediction.keypoints;
|
|
3369
3499
|
if (keypoints) {
|
|
3370
3500
|
const top = keypoints[10]; // Top of forehead
|
|
@@ -3546,16 +3676,19 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3546
3676
|
|
|
3547
3677
|
blinkInfo = detectBlinks();
|
|
3548
3678
|
mouthInfo = detectMouthOpen();
|
|
3549
|
-
if (blinkInfo.rightEye.open
|
|
3550
|
-
|
|
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);
|
|
3551
3685
|
}
|
|
3552
|
-
if (
|
|
3686
|
+
if (sleepGestureProgress >= 1) {
|
|
3687
|
+
sleepGestureProgress = 0;
|
|
3553
3688
|
if (s.closeEyesToToggle) {
|
|
3554
3689
|
paused = !paused;
|
|
3555
3690
|
updatePaused();
|
|
3556
|
-
|
|
3557
|
-
// TODO: try to keep variable names meaningful
|
|
3558
|
-
lastTimeWhenAnEyeWasOpen = Infinity;
|
|
3691
|
+
sleepSweep?.sleepModeWasToggled(paused);
|
|
3559
3692
|
}
|
|
3560
3693
|
}
|
|
3561
3694
|
|
|
@@ -3617,10 +3750,41 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3617
3750
|
|
|
3618
3751
|
const buttonNames = ["left", "middle", "right"];
|
|
3619
3752
|
for (let buttonIndex = 0; buttonIndex < 3; buttonIndex++) {
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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) {
|
|
3624
3788
|
lastMouseDownTime = performance.now();
|
|
3625
3789
|
} else {
|
|
3626
3790
|
// Limit "Delay Before Dragging" effect to the duration of a click.
|
|
@@ -3893,14 +4057,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3893
4057
|
|
|
3894
4058
|
// cull points to those within useful facial region
|
|
3895
4059
|
pointTracker.filterPoints((pointIndex) => {
|
|
3896
|
-
|
|
4060
|
+
let pointOffset = pointIndex * 2;
|
|
3897
4061
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
3898
|
-
|
|
4062
|
+
let distance = Math.hypot(
|
|
3899
4063
|
(face[62][0] - pointTracker.curXY[pointOffset]) * 1.4,
|
|
3900
4064
|
face[62][1] - pointTracker.curXY[pointOffset + 1]
|
|
3901
4065
|
);
|
|
3902
4066
|
// distance based on outer eye corners
|
|
3903
|
-
|
|
4067
|
+
let headSize = Math.hypot(
|
|
3904
4068
|
face[23][0] - face[28][0],
|
|
3905
4069
|
face[23][1] - face[28][1]
|
|
3906
4070
|
);
|
|
@@ -3928,18 +4092,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3928
4092
|
const screenWidth = window.electronAPI ? screen.width : innerWidth;
|
|
3929
4093
|
const screenHeight = window.electronAPI ? screen.height : innerHeight;
|
|
3930
4094
|
|
|
3931
|
-
|
|
4095
|
+
let [movementX, movementY] = pointTracker.getMovement();
|
|
3932
4096
|
|
|
3933
4097
|
// Acceleration curves add a lot of stability,
|
|
3934
4098
|
// letting you focus on a specific point without jitter, but still move quickly.
|
|
3935
4099
|
|
|
3936
|
-
//
|
|
3937
|
-
//
|
|
3938
|
-
|
|
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);
|
|
3939
4103
|
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
4104
|
+
let distance = Math.hypot(movementX, movementY);
|
|
4105
|
+
let deltaX = accelerate(movementX * s.headTrackingSensitivityX, distance);
|
|
4106
|
+
let deltaY = accelerate(movementY * s.headTrackingSensitivityY, distance);
|
|
3943
4107
|
|
|
3944
4108
|
if (s.headTrackingTiltInfluence > 0) {
|
|
3945
4109
|
const yawRange = [
|
|
@@ -4112,7 +4276,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4112
4276
|
|
|
4113
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)
|
|
4114
4278
|
const iid = setInterval(function animationLoop() {
|
|
4115
|
-
draw(!paused || document.visibilityState === "visible");
|
|
4279
|
+
draw(!paused || document.visibilityState === "visible" || isDesktopApp);
|
|
4116
4280
|
}, 15);
|
|
4117
4281
|
|
|
4118
4282
|
let autoDemo = false;
|