tracky-mouse 2.3.0 → 2.5.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 +15 -5
- package/locales/ar/translation.json +204 -0
- package/locales/ar-EG/translation.json +204 -0
- package/locales/bg/translation.json +204 -0
- package/locales/bn/translation.json +204 -0
- package/locales/ca/translation.json +204 -0
- package/locales/ce/translation.json +204 -0
- package/locales/ceb/translation.json +204 -0
- package/locales/cs/translation.json +204 -0
- package/locales/da/translation.json +204 -0
- package/locales/de/translation.json +204 -0
- package/locales/el/translation.json +204 -0
- package/locales/emoji/emoji-translation-notes.md +147 -0
- package/locales/emoji/translation.json +204 -0
- package/locales/en/translation.json +204 -0
- package/locales/eo/translation.json +204 -0
- package/locales/es/translation.json +204 -0
- package/locales/eu/translation.json +204 -0
- package/locales/fa/translation.json +204 -0
- package/locales/fi/translation.json +204 -0
- package/locales/fr/translation.json +204 -0
- package/locales/gu/translation.json +204 -0
- package/locales/ha/translation.json +204 -0
- package/locales/he/translation.json +204 -0
- package/locales/hi/translation.json +204 -0
- package/locales/hr/translation.json +204 -0
- package/locales/hu/translation.json +204 -0
- package/locales/hy/translation.json +204 -0
- package/locales/id/translation.json +204 -0
- package/locales/it/translation.json +204 -0
- package/locales/ja/translation.json +204 -0
- package/locales/jv/translation.json +204 -0
- package/locales/ko/translation.json +204 -0
- package/locales/mr/translation.json +204 -0
- package/locales/ms/translation.json +204 -0
- package/locales/nan/translation.json +204 -0
- package/locales/nb/translation.json +204 -0
- package/locales/nl/translation.json +204 -0
- package/locales/pa/translation.json +204 -0
- package/locales/pl/translation.json +204 -0
- package/locales/pt/translation.json +204 -0
- package/locales/pt-BR/translation.json +204 -0
- package/locales/ro/translation.json +204 -0
- package/locales/ru/translation.json +204 -0
- package/locales/sk/translation.json +204 -0
- package/locales/sl/translation.json +204 -0
- package/locales/sr/translation.json +204 -0
- package/locales/sv/translation.json +204 -0
- package/locales/sw/translation.json +204 -0
- package/locales/ta/translation.json +204 -0
- package/locales/te/translation.json +204 -0
- package/locales/th/translation.json +204 -0
- package/locales/tl/translation.json +204 -0
- package/locales/tr/translation.json +204 -0
- package/locales/tt/translation.json +204 -0
- package/locales/uk/translation.json +204 -0
- package/locales/ur/translation.json +204 -0
- package/locales/uz/translation.json +204 -0
- package/locales/vi/translation.json +204 -0
- package/locales/war/translation.json +204 -0
- package/locales/zh/translation.json +204 -0
- package/locales/zh-simplified/translation.json +204 -0
- package/package.json +2 -1
- package/tracky-mouse.css +7 -6
- package/tracky-mouse.js +1591 -217
package/tracky-mouse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* global jsfeat, Stats, clm, faceLandmarksDetection, OneEuroFilter */
|
|
2
|
+
|
|
2
3
|
const TrackyMouse = {
|
|
3
4
|
dependenciesRoot: "./tracky-mouse",
|
|
4
5
|
};
|
|
@@ -26,13 +27,20 @@ TrackyMouse.loadDependencies = function ({ statsJs = false } = {}) {
|
|
|
26
27
|
`${TrackyMouse.dependenciesRoot}/lib/no-eval.js`, // generated with eval-is-evil.html, this instruments clmtrackr.js so I don't need unsafe-eval in the CSP
|
|
27
28
|
`${TrackyMouse.dependenciesRoot}/lib/clmtrackr.js`,
|
|
28
29
|
`${TrackyMouse.dependenciesRoot}/lib/face_mesh/face_mesh.js`,
|
|
29
|
-
`${TrackyMouse.dependenciesRoot}/lib/face-landmarks-detection.min.js`,
|
|
30
30
|
`${TrackyMouse.dependenciesRoot}/lib/OneEuroFilter.js`,
|
|
31
31
|
];
|
|
32
|
+
// face-landmarks-detection.min.js depends on face_mesh.js
|
|
33
|
+
// avoid sporadic "TypeError: o.Facemesh is not a constructor" by loading face-landmarks-detection after face_mesh.js
|
|
34
|
+
// TODO: preload in parallel?
|
|
35
|
+
const moreScriptFiles = [
|
|
36
|
+
`${TrackyMouse.dependenciesRoot}/lib/face-landmarks-detection.min.js`,
|
|
37
|
+
];
|
|
32
38
|
if (statsJs) {
|
|
33
39
|
scriptFiles.push(`${TrackyMouse.dependenciesRoot}/lib/stats.js`);
|
|
34
40
|
}
|
|
35
|
-
return Promise.all(scriptFiles.map(loadScript))
|
|
41
|
+
return Promise.all(scriptFiles.map(loadScript)).then(() => {
|
|
42
|
+
return Promise.all(moreScriptFiles.map(loadScript));
|
|
43
|
+
});
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
const isSelectorValid = ((dummyElement) =>
|
|
@@ -62,77 +70,81 @@ const initDwellClicking = (config) => {
|
|
|
62
70
|
- `config.beforePointerDownDispatch()` (optional): a function to call before a `pointerdown` event is dispatched. Likely to be merged with `config.beforeDispatch()` in the future.
|
|
63
71
|
- `config.isHeld()` (optional): a function that returns true if the next dwell should be a release (triggering `pointerup`).
|
|
64
72
|
*/
|
|
73
|
+
|
|
74
|
+
/** translation placeholder */
|
|
75
|
+
const t = (s) => s;
|
|
76
|
+
|
|
65
77
|
if (typeof config !== "object") {
|
|
66
|
-
throw new Error("configuration object required for initDwellClicking");
|
|
78
|
+
throw new Error(t("configuration object required for initDwellClicking"));
|
|
67
79
|
}
|
|
68
80
|
if (config.targets === undefined) {
|
|
69
|
-
throw new Error("config.targets is required (must be a CSS selector)");
|
|
81
|
+
throw new Error(t("config.targets is required (must be a CSS selector)"));
|
|
70
82
|
}
|
|
71
83
|
if (typeof config.targets !== "string") {
|
|
72
|
-
throw new Error("config.targets must be a string (a CSS selector)");
|
|
84
|
+
throw new Error(t("config.targets must be a string (a CSS selector)"));
|
|
73
85
|
}
|
|
74
86
|
if (!isSelectorValid(config.targets)) {
|
|
75
|
-
throw new Error("config.targets is not a valid CSS selector");
|
|
87
|
+
throw new Error(t("config.targets is not a valid CSS selector"));
|
|
76
88
|
}
|
|
77
89
|
if (config.click === undefined) {
|
|
78
|
-
throw new Error("config.click is required");
|
|
90
|
+
throw new Error(t("config.click is required"));
|
|
79
91
|
}
|
|
80
92
|
if (typeof config.click !== "function") {
|
|
81
|
-
throw new Error("config.click must be a function");
|
|
93
|
+
throw new Error(t("config.click must be a function"));
|
|
82
94
|
}
|
|
83
95
|
if (config.shouldDrag !== undefined && typeof config.shouldDrag !== "function") {
|
|
84
|
-
throw new Error("config.shouldDrag must be a function");
|
|
96
|
+
throw new Error(t("config.shouldDrag must be a function"));
|
|
85
97
|
}
|
|
86
98
|
if (config.noCenter !== undefined && typeof config.noCenter !== "function") {
|
|
87
|
-
throw new Error("config.noCenter must be a function");
|
|
99
|
+
throw new Error(t("config.noCenter must be a function"));
|
|
88
100
|
}
|
|
89
101
|
if (config.isEquivalentTarget !== undefined && typeof config.isEquivalentTarget !== "function") {
|
|
90
|
-
throw new Error("config.isEquivalentTarget must be a function");
|
|
102
|
+
throw new Error(t("config.isEquivalentTarget must be a function"));
|
|
91
103
|
}
|
|
92
104
|
if (config.dwellClickEvenIfPaused !== undefined && typeof config.dwellClickEvenIfPaused !== "function") {
|
|
93
|
-
throw new Error("config.dwellClickEvenIfPaused must be a function");
|
|
105
|
+
throw new Error(t("config.dwellClickEvenIfPaused must be a function"));
|
|
94
106
|
}
|
|
95
107
|
if (config.beforeDispatch !== undefined && typeof config.beforeDispatch !== "function") {
|
|
96
|
-
throw new Error("config.beforeDispatch must be a function");
|
|
108
|
+
throw new Error(t("config.beforeDispatch must be a function"));
|
|
97
109
|
}
|
|
98
110
|
if (config.afterDispatch !== undefined && typeof config.afterDispatch !== "function") {
|
|
99
|
-
throw new Error("config.afterDispatch must be a function");
|
|
111
|
+
throw new Error(t("config.afterDispatch must be a function"));
|
|
100
112
|
}
|
|
101
113
|
if (config.beforePointerDownDispatch !== undefined && typeof config.beforePointerDownDispatch !== "function") {
|
|
102
|
-
throw new Error("config.beforePointerDownDispatch must be a function");
|
|
114
|
+
throw new Error(t("config.beforePointerDownDispatch must be a function"));
|
|
103
115
|
}
|
|
104
116
|
if (config.isHeld !== undefined && typeof config.isHeld !== "function") {
|
|
105
|
-
throw new Error("config.isHeld must be a function");
|
|
117
|
+
throw new Error(t("config.isHeld must be a function"));
|
|
106
118
|
}
|
|
107
119
|
if (config.retarget !== undefined) {
|
|
108
120
|
if (!Array.isArray(config.retarget)) {
|
|
109
|
-
throw new Error("config.retarget must be an array of objects");
|
|
121
|
+
throw new Error(t("config.retarget must be an array of objects"));
|
|
110
122
|
}
|
|
111
123
|
for (let i = 0; i < config.retarget.length; i++) {
|
|
112
124
|
const rule = config.retarget[i];
|
|
113
125
|
if (typeof rule !== "object") {
|
|
114
|
-
throw new Error("config.retarget must be an array of objects");
|
|
126
|
+
throw new Error(t("config.retarget must be an array of objects"));
|
|
115
127
|
}
|
|
116
128
|
if (rule.from === undefined) {
|
|
117
|
-
throw new Error(
|
|
129
|
+
throw new Error(t("config.retarget[%0].from is required").replace("%0", i));
|
|
118
130
|
}
|
|
119
131
|
if (rule.to === undefined) {
|
|
120
|
-
throw new Error(
|
|
132
|
+
throw new Error(t("config.retarget[%0].to is required (although can be null to ignore the element)").replace("%0", i));
|
|
121
133
|
}
|
|
122
134
|
if (rule.withinMargin !== undefined && typeof rule.withinMargin !== "number") {
|
|
123
|
-
throw new Error(
|
|
135
|
+
throw new Error(t("config.retarget[%0].withinMargin must be a number").replace("%0", i));
|
|
124
136
|
}
|
|
125
137
|
if (typeof rule.from !== "string" && typeof rule.from !== "function" && !(rule.from instanceof Element)) {
|
|
126
|
-
throw new Error(
|
|
138
|
+
throw new Error(t("config.retarget[%0].from must be a CSS selector string, an Element, or a function").replace("%0", i));
|
|
127
139
|
}
|
|
128
140
|
if (typeof rule.to !== "string" && typeof rule.to !== "function" && !(rule.to instanceof Element) && rule.to !== null) {
|
|
129
|
-
throw new Error(
|
|
141
|
+
throw new Error(t("config.retarget[%0].to must be a CSS selector string, an Element, a function, or null").replace("%0", i));
|
|
130
142
|
}
|
|
131
143
|
if (typeof rule.from === "string" && !isSelectorValid(rule.from)) {
|
|
132
|
-
throw new Error(
|
|
144
|
+
throw new Error(t("config.retarget[%0].from is not a valid CSS selector").replace("%0", i));
|
|
133
145
|
}
|
|
134
146
|
if (typeof rule.to === "string" && !isSelectorValid(rule.to)) {
|
|
135
|
-
throw new Error(
|
|
147
|
+
throw new Error(t("config.retarget[%0].to is not a valid CSS selector").replace("%0", i));
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -559,27 +571,1044 @@ TrackyMouse.cleanupDwellClicking = function () {
|
|
|
559
571
|
}
|
|
560
572
|
};
|
|
561
573
|
|
|
562
|
-
TrackyMouse.
|
|
574
|
+
TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
575
|
+
|
|
576
|
+
const isDesktopApp = !!window.electronAPI;
|
|
577
|
+
|
|
578
|
+
let translations = {};
|
|
579
|
+
let locale = navigator.language || "en";
|
|
580
|
+
// Transform en-US to en, etc.
|
|
581
|
+
// We don't support variants yet
|
|
582
|
+
if (locale.includes("-")) {
|
|
583
|
+
locale = locale.split("-")[0];
|
|
584
|
+
}
|
|
585
|
+
const availableLanguages = [
|
|
586
|
+
// GENERATED by scripts/update-locales.js
|
|
587
|
+
"ar", "ar-EG", "bg", "bn", "ca", "ce", "ceb", "cs", "da", "de", "el", "emoji", "en", "eo", "es", "eu", "fa", "fi", "fr", "gu", "ha", "he", "hi", "hr", "hu", "hy", "id", "it", "ja", "jv", "ko", "mr", "ms", "nan", "nb", "nl", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sl", "sr", "sv", "sw", "ta", "te", "th", "tl", "tr", "tt", "uk", "ur", "uz", "vi", "war", "zh", "zh-simplified"
|
|
588
|
+
// END GENERATED
|
|
589
|
+
];
|
|
590
|
+
// Fallback to a valid dropdown value for unsupported locales
|
|
591
|
+
if (!availableLanguages.includes(locale)) {
|
|
592
|
+
locale = "en";
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
// Load settings early so that they can be used to define settings (among other things)
|
|
596
|
+
// It's a bit hacky to load them twice but yeah
|
|
597
|
+
// (Actually in the desktop app it's even more hacky because I
|
|
598
|
+
// added code in electron-app.html to load the settings via the electron API
|
|
599
|
+
// and populate localStorage so that this code will work)
|
|
600
|
+
const settingsJSON = localStorage.getItem("tracky-mouse-settings");
|
|
601
|
+
if (settingsJSON) {
|
|
602
|
+
locale = JSON.parse(settingsJSON)?.globalSettings?.language || locale;
|
|
603
|
+
}
|
|
604
|
+
if (locale !== "en") {
|
|
605
|
+
// synchronous XHR baby!
|
|
606
|
+
const request = new XMLHttpRequest();
|
|
607
|
+
request.open("GET", `${TrackyMouse.dependenciesRoot}/locales/${locale}/translation.json`, false);
|
|
608
|
+
request.send(null);
|
|
609
|
+
if (request.status === 200) {
|
|
610
|
+
translations = JSON.parse(request.responseText);
|
|
611
|
+
} else {
|
|
612
|
+
console.warn(`Could not load translations for locale ${locale} (status ${request.status})`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch (e) {
|
|
616
|
+
console.warn("Could not load translations for TrackyMouse UI:", e);
|
|
617
|
+
}
|
|
618
|
+
const rtlLanguages = ["ar", "he", "fa", "ur"]; // Right-to-left languages (current and future)
|
|
619
|
+
const isRTL = rtlLanguages.includes(locale.split("-")[0]);
|
|
620
|
+
const t = (s) => translations[s] ?? s;
|
|
621
|
+
// console.trace("Initializing UI with locale", locale);
|
|
622
|
+
|
|
623
|
+
// language name mappings marked with * may not be ISO 639-1
|
|
624
|
+
// they may be ISO 639-3 or bespoke
|
|
625
|
+
// spell-checker:disable
|
|
626
|
+
const languageNames = {
|
|
627
|
+
// "639-1": [["ISO language name"], ["Native name (endonym)"]],
|
|
628
|
+
ab: [["Abkhazian"], ["Аҧсуа Бызшәа", "Аҧсшәа"]],
|
|
629
|
+
aa: [["Afar"], ["Afaraf"]],
|
|
630
|
+
af: [["Afrikaans"], ["Afrikaans"]],
|
|
631
|
+
ak: [["Akan"], ["Akan"]],
|
|
632
|
+
sq: [["Albanian"], ["Shqip"]],
|
|
633
|
+
am: [["Amharic"], ["አማርኛ"]],
|
|
634
|
+
ar: [["Arabic"], ["العربية"]],
|
|
635
|
+
"ar-EG": [["Egyptian Arabic"], ["العربية المصرية"]],//*
|
|
636
|
+
an: [["Aragonese"], ["Aragonés"]],
|
|
637
|
+
hy: [["Armenian"], ["Հայերեն"]],
|
|
638
|
+
as: [["Assamese"], ["অসমীয়া"]],
|
|
639
|
+
av: [["Avaric"], ["Авар МацӀ", "МагӀарул МацӀ"]],
|
|
640
|
+
ae: [["Avestan"], ["Avesta"]],
|
|
641
|
+
ay: [["Aymara"], ["Aymar Aru"]],
|
|
642
|
+
az: [["Azerbaijani"], ["Azərbaycan Dili"]],
|
|
643
|
+
bm: [["Bambara"], ["Bamanankan"]],
|
|
644
|
+
ba: [["Bashkir"], ["Башҡорт Теле"]],
|
|
645
|
+
emoji: [["Emoji"], ["😃📝"]],//*
|
|
646
|
+
eu: [["Basque"], ["Euskara", "Euskera"]],
|
|
647
|
+
be: [["Belarusian"], ["Беларуская Мова"]],
|
|
648
|
+
bn: [["Bengali"], ["বাংলা"]],
|
|
649
|
+
bh: [["Bihari Languages"], ["भोजपुरी"]],
|
|
650
|
+
bi: [["Bislama"], ["Bislama"]],
|
|
651
|
+
bs: [["Bosnian"], ["Bosanski Jezik"]],
|
|
652
|
+
br: [["Breton"], ["Brezhoneg"]],
|
|
653
|
+
bg: [["Bulgarian"], ["Български Език"]],
|
|
654
|
+
my: [["Burmese"], ["ဗမာစာ"]],
|
|
655
|
+
ca: [["Catalan", "Valencian"], ["Català", "Valencià"]],
|
|
656
|
+
ch: [["Chamorro"], ["Chamoru"]],
|
|
657
|
+
ce: [["Chechen"], ["Нохчийн Мотт"]],
|
|
658
|
+
ceb: [["Cebuano"], ["Bisayâ", "Binisayâ"]],//*
|
|
659
|
+
ny: [["Chichewa", "Chewa", "Nyanja"], ["ChiCheŵa", "Chinyanja"]],
|
|
660
|
+
// zh: [["Chinese"], ["中文", "Zhōngwén", "汉语", "漢語"]],
|
|
661
|
+
// The ISO 639-1 code "zh" doesn't refer to Traditional Chinese specifically,
|
|
662
|
+
// but we want to show the distinction between Chinese varieties in the Language menu,
|
|
663
|
+
// so this is overly specific for now.
|
|
664
|
+
// @TODO: do this cleaner by establishing a mapping between ISO codes (such as "zh") and default language IDs (such as "zh-traditional")
|
|
665
|
+
zh: [["Traditional Chinese"], ["繁體中文", "傳統中文", "正體中文", "繁体中文"]],
|
|
666
|
+
"zh-traditional": [["Traditional Chinese"], ["繁體中文", "傳統中文", "正體中文", "繁体中文"]], //*
|
|
667
|
+
"zh-simplified": [["Simplified Chinese"], ["简体中文"]], //*
|
|
668
|
+
cv: [["Chuvash"], ["Чӑваш Чӗлхи"]],
|
|
669
|
+
kw: [["Cornish"], ["Kernewek"]],
|
|
670
|
+
co: [["Corsican"], ["Corsu", "Lingua Corsa"]],
|
|
671
|
+
cr: [["Cree"], ["ᓀᐦᐃᔭᐍᐏᐣ"]],
|
|
672
|
+
hr: [["Croatian"], ["Hrvatski Jezik"]],
|
|
673
|
+
cs: [["Czech"], ["Čeština", "Český Jazyk"]],
|
|
674
|
+
da: [["Danish"], ["Dansk"]],
|
|
675
|
+
dv: [["Divehi", "Dhivehi", "Maldivian"], ["ދިވެހި"]],
|
|
676
|
+
nl: [["Dutch", "Flemish"], ["Nederlands", "Vlaams"]],
|
|
677
|
+
dz: [["Dzongkha"], ["རྫོང་ཁ"]],
|
|
678
|
+
en: [["English"], ["English"]],
|
|
679
|
+
eo: [["Esperanto"], ["Esperanto"]],
|
|
680
|
+
et: [["Estonian"], ["Eesti", "Eesti Keel"]],
|
|
681
|
+
ee: [["Ewe"], ["Eʋegbe"]],
|
|
682
|
+
fo: [["Faroese"], ["Føroyskt"]],
|
|
683
|
+
fj: [["Fijian"], ["Vosa Vakaviti"]],
|
|
684
|
+
fi: [["Finnish"], ["Suomi", "Suomen Kieli"]],
|
|
685
|
+
fr: [["French"], ["Français", "Langue Française"]],
|
|
686
|
+
ff: [["Fulah"], ["Fulfulde", "Pulaar", "Pular"]],
|
|
687
|
+
gl: [["Galician"], ["Galego"]],
|
|
688
|
+
ka: [["Georgian"], ["ქართული"]],
|
|
689
|
+
de: [["German"], ["Deutsch"]],
|
|
690
|
+
el: [["Greek"], ["Ελληνικά"]],
|
|
691
|
+
gn: [["Guarani"], ["Avañe'ẽ"]],
|
|
692
|
+
gu: [["Gujarati"], ["ગુજરાતી"]],
|
|
693
|
+
ht: [["Haitian", "Haitian Creole"], ["Kreyòl Ayisyen"]],
|
|
694
|
+
ha: [["Hausa"], ["هَوُسَ"]],
|
|
695
|
+
he: [["Hebrew"], ["עברית"]],
|
|
696
|
+
hz: [["Herero"], ["Otjiherero"]],
|
|
697
|
+
hi: [["Hindi"], ["हिन्दी", "हिंदी"]],
|
|
698
|
+
ho: [["Hiri Motu"], ["Hiri Motu"]],
|
|
699
|
+
hu: [["Hungarian"], ["Magyar"]],
|
|
700
|
+
ia: [["Interlingua"], ["Interlingua"]],
|
|
701
|
+
id: [["Indonesian"], ["Bahasa Indonesia"]],
|
|
702
|
+
ie: [["Interlingue", "Occidental"], ["Interlingue", "Occidental"]],
|
|
703
|
+
ga: [["Irish"], ["Gaeilge"]],
|
|
704
|
+
ig: [["Igbo"], ["Asụsụ Igbo"]],
|
|
705
|
+
ik: [["Inupiaq"], ["Iñupiaq", "Iñupiatun"]],
|
|
706
|
+
io: [["Ido"], ["Ido"]],
|
|
707
|
+
is: [["Icelandic"], ["Íslenska"]],
|
|
708
|
+
it: [["Italian"], ["Italiano"]],
|
|
709
|
+
iu: [["Inuktitut"], ["ᐃᓄᒃᑎᑐᑦ"]],
|
|
710
|
+
ja: [["Japanese"], ["日本語", "にほんご"]],
|
|
711
|
+
jv: [["Javanese"], ["ꦧꦱꦗꦮ", "Basa Jawa"]],
|
|
712
|
+
kl: [["Kalaallisut", "Greenlandic"], ["Kalaallisut", "Kalaallit Oqaasii"]],
|
|
713
|
+
kn: [["Kannada"], ["ಕನ್ನಡ"]],
|
|
714
|
+
kr: [["Kanuri"], ["Kanuri"]],
|
|
715
|
+
ks: [["Kashmiri"], ["कश्मीरी", "كشميري"]],
|
|
716
|
+
kk: [["Kazakh"], ["Қазақ Тілі"]],
|
|
717
|
+
km: [["Central Khmer"], ["ខ្មែរ", "ខេមរភាសា", "ភាសាខ្មែរ"]],
|
|
718
|
+
ki: [["Kikuyu", "Gikuyu"], ["Gĩkũyũ"]],
|
|
719
|
+
rw: [["Kinyarwanda"], ["Ikinyarwanda"]],
|
|
720
|
+
ky: [["Kirghiz", "Kyrgyz"], ["Кыргызча", "Кыргыз Тили"]],
|
|
721
|
+
kv: [["Komi"], ["Коми Кыв"]],
|
|
722
|
+
kg: [["Kongo"], ["Kikongo"]],
|
|
723
|
+
ko: [["Korean"], ["한국어"]],
|
|
724
|
+
ku: [["Kurdish"], ["Kurdî", "کوردی"]],
|
|
725
|
+
kj: [["Kuanyama", "Kwanyama"], ["Kuanyama"]],
|
|
726
|
+
la: [["Latin"], ["Latine", "Lingua Latina"]],
|
|
727
|
+
lb: [["Luxembourgish", "Letzeburgesch"], ["Lëtzebuergesch"]],
|
|
728
|
+
lg: [["Ganda"], ["Luganda"]],
|
|
729
|
+
li: [["Limburgan", "Limburger", "Limburgish"], ["Limburgs"]],
|
|
730
|
+
ln: [["Lingala"], ["Lingála"]],
|
|
731
|
+
lo: [["Lao"], ["ພາສາລາວ"]],
|
|
732
|
+
lt: [["Lithuanian"], ["Lietuvių Kalba"]],
|
|
733
|
+
lu: [["Luba-Katanga"], ["Kiluba"]],
|
|
734
|
+
lv: [["Latvian"], ["Latviešu Valoda"]],
|
|
735
|
+
gv: [["Manx"], ["Gaelg", "Gailck"]],
|
|
736
|
+
mk: [["Macedonian"], ["Македонски Јазик"]],
|
|
737
|
+
mg: [["Malagasy"], ["Fiteny Malagasy"]],
|
|
738
|
+
ms: [["Malay"], ["Bahasa Melayu", "بهاس ملايو"]],
|
|
739
|
+
ml: [["Malayalam"], ["മലയാളം"]],
|
|
740
|
+
mt: [["Maltese"], ["Malti"]],
|
|
741
|
+
mi: [["Maori"], ["Te Reo Māori"]],
|
|
742
|
+
mr: [["Marathi"], ["मराठी"]],
|
|
743
|
+
mh: [["Marshallese"], ["Kajin M̧ajeļ"]],
|
|
744
|
+
mn: [["Mongolian"], ["Монгол Хэл"]],
|
|
745
|
+
na: [["Nauru"], ["Dorerin Naoero"]],
|
|
746
|
+
nan: [["Minnan", "Taiwanese Hokkien"], ["閩南語", "闽南语", "Bàn-lâm-gú", "Bân-lâm-gí"]],//* (technically Hokkien is a branch of Minnan; also idk what names are preferred)
|
|
747
|
+
nv: [["Navajo", "Navaho"], ["Diné Bizaad"]],
|
|
748
|
+
nd: [["North Ndebele"], ["IsiNdebele"]],
|
|
749
|
+
ne: [["Nepali"], ["नेपाली"]],
|
|
750
|
+
ng: [["Ndonga"], ["Owambo"]],
|
|
751
|
+
nb: [["Norwegian Bokmål"], ["Norsk Bokmål"]],
|
|
752
|
+
nn: [["Norwegian Nynorsk"], ["Norsk Nynorsk"]],
|
|
753
|
+
no: [["Norwegian"], ["Norsk"]],
|
|
754
|
+
ii: [["Sichuan Yi", "Nuosu"], ["ꆈꌠ꒿", "Nuosuhxop"]],
|
|
755
|
+
nr: [["South Ndebele"], ["IsiNdebele"]],
|
|
756
|
+
oc: [["Occitan"], ["Occitan", "Lenga d'Òc"]],
|
|
757
|
+
oj: [["Ojibwa"], ["ᐊᓂᔑᓈᐯᒧᐎᓐ"]],
|
|
758
|
+
cu: [["Church Slavic", "Old Slavonic", "Church Slavonic", "Old Bulgarian", "Old Church Slavonic"], ["Ѩзыкъ Словѣньскъ"]],
|
|
759
|
+
om: [["Oromo"], ["Afaan Oromoo"]],
|
|
760
|
+
or: [["Oriya"], ["ଓଡ଼ିଆ"]],
|
|
761
|
+
os: [["Ossetian", "Ossetic"], ["Ирон Æвзаг"]],
|
|
762
|
+
pa: [["Punjabi", "Panjabi"], ["ਪੰਜਾਬੀ", "پنجابی"]],
|
|
763
|
+
pi: [["Pali"], ["पालि", "पाळि"]],
|
|
764
|
+
fa: [["Persian"], ["فارسی"]],
|
|
765
|
+
pl: [["Polish"], ["Język Polski", "Polszczyzna"]],
|
|
766
|
+
ps: [["Pashto", "Pushto"], ["پښتو"]],
|
|
767
|
+
pt: [["Portuguese"], ["Português"]],
|
|
768
|
+
"pt-BR": [["Brazilian Portuguese"], ["Português Brasileiro"]],
|
|
769
|
+
"pt-PT": [["Portuguese (Portugal)"], ["Português De Portugal"]],
|
|
770
|
+
qu: [["Quechua"], ["Runa Simi", "Kichwa"]],
|
|
771
|
+
rm: [["Romansh"], ["Rumantsch Grischun"]],
|
|
772
|
+
rn: [["Rundi"], ["Ikirundi"]],
|
|
773
|
+
ro: [["Romanian", "Moldavian", "Moldovan"], ["Română"]],
|
|
774
|
+
ru: [["Russian"], ["Русский"]],
|
|
775
|
+
sa: [["Sanskrit"], ["संस्कृतम्"]],
|
|
776
|
+
sc: [["Sardinian"], ["Sardu"]],
|
|
777
|
+
sd: [["Sindhi"], ["सिन्धी", "سنڌي، سندھی"]],
|
|
778
|
+
se: [["Northern Sami"], ["Davvisámegiella"]],
|
|
779
|
+
sm: [["Samoan"], ["Gagana Fa'a Samoa"]],
|
|
780
|
+
sg: [["Sango"], ["Yângâ Tî Sängö"]],
|
|
781
|
+
sr: [["Serbian"], ["Српски Језик"]],
|
|
782
|
+
gd: [["Gaelic", "Scottish Gaelic"], ["Gàidhlig"]],
|
|
783
|
+
sn: [["Shona"], ["ChiShona"]],
|
|
784
|
+
si: [["Sinhala", "Sinhalese"], ["සිංහල"]],
|
|
785
|
+
sk: [["Slovak"], ["Slovenčina", "Slovenský Jazyk"]],
|
|
786
|
+
sl: [["Slovenian"], ["Slovenski Jezik", "Slovenščina"]],
|
|
787
|
+
so: [["Somali"], ["Soomaaliga", "Af Soomaali"]],
|
|
788
|
+
st: [["Southern Sotho"], ["Sesotho"]],
|
|
789
|
+
es: [["Spanish", "Castilian"], ["Español"]],
|
|
790
|
+
su: [["Sundanese"], ["Basa Sunda"]],
|
|
791
|
+
sw: [["Swahili"], ["Kiswahili"]],
|
|
792
|
+
ss: [["Swati"], ["SiSwati"]],
|
|
793
|
+
sv: [["Swedish"], ["Svenska"]],
|
|
794
|
+
ta: [["Tamil"], ["தமிழ்"]],
|
|
795
|
+
te: [["Telugu"], ["తెలుగు"]],
|
|
796
|
+
tg: [["Tajik"], ["Тоҷикӣ", "Toçikī", "تاجیکی"]],
|
|
797
|
+
th: [["Thai"], ["ไทย"]],
|
|
798
|
+
ti: [["Tigrinya"], ["ትግርኛ"]],
|
|
799
|
+
bo: [["Tibetan"], ["བོད་ཡིག"]],
|
|
800
|
+
tk: [["Turkmen"], ["Türkmen", "Түркмен"]],
|
|
801
|
+
tl: [["Tagalog"], ["Wikang Tagalog"]],
|
|
802
|
+
tn: [["Tswana"], ["Setswana"]],
|
|
803
|
+
to: [["Tonga"], ["Faka Tonga"]],
|
|
804
|
+
tr: [["Turkish"], ["Türkçe"]],
|
|
805
|
+
ts: [["Tsonga"], ["Xitsonga"]],
|
|
806
|
+
tt: [["Tatar"], ["Татар Теле", "Tatar Tele"]],
|
|
807
|
+
tw: [["Twi"], ["Twi"]],
|
|
808
|
+
ty: [["Tahitian"], ["Reo Tahiti"]],
|
|
809
|
+
ug: [["Uighur", "Uyghur"], ["ئۇيغۇرچە", "Uyghurche"]],
|
|
810
|
+
uk: [["Ukrainian"], ["Українська"]],
|
|
811
|
+
ur: [["Urdu"], ["اردو"]],
|
|
812
|
+
uz: [["Uzbek"], ["Oʻzbek", "Ўзбек", "أۇزبېك"]],
|
|
813
|
+
ve: [["Venda"], ["Tshivenḓa"]],
|
|
814
|
+
vi: [["Vietnamese"], ["Tiếng Việt"]],
|
|
815
|
+
vo: [["Volapük"], ["Volapük"]],
|
|
816
|
+
wa: [["Walloon"], ["Walon"]],
|
|
817
|
+
war: [["Waray"], ["Winaray"]],//*
|
|
818
|
+
cy: [["Welsh"], ["Cymraeg"]],
|
|
819
|
+
wo: [["Wolof"], ["Wollof"]],
|
|
820
|
+
fy: [["Western Frisian"], ["Frysk"]],
|
|
821
|
+
xh: [["Xhosa"], ["IsiXhosa"]],
|
|
822
|
+
yi: [["Yiddish"], ["ייִדיש"]],
|
|
823
|
+
yo: [["Yoruba"], ["Yorùbá"]],
|
|
824
|
+
za: [["Zhuang", "Chuang"], ["Saɯ Cueŋƅ", "Saw Cuengh"]],
|
|
825
|
+
zu: [["Zulu"], ["IsiZulu"]],
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
var languageToDefaultRegion = {
|
|
830
|
+
aa: "ET",
|
|
831
|
+
ab: "GE",
|
|
832
|
+
abr: "GH",
|
|
833
|
+
ace: "ID",
|
|
834
|
+
ach: "UG",
|
|
835
|
+
ada: "GH",
|
|
836
|
+
ady: "RU",
|
|
837
|
+
ae: "IR",
|
|
838
|
+
aeb: "TN",
|
|
839
|
+
af: "ZA",
|
|
840
|
+
agq: "CM",
|
|
841
|
+
aho: "IN",
|
|
842
|
+
ak: "GH",
|
|
843
|
+
akk: "IQ",
|
|
844
|
+
aln: "XK",
|
|
845
|
+
alt: "RU",
|
|
846
|
+
am: "ET",
|
|
847
|
+
amo: "NG",
|
|
848
|
+
aoz: "ID",
|
|
849
|
+
apd: "TG",
|
|
850
|
+
ar: "EG",
|
|
851
|
+
arc: "IR",
|
|
852
|
+
"arc-Nbat": "JO",
|
|
853
|
+
"arc-Palm": "SY",
|
|
854
|
+
arn: "CL",
|
|
855
|
+
aro: "BO",
|
|
856
|
+
arq: "DZ",
|
|
857
|
+
ary: "MA",
|
|
858
|
+
arz: "EG",
|
|
859
|
+
as: "IN",
|
|
860
|
+
asa: "TZ",
|
|
861
|
+
ase: "US",
|
|
862
|
+
ast: "ES",
|
|
863
|
+
atj: "CA",
|
|
864
|
+
av: "RU",
|
|
865
|
+
awa: "IN",
|
|
866
|
+
ay: "BO",
|
|
867
|
+
az: "AZ",
|
|
868
|
+
"az-Arab": "IR",
|
|
869
|
+
ba: "RU",
|
|
870
|
+
bal: "PK",
|
|
871
|
+
ban: "ID",
|
|
872
|
+
bap: "NP",
|
|
873
|
+
bar: "AT",
|
|
874
|
+
bas: "CM",
|
|
875
|
+
bax: "CM",
|
|
876
|
+
bbc: "ID",
|
|
877
|
+
bbj: "CM",
|
|
878
|
+
bci: "CI",
|
|
879
|
+
be: "BY",
|
|
880
|
+
bej: "SD",
|
|
881
|
+
bem: "ZM",
|
|
882
|
+
bew: "ID",
|
|
883
|
+
bez: "TZ",
|
|
884
|
+
bfd: "CM",
|
|
885
|
+
bfq: "IN",
|
|
886
|
+
bft: "PK",
|
|
887
|
+
bfy: "IN",
|
|
888
|
+
bg: "BG",
|
|
889
|
+
bgc: "IN",
|
|
890
|
+
bgn: "PK",
|
|
891
|
+
bgx: "TR",
|
|
892
|
+
bhb: "IN",
|
|
893
|
+
bhi: "IN",
|
|
894
|
+
bhk: "PH",
|
|
895
|
+
bho: "IN",
|
|
896
|
+
bi: "VU",
|
|
897
|
+
bik: "PH",
|
|
898
|
+
bin: "NG",
|
|
899
|
+
bjj: "IN",
|
|
900
|
+
bjn: "ID",
|
|
901
|
+
bjt: "SN",
|
|
902
|
+
bkm: "CM",
|
|
903
|
+
bku: "PH",
|
|
904
|
+
blt: "VN",
|
|
905
|
+
bm: "ML",
|
|
906
|
+
bmq: "ML",
|
|
907
|
+
bn: "BD",
|
|
908
|
+
bo: "CN",
|
|
909
|
+
bpy: "IN",
|
|
910
|
+
bqi: "IR",
|
|
911
|
+
bqv: "CI",
|
|
912
|
+
br: "FR",
|
|
913
|
+
bra: "IN",
|
|
914
|
+
brh: "PK",
|
|
915
|
+
brx: "IN",
|
|
916
|
+
bs: "BA",
|
|
917
|
+
bsq: "LR",
|
|
918
|
+
bss: "CM",
|
|
919
|
+
bto: "PH",
|
|
920
|
+
btv: "PK",
|
|
921
|
+
bua: "RU",
|
|
922
|
+
buc: "YT",
|
|
923
|
+
bug: "ID",
|
|
924
|
+
bum: "CM",
|
|
925
|
+
bvb: "GQ",
|
|
926
|
+
byn: "ER",
|
|
927
|
+
byv: "CM",
|
|
928
|
+
bze: "ML",
|
|
929
|
+
ca: "ES",
|
|
930
|
+
cch: "NG",
|
|
931
|
+
ccp: "BD",
|
|
932
|
+
ce: "RU",
|
|
933
|
+
ceb: "PH",
|
|
934
|
+
cgg: "UG",
|
|
935
|
+
ch: "GU",
|
|
936
|
+
chk: "FM",
|
|
937
|
+
chm: "RU",
|
|
938
|
+
cho: "US",
|
|
939
|
+
chp: "CA",
|
|
940
|
+
chr: "US",
|
|
941
|
+
cja: "KH",
|
|
942
|
+
cjm: "VN",
|
|
943
|
+
ckb: "IQ",
|
|
944
|
+
co: "FR",
|
|
945
|
+
cop: "EG",
|
|
946
|
+
cps: "PH",
|
|
947
|
+
cr: "CA",
|
|
948
|
+
crh: "UA",
|
|
949
|
+
crj: "CA",
|
|
950
|
+
crk: "CA",
|
|
951
|
+
crl: "CA",
|
|
952
|
+
crm: "CA",
|
|
953
|
+
crs: "SC",
|
|
954
|
+
cs: "CZ",
|
|
955
|
+
csb: "PL",
|
|
956
|
+
csw: "CA",
|
|
957
|
+
ctd: "MM",
|
|
958
|
+
cu: "RU",
|
|
959
|
+
"cu-Glag": "BG",
|
|
960
|
+
cv: "RU",
|
|
961
|
+
cy: "GB",
|
|
962
|
+
da: "DK",
|
|
963
|
+
dak: "US",
|
|
964
|
+
dar: "RU",
|
|
965
|
+
dav: "KE",
|
|
966
|
+
dcc: "IN",
|
|
967
|
+
de: "DE",
|
|
968
|
+
den: "CA",
|
|
969
|
+
dgr: "CA",
|
|
970
|
+
dje: "NE",
|
|
971
|
+
dnj: "CI",
|
|
972
|
+
doi: "IN",
|
|
973
|
+
dsb: "DE",
|
|
974
|
+
dtm: "ML",
|
|
975
|
+
dtp: "MY",
|
|
976
|
+
dty: "NP",
|
|
977
|
+
dua: "CM",
|
|
978
|
+
dv: "MV",
|
|
979
|
+
dyo: "SN",
|
|
980
|
+
dyu: "BF",
|
|
981
|
+
dz: "BT",
|
|
982
|
+
ebu: "KE",
|
|
983
|
+
ee: "GH",
|
|
984
|
+
efi: "NG",
|
|
985
|
+
egl: "IT",
|
|
986
|
+
egy: "EG",
|
|
987
|
+
eky: "MM",
|
|
988
|
+
el: "GR",
|
|
989
|
+
en: "US",
|
|
990
|
+
"en-Shaw": "GB",
|
|
991
|
+
es: "ES",
|
|
992
|
+
esu: "US",
|
|
993
|
+
et: "EE",
|
|
994
|
+
ett: "IT",
|
|
995
|
+
eu: "ES",
|
|
996
|
+
ewo: "CM",
|
|
997
|
+
ext: "ES",
|
|
998
|
+
fa: "IR",
|
|
999
|
+
fan: "GQ",
|
|
1000
|
+
ff: "SN",
|
|
1001
|
+
"ff-Adlm": "GN",
|
|
1002
|
+
ffm: "ML",
|
|
1003
|
+
fi: "FI",
|
|
1004
|
+
fia: "SD",
|
|
1005
|
+
fil: "PH",
|
|
1006
|
+
fit: "SE",
|
|
1007
|
+
fj: "FJ",
|
|
1008
|
+
fo: "FO",
|
|
1009
|
+
fon: "BJ",
|
|
1010
|
+
fr: "FR",
|
|
1011
|
+
frc: "US",
|
|
1012
|
+
frp: "FR",
|
|
1013
|
+
frr: "DE",
|
|
1014
|
+
frs: "DE",
|
|
1015
|
+
fub: "CM",
|
|
1016
|
+
fud: "WF",
|
|
1017
|
+
fuf: "GN",
|
|
1018
|
+
fuq: "NE",
|
|
1019
|
+
fur: "IT",
|
|
1020
|
+
fuv: "NG",
|
|
1021
|
+
fvr: "SD",
|
|
1022
|
+
fy: "NL",
|
|
1023
|
+
ga: "IE",
|
|
1024
|
+
gaa: "GH",
|
|
1025
|
+
gag: "MD",
|
|
1026
|
+
gan: "CN",
|
|
1027
|
+
gay: "ID",
|
|
1028
|
+
gbm: "IN",
|
|
1029
|
+
gbz: "IR",
|
|
1030
|
+
gcr: "GF",
|
|
1031
|
+
gd: "GB",
|
|
1032
|
+
gez: "ET",
|
|
1033
|
+
ggn: "NP",
|
|
1034
|
+
gil: "KI",
|
|
1035
|
+
gjk: "PK",
|
|
1036
|
+
gju: "PK",
|
|
1037
|
+
gl: "ES",
|
|
1038
|
+
glk: "IR",
|
|
1039
|
+
gn: "PY",
|
|
1040
|
+
gom: "IN",
|
|
1041
|
+
gon: "IN",
|
|
1042
|
+
gor: "ID",
|
|
1043
|
+
gos: "NL",
|
|
1044
|
+
got: "UA",
|
|
1045
|
+
grc: "CY",
|
|
1046
|
+
"grc-Linb": "GR",
|
|
1047
|
+
grt: "IN",
|
|
1048
|
+
gsw: "CH",
|
|
1049
|
+
gu: "IN",
|
|
1050
|
+
gub: "BR",
|
|
1051
|
+
guc: "CO",
|
|
1052
|
+
gur: "GH",
|
|
1053
|
+
guz: "KE",
|
|
1054
|
+
gv: "IM",
|
|
1055
|
+
gvr: "NP",
|
|
1056
|
+
gwi: "CA",
|
|
1057
|
+
ha: "NG",
|
|
1058
|
+
hak: "CN",
|
|
1059
|
+
haw: "US",
|
|
1060
|
+
haz: "AF",
|
|
1061
|
+
he: "IL",
|
|
1062
|
+
hi: "IN",
|
|
1063
|
+
hif: "FJ",
|
|
1064
|
+
hil: "PH",
|
|
1065
|
+
hlu: "TR",
|
|
1066
|
+
hmd: "CN",
|
|
1067
|
+
hnd: "PK",
|
|
1068
|
+
hne: "IN",
|
|
1069
|
+
hnj: "LA",
|
|
1070
|
+
hnn: "PH",
|
|
1071
|
+
hno: "PK",
|
|
1072
|
+
ho: "PG",
|
|
1073
|
+
hoc: "IN",
|
|
1074
|
+
hoj: "IN",
|
|
1075
|
+
hr: "HR",
|
|
1076
|
+
hsb: "DE",
|
|
1077
|
+
hsn: "CN",
|
|
1078
|
+
ht: "HT",
|
|
1079
|
+
hu: "HU",
|
|
1080
|
+
hy: "AM",
|
|
1081
|
+
hz: "NA",
|
|
1082
|
+
ia: "FR",
|
|
1083
|
+
iba: "MY",
|
|
1084
|
+
ibb: "NG",
|
|
1085
|
+
id: "ID",
|
|
1086
|
+
ife: "TG",
|
|
1087
|
+
ig: "NG",
|
|
1088
|
+
ii: "CN",
|
|
1089
|
+
ik: "US",
|
|
1090
|
+
ikt: "CA",
|
|
1091
|
+
ilo: "PH",
|
|
1092
|
+
in: "ID",
|
|
1093
|
+
inh: "RU",
|
|
1094
|
+
is: "IS",
|
|
1095
|
+
it: "IT",
|
|
1096
|
+
iu: "CA",
|
|
1097
|
+
iw: "IL",
|
|
1098
|
+
izh: "RU",
|
|
1099
|
+
ja: "JP",
|
|
1100
|
+
jam: "JM",
|
|
1101
|
+
jgo: "CM",
|
|
1102
|
+
ji: "UA",
|
|
1103
|
+
jmc: "TZ",
|
|
1104
|
+
jml: "NP",
|
|
1105
|
+
jut: "DK",
|
|
1106
|
+
jv: "ID",
|
|
1107
|
+
jw: "ID",
|
|
1108
|
+
ka: "GE",
|
|
1109
|
+
kaa: "UZ",
|
|
1110
|
+
kab: "DZ",
|
|
1111
|
+
kac: "MM",
|
|
1112
|
+
kaj: "NG",
|
|
1113
|
+
kam: "KE",
|
|
1114
|
+
kao: "ML",
|
|
1115
|
+
kbd: "RU",
|
|
1116
|
+
kby: "NE",
|
|
1117
|
+
kcg: "NG",
|
|
1118
|
+
kck: "ZW",
|
|
1119
|
+
kde: "TZ",
|
|
1120
|
+
kdh: "TG",
|
|
1121
|
+
kdt: "TH",
|
|
1122
|
+
kea: "CV",
|
|
1123
|
+
ken: "CM",
|
|
1124
|
+
kfo: "CI",
|
|
1125
|
+
kfr: "IN",
|
|
1126
|
+
kfy: "IN",
|
|
1127
|
+
kg: "CD",
|
|
1128
|
+
kge: "ID",
|
|
1129
|
+
kgp: "BR",
|
|
1130
|
+
kha: "IN",
|
|
1131
|
+
khb: "CN",
|
|
1132
|
+
khn: "IN",
|
|
1133
|
+
khq: "ML",
|
|
1134
|
+
kht: "IN",
|
|
1135
|
+
khw: "PK",
|
|
1136
|
+
ki: "KE",
|
|
1137
|
+
kiu: "TR",
|
|
1138
|
+
kj: "NA",
|
|
1139
|
+
kjg: "LA",
|
|
1140
|
+
kk: "KZ",
|
|
1141
|
+
"kk-Arab": "CN",
|
|
1142
|
+
kkj: "CM",
|
|
1143
|
+
kl: "GL",
|
|
1144
|
+
kln: "KE",
|
|
1145
|
+
km: "KH",
|
|
1146
|
+
kmb: "AO",
|
|
1147
|
+
kn: "IN",
|
|
1148
|
+
knf: "SN",
|
|
1149
|
+
ko: "KR",
|
|
1150
|
+
koi: "RU",
|
|
1151
|
+
kok: "IN",
|
|
1152
|
+
kos: "FM",
|
|
1153
|
+
kpe: "LR",
|
|
1154
|
+
krc: "RU",
|
|
1155
|
+
kri: "SL",
|
|
1156
|
+
krj: "PH",
|
|
1157
|
+
krl: "RU",
|
|
1158
|
+
kru: "IN",
|
|
1159
|
+
ks: "IN",
|
|
1160
|
+
ksb: "TZ",
|
|
1161
|
+
ksf: "CM",
|
|
1162
|
+
ksh: "DE",
|
|
1163
|
+
ku: "TR",
|
|
1164
|
+
"ku-Arab": "IQ",
|
|
1165
|
+
kum: "RU",
|
|
1166
|
+
kv: "RU",
|
|
1167
|
+
kvr: "ID",
|
|
1168
|
+
kvx: "PK",
|
|
1169
|
+
kw: "GB",
|
|
1170
|
+
kxm: "TH",
|
|
1171
|
+
kxp: "PK",
|
|
1172
|
+
ky: "KG",
|
|
1173
|
+
"ky-Arab": "CN",
|
|
1174
|
+
"ky-Latn": "TR",
|
|
1175
|
+
la: "VA",
|
|
1176
|
+
lab: "GR",
|
|
1177
|
+
lad: "IL",
|
|
1178
|
+
lag: "TZ",
|
|
1179
|
+
lah: "PK",
|
|
1180
|
+
laj: "UG",
|
|
1181
|
+
lb: "LU",
|
|
1182
|
+
lbe: "RU",
|
|
1183
|
+
lbw: "ID",
|
|
1184
|
+
lcp: "CN",
|
|
1185
|
+
lep: "IN",
|
|
1186
|
+
lez: "RU",
|
|
1187
|
+
lg: "UG",
|
|
1188
|
+
li: "NL",
|
|
1189
|
+
lif: "NP",
|
|
1190
|
+
"lif-Limb": "IN",
|
|
1191
|
+
lij: "IT",
|
|
1192
|
+
lis: "CN",
|
|
1193
|
+
ljp: "ID",
|
|
1194
|
+
lki: "IR",
|
|
1195
|
+
lkt: "US",
|
|
1196
|
+
lmn: "IN",
|
|
1197
|
+
lmo: "IT",
|
|
1198
|
+
ln: "CD",
|
|
1199
|
+
lo: "LA",
|
|
1200
|
+
lol: "CD",
|
|
1201
|
+
loz: "ZM",
|
|
1202
|
+
lrc: "IR",
|
|
1203
|
+
lt: "LT",
|
|
1204
|
+
ltg: "LV",
|
|
1205
|
+
lu: "CD",
|
|
1206
|
+
lua: "CD",
|
|
1207
|
+
luo: "KE",
|
|
1208
|
+
luy: "KE",
|
|
1209
|
+
luz: "IR",
|
|
1210
|
+
lv: "LV",
|
|
1211
|
+
lwl: "TH",
|
|
1212
|
+
lzh: "CN",
|
|
1213
|
+
lzz: "TR",
|
|
1214
|
+
mad: "ID",
|
|
1215
|
+
maf: "CM",
|
|
1216
|
+
mag: "IN",
|
|
1217
|
+
mai: "IN",
|
|
1218
|
+
mak: "ID",
|
|
1219
|
+
man: "GM",
|
|
1220
|
+
"man-Nkoo": "GN",
|
|
1221
|
+
mas: "KE",
|
|
1222
|
+
maz: "MX",
|
|
1223
|
+
mdf: "RU",
|
|
1224
|
+
mdh: "PH",
|
|
1225
|
+
mdr: "ID",
|
|
1226
|
+
men: "SL",
|
|
1227
|
+
mer: "KE",
|
|
1228
|
+
mfa: "TH",
|
|
1229
|
+
mfe: "MU",
|
|
1230
|
+
mg: "MG",
|
|
1231
|
+
mgh: "MZ",
|
|
1232
|
+
mgo: "CM",
|
|
1233
|
+
mgp: "NP",
|
|
1234
|
+
mgy: "TZ",
|
|
1235
|
+
mh: "MH",
|
|
1236
|
+
mi: "NZ",
|
|
1237
|
+
min: "ID",
|
|
1238
|
+
mis: "IQ",
|
|
1239
|
+
mk: "MK",
|
|
1240
|
+
ml: "IN",
|
|
1241
|
+
mls: "SD",
|
|
1242
|
+
mn: "MN",
|
|
1243
|
+
"mn-Mong": "CN",
|
|
1244
|
+
mni: "IN",
|
|
1245
|
+
mnw: "MM",
|
|
1246
|
+
moe: "CA",
|
|
1247
|
+
moh: "CA",
|
|
1248
|
+
mos: "BF",
|
|
1249
|
+
mr: "IN",
|
|
1250
|
+
mrd: "NP",
|
|
1251
|
+
mrj: "RU",
|
|
1252
|
+
mro: "BD",
|
|
1253
|
+
ms: "MY",
|
|
1254
|
+
mt: "MT",
|
|
1255
|
+
mtr: "IN",
|
|
1256
|
+
mua: "CM",
|
|
1257
|
+
mus: "US",
|
|
1258
|
+
mvy: "PK",
|
|
1259
|
+
mwk: "ML",
|
|
1260
|
+
mwr: "IN",
|
|
1261
|
+
mwv: "ID",
|
|
1262
|
+
mxc: "ZW",
|
|
1263
|
+
my: "MM",
|
|
1264
|
+
myv: "RU",
|
|
1265
|
+
myx: "UG",
|
|
1266
|
+
myz: "IR",
|
|
1267
|
+
mzn: "IR",
|
|
1268
|
+
na: "NR",
|
|
1269
|
+
nan: "CN",
|
|
1270
|
+
nap: "IT",
|
|
1271
|
+
naq: "NA",
|
|
1272
|
+
nb: "NO",
|
|
1273
|
+
nch: "MX",
|
|
1274
|
+
nd: "ZW",
|
|
1275
|
+
ndc: "MZ",
|
|
1276
|
+
nds: "DE",
|
|
1277
|
+
ne: "NP",
|
|
1278
|
+
new: "NP",
|
|
1279
|
+
ng: "NA",
|
|
1280
|
+
ngl: "MZ",
|
|
1281
|
+
nhe: "MX",
|
|
1282
|
+
nhw: "MX",
|
|
1283
|
+
nij: "ID",
|
|
1284
|
+
niu: "NU",
|
|
1285
|
+
njo: "IN",
|
|
1286
|
+
nl: "NL",
|
|
1287
|
+
nmg: "CM",
|
|
1288
|
+
nn: "NO",
|
|
1289
|
+
nnh: "CM",
|
|
1290
|
+
no: "NO",
|
|
1291
|
+
nod: "TH",
|
|
1292
|
+
noe: "IN",
|
|
1293
|
+
non: "SE",
|
|
1294
|
+
nqo: "GN",
|
|
1295
|
+
nr: "ZA",
|
|
1296
|
+
nsk: "CA",
|
|
1297
|
+
nso: "ZA",
|
|
1298
|
+
nus: "SS",
|
|
1299
|
+
nv: "US",
|
|
1300
|
+
nxq: "CN",
|
|
1301
|
+
ny: "MW",
|
|
1302
|
+
nym: "TZ",
|
|
1303
|
+
nyn: "UG",
|
|
1304
|
+
nzi: "GH",
|
|
1305
|
+
oc: "FR",
|
|
1306
|
+
om: "ET",
|
|
1307
|
+
or: "IN",
|
|
1308
|
+
os: "GE",
|
|
1309
|
+
osa: "US",
|
|
1310
|
+
otk: "MN",
|
|
1311
|
+
pa: "IN",
|
|
1312
|
+
"pa-Arab": "PK",
|
|
1313
|
+
pag: "PH",
|
|
1314
|
+
pal: "IR",
|
|
1315
|
+
"pal-Phlp": "CN",
|
|
1316
|
+
pam: "PH",
|
|
1317
|
+
pap: "AW",
|
|
1318
|
+
pau: "PW",
|
|
1319
|
+
pcd: "FR",
|
|
1320
|
+
pcm: "NG",
|
|
1321
|
+
pdc: "US",
|
|
1322
|
+
pdt: "CA",
|
|
1323
|
+
peo: "IR",
|
|
1324
|
+
pfl: "DE",
|
|
1325
|
+
phn: "LB",
|
|
1326
|
+
pka: "IN",
|
|
1327
|
+
pko: "KE",
|
|
1328
|
+
pl: "PL",
|
|
1329
|
+
pms: "IT",
|
|
1330
|
+
pnt: "GR",
|
|
1331
|
+
pon: "FM",
|
|
1332
|
+
pra: "PK",
|
|
1333
|
+
prd: "IR",
|
|
1334
|
+
ps: "AF",
|
|
1335
|
+
pt: "PT", //"BR",
|
|
1336
|
+
puu: "GA",
|
|
1337
|
+
qu: "PE",
|
|
1338
|
+
quc: "GT",
|
|
1339
|
+
qug: "EC",
|
|
1340
|
+
raj: "IN",
|
|
1341
|
+
rcf: "RE",
|
|
1342
|
+
rej: "ID",
|
|
1343
|
+
rgn: "IT",
|
|
1344
|
+
ria: "IN",
|
|
1345
|
+
rif: "MA",
|
|
1346
|
+
rjs: "NP",
|
|
1347
|
+
rkt: "BD",
|
|
1348
|
+
rm: "CH",
|
|
1349
|
+
rmf: "FI",
|
|
1350
|
+
rmo: "CH",
|
|
1351
|
+
rmt: "IR",
|
|
1352
|
+
rmu: "SE",
|
|
1353
|
+
rn: "BI",
|
|
1354
|
+
rng: "MZ",
|
|
1355
|
+
ro: "RO",
|
|
1356
|
+
rob: "ID",
|
|
1357
|
+
rof: "TZ",
|
|
1358
|
+
rtm: "FJ",
|
|
1359
|
+
ru: "RU",
|
|
1360
|
+
rue: "UA",
|
|
1361
|
+
rug: "SB",
|
|
1362
|
+
rw: "RW",
|
|
1363
|
+
rwk: "TZ",
|
|
1364
|
+
ryu: "JP",
|
|
1365
|
+
sa: "IN",
|
|
1366
|
+
saf: "GH",
|
|
1367
|
+
sah: "RU",
|
|
1368
|
+
saq: "KE",
|
|
1369
|
+
sas: "ID",
|
|
1370
|
+
sat: "IN",
|
|
1371
|
+
sav: "SN",
|
|
1372
|
+
saz: "IN",
|
|
1373
|
+
sbp: "TZ",
|
|
1374
|
+
sc: "IT",
|
|
1375
|
+
sck: "IN",
|
|
1376
|
+
scn: "IT",
|
|
1377
|
+
sco: "GB",
|
|
1378
|
+
scs: "CA",
|
|
1379
|
+
sd: "PK",
|
|
1380
|
+
"sd-Deva": "IN",
|
|
1381
|
+
"sd-Khoj": "IN",
|
|
1382
|
+
"sd-Sind": "IN",
|
|
1383
|
+
sdc: "IT",
|
|
1384
|
+
sdh: "IR",
|
|
1385
|
+
se: "NO",
|
|
1386
|
+
sef: "CI",
|
|
1387
|
+
seh: "MZ",
|
|
1388
|
+
sei: "MX",
|
|
1389
|
+
ses: "ML",
|
|
1390
|
+
sg: "CF",
|
|
1391
|
+
sga: "IE",
|
|
1392
|
+
sgs: "LT",
|
|
1393
|
+
shi: "MA",
|
|
1394
|
+
shn: "MM",
|
|
1395
|
+
si: "LK",
|
|
1396
|
+
sid: "ET",
|
|
1397
|
+
sk: "SK",
|
|
1398
|
+
skr: "PK",
|
|
1399
|
+
sl: "SI",
|
|
1400
|
+
sli: "PL",
|
|
1401
|
+
sly: "ID",
|
|
1402
|
+
sm: "WS",
|
|
1403
|
+
sma: "SE",
|
|
1404
|
+
smj: "SE",
|
|
1405
|
+
smn: "FI",
|
|
1406
|
+
smp: "IL",
|
|
1407
|
+
sms: "FI",
|
|
1408
|
+
sn: "ZW",
|
|
1409
|
+
snk: "ML",
|
|
1410
|
+
so: "SO",
|
|
1411
|
+
sou: "TH",
|
|
1412
|
+
sq: "AL",
|
|
1413
|
+
sr: "RS",
|
|
1414
|
+
srb: "IN",
|
|
1415
|
+
srn: "SR",
|
|
1416
|
+
srr: "SN",
|
|
1417
|
+
srx: "IN",
|
|
1418
|
+
ss: "ZA",
|
|
1419
|
+
ssy: "ER",
|
|
1420
|
+
st: "ZA",
|
|
1421
|
+
stq: "DE",
|
|
1422
|
+
su: "ID",
|
|
1423
|
+
suk: "TZ",
|
|
1424
|
+
sus: "GN",
|
|
1425
|
+
sv: "SE",
|
|
1426
|
+
sw: "TZ",
|
|
1427
|
+
swb: "YT",
|
|
1428
|
+
swc: "CD",
|
|
1429
|
+
swg: "DE",
|
|
1430
|
+
swv: "IN",
|
|
1431
|
+
sxn: "ID",
|
|
1432
|
+
syl: "BD",
|
|
1433
|
+
syr: "IQ",
|
|
1434
|
+
szl: "PL",
|
|
1435
|
+
ta: "IN",
|
|
1436
|
+
taj: "NP",
|
|
1437
|
+
tbw: "PH",
|
|
1438
|
+
tcy: "IN",
|
|
1439
|
+
tdd: "CN",
|
|
1440
|
+
tdg: "NP",
|
|
1441
|
+
tdh: "NP",
|
|
1442
|
+
te: "IN",
|
|
1443
|
+
tem: "SL",
|
|
1444
|
+
teo: "UG",
|
|
1445
|
+
tet: "TL",
|
|
1446
|
+
tg: "TJ",
|
|
1447
|
+
"tg-Arab": "PK",
|
|
1448
|
+
th: "TH",
|
|
1449
|
+
thl: "NP",
|
|
1450
|
+
thq: "NP",
|
|
1451
|
+
thr: "NP",
|
|
1452
|
+
ti: "ET",
|
|
1453
|
+
tig: "ER",
|
|
1454
|
+
tiv: "NG",
|
|
1455
|
+
tk: "TM",
|
|
1456
|
+
tkl: "TK",
|
|
1457
|
+
tkr: "AZ",
|
|
1458
|
+
tkt: "NP",
|
|
1459
|
+
tl: "PH",
|
|
1460
|
+
tly: "AZ",
|
|
1461
|
+
tmh: "NE",
|
|
1462
|
+
tn: "ZA",
|
|
1463
|
+
to: "TO",
|
|
1464
|
+
tog: "MW",
|
|
1465
|
+
tpi: "PG",
|
|
1466
|
+
tr: "TR",
|
|
1467
|
+
tru: "TR",
|
|
1468
|
+
trv: "TW",
|
|
1469
|
+
ts: "ZA",
|
|
1470
|
+
tsd: "GR",
|
|
1471
|
+
tsf: "NP",
|
|
1472
|
+
tsg: "PH",
|
|
1473
|
+
tsj: "BT",
|
|
1474
|
+
tt: "RU",
|
|
1475
|
+
ttj: "UG",
|
|
1476
|
+
tts: "TH",
|
|
1477
|
+
ttt: "AZ",
|
|
1478
|
+
tum: "MW",
|
|
1479
|
+
tvl: "TV",
|
|
1480
|
+
twq: "NE",
|
|
1481
|
+
txg: "CN",
|
|
1482
|
+
ty: "PF",
|
|
1483
|
+
tyv: "RU",
|
|
1484
|
+
tzm: "MA",
|
|
1485
|
+
udm: "RU",
|
|
1486
|
+
ug: "CN",
|
|
1487
|
+
"ug-Cyrl": "KZ",
|
|
1488
|
+
uga: "SY",
|
|
1489
|
+
uk: "UA",
|
|
1490
|
+
uli: "FM",
|
|
1491
|
+
umb: "AO",
|
|
1492
|
+
und: "US",
|
|
1493
|
+
unr: "IN",
|
|
1494
|
+
"unr-Deva": "NP",
|
|
1495
|
+
unx: "IN",
|
|
1496
|
+
ur: "PK",
|
|
1497
|
+
uz: "UZ",
|
|
1498
|
+
"uz-Arab": "AF",
|
|
1499
|
+
vai: "LR",
|
|
1500
|
+
ve: "ZA",
|
|
1501
|
+
vec: "IT",
|
|
1502
|
+
vep: "RU",
|
|
1503
|
+
vi: "VN",
|
|
1504
|
+
vic: "SX",
|
|
1505
|
+
vls: "BE",
|
|
1506
|
+
vmf: "DE",
|
|
1507
|
+
vmw: "MZ",
|
|
1508
|
+
vot: "RU",
|
|
1509
|
+
vro: "EE",
|
|
1510
|
+
vun: "TZ",
|
|
1511
|
+
wa: "BE",
|
|
1512
|
+
wae: "CH",
|
|
1513
|
+
wal: "ET",
|
|
1514
|
+
war: "PH",
|
|
1515
|
+
wbp: "AU",
|
|
1516
|
+
wbq: "IN",
|
|
1517
|
+
wbr: "IN",
|
|
1518
|
+
wls: "WF",
|
|
1519
|
+
wni: "KM",
|
|
1520
|
+
wo: "SN",
|
|
1521
|
+
wtm: "IN",
|
|
1522
|
+
wuu: "CN",
|
|
1523
|
+
xav: "BR",
|
|
1524
|
+
xcr: "TR",
|
|
1525
|
+
xh: "ZA",
|
|
1526
|
+
xlc: "TR",
|
|
1527
|
+
xld: "TR",
|
|
1528
|
+
xmf: "GE",
|
|
1529
|
+
xmn: "CN",
|
|
1530
|
+
xmr: "SD",
|
|
1531
|
+
xna: "SA",
|
|
1532
|
+
xnr: "IN",
|
|
1533
|
+
xog: "UG",
|
|
1534
|
+
xpr: "IR",
|
|
1535
|
+
xsa: "YE",
|
|
1536
|
+
xsr: "NP",
|
|
1537
|
+
yao: "MZ",
|
|
1538
|
+
yap: "FM",
|
|
1539
|
+
yav: "CM",
|
|
1540
|
+
ybb: "CM",
|
|
1541
|
+
yo: "NG",
|
|
1542
|
+
yrl: "BR",
|
|
1543
|
+
yua: "MX",
|
|
1544
|
+
yue: "HK",
|
|
1545
|
+
"yue-Hans": "CN",
|
|
1546
|
+
za: "CN",
|
|
1547
|
+
zag: "SD",
|
|
1548
|
+
zdj: "KM",
|
|
1549
|
+
zea: "NL",
|
|
1550
|
+
zgh: "MA",
|
|
1551
|
+
zh: "CN",
|
|
1552
|
+
"zh-Bopo": "TW",
|
|
1553
|
+
"zh-Hanb": "TW",
|
|
1554
|
+
"zh-Hant": "TW",
|
|
1555
|
+
zlm: "TG",
|
|
1556
|
+
zmi: "MY",
|
|
1557
|
+
zu: "ZA",
|
|
1558
|
+
zza: "TR",
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
function getLanguageFlagEmoji(locale) {
|
|
1562
|
+
|
|
1563
|
+
if (locale === "emoji") {
|
|
1564
|
+
return "🏳️🌈";
|
|
1565
|
+
} else if (locale === "eo") {
|
|
1566
|
+
// return "🏴🟩";
|
|
1567
|
+
return "🟩";
|
|
1568
|
+
// return `<svg viewBox="0 0 600 400" height="20">
|
|
1569
|
+
// <path fill="#FFF" d="m0,0h202v202H0"/>
|
|
1570
|
+
// <path fill="#090" d="m0,200H200V0H600V400H0m58-243 41-126 41,126-107-78h133"/>
|
|
1571
|
+
// </svg>`;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
var split = locale.toUpperCase().split(/-|_/);
|
|
1575
|
+
var lang = split.shift();
|
|
1576
|
+
var code = split.pop();
|
|
1577
|
+
|
|
1578
|
+
if (!/^[A-Z]{2}$/.test(code)) {
|
|
1579
|
+
code = languageToDefaultRegion[lang.toLowerCase()];
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (!code) {
|
|
1583
|
+
return "";
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const a = String.fromCodePoint(code.codePointAt(0) - 0x41 + 0x1F1E6);
|
|
1587
|
+
const b = String.fromCodePoint(code.codePointAt(1) - 0x41 + 0x1F1E6);
|
|
1588
|
+
return a + b;
|
|
1589
|
+
}
|
|
563
1590
|
|
|
564
1591
|
var uiContainer = div || document.createElement("div");
|
|
565
1592
|
uiContainer.classList.add("tracky-mouse-ui");
|
|
1593
|
+
uiContainer.classList.toggle("tracky-mouse-rtl", isRTL);
|
|
1594
|
+
uiContainer.dir = isRTL ? "rtl" : "ltr";
|
|
566
1595
|
uiContainer.innerHTML = `
|
|
567
1596
|
<div class="tracky-mouse-controls">
|
|
568
|
-
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9"
|
|
1597
|
+
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("Start")}</button>
|
|
569
1598
|
</div>
|
|
570
1599
|
<div class="tracky-mouse-canvas-container-container">
|
|
571
1600
|
<div class="tracky-mouse-canvas-container">
|
|
572
1601
|
<div class="tracky-mouse-canvas-overlay">
|
|
573
|
-
<button class="tracky-mouse-use-camera-button"
|
|
574
|
-
<!--<button class="tracky-mouse-use-camera-button"
|
|
575
|
-
<button class="tracky-mouse-use-demo-footage-button" hidden
|
|
1602
|
+
<button class="tracky-mouse-use-camera-button">${t("Allow Camera Access")}</button>
|
|
1603
|
+
<!--<button class="tracky-mouse-use-camera-button">${t("Use my camera")}</button>-->
|
|
1604
|
+
<button class="tracky-mouse-use-demo-footage-button" hidden>${t("Use demo footage")}</button>
|
|
576
1605
|
<div class="tracky-mouse-error-message" role="alert" hidden></div>
|
|
577
1606
|
</div>
|
|
578
1607
|
<canvas class="tracky-mouse-canvas"></canvas>
|
|
579
1608
|
</div>
|
|
580
1609
|
</div>
|
|
581
1610
|
<p class="tracky-mouse-desktop-app-download-message">
|
|
582
|
-
You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.
|
|
1611
|
+
${t('You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.')}
|
|
583
1612
|
</p>
|
|
584
1613
|
`;
|
|
585
1614
|
if (!div) {
|
|
@@ -602,10 +1631,10 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
602
1631
|
const settingsCategories = [
|
|
603
1632
|
{
|
|
604
1633
|
type: "group",
|
|
605
|
-
label: "Cursor Movement",
|
|
1634
|
+
label: t("Cursor Movement"),
|
|
606
1635
|
settings: [
|
|
607
1636
|
{
|
|
608
|
-
label: "Tilt influence",
|
|
1637
|
+
label: t("Tilt influence"),
|
|
609
1638
|
className: "tracky-mouse-tilt-influence",
|
|
610
1639
|
key: "headTrackingTiltInfluence",
|
|
611
1640
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -615,20 +1644,20 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
615
1644
|
max: 100,
|
|
616
1645
|
default: 0,
|
|
617
1646
|
labels: {
|
|
618
|
-
// min: "Optical flow", // too technical
|
|
619
|
-
// min: "Point tracking", // still technical but at least it's terminology we're already using
|
|
620
|
-
min: "Point tracking (2D)",
|
|
621
|
-
// max: "Head tilt",
|
|
622
|
-
max: "Head tilt (3D)",
|
|
1647
|
+
// min: t("Optical flow"), // too technical
|
|
1648
|
+
// min: t("Point tracking"), // still technical but at least it's terminology we're already using
|
|
1649
|
+
min: t("Point tracking (2D)"),
|
|
1650
|
+
// max: t("Head tilt"),
|
|
1651
|
+
max: t("Head tilt (3D)"),
|
|
623
1652
|
},
|
|
624
|
-
// description: "Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed.",
|
|
625
|
-
description: `Blends between using point tracking (2D) and detected head tilt (3D).
|
|
1653
|
+
// description: t("Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed."),
|
|
1654
|
+
description: t(`Blends between using point tracking (2D) and detected head tilt (3D).
|
|
626
1655
|
- At 0% it will use only point tracking. This moves the cursor according to visible movement of 2D points on your face within the camera's view, so it responds to both head rotation and translation.
|
|
627
1656
|
- At 100% it will use only head tilt. This uses an estimate of your face's orientation in 3D space, and ignores head translation. Note that this is smoothed, so it's not as responsive as point tracking. In this mode you never need to recenter by pushing the cursor to the edge of the screen.
|
|
628
|
-
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it
|
|
1657
|
+
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it.`),
|
|
629
1658
|
},
|
|
630
1659
|
{
|
|
631
|
-
label: "Motion threshold",
|
|
1660
|
+
label: t("Motion threshold"),
|
|
632
1661
|
className: "tracky-mouse-min-distance",
|
|
633
1662
|
key: "headTrackingMinDistance",
|
|
634
1663
|
type: "slider",
|
|
@@ -636,20 +1665,20 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
636
1665
|
max: 10,
|
|
637
1666
|
default: 0,
|
|
638
1667
|
labels: {
|
|
639
|
-
min: "Free",
|
|
640
|
-
max: "Steady",
|
|
1668
|
+
min: t("Free"),
|
|
1669
|
+
max: t("Steady"),
|
|
641
1670
|
},
|
|
642
|
-
description: "Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor.",
|
|
643
|
-
// description: "Movement less than this distance in pixels will be ignored.",
|
|
644
|
-
// description: "Speed in pixels/frame required to move the cursor.",
|
|
1671
|
+
description: t("Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor."),
|
|
1672
|
+
// description: t("Movement less than this distance in pixels will be ignored."),
|
|
1673
|
+
// description: t("Speed in pixels/frame required to move the cursor."),
|
|
645
1674
|
},
|
|
646
1675
|
{
|
|
647
1676
|
type: "group",
|
|
648
|
-
label: "Point tracking",
|
|
1677
|
+
label: t("Point tracking"),
|
|
649
1678
|
disabled: () => s.headTrackingTiltInfluence === 1,
|
|
650
1679
|
settings: [
|
|
651
1680
|
{
|
|
652
|
-
label: "Horizontal sensitivity",
|
|
1681
|
+
label: t("Horizontal sensitivity"),
|
|
653
1682
|
className: "tracky-mouse-sensitivity-x",
|
|
654
1683
|
key: "headTrackingSensitivityX",
|
|
655
1684
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -659,13 +1688,13 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
659
1688
|
max: 100,
|
|
660
1689
|
default: 25,
|
|
661
1690
|
labels: {
|
|
662
|
-
min: "Slow",
|
|
663
|
-
max: "Fast",
|
|
1691
|
+
min: t("Slow"),
|
|
1692
|
+
max: t("Fast"),
|
|
664
1693
|
},
|
|
665
|
-
description: "Speed of cursor movement in response to horizontal head movement.",
|
|
1694
|
+
description: t("Speed of cursor movement in response to horizontal head movement."),
|
|
666
1695
|
},
|
|
667
1696
|
{
|
|
668
|
-
label: "Vertical sensitivity",
|
|
1697
|
+
label: t("Vertical sensitivity"),
|
|
669
1698
|
className: "tracky-mouse-sensitivity-y",
|
|
670
1699
|
key: "headTrackingSensitivityY",
|
|
671
1700
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -675,13 +1704,13 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
675
1704
|
max: 100,
|
|
676
1705
|
default: 50,
|
|
677
1706
|
labels: {
|
|
678
|
-
min: "Slow",
|
|
679
|
-
max: "Fast",
|
|
1707
|
+
min: t("Slow"),
|
|
1708
|
+
max: t("Fast"),
|
|
680
1709
|
},
|
|
681
|
-
description: "Speed of cursor movement in response to vertical head movement.",
|
|
1710
|
+
description: t("Speed of cursor movement in response to vertical head movement."),
|
|
682
1711
|
},
|
|
683
1712
|
// {
|
|
684
|
-
// label: "Smoothing",
|
|
1713
|
+
// label: t("Smoothing"),
|
|
685
1714
|
// className: "tracky-mouse-smoothing",
|
|
686
1715
|
// key: "headTrackingSmoothing",
|
|
687
1716
|
// type: "slider",
|
|
@@ -689,8 +1718,8 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
689
1718
|
// max: 100,
|
|
690
1719
|
// default: 50,
|
|
691
1720
|
// labels: {
|
|
692
|
-
// min: "Linear", // or "Direct", "Raw", "None"
|
|
693
|
-
// max: "Smooth", // or "Smoothed"
|
|
1721
|
+
// min: t("Linear"), // or "Direct", "Raw", "None"
|
|
1722
|
+
// max: t("Smooth"), // or "Smoothed"
|
|
694
1723
|
// },
|
|
695
1724
|
// },
|
|
696
1725
|
|
|
@@ -703,7 +1732,7 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
703
1732
|
// Should it be swapped? What does other software with acceleration control look like?
|
|
704
1733
|
// In Windows it's just a checkbox apparently, but it could go as far as a custom curve editor.
|
|
705
1734
|
{
|
|
706
|
-
label: "Acceleration",
|
|
1735
|
+
label: t("Acceleration"),
|
|
707
1736
|
className: "tracky-mouse-acceleration",
|
|
708
1737
|
key: "headTrackingAcceleration",
|
|
709
1738
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -713,23 +1742,23 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
713
1742
|
max: 100,
|
|
714
1743
|
default: 50,
|
|
715
1744
|
labels: {
|
|
716
|
-
min: "Linear", // or "Direct", "Raw"
|
|
717
|
-
max: "Smooth",
|
|
1745
|
+
min: t("Linear"), // or "Direct", "Raw"
|
|
1746
|
+
max: t("Smooth"),
|
|
718
1747
|
},
|
|
719
|
-
// description: "Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly.",
|
|
720
|
-
// description: "Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor.",
|
|
721
|
-
description: `Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
|
|
722
|
-
Helps to stabilize the cursor. However, when using point tracking in combination with head tilt, a lower value may work better since head tilt is linear, and you want the point tracking to roughly match the head tracking for it to act as a seamless auto- calibration
|
|
1748
|
+
// description: t("Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly."),
|
|
1749
|
+
// description: t("Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor."),
|
|
1750
|
+
description: t(`Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
|
|
1751
|
+
Helps to stabilize the cursor. However, when using point tracking in combination with head tilt, a lower value may work better since head tilt is linear, and you want the point tracking to roughly match the head tracking for it to act as a seamless auto- calibration.`),
|
|
723
1752
|
},
|
|
724
1753
|
],
|
|
725
1754
|
},
|
|
726
1755
|
{
|
|
727
1756
|
type: "group",
|
|
728
|
-
label: "Head tilt calibration",
|
|
1757
|
+
label: t("Head tilt calibration"),
|
|
729
1758
|
disabled: () => s.headTrackingTiltInfluence === 0,
|
|
730
1759
|
settings: [
|
|
731
1760
|
{
|
|
732
|
-
label: "Horizontal tilt range",
|
|
1761
|
+
label: t("Horizontal tilt range"),
|
|
733
1762
|
className: "tracky-mouse-head-tilt-yaw-range",
|
|
734
1763
|
key: "headTiltYawRange",
|
|
735
1764
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -739,16 +1768,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
739
1768
|
max: 90,
|
|
740
1769
|
default: 60,
|
|
741
1770
|
labels: {
|
|
742
|
-
min: "Little neck movement",
|
|
743
|
-
max: "Large neck movement",
|
|
1771
|
+
min: t("Little neck movement"),
|
|
1772
|
+
max: t("Large neck movement"),
|
|
744
1773
|
},
|
|
745
|
-
// description: "Range of horizontal head tilt that moves the cursor from one side of the screen to the other.",
|
|
746
|
-
// description: "How much you need to tilt your head left and right to reach the edges of the screen.",
|
|
747
|
-
// description: "How much you need to tilt your head left or right to reach the edge of the screen.",
|
|
748
|
-
description: "Controls how much you need to tilt your head left or right to reach the edge of the screen.",
|
|
1774
|
+
// description: t("Range of horizontal head tilt that moves the cursor from one side of the screen to the other."),
|
|
1775
|
+
// description: t("How much you need to tilt your head left and right to reach the edges of the screen."),
|
|
1776
|
+
// description: t("How much you need to tilt your head left or right to reach the edge of the screen."),
|
|
1777
|
+
description: t("Controls how much you need to tilt your head left or right to reach the edge of the screen."),
|
|
749
1778
|
},
|
|
750
1779
|
{
|
|
751
|
-
label: "Vertical tilt range",
|
|
1780
|
+
label: t("Vertical tilt range"),
|
|
752
1781
|
className: "tracky-mouse-head-tilt-pitch-range",
|
|
753
1782
|
key: "headTiltPitchRange",
|
|
754
1783
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -758,17 +1787,17 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
758
1787
|
max: 60,
|
|
759
1788
|
default: 25,
|
|
760
1789
|
labels: {
|
|
761
|
-
min: "Little neck movement",
|
|
762
|
-
max: "Large neck movement",
|
|
1790
|
+
min: t("Little neck movement"),
|
|
1791
|
+
max: t("Large neck movement"),
|
|
763
1792
|
},
|
|
764
|
-
// description: "Range of vertical head tilt required to move the cursor from the top to the bottom of the screen.",
|
|
765
|
-
// description: "How much you need to tilt your head up and down to reach the edges of the screen.",
|
|
766
|
-
// description: "How much you need to tilt your head up or down to reach the edge of the screen.",
|
|
767
|
-
description: "Controls how much you need to tilt your head up or down to reach the edge of the screen.",
|
|
1793
|
+
// description: t("Range of vertical head tilt required to move the cursor from the top to the bottom of the screen."),
|
|
1794
|
+
// description: t("How much you need to tilt your head up and down to reach the edges of the screen."),
|
|
1795
|
+
// description: t("How much you need to tilt your head up or down to reach the edge of the screen."),
|
|
1796
|
+
description: t("Controls how much you need to tilt your head up or down to reach the edge of the screen."),
|
|
768
1797
|
},
|
|
769
1798
|
{
|
|
770
1799
|
// label: "Horizontal tilt offset",
|
|
771
|
-
label: "Horizontal cursor offset",
|
|
1800
|
+
label: t("Horizontal cursor offset"),
|
|
772
1801
|
className: "tracky-mouse-head-tilt-yaw-offset",
|
|
773
1802
|
key: "headTiltYawOffset",
|
|
774
1803
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -778,8 +1807,8 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
778
1807
|
max: 45,
|
|
779
1808
|
default: 0,
|
|
780
1809
|
labels: {
|
|
781
|
-
min: "Left",
|
|
782
|
-
max: "Right",
|
|
1810
|
+
min: t("Left"),
|
|
1811
|
+
max: t("Right"),
|
|
783
1812
|
},
|
|
784
1813
|
// TODO: how to describe this??
|
|
785
1814
|
// Specifically, how to disambiguate which direction is which / which way to adjust it?
|
|
@@ -787,15 +1816,15 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
787
1816
|
// Since it's opposite, even though it's technically yaw (angle units), it's easier to think of as moving the cursor.
|
|
788
1817
|
// Hence I've renamed the setting.
|
|
789
1818
|
// A later update might change the definitions and include a settings file format upgrade step.
|
|
790
|
-
// description: "Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible.",
|
|
791
|
-
// description: "Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible.",
|
|
1819
|
+
// description: t("Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible."),
|
|
1820
|
+
// description: t("Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible."),
|
|
792
1821
|
// TODO: should this say "horizontal" in the (main part of the) description?
|
|
793
|
-
description: `Adjusts the position of the cursor when the camera sees the head facing straight ahead.
|
|
794
|
-
⚠️ This horizontal offset is not recommended. Move the camera instead if possible.
|
|
1822
|
+
description: t(`Adjusts the position of the cursor when the camera sees the head facing straight ahead.
|
|
1823
|
+
⚠️ This horizontal offset is not recommended. Move the camera instead if possible. 📷`),
|
|
795
1824
|
},
|
|
796
1825
|
{
|
|
797
1826
|
// label: "Vertical tilt offset",
|
|
798
|
-
label: "Vertical cursor offset",
|
|
1827
|
+
label: t("Vertical cursor offset"),
|
|
799
1828
|
className: "tracky-mouse-head-tilt-pitch-offset",
|
|
800
1829
|
key: "headTiltPitchOffset",
|
|
801
1830
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -805,11 +1834,11 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
805
1834
|
max: 30,
|
|
806
1835
|
default: 2.5,
|
|
807
1836
|
labels: {
|
|
808
|
-
min: "Down",
|
|
809
|
-
max: "Up",
|
|
1837
|
+
min: t("Down"),
|
|
1838
|
+
max: t("Up"),
|
|
810
1839
|
},
|
|
811
|
-
// description: "Adjusts the center position of vertical head tilt.",
|
|
812
|
-
description:
|
|
1840
|
+
// description: t("Adjusts the center position of vertical head tilt."),
|
|
1841
|
+
description: t("Adjusts the position of the cursor when the camera sees the head facing straight ahead."),
|
|
813
1842
|
},
|
|
814
1843
|
],
|
|
815
1844
|
},
|
|
@@ -829,40 +1858,42 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
829
1858
|
// which awkwardly affects what mouse button serenade-driver sends; this doesn't affect the web version.
|
|
830
1859
|
{
|
|
831
1860
|
type: "group",
|
|
832
|
-
label: "Clicking",
|
|
1861
|
+
label: t("Clicking"),
|
|
833
1862
|
settings: [
|
|
834
1863
|
{
|
|
835
|
-
label: "Clicking mode:", // TODO: ":"?
|
|
1864
|
+
label: t("Clicking mode:"), // TODO: ":"?
|
|
836
1865
|
className: "tracky-mouse-clicking-mode",
|
|
837
1866
|
key: "clickingMode",
|
|
838
1867
|
type: "dropdown",
|
|
839
1868
|
options: [
|
|
840
|
-
{ value: "dwell", label: "Dwell to click" },
|
|
841
|
-
{ value: "blink", label: "Wink to click" },
|
|
842
|
-
|
|
843
|
-
|
|
1869
|
+
{ value: "dwell", label: t("Dwell to click"), description: t("Hold the cursor in place for a short time to click.") },
|
|
1870
|
+
{ value: "blink", label: t("Wink to click"), description: t("Close one eye to click. Left eye for left click, right eye for right click.") },
|
|
1871
|
+
// TODO: clarify that ooh works better than ah
|
|
1872
|
+
// "open wide" refers to height, but could be misinterpreted as opposite advice - a wide mouth shape when narrow works better
|
|
1873
|
+
// "open wide" is also perhaps unnecessary considering detection is improved... but who knows. maybe someone will try opening their mouth only slightly and expect it to work
|
|
1874
|
+
// Some people may understand "tall and narrow" better than "ooh rather than ah" and visa-versa
|
|
1875
|
+
{ value: "open-mouth-simple", label: t("Open mouth to click (simple)"), description: t("Open your mouth wide to click. At least one eye must be open to click.") },
|
|
1876
|
+
{ value: "open-mouth-ignoring-eyes", label: t("Open mouth to click (ignoring eyes)"), description: t("Open your mouth wide to click. Eye state is ignored.") },
|
|
1877
|
+
{ value: "open-mouth", label: t("Open mouth to click (with eye modifiers)"), description: t("Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click.") },
|
|
1878
|
+
{ value: "off", label: t("Off"), description: t("Disable clicking. Use with an external switch or programs that provide their own dwell clicking.") },
|
|
844
1879
|
],
|
|
845
1880
|
default: "dwell",
|
|
846
|
-
|
|
847
|
-
description:
|
|
848
|
-
- Dwell to click: Hold the cursor in place for a short time to click.
|
|
849
|
-
- Wink to click: Close one eye to click. Left eye for left click, right eye for right click.
|
|
850
|
-
- Open mouth to click: Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click.
|
|
851
|
-
- Off: Disable clicking. Use with an external switch or programs that provide their own dwell clicking.`,
|
|
1881
|
+
visible: () => isDesktopApp,
|
|
1882
|
+
description: t("Choose how to perform mouse clicks."),
|
|
852
1883
|
},
|
|
853
1884
|
{
|
|
854
1885
|
// on Windows, currently, when buttons are swapped at the system level, it affects serenade-driver's click()
|
|
855
1886
|
// "swap" is purposefully generic language so we don't have to know what system-level setting is
|
|
856
1887
|
// (also this may be seen as a weirdly named/designed option for right-clicking with the dwell clicker)
|
|
857
|
-
label: "Swap mouse buttons",
|
|
1888
|
+
label: t("Swap mouse buttons"),
|
|
858
1889
|
className: "tracky-mouse-swap-mouse-buttons",
|
|
859
1890
|
key: "swapMouseButtons",
|
|
860
1891
|
type: "checkbox",
|
|
861
1892
|
default: false,
|
|
862
|
-
|
|
863
|
-
description: `Switches the left and right mouse buttons.
|
|
1893
|
+
visible: () => isDesktopApp,
|
|
1894
|
+
description: t(`Switches the left and right mouse buttons.
|
|
864
1895
|
Useful if your system's mouse buttons are swapped.
|
|
865
|
-
Could also be used to right click with the dwell clicker in a pinch
|
|
1896
|
+
Could also be used to right click with the dwell clicker in a pinch.`),
|
|
866
1897
|
},
|
|
867
1898
|
|
|
868
1899
|
// This setting could called "click stabilization", "drag delay", "delay before dragging", "click drag delay", "drag prevention", etc.
|
|
@@ -871,33 +1902,33 @@ Could also be used to right click with the dwell clicker in a pinch.`,
|
|
|
871
1902
|
// at the end of the slider, although you shouldn't need to do that to effectively avoid dragging when trying to click,
|
|
872
1903
|
// and it might complicate the design of the slider labeling.
|
|
873
1904
|
{
|
|
874
|
-
label: "Delay before dragging ", // TODO: avoid non-breaking space hack
|
|
1905
|
+
label: t("Delay before dragging "), // TODO: avoid non-breaking space hack
|
|
875
1906
|
className: "tracky-mouse-delay-before-dragging",
|
|
876
1907
|
key: "delayBeforeDragging",
|
|
877
1908
|
type: "slider",
|
|
878
1909
|
min: 0,
|
|
879
1910
|
max: 1000,
|
|
880
1911
|
labels: {
|
|
881
|
-
min: "Easy to drag",
|
|
882
|
-
max: "Easy to click",
|
|
1912
|
+
min: t("Easy to drag"),
|
|
1913
|
+
max: t("Easy to click"),
|
|
883
1914
|
},
|
|
884
|
-
default:
|
|
885
|
-
|
|
1915
|
+
default: 800,
|
|
1916
|
+
visible: () => isDesktopApp,
|
|
886
1917
|
disabled: () => s.clickingMode === "off" || s.clickingMode === "dwell",
|
|
887
|
-
// description: "Locks mouse movement during the start of a click to prevent accidental dragging.",
|
|
888
|
-
// description: `Prevents mouse movement for the specified time after a click starts.
|
|
889
|
-
// You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click
|
|
890
|
-
description: `Locks mouse movement for the given duration during the start of a click.
|
|
891
|
-
You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click
|
|
1918
|
+
// description: t("Locks mouse movement during the start of a click to prevent accidental dragging."),
|
|
1919
|
+
// description: t(`Prevents mouse movement for the specified time after a click starts.
|
|
1920
|
+
// You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.`),
|
|
1921
|
+
description: t(`Locks mouse movement for the given duration during the start of a click.
|
|
1922
|
+
You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.`),
|
|
892
1923
|
},
|
|
893
1924
|
],
|
|
894
1925
|
},
|
|
895
1926
|
{
|
|
896
1927
|
type: "group",
|
|
897
|
-
label: "Video",
|
|
1928
|
+
label: t("Video"),
|
|
898
1929
|
settings: [
|
|
899
1930
|
{
|
|
900
|
-
label: "Camera source",
|
|
1931
|
+
label: t("Camera source"),
|
|
901
1932
|
className: "tracky-mouse-camera-select",
|
|
902
1933
|
key: "cameraDeviceId",
|
|
903
1934
|
handleSettingChange: () => {
|
|
@@ -905,63 +1936,63 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
905
1936
|
},
|
|
906
1937
|
type: "dropdown",
|
|
907
1938
|
options: [
|
|
908
|
-
{ value: "", label: "Default" },
|
|
1939
|
+
{ value: "", label: t("Default") },
|
|
909
1940
|
],
|
|
910
1941
|
default: "",
|
|
911
|
-
// description: "Select which camera to use for head tracking.",
|
|
912
|
-
description: "Selects which camera is used for head tracking.",
|
|
1942
|
+
// description: t("Select which camera to use for head tracking."),
|
|
1943
|
+
description: t("Selects which camera is used for head tracking."),
|
|
913
1944
|
},
|
|
914
1945
|
// TODO: move this inline with the camera source dropdown?
|
|
915
1946
|
{
|
|
916
|
-
label: "Open Camera Settings",
|
|
1947
|
+
label: t("Open Camera Settings"),
|
|
917
1948
|
className: "tracky-mouse-open-camera-settings",
|
|
918
1949
|
key: "openCameraSettings",
|
|
919
1950
|
type: "button",
|
|
920
|
-
|
|
1951
|
+
visible: () => isDesktopApp,
|
|
921
1952
|
onClick: async () => {
|
|
922
1953
|
let knownCameras = {};
|
|
923
1954
|
try {
|
|
924
1955
|
knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
|
|
925
1956
|
} catch (error) {
|
|
926
|
-
alert("Failed to open camera settings:\n" + "Failed to parse known cameras from localStorage:\n" + error.message);
|
|
1957
|
+
alert(t("Failed to open camera settings:\n") + t("Failed to parse known cameras from localStorage:\n") + error.message);
|
|
927
1958
|
return;
|
|
928
1959
|
}
|
|
929
1960
|
|
|
930
1961
|
const activeStream = cameraVideo.srcObject;
|
|
931
1962
|
const activeDeviceId = activeStream?.getVideoTracks()[0]?.getSettings()?.deviceId;
|
|
932
|
-
const selectedDeviceName = knownCameras[activeDeviceId]?.name || "Default";
|
|
1963
|
+
const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("Default");
|
|
933
1964
|
|
|
934
1965
|
try {
|
|
935
1966
|
const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
|
|
936
1967
|
if (result?.error) {
|
|
937
|
-
alert("Failed to open camera settings:\n" + result.error);
|
|
1968
|
+
alert(t("Failed to open camera settings:\n") + result.error);
|
|
938
1969
|
}
|
|
939
1970
|
} catch (error) {
|
|
940
|
-
alert("Failed to open camera settings:\n" + error.message);
|
|
1971
|
+
alert(t("Failed to open camera settings:\n") + error.message);
|
|
941
1972
|
}
|
|
942
1973
|
},
|
|
943
|
-
// description: "Open your camera's system settings window to adjust properties like brightness and contrast.",
|
|
944
|
-
// description: "Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure.",
|
|
945
|
-
description: "Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure.",
|
|
1974
|
+
// description: t("Open your camera's system settings window to adjust properties like brightness and contrast."),
|
|
1975
|
+
// description: t("Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure."),
|
|
1976
|
+
description: t("Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure."),
|
|
946
1977
|
},
|
|
947
1978
|
// TODO: try moving this to the corner of the camera view, so it's clearer it applies only to the camera view
|
|
948
1979
|
{
|
|
949
|
-
label: "Mirror",
|
|
1980
|
+
label: t("Mirror"),
|
|
950
1981
|
className: "tracky-mouse-mirror",
|
|
951
1982
|
key: "mirror",
|
|
952
1983
|
type: "checkbox",
|
|
953
1984
|
default: true,
|
|
954
|
-
description: "Mirrors the camera view horizontally.",
|
|
1985
|
+
description: t("Mirrors the camera view horizontally."),
|
|
955
1986
|
},
|
|
956
1987
|
]
|
|
957
1988
|
},
|
|
958
1989
|
{
|
|
959
1990
|
type: "group",
|
|
960
|
-
label: "General",
|
|
1991
|
+
label: t("General"),
|
|
961
1992
|
settings: [
|
|
962
1993
|
// opposite, "Start paused", might be clearer, especially if I add a "pause" button
|
|
963
1994
|
{
|
|
964
|
-
label: "Start enabled",
|
|
1995
|
+
label: t("Start enabled"),
|
|
965
1996
|
className: "tracky-mouse-start-enabled",
|
|
966
1997
|
key: "startEnabled",
|
|
967
1998
|
afterInitialLoad: () => { // TODO: does this hook make sense? right now it's the only usage. could this code not just be called later?
|
|
@@ -969,10 +2000,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
969
2000
|
},
|
|
970
2001
|
type: "checkbox",
|
|
971
2002
|
default: false,
|
|
972
|
-
description: "If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched.",
|
|
973
|
-
// description: "Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready.",
|
|
974
|
-
// description: "Makes Tracky Mouse active as soon as it's launched.",
|
|
975
|
-
// description: "Automatically starts Tracky Mouse as soon as it's run.",
|
|
2003
|
+
description: t("If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched."),
|
|
2004
|
+
// description: t("Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready."),
|
|
2005
|
+
// description: t("Makes Tracky Mouse active as soon as it's launched."),
|
|
2006
|
+
// description: t("Automatically starts Tracky Mouse as soon as it's run."),
|
|
976
2007
|
},
|
|
977
2008
|
{
|
|
978
2009
|
// For "experimental" label:
|
|
@@ -980,33 +2011,55 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
980
2011
|
// - I considered adding "⚠︎" but it feels a little too alarming
|
|
981
2012
|
// 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>)",
|
|
982
2013
|
// 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>)",
|
|
983
|
-
label: "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>)",
|
|
2014
|
+
label: t("Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• There is currently no visual or auditory feedback.\n• There are no settings for duration(s) to toggle on and off.\n• It is affected by false positive blink detections, especially when looking downward.\">Experimental</span>)"),
|
|
984
2015
|
className: "tracky-mouse-close-eyes-to-toggle",
|
|
985
2016
|
key: "closeEyesToToggle",
|
|
986
2017
|
type: "checkbox",
|
|
987
2018
|
default: false,
|
|
988
|
-
description: "If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds.",
|
|
2019
|
+
description: t("If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds."),
|
|
989
2020
|
},
|
|
990
2021
|
{
|
|
991
|
-
label: "Run at login",
|
|
2022
|
+
label: t("Run at login"),
|
|
992
2023
|
className: "tracky-mouse-run-at-login",
|
|
993
2024
|
key: "runAtLogin",
|
|
994
2025
|
type: "checkbox",
|
|
995
2026
|
default: false,
|
|
996
|
-
|
|
997
|
-
description: "If enabled, Tracky Mouse will automatically start when you log into your computer.",
|
|
998
|
-
// description: "Makes Tracky Mouse start automatically when you log into your computer.",
|
|
2027
|
+
visible: () => isDesktopApp,
|
|
2028
|
+
description: t("If enabled, Tracky Mouse will automatically start when you log into your computer."),
|
|
2029
|
+
// description: t("Makes Tracky Mouse start automatically when you log into your computer."),
|
|
999
2030
|
},
|
|
1000
2031
|
{
|
|
1001
|
-
label: "Check for updates",
|
|
2032
|
+
label: t("Check for updates"),
|
|
1002
2033
|
className: "tracky-mouse-check-for-updates",
|
|
1003
2034
|
key: "checkForUpdates",
|
|
1004
2035
|
type: "checkbox",
|
|
1005
2036
|
default: true,
|
|
1006
|
-
|
|
1007
|
-
description: "If enabled, Tracky Mouse will automatically check for updates when it starts.",
|
|
1008
|
-
// description: "Notifies you of new versions of Tracky Mouse.",
|
|
1009
|
-
// description: "Notifies you when a new version of Tracky Mouse is available.",
|
|
2037
|
+
visible: () => isDesktopApp,
|
|
2038
|
+
description: t("If enabled, Tracky Mouse will automatically check for updates when it starts."),
|
|
2039
|
+
// description: t("Notifies you of new versions of Tracky Mouse."),
|
|
2040
|
+
// description: t("Notifies you when a new version of Tracky Mouse is available."),
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
label: t("Language"),
|
|
2044
|
+
className: "tracky-mouse-language",
|
|
2045
|
+
key: "language",
|
|
2046
|
+
type: "dropdown",
|
|
2047
|
+
options: availableLanguages.map(lang => ({ value: lang, label: `${getLanguageFlagEmoji(lang)} ${languageNames[lang]?.[1]?.[0] || lang} (${languageNames[lang]?.[0]?.[0] || "?"})` })),
|
|
2048
|
+
default: locale,
|
|
2049
|
+
handleSettingChange: () => {
|
|
2050
|
+
// console.trace("handleSettingChange for language setting");
|
|
2051
|
+
// HACK: update localStorage because it's what's used to determine the language
|
|
2052
|
+
// This is needed for the desktop app which otherwise saves to a file not localStorage
|
|
2053
|
+
try {
|
|
2054
|
+
localStorage.setItem("tracky-mouse-settings", JSON.stringify(serializeSettings()));
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
console.error("Error saving options to localStorage:", error);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
reinit();
|
|
2060
|
+
},
|
|
2061
|
+
description: t("Select the language for the Tracky Mouse interface."),
|
|
2062
|
+
// description: t("Changes the language Tracky Mouse is displayed in."),
|
|
1010
2063
|
},
|
|
1011
2064
|
],
|
|
1012
2065
|
},
|
|
@@ -1065,9 +2118,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1065
2118
|
function buildSettingGroupUI(group) {
|
|
1066
2119
|
const detailsEl = document.createElement("details");
|
|
1067
2120
|
// detailsEl.className = "tracky-mouse-settings-group";
|
|
1068
|
-
// TODO: recursive check for
|
|
1069
|
-
if (group.settings.every(setting => setting.
|
|
1070
|
-
detailsEl.
|
|
2121
|
+
// TODO: recursive check for visibility - or just define visible() on groups
|
|
2122
|
+
if (group.settings.every(setting => setting.visible?.() === false)) {
|
|
2123
|
+
detailsEl.hidden = true;
|
|
1071
2124
|
}
|
|
1072
2125
|
const summaryEl = document.createElement("summary");
|
|
1073
2126
|
summaryEl.textContent = group.label;
|
|
@@ -1127,13 +2180,16 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1127
2180
|
${optionsHtml}
|
|
1128
2181
|
</select>
|
|
1129
2182
|
`;
|
|
2183
|
+
if (setting.options.some(option => option.description)) {
|
|
2184
|
+
setting.description += t("\n\nOptions:\n") + setting.options.map(option => `• ${option.label}${option.description ? `: ${option.description}` : ''}`).join("\n");
|
|
2185
|
+
}
|
|
1130
2186
|
} else if (setting.type === "button") {
|
|
1131
2187
|
rowEl.innerHTML = `
|
|
1132
2188
|
<button class="${setting.className}">${setting.label}</button>
|
|
1133
2189
|
`;
|
|
1134
2190
|
}
|
|
1135
|
-
if (setting.
|
|
1136
|
-
rowEl.
|
|
2191
|
+
if (setting.visible?.() === false) {
|
|
2192
|
+
rowEl.hidden = true;
|
|
1137
2193
|
}
|
|
1138
2194
|
|
|
1139
2195
|
if (setting.description) {
|
|
@@ -1226,14 +2282,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1226
2282
|
window.electronAPI.getIsPackaged().then((isPackaged) => {
|
|
1227
2283
|
runAtLoginCheckbox.disabled = !isPackaged;
|
|
1228
2284
|
});
|
|
1229
|
-
} else {
|
|
1230
|
-
for (const elementToHide of uiContainer.querySelectorAll('.tracky-mouse-desktop-only')) {
|
|
1231
|
-
elementToHide.hidden = true;
|
|
1232
|
-
}
|
|
1233
2285
|
}
|
|
1234
2286
|
|
|
1235
2287
|
var canvas = uiContainer.querySelector(".tracky-mouse-canvas");
|
|
1236
|
-
var ctx = canvas.getContext('2d');
|
|
2288
|
+
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
1237
2289
|
|
|
1238
2290
|
var debugEyeCanvas = document.createElement("canvas");
|
|
1239
2291
|
debugEyeCanvas.className = "tracky-mouse-debug-eye-canvas";
|
|
@@ -1397,6 +2449,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1397
2449
|
} catch (error) {
|
|
1398
2450
|
detector = null;
|
|
1399
2451
|
// TODO: avoid alert
|
|
2452
|
+
console.error("Failed to create facemesh detector:", error);
|
|
1400
2453
|
alert(error);
|
|
1401
2454
|
}
|
|
1402
2455
|
|
|
@@ -1417,6 +2470,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1417
2470
|
detector.dispose();
|
|
1418
2471
|
detector = null;
|
|
1419
2472
|
// TODO: avoid alert
|
|
2473
|
+
console.error("Facemesh estimation failed:", error);
|
|
1420
2474
|
alert(error);
|
|
1421
2475
|
}
|
|
1422
2476
|
return [];
|
|
@@ -1472,7 +2526,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1472
2526
|
};
|
|
1473
2527
|
const loadOptions = async (initialLoad = false) => {
|
|
1474
2528
|
if (window.electronAPI) {
|
|
1475
|
-
|
|
2529
|
+
// Desktop app: start from any saved settings in the main process,
|
|
2530
|
+
// then, on first load, push the renderer's canonical defaults back
|
|
2531
|
+
// so the main process has the same effective settings (and can
|
|
2532
|
+
// correctly drive features like dwell clicking on first run).
|
|
2533
|
+
const stored = await window.electronAPI.getOptions();
|
|
2534
|
+
deserializeSettings(stored, initialLoad);
|
|
2535
|
+
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2536
|
+
setOptions(serializeSettings());
|
|
2537
|
+
}
|
|
1476
2538
|
} else {
|
|
1477
2539
|
try {
|
|
1478
2540
|
if (localStorage.getItem("tracky-mouse-settings")) {
|
|
@@ -1486,9 +2548,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1486
2548
|
|
|
1487
2549
|
paused = !s.startEnabled;
|
|
1488
2550
|
|
|
1489
|
-
|
|
2551
|
+
// Basically Promise.withResolvers (but I'm not sure browser support is good enough)
|
|
2552
|
+
function createDeferred() {
|
|
2553
|
+
let resolve, reject;
|
|
2554
|
+
const promise = new Promise((res, rej) => {
|
|
2555
|
+
resolve = res;
|
|
2556
|
+
reject = rej;
|
|
2557
|
+
});
|
|
2558
|
+
return { promise, resolve, reject };
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
let populateCameraList = () => { return Promise.resolve(); };
|
|
1490
2562
|
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
|
1491
2563
|
populateCameraList = () => {
|
|
2564
|
+
let matchedCameraIdDeferred = createDeferred();
|
|
1492
2565
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
1493
2566
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
|
1494
2567
|
|
|
@@ -1519,31 +2592,40 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1519
2592
|
|
|
1520
2593
|
const defaultOption = document.createElement("option");
|
|
1521
2594
|
defaultOption.value = "";
|
|
1522
|
-
defaultOption.text = "Default";
|
|
2595
|
+
defaultOption.text = t("Default");
|
|
1523
2596
|
cameraSelect.appendChild(defaultOption);
|
|
1524
2597
|
|
|
1525
|
-
let
|
|
2598
|
+
let matchingDeviceId = "";
|
|
1526
2599
|
for (const device of videoDevices) {
|
|
1527
2600
|
const option = document.createElement('option');
|
|
1528
2601
|
option.value = device.deviceId;
|
|
1529
|
-
option.text = device.label ||
|
|
2602
|
+
option.text = device.label || t("Camera %0").replace("%0", cameraSelect.length);
|
|
1530
2603
|
cameraSelect.appendChild(option);
|
|
1531
2604
|
if (device.deviceId === s.cameraDeviceId) {
|
|
1532
|
-
|
|
2605
|
+
matchingDeviceId = device.deviceId;
|
|
2606
|
+
} else if (device.label === knownCameras[s.cameraDeviceId]?.name) {
|
|
2607
|
+
matchingDeviceId ||= device.deviceId;
|
|
1533
2608
|
}
|
|
1534
2609
|
}
|
|
1535
|
-
|
|
2610
|
+
|
|
2611
|
+
// Defaulting to "Default" would imply a preference isn't stored...
|
|
2612
|
+
// but would it be more friendly anyways?
|
|
1536
2613
|
// cameraSelect.value = found ? s.cameraDeviceId : "";
|
|
2614
|
+
|
|
1537
2615
|
// Show a placeholder for the selected camera
|
|
1538
|
-
if (s.cameraDeviceId && !
|
|
2616
|
+
if (s.cameraDeviceId && !matchingDeviceId) {
|
|
1539
2617
|
const option = document.createElement("option");
|
|
1540
2618
|
option.value = s.cameraDeviceId;
|
|
1541
2619
|
const knownInfo = knownCameras[s.cameraDeviceId];
|
|
1542
|
-
option.text = knownInfo ? `${knownInfo.name} (Unavailable)` : "Unavailable camera";
|
|
2620
|
+
option.text = knownInfo ? `${knownInfo.name} (${t("Unavailable")})` : t("Unavailable camera");
|
|
1543
2621
|
cameraSelect.appendChild(option);
|
|
2622
|
+
cameraSelect.value = s.cameraDeviceId;
|
|
2623
|
+
} else {
|
|
2624
|
+
cameraSelect.value = matchingDeviceId;
|
|
1544
2625
|
}
|
|
1545
|
-
|
|
2626
|
+
matchedCameraIdDeferred.resolve(matchingDeviceId);
|
|
1546
2627
|
});
|
|
2628
|
+
return matchedCameraIdDeferred.promise;
|
|
1547
2629
|
};
|
|
1548
2630
|
populateCameraList();
|
|
1549
2631
|
navigator.mediaDevices.addEventListener('devicechange', populateCameraList);
|
|
@@ -1594,13 +2676,81 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1594
2676
|
faceScore = 0;
|
|
1595
2677
|
faceConvergence = 0;
|
|
1596
2678
|
lastTimeWhenAnEyeWasOpen = Infinity; // far future rather than far past so that sleep gesture doesn't trigger initially, skipping the delay
|
|
1597
|
-
|
|
1598
|
-
startStopButton.textContent = "Start";
|
|
1599
|
-
startStopButton.setAttribute("aria-pressed", "false");
|
|
2679
|
+
updateStartStopButton();
|
|
1600
2680
|
};
|
|
1601
2681
|
|
|
1602
|
-
useCameraButton.onclick = TrackyMouse.useCamera = async () => {
|
|
2682
|
+
useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
|
|
2683
|
+
// Phases:
|
|
2684
|
+
// 1. "tryPreferredCamera"
|
|
2685
|
+
// Use the configured device ID to try to access the preferred camera.
|
|
2686
|
+
// If the permission has been revoked, the browser may
|
|
2687
|
+
// switch to a mode where `enumerateDevices` gives FAKE data
|
|
2688
|
+
// and `getUserMedia` will fail with OverconstrainedError
|
|
2689
|
+
// when trying to access a real device
|
|
2690
|
+
// (without even triggering a permission prompt that might
|
|
2691
|
+
// lead to getting the real list of devices.)
|
|
2692
|
+
// 2. "justGetPermission"
|
|
2693
|
+
// Request any camera in order to get camera permission
|
|
2694
|
+
// in general and get real data from `enumerateDevices`
|
|
2695
|
+
// in phase 3.
|
|
2696
|
+
// Close the stream immediately, as it may not be the
|
|
2697
|
+
// stream we want, and we can't tell, as far I know.
|
|
2698
|
+
// Then populate the camera list with real data.
|
|
2699
|
+
// 3. "retryPreferredCamera"
|
|
2700
|
+
// Now that we have a real list of devices,
|
|
2701
|
+
// and are allowed to access real devices,
|
|
2702
|
+
// try again with a specific device ID.
|
|
2703
|
+
// If there's a match by name and not ID, we use that.
|
|
2704
|
+
//
|
|
2705
|
+
// Q: Why not get rid of phase 1? Shouldn't 2+3 handle it?
|
|
2706
|
+
// In Electron, closing the stream and re-requesting access
|
|
2707
|
+
// often gives a "camera in use" error.
|
|
2708
|
+
// Plus, _ideally_ phase 1 means it can connect faster in browsers.
|
|
2709
|
+
// However, phase 1 may only get OverconstrainedError in browsers
|
|
2710
|
+
// as it's implemented.
|
|
2711
|
+
// We could get rid of phase 1 in browsers, basically separating the flows.
|
|
2712
|
+
// But...
|
|
2713
|
+
// I wonder if revoking camera access is what changes device IDs,
|
|
2714
|
+
// and if device IDs changing is what gives OverconstrainedError,
|
|
2715
|
+
// not the "fake device list" behavior. Is it perhaps only hiding labels in that mode,
|
|
2716
|
+
// but separately permanently scrambling IDs as a single event?
|
|
2717
|
+
// If device IDs are changed, storing the new device ID to try in phase 1
|
|
2718
|
+
// might make phase 1 work as an optimization in browsers.
|
|
2719
|
+
//
|
|
2720
|
+
// Q: If Electron has such a problem, would it not occur in the later phases?
|
|
2721
|
+
// Phase 2+3 should never occur in Electron.
|
|
2722
|
+
// In fact, we can guard against this.
|
|
2723
|
+
// Although, if phase 2+3 are only enterred on failure,
|
|
2724
|
+
// it can't really be a problem, can it?
|
|
2725
|
+
//
|
|
2726
|
+
// Q: Will this cause unnecessary prompts?
|
|
2727
|
+
// In the case of one existing camera, no.
|
|
2728
|
+
// In the case that there are multiple existing cameras,
|
|
2729
|
+
// and the user grants access to a different one than is configured,
|
|
2730
|
+
// it may cause an extra prompt.
|
|
2731
|
+
// In Firefox, you can choose to allow all cameras with a checkbox.
|
|
2732
|
+
// If you check that box, or select the matching camera before clicking Allow,
|
|
2733
|
+
// there should be only one prompt.
|
|
2734
|
+
//
|
|
2735
|
+
// Q: Why not use a library for this?
|
|
2736
|
+
// The mic-check package uses a similar approach, but seems to
|
|
2737
|
+
// encourage a pattern where nice error handling is applied
|
|
2738
|
+
// only to a "just get permission" equivalent phase,
|
|
2739
|
+
// whereas by using recursion or separating out the error handling
|
|
2740
|
+
// into a function, one can handle errors nicely always.
|
|
2741
|
+
// mic-check provides only the one phase, and presumably
|
|
2742
|
+
// is meant for a two-phase solution.
|
|
2743
|
+
//
|
|
2744
|
+
// Q: What happens if there are multiple overlapping calls to `useCamera`?
|
|
2745
|
+
// I don't know. TODO: test this.
|
|
2746
|
+
//
|
|
2747
|
+
// P.S. I gave a talk about this at Rubber Duck Conf 2026
|
|
2748
|
+
// You can view the slides here: https://websim.com/@1j01/ughaaaaaa
|
|
2749
|
+
|
|
1603
2750
|
await settingsLoadedPromise;
|
|
2751
|
+
|
|
2752
|
+
const phase = optionsOrEvent.phase ?? "tryPreferredCamera";
|
|
2753
|
+
|
|
1604
2754
|
const constraints = {
|
|
1605
2755
|
audio: false,
|
|
1606
2756
|
video: {
|
|
@@ -1609,26 +2759,51 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1609
2759
|
facingMode: "user",
|
|
1610
2760
|
}
|
|
1611
2761
|
};
|
|
1612
|
-
|
|
2762
|
+
const deviceIdToTry = phase === "retryPreferredCamera" ?
|
|
2763
|
+
optionsOrEvent.retryWithCameraDeviceId :
|
|
2764
|
+
phase === "tryPreferredCamera" ?
|
|
2765
|
+
s.cameraDeviceId :
|
|
2766
|
+
"";
|
|
2767
|
+
if (deviceIdToTry) {
|
|
1613
2768
|
delete constraints.video.facingMode;
|
|
1614
|
-
constraints.video.deviceId = { exact:
|
|
2769
|
+
constraints.video.deviceId = { exact: deviceIdToTry };
|
|
1615
2770
|
}
|
|
1616
|
-
|
|
2771
|
+
console.log("TrackyMouse.useCamera phase", phase, "constraints", constraints);
|
|
2772
|
+
navigator.mediaDevices.getUserMedia(constraints).then(async (stream) => {
|
|
2773
|
+
if (phase === "justGetPermission") {
|
|
2774
|
+
for (const track of stream.getTracks()) {
|
|
2775
|
+
track.stop();
|
|
2776
|
+
}
|
|
2777
|
+
// This is giving me User Gesture Hinged Access and Async Authorization Asking Attempt Absorption Anxiety,
|
|
2778
|
+
// or "UGHAaAAAAAA" (I'm coining that term)
|
|
2779
|
+
// (Look I made a presentation about it: https://websim.com/@1j01/ughaaaaaa)
|
|
2780
|
+
const matchedCameraId = await populateCameraList();
|
|
2781
|
+
if (matchedCameraId) {
|
|
2782
|
+
TrackyMouse.useCamera({ retryWithCameraDeviceId: matchedCameraId, phase: "retryPreferredCamera" });
|
|
2783
|
+
} else {
|
|
2784
|
+
TrackyMouse.useCamera({ retryWithCameraDeviceId: "", phase: "retryPreferredCamera" });
|
|
2785
|
+
}
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
1617
2788
|
populateCameraList();
|
|
1618
2789
|
reset();
|
|
1619
2790
|
|
|
1620
2791
|
cameraVideo.srcObject = stream;
|
|
1621
2792
|
useCameraButton.hidden = true;
|
|
1622
2793
|
errorMessage.hidden = true;
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2794
|
+
}, async (error) => {
|
|
2795
|
+
console.log("TrackyMouse.useCamera phase", phase, "error", error);
|
|
2796
|
+
if (
|
|
2797
|
+
phase === "tryPreferredCamera" &&
|
|
2798
|
+
(error.name === "OverconstrainedError" || error.name == "ConstraintNotSatisfiedError") &&
|
|
2799
|
+
!window.electronAPI
|
|
2800
|
+
) {
|
|
2801
|
+
TrackyMouse.useCamera({ phase: "justGetPermission" });
|
|
2802
|
+
return;
|
|
1626
2803
|
}
|
|
1627
|
-
}, (error) => {
|
|
1628
|
-
console.log(error);
|
|
1629
2804
|
if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
|
|
1630
2805
|
// required track is missing
|
|
1631
|
-
errorMessage.textContent = "No camera found. Please make sure you have a camera connected and enabled.";
|
|
2806
|
+
errorMessage.textContent = t("No camera found. Please make sure you have a camera connected and enabled.");
|
|
1632
2807
|
} else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
|
|
1633
2808
|
// webcam is already in use
|
|
1634
2809
|
// or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
|
|
@@ -1636,25 +2811,50 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1636
2811
|
// (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
|
|
1637
2812
|
// 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
|
|
1638
2813
|
// or "1 camera source detected" preceding it)
|
|
1639
|
-
errorMessage.textContent = "Webcam is already in use. Please make sure you have no other programs using the camera.";
|
|
2814
|
+
errorMessage.textContent = t("Webcam is already in use. Please make sure you have no other programs using the camera.");
|
|
1640
2815
|
} else if (error.name === "AbortError") {
|
|
1641
2816
|
// webcam is likely already in use
|
|
1642
2817
|
// I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
|
|
1643
|
-
|
|
2818
|
+
// Update: it definitely isn't, but I can't say exactly what it means in other cases.
|
|
2819
|
+
// Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
|
|
2820
|
+
// I really hope that isn't the problem.
|
|
2821
|
+
// errorMessage.textContent = "Webcam may already be in use. Please make sure you have no other programs using the camera.";
|
|
2822
|
+
errorMessage.textContent = t("Please make sure no other programs are using the camera and try again.");
|
|
2823
|
+
// A more honest/helpful message might be:
|
|
2824
|
+
// errorMessage.textContent = "Please try again and then make sure no other programs are using the camera and try again again.";
|
|
2825
|
+
// errorMessage.textContent = "Please try again before/after making sure no other programs are using the camera.";
|
|
2826
|
+
// if it were not to be confusing.
|
|
2827
|
+
// 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,
|
|
2828
|
+
// because sometimes that's enough.
|
|
1644
2829
|
} else if (error.name == "OverconstrainedError" || error.name == "ConstraintNotSatisfiedError") {
|
|
1645
|
-
// constraints
|
|
1646
|
-
|
|
2830
|
+
// constraints cannot be satisfied by available devices
|
|
2831
|
+
|
|
2832
|
+
// OverconstrainedError can be caused by `deviceId` not matching,
|
|
2833
|
+
// either due to the device not being present, or the ID having changed (don't ask me why that can happen but it can)
|
|
2834
|
+
// Note: OverconstrainedError has a `constraint` property but not in Firefox so it's not very helpful.
|
|
2835
|
+
if (constraints.video.deviceId?.exact) {
|
|
2836
|
+
// errorMessage.textContent = "The previously selected camera is not available. Please select a different camera from the dropdown and try again.";
|
|
2837
|
+
// errorMessage.textContent = "The previously selected camera is not available. Please mess around with Video > Camera source.";
|
|
2838
|
+
// errorMessage.textContent = "The previously selected camera is not available. Try changing Video > Camera source.";
|
|
2839
|
+
// errorMessage.textContent = "The previously selected camera is not available. Please select a camera from the \"Camera source\" dropdown in the Video settings and if it doesn't show up, it might after you select Default.";
|
|
2840
|
+
errorMessage.textContent = t("The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to.");
|
|
2841
|
+
// It's awkward but that's my best attempt at conveying how you may need to proceed
|
|
2842
|
+
// without complicated description of how/why the dropdown might be populated with
|
|
2843
|
+
// fake information until a camera stream is successfully opened.
|
|
2844
|
+
} else {
|
|
2845
|
+
errorMessage.textContent = t("Webcam does not support the required resolution. Please change your settings.");
|
|
2846
|
+
}
|
|
1647
2847
|
} else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
|
|
1648
2848
|
// permission denied in browser
|
|
1649
|
-
errorMessage.textContent = "Permission denied. Please enable access to the camera.";
|
|
2849
|
+
errorMessage.textContent = t("Permission denied. Please enable access to the camera.");
|
|
1650
2850
|
} else if (error.name == "TypeError") {
|
|
1651
2851
|
// empty constraints object
|
|
1652
|
-
errorMessage.textContent =
|
|
2852
|
+
errorMessage.textContent = `${t("Something went wrong accessing the camera.")} (${error.name}: ${error.message})`;
|
|
1653
2853
|
} else {
|
|
1654
2854
|
// other errors
|
|
1655
|
-
errorMessage.textContent =
|
|
2855
|
+
errorMessage.textContent = `${t("Something went wrong accessing the camera. Please try again.")} (${error.name}: ${error.message})`;
|
|
1656
2856
|
}
|
|
1657
|
-
errorMessage.textContent =
|
|
2857
|
+
errorMessage.textContent = `${t("⚠️ ")}${errorMessage.textContent}`;
|
|
1658
2858
|
errorMessage.hidden = false;
|
|
1659
2859
|
});
|
|
1660
2860
|
};
|
|
@@ -1667,9 +2867,6 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1667
2867
|
startStopButton.onclick = () => {
|
|
1668
2868
|
if (!useCameraButton.hidden) {
|
|
1669
2869
|
TrackyMouse.useCamera();
|
|
1670
|
-
if (!paused) {
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
2870
|
}
|
|
1674
2871
|
handleShortcut("toggle-tracking");
|
|
1675
2872
|
};
|
|
@@ -2320,11 +3517,16 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2320
3517
|
clickButton = 2;
|
|
2321
3518
|
}
|
|
2322
3519
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
3520
|
+
if (s.clickingMode === "open-mouth-ignoring-eyes") {
|
|
3521
|
+
mouthInfo.used = true;
|
|
3522
|
+
if (mouthInfo.thresholdMet) {
|
|
3523
|
+
clickButton = 0;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
if (s.clickingMode === "open-mouth" || s.clickingMode === "open-mouth-simple") {
|
|
2326
3527
|
mouthInfo.used = true;
|
|
2327
3528
|
blinkInfo.used = true;
|
|
3529
|
+
const allowModifiers = s.clickingMode !== "open-mouth-simple";
|
|
2328
3530
|
// Modifiers with eye closing trigger different buttons,
|
|
2329
3531
|
// making this a three-button mouse.
|
|
2330
3532
|
// (Eyebrow raising could be another alternative modifier.)
|
|
@@ -2332,9 +3534,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2332
3534
|
// so you can continue to scroll a webpage without trying to
|
|
2333
3535
|
// read with one eye closed (for example).
|
|
2334
3536
|
if (mouthInfo.thresholdMet && !prevMouthOpen) {
|
|
2335
|
-
if (blinkInfo.rightEye.active) {
|
|
3537
|
+
if (blinkInfo.rightEye.active && allowModifiers) {
|
|
2336
3538
|
mouseButtonUntilMouthCloses = 1;
|
|
2337
|
-
} else if (blinkInfo.leftEye.active) {
|
|
3539
|
+
} else if (blinkInfo.leftEye.active && allowModifiers) {
|
|
2338
3540
|
mouseButtonUntilMouthCloses = 2;
|
|
2339
3541
|
} else if (!blinkInfo.rightEye.open && !blinkInfo.leftEye.open) {
|
|
2340
3542
|
mouseButtonUntilMouthCloses = -1;
|
|
@@ -2349,7 +3551,12 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2349
3551
|
mouthInfo.active = false;
|
|
2350
3552
|
// TODO: show eyes as yellow too regardless of eye state?
|
|
2351
3553
|
}
|
|
2352
|
-
|
|
3554
|
+
}
|
|
3555
|
+
// In the mode with modifiers, it's helpful to preview a click's modifiers,
|
|
3556
|
+
// but in simple mode, it may be confusing to show any active state
|
|
3557
|
+
// when you're not clicking.
|
|
3558
|
+
if (mouthInfo.thresholdMet || s.clickingMode === "open-mouth-simple") {
|
|
3559
|
+
// TODO: DRY mapping (deduplicate the association of eyes to buttons)
|
|
2353
3560
|
blinkInfo.rightEye.active = clickButton === 1;
|
|
2354
3561
|
blinkInfo.leftEye.active = clickButton === 2;
|
|
2355
3562
|
}
|
|
@@ -2433,9 +3640,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2433
3640
|
const textYStart = -10;
|
|
2434
3641
|
|
|
2435
3642
|
|
|
2436
|
-
const pitchText =
|
|
2437
|
-
const yawText =
|
|
2438
|
-
const rollText =
|
|
3643
|
+
const pitchText = t("Pitch: ") + `${(headTilt.pitch * 180 / Math.PI).toFixed(1)}°`;
|
|
3644
|
+
const yawText = t("Yaw: ") + `${(headTilt.yaw * 180 / Math.PI).toFixed(1)}°`;
|
|
3645
|
+
const rollText = t("Roll: ") + `${(headTilt.roll * 180 / Math.PI).toFixed(1)}°`;
|
|
2439
3646
|
|
|
2440
3647
|
const boxWidth = Math.max(
|
|
2441
3648
|
ctx.measureText(pitchText).width,
|
|
@@ -2823,9 +4030,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2823
4030
|
ctx.lineWidth = 3;
|
|
2824
4031
|
ctx.font = "20px sans-serif";
|
|
2825
4032
|
ctx.beginPath();
|
|
2826
|
-
const text3 = "Face convergence score: " + ((useFacemesh && facemeshPrediction) ? "N/A" : faceConvergence.toFixed(4));
|
|
2827
|
-
const text1 = "Face tracking score: " + ((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4);
|
|
2828
|
-
const text2 = "Points based on score: " + ((useFacemesh && facemeshPrediction) ? pointsBasedOnFaceInViewConfidence : pointsBasedOnFaceScore).toFixed(4);
|
|
4033
|
+
const text3 = t("Face convergence score: ") + ((useFacemesh && facemeshPrediction) ? t("N/A") : faceConvergence.toFixed(4));
|
|
4034
|
+
const text1 = t("Face tracking score: ") + ((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4);
|
|
4035
|
+
const text2 = t("Points based on score: ") + ((useFacemesh && facemeshPrediction) ? pointsBasedOnFaceInViewConfidence : pointsBasedOnFaceScore).toFixed(4);
|
|
2829
4036
|
ctx.strokeText(text1, 50, 50);
|
|
2830
4037
|
ctx.fillText(text1, 50, 50);
|
|
2831
4038
|
ctx.strokeText(text2, 50, 70);
|
|
@@ -2847,7 +4054,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2847
4054
|
}
|
|
2848
4055
|
|
|
2849
4056
|
// 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)
|
|
2850
|
-
setInterval(function animationLoop() {
|
|
4057
|
+
const iid = setInterval(function animationLoop() {
|
|
2851
4058
|
draw(!paused || document.visibilityState === "visible");
|
|
2852
4059
|
}, 15);
|
|
2853
4060
|
|
|
@@ -2863,18 +4070,21 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2863
4070
|
TrackyMouse.useCamera();
|
|
2864
4071
|
}
|
|
2865
4072
|
|
|
2866
|
-
const
|
|
2867
|
-
mouseNeedsInitPos = true;
|
|
4073
|
+
const updateStartStopButton = () => {
|
|
2868
4074
|
if (paused) {
|
|
2869
|
-
|
|
2870
|
-
}
|
|
2871
|
-
if (paused) {
|
|
2872
|
-
startStopButton.textContent = "Start";
|
|
4075
|
+
startStopButton.textContent = t("Start");
|
|
2873
4076
|
startStopButton.setAttribute("aria-pressed", "false");
|
|
2874
4077
|
} else {
|
|
2875
|
-
startStopButton.textContent = "Stop";
|
|
4078
|
+
startStopButton.textContent = t("Stop");
|
|
2876
4079
|
startStopButton.setAttribute("aria-pressed", "true");
|
|
2877
4080
|
}
|
|
4081
|
+
};
|
|
4082
|
+
const updatePaused = () => {
|
|
4083
|
+
mouseNeedsInitPos = true;
|
|
4084
|
+
if (paused) {
|
|
4085
|
+
pointerEl.style.display = "none";
|
|
4086
|
+
}
|
|
4087
|
+
updateStartStopButton();
|
|
2878
4088
|
if (window.electronAPI) {
|
|
2879
4089
|
window.electronAPI.notifyToggleState(!paused);
|
|
2880
4090
|
}
|
|
@@ -2890,8 +4100,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2890
4100
|
// Try to handle both the global and local shortcuts
|
|
2891
4101
|
// If the global shortcut successfully registered, keydown shouldn't occur for the shortcut, right?
|
|
2892
4102
|
// I hope there's no cross-platform issue with this.
|
|
4103
|
+
let removeShortcutListener = null;
|
|
2893
4104
|
if (window.electronAPI) {
|
|
2894
|
-
window.electronAPI.onShortcut(handleShortcut);
|
|
4105
|
+
removeShortcutListener = window.electronAPI.onShortcut(handleShortcut);
|
|
2895
4106
|
}
|
|
2896
4107
|
const handleKeydown = (event) => {
|
|
2897
4108
|
// Same shortcut as the global shortcut in the electron app
|
|
@@ -2902,6 +4113,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2902
4113
|
addEventListener("keydown", handleKeydown);
|
|
2903
4114
|
|
|
2904
4115
|
return {
|
|
4116
|
+
_element: uiContainer,
|
|
4117
|
+
_setPaused(value) {
|
|
4118
|
+
paused = value;
|
|
4119
|
+
updatePaused();
|
|
4120
|
+
},
|
|
4121
|
+
_getPaused() {
|
|
4122
|
+
return paused;
|
|
4123
|
+
},
|
|
4124
|
+
_waitForSettingsLoaded() {
|
|
4125
|
+
return settingsLoadedPromise;
|
|
4126
|
+
},
|
|
2905
4127
|
dispose() {
|
|
2906
4128
|
// TODO: re-structure so that cleanup can succeed even if initialization fails
|
|
2907
4129
|
// OOP would help with this, by storing references in an object, but it doesn't necessarily
|
|
@@ -2910,6 +4132,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2910
4132
|
// (Would also be easy to maintain backwards compatibility while switching to using a class,
|
|
2911
4133
|
// returning an instance of the class from `TrackyMouse.init` but deprecating it in favor of constructing the class.)
|
|
2912
4134
|
|
|
4135
|
+
clearInterval(iid);
|
|
4136
|
+
|
|
2913
4137
|
// stopping camera stream is important, not sure about other resetting
|
|
2914
4138
|
reset();
|
|
2915
4139
|
|
|
@@ -2931,6 +4155,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2931
4155
|
|
|
2932
4156
|
removeEventListener("keydown", handleKeydown);
|
|
2933
4157
|
|
|
4158
|
+
removeShortcutListener?.();
|
|
4159
|
+
|
|
2934
4160
|
// This is a little awkward, reversing the initialization based on a possibly-preexisting element
|
|
2935
4161
|
// Could save and restore innerHTML but that won't restore event listeners, references, etc.
|
|
2936
4162
|
// and may not even be desired if the HTML was placeholder text mentioning it not yet being initialized for example.
|
|
@@ -2943,6 +4169,154 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2943
4169
|
};
|
|
2944
4170
|
};
|
|
2945
4171
|
|
|
4172
|
+
// Wrapper that manages an inner instance and recreates it when the language is changed.
|
|
4173
|
+
TrackyMouse.init = function (div, opts = {}) {
|
|
4174
|
+
let inner = null;
|
|
4175
|
+
|
|
4176
|
+
// UI state saving could be cleaner as part of the inner instance idk
|
|
4177
|
+
// Or, you know, ideally we update the UI text reactively without
|
|
4178
|
+
// stopping/starting the camera stream etc. when switching languages.
|
|
4179
|
+
const saveUIState = () => {
|
|
4180
|
+
const paused = inner._getPaused();
|
|
4181
|
+
const collapsibles = inner._element.querySelectorAll("details");
|
|
4182
|
+
const openStates = Array.from(collapsibles).map(c => c.open);
|
|
4183
|
+
const scrollables = inner._element.querySelectorAll("*");
|
|
4184
|
+
const scrollPositions = Array.from(scrollables).map(s => [s.scrollLeft, s.scrollTop]);
|
|
4185
|
+
const focusedElementSelector = Array.from(document.activeElement?.classList || []).map(c => `.${c}`).join("");
|
|
4186
|
+
return { paused, openStates, scrollPositions, focusedElementSelector };
|
|
4187
|
+
};
|
|
4188
|
+
const restoreUIState = ({ paused, openStates, scrollPositions, focusedElementSelector }) => {
|
|
4189
|
+
inner._waitForSettingsLoaded().then(() => {
|
|
4190
|
+
inner._setPaused(paused);
|
|
4191
|
+
});
|
|
4192
|
+
// assuming DOM structure doesn't change
|
|
4193
|
+
const collapsibles = inner._element.querySelectorAll("details");
|
|
4194
|
+
for (let i = 0; i < collapsibles.length; i++) {
|
|
4195
|
+
collapsibles[i].open = openStates[i];
|
|
4196
|
+
}
|
|
4197
|
+
const scrollables = inner._element.querySelectorAll("*");
|
|
4198
|
+
for (let i = 0; i < scrollables.length; i++) {
|
|
4199
|
+
const [scrollLeft, scrollTop] = scrollPositions[i];
|
|
4200
|
+
scrollables[i].scrollLeft = scrollLeft;
|
|
4201
|
+
scrollables[i].scrollTop = scrollTop;
|
|
4202
|
+
}
|
|
4203
|
+
if (focusedElementSelector) {
|
|
4204
|
+
const elementToFocus = inner._element.querySelector(focusedElementSelector);
|
|
4205
|
+
elementToFocus?.focus();
|
|
4206
|
+
}
|
|
4207
|
+
};
|
|
4208
|
+
const reinit = () => {
|
|
4209
|
+
const uiState = saveUIState();
|
|
4210
|
+
inner.dispose();
|
|
4211
|
+
createInner();
|
|
4212
|
+
restoreUIState(uiState);
|
|
4213
|
+
};
|
|
4214
|
+
|
|
4215
|
+
const createInner = () => {
|
|
4216
|
+
inner = TrackyMouse._initInner(div, opts, reinit);
|
|
4217
|
+
};
|
|
4218
|
+
|
|
4219
|
+
createInner();
|
|
4220
|
+
|
|
4221
|
+
return {
|
|
4222
|
+
dispose() {
|
|
4223
|
+
inner.dispose();
|
|
4224
|
+
},
|
|
4225
|
+
};
|
|
4226
|
+
|
|
4227
|
+
};
|
|
4228
|
+
|
|
4229
|
+
TrackyMouse.initScreenOverlay = () => {
|
|
4230
|
+
|
|
4231
|
+
const template = `
|
|
4232
|
+
<div class="tracky-mouse-absolute-center">
|
|
4233
|
+
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
|
|
4234
|
+
<img src="../images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
|
|
4235
|
+
</div>
|
|
4236
|
+
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
|
|
4237
|
+
<img src="../images/head-not-found.svg" alt="head not found" width="128" height="128">
|
|
4238
|
+
</div>
|
|
4239
|
+
</div>
|
|
4240
|
+
<div id="tracky-mouse-screen-overlay-message"></div>
|
|
4241
|
+
`;
|
|
4242
|
+
const fragment = document.createRange().createContextualFragment(template);
|
|
4243
|
+
document.body.appendChild(fragment);
|
|
4244
|
+
|
|
4245
|
+
const message = document.getElementById("tracky-mouse-screen-overlay-message");
|
|
4246
|
+
message.dir = "auto";
|
|
4247
|
+
|
|
4248
|
+
const inputFeedbackCanvas = document.createElement("canvas");
|
|
4249
|
+
inputFeedbackCanvas.style.position = "absolute";
|
|
4250
|
+
inputFeedbackCanvas.style.top = "0";
|
|
4251
|
+
inputFeedbackCanvas.style.left = "0";
|
|
4252
|
+
inputFeedbackCanvas.style.pointerEvents = "none";
|
|
4253
|
+
inputFeedbackCanvas.width = 32;
|
|
4254
|
+
inputFeedbackCanvas.height = 32;
|
|
4255
|
+
document.body.appendChild(inputFeedbackCanvas);
|
|
4256
|
+
const inputFeedbackCtx = inputFeedbackCanvas.getContext("2d");
|
|
4257
|
+
function drawInputFeedback({ inputFeedback, isEnabled }) {
|
|
4258
|
+
const { blinkInfo, mouthInfo } = inputFeedback;
|
|
4259
|
+
inputFeedbackCtx.clearRect(0, 0, inputFeedbackCanvas.width, inputFeedbackCanvas.height);
|
|
4260
|
+
if (!isEnabled) {
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
// draw meters for blink and mouth openness
|
|
4264
|
+
// TODO: draw meter backings to disambiguate showing zero vs being occluded by taskbar
|
|
4265
|
+
// (Ideally it should stay on top of the taskbar and context menus all the time
|
|
4266
|
+
// but that's another issue: https://github.com/1j01/tracky-mouse/issues/14)
|
|
4267
|
+
const drawMeter = (x, yCenter, width, height, { active, thresholdMet }) => {
|
|
4268
|
+
inputFeedbackCtx.fillStyle = active ? "red" : thresholdMet ? "yellow" : "cyan";
|
|
4269
|
+
inputFeedbackCtx.fillRect(x, yCenter - height / 2, width, height);
|
|
4270
|
+
};
|
|
4271
|
+
if (blinkInfo?.used) {
|
|
4272
|
+
for (const eye of [blinkInfo.leftEye, blinkInfo.rightEye]) {
|
|
4273
|
+
drawMeter(eye === blinkInfo.leftEye ? 5 : 20, 5, 10, Math.max(2, 20 * eye.heightRatio), eye);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
if (mouthInfo?.used) {
|
|
4277
|
+
drawMeter(0, 20, 23, Math.max(2, 40 * mouthInfo.heightRatio), mouthInfo);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
function updateMousePos(x, y) {
|
|
4282
|
+
// inputFeedbackCanvas.style.transform = `translate(${x - inputFeedbackCanvas.width / 2}px, ${y - inputFeedbackCanvas.height / 2}px)`;
|
|
4283
|
+
// inputFeedbackCanvas.style.transform = `translate(${x}px, ${y}px)`;
|
|
4284
|
+
inputFeedbackCanvas.style.transform = `translate(${Math.min(x, window.innerWidth - inputFeedbackCanvas.width)}px, ${Math.min(y, window.innerHeight - inputFeedbackCanvas.height)}px)`;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function update(data) {
|
|
4288
|
+
const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset } = data;
|
|
4289
|
+
|
|
4290
|
+
message.style.bottom = `${bottomOffset}px`;
|
|
4291
|
+
|
|
4292
|
+
// Other diagnostics in the future would be stuff like:
|
|
4293
|
+
// - head too far away (smaller than a certain size) https://github.com/1j01/tracky-mouse/issues/49
|
|
4294
|
+
// - bad lighting conditions
|
|
4295
|
+
// see: https://github.com/1j01/tracky-mouse/issues/26
|
|
4296
|
+
|
|
4297
|
+
document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback);
|
|
4298
|
+
document.body.classList.toggle("tracky-mouse-head-not-found", inputFeedback.headNotFound);
|
|
4299
|
+
|
|
4300
|
+
message.innerText = messageText;
|
|
4301
|
+
|
|
4302
|
+
if (!isEnabled && !isManualTakeback) {
|
|
4303
|
+
// Fade out the message after a little while so it doesn't get in the way.
|
|
4304
|
+
// TODO: make sure animation isn't interrupted by inputFeedback updates.
|
|
4305
|
+
message.style.animation = "tracky-mouse-screen-overlay-message-fade-out 2s ease-in-out forwards 10s";
|
|
4306
|
+
} else {
|
|
4307
|
+
message.style.animation = "";
|
|
4308
|
+
message.style.opacity = "1";
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
drawInputFeedback(data);
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
return {
|
|
4315
|
+
update,
|
|
4316
|
+
updateMousePos,
|
|
4317
|
+
};
|
|
4318
|
+
};
|
|
4319
|
+
|
|
2946
4320
|
// CommonJS export is untested. Script tag usage recommended.
|
|
2947
4321
|
// Just including this in case it is somehow useful.
|
|
2948
4322
|
// eslint-disable-next-line no-undef
|