tracky-mouse 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/face-landmarks-detection.min.js +1 -1
  2. package/lib/face_mesh/face_mesh.js +1 -1
  3. package/locales/ar/translation.json +5 -1
  4. package/locales/ar-EG/translation.json +5 -1
  5. package/locales/bg/translation.json +5 -1
  6. package/locales/bn/translation.json +5 -1
  7. package/locales/ca/translation.json +5 -1
  8. package/locales/ce/translation.json +5 -1
  9. package/locales/ceb/translation.json +6 -2
  10. package/locales/cs/translation.json +5 -1
  11. package/locales/da/translation.json +5 -1
  12. package/locales/de/translation.json +5 -1
  13. package/locales/el/translation.json +5 -1
  14. package/locales/emoji/emoji-translation-notes.md +1 -0
  15. package/locales/emoji/translation.json +9 -5
  16. package/locales/en/translation.json +5 -1
  17. package/locales/eo/translation.json +5 -1
  18. package/locales/es/translation.json +5 -1
  19. package/locales/eu/translation.json +5 -1
  20. package/locales/fa/translation.json +5 -1
  21. package/locales/fi/translation.json +5 -1
  22. package/locales/fr/translation.json +5 -1
  23. package/locales/gu/translation.json +5 -1
  24. package/locales/ha/translation.json +5 -1
  25. package/locales/he/translation.json +5 -1
  26. package/locales/hi/translation.json +5 -1
  27. package/locales/hr/translation.json +5 -1
  28. package/locales/hu/translation.json +5 -1
  29. package/locales/hy/translation.json +5 -1
  30. package/locales/id/translation.json +5 -1
  31. package/locales/it/translation.json +5 -1
  32. package/locales/ja/translation.json +5 -1
  33. package/locales/jv/translation.json +6 -2
  34. package/locales/ko/translation.json +5 -1
  35. package/locales/mr/translation.json +5 -1
  36. package/locales/ms/translation.json +5 -1
  37. package/locales/nan/translation.json +5 -1
  38. package/locales/nb/translation.json +5 -1
  39. package/locales/nl/translation.json +5 -1
  40. package/locales/pa/translation.json +5 -1
  41. package/locales/pl/translation.json +5 -1
  42. package/locales/pt/translation.json +5 -1
  43. package/locales/pt-BR/translation.json +5 -1
  44. package/locales/ro/translation.json +5 -1
  45. package/locales/ru/translation.json +5 -1
  46. package/locales/sk/translation.json +5 -1
  47. package/locales/sl/translation.json +5 -1
  48. package/locales/sr/translation.json +5 -1
  49. package/locales/sv/translation.json +5 -1
  50. package/locales/sw/translation.json +5 -1
  51. package/locales/ta/translation.json +5 -1
  52. package/locales/te/translation.json +5 -1
  53. package/locales/th/translation.json +5 -1
  54. package/locales/tl/translation.json +6 -2
  55. package/locales/tr/translation.json +5 -1
  56. package/locales/tt/translation.json +5 -1
  57. package/locales/uk/translation.json +5 -1
  58. package/locales/ur/translation.json +5 -1
  59. package/locales/uz/translation.json +5 -1
  60. package/locales/vi/translation.json +5 -1
  61. package/locales/war/translation.json +6 -2
  62. package/locales/zh/translation.json +5 -1
  63. package/locales/zh-simplified/translation.json +5 -1
  64. package/package.json +2 -2
  65. package/{audio.js → src/audio.js} +1 -1
  66. package/src/autoscroll.js +189 -0
  67. package/src/input-simulator.js +518 -0
  68. package/tracky-mouse.css +33 -2
  69. package/tracky-mouse.js +166 -58
package/tracky-mouse.js CHANGED
@@ -601,7 +601,7 @@ TrackyMouse._initAudio = async function () {
601
601
  let module;
602
602
  try {
603
603
  // console.log("Loading audio support...");
604
- module = await import("./audio.js");
604
+ module = await import("./src/audio.js");
605
605
  } catch (e) {
606
606
  console.warn("Failed to load audio module, click sounds will be disabled:", e);
607
607
  }
@@ -1672,7 +1672,7 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
1672
1672
  <div class="tracky-mouse-controls">
1673
1673
  <button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("ui.startStopButton.start", { defaultValue: "Start" })}</button>
1674
1674
  </div>
1675
- <div class="tracky-mouse-canvas-container-container">
1675
+ <div class="tracky-mouse-camera-area">
1676
1676
  <div class="tracky-mouse-canvas-container">
1677
1677
  <div class="tracky-mouse-canvas-overlay">
1678
1678
  <button class="tracky-mouse-use-camera-button">${t("ui.camera.allowAccess", { defaultValue: "Allow Camera Access" })}</button>
@@ -1697,6 +1697,37 @@ TrackyMouse._initInner = function (div, initOptions, reinit) {
1697
1697
  let canvasContainer = uiContainer.querySelector('.tracky-mouse-canvas-container');
1698
1698
  let desktopAppDownloadMessage = uiContainer.querySelector('.tracky-mouse-desktop-app-download-message');
1699
1699
 
1700
+ let lastShownErrorDetails = null;
1701
+ function showError(message, error, { warningIcon = true, errorClass = "other" } = {}) {
1702
+ const alreadyShown = !errorMessage.hidden && lastShownErrorDetails?.message === message && lastShownErrorDetails?.error?.name === error?.name && lastShownErrorDetails?.error?.message === error?.message;
1703
+ if (alreadyShown) {
1704
+ // Play CSS animation to indicate repeated errors
1705
+ // but not if they're occurring constantly
1706
+ // Note: for constant errors, with this scheme, it may animate
1707
+ // when returning to the tab due to timer throttling, or due to lag.
1708
+ if (performance.now() > lastShownErrorDetails.time + 100) {
1709
+ errorMessage.style.animation = "none";
1710
+ if (alreadyShown) {
1711
+ void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
1712
+ errorMessage.style.animation = "";
1713
+ }
1714
+ }
1715
+ } else {
1716
+ if (warningIcon) {
1717
+ errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${message}`;
1718
+ } else {
1719
+ errorMessage.textContent = message;
1720
+ }
1721
+ if (error) {
1722
+ const pre = document.createElement("pre");
1723
+ pre.textContent = error.name + ": " + error.message;
1724
+ errorMessage.appendChild(pre);
1725
+ }
1726
+ errorMessage.hidden = false;
1727
+ }
1728
+ lastShownErrorDetails = { message, error, time: performance.now(), errorClass };
1729
+ }
1730
+
1700
1731
  // Settings (initialized later; defaults are defined in settingsCategories)
1701
1732
  const s = {};
1702
1733
 
@@ -2034,11 +2065,21 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2034
2065
  type: "button",
2035
2066
  visible: () => isDesktopApp,
2036
2067
  onClick: async () => {
2068
+ function showToast(message) {
2069
+ const toast = document.createElement("div");
2070
+ toast.className = "tracky-mouse-toast";
2071
+ toast.textContent = message;
2072
+ document.body.appendChild(toast);
2073
+ setTimeout(() => {
2074
+ toast.remove();
2075
+ }, 5000);
2076
+ }
2077
+
2037
2078
  let knownCameras = {};
2038
2079
  try {
2039
2080
  knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
2040
2081
  } catch (error) {
2041
- alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + t("openCameraSettings.errors.parseKnownCameras", { defaultValue: "Failed to parse known cameras from localStorage:" }) + "\n" + error.message);
2082
+ showToast(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + t("openCameraSettings.errors.parseKnownCameras", { defaultValue: "Failed to parse known cameras from localStorage:" }) + "\n" + error.name + ": " + error.message);
2042
2083
  return;
2043
2084
  }
2044
2085
 
@@ -2049,10 +2090,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2049
2090
  try {
2050
2091
  const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
2051
2092
  if (result?.error) {
2052
- alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + result.error);
2093
+ showToast(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + result.error);
2053
2094
  }
2054
2095
  } catch (error) {
2055
- alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + error.message);
2096
+ showToast(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + error.name + ": " + error.message);
2056
2097
  }
2057
2098
  },
2058
2099
  // description: t("settings.openCameraSettings.description.alt1", { defaultValue: "Open your camera's system settings window to adjust properties like brightness and contrast." }),
@@ -2389,7 +2430,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2389
2430
  let debugEyeCanvas = document.createElement("canvas");
2390
2431
  debugEyeCanvas.className = "tracky-mouse-debug-eye-canvas";
2391
2432
  debugEyeCanvas.style.display = "none";
2392
- uiContainer.querySelector(".tracky-mouse-canvas-container-container").appendChild(debugEyeCanvas);
2433
+ uiContainer.querySelector(".tracky-mouse-camera-area").appendChild(debugEyeCanvas);
2393
2434
  let debugEyeCtx = debugEyeCanvas.getContext('2d');
2394
2435
 
2395
2436
  let pointerEl = document.createElement('div');
@@ -2477,6 +2518,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2477
2518
  let lastMouseDownTime = -Infinity;
2478
2519
  let mouseNeedsInitPos = true;
2479
2520
 
2521
+ // Virtual display bounds cache (Electron only); covers all connected monitors.
2522
+ let virtualDisplayBounds = null;
2523
+ if (window.electronAPI?.getVirtualDisplayBounds) {
2524
+ window.electronAPI.getVirtualDisplayBounds().then((bounds) => {
2525
+ virtualDisplayBounds = bounds;
2526
+ });
2527
+ window.electronAPI.onVirtualDisplayBoundsChanged?.((bounds) => {
2528
+ virtualDisplayBounds = bounds;
2529
+ mouseNeedsInitPos = true;
2530
+ });
2531
+ }
2532
+
2480
2533
  // Other state
2481
2534
  let paused = true;
2482
2535
  let pointTracker;
@@ -2548,14 +2601,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2548
2601
 
2549
2602
  try {
2550
2603
  detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
2604
+ if (lastShownErrorDetails?.errorClass === "faceLandmarksDetection.createDetector") {
2605
+ errorMessage.hidden = true;
2606
+ }
2551
2607
  } catch (error) {
2552
2608
  detector = null;
2553
- // TODO: avoid alert
2554
2609
  console.error("Failed to create facemesh detector:", error);
2555
- alert(error);
2610
+ showError(t("faceDetectorInitError", { defaultValue: "Failed to create face detector" }), error, { errorClass: "faceLandmarksDetection.createDetector" });
2556
2611
  }
2557
2612
 
2558
2613
  facemeshLoaded = true;
2614
+ let loggedDetectorError = false;
2559
2615
  facemeshEstimateFaces = async () => {
2560
2616
  const imageData = currentCameraImageData;//getCameraImageData();
2561
2617
  if (!imageData) {
@@ -2569,11 +2625,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2569
2625
  }
2570
2626
  return faces;
2571
2627
  } catch (error) {
2572
- detector.dispose();
2628
+ if (!loggedDetectorError) {
2629
+ console.error("Facemesh estimation failed:", error);
2630
+ loggedDetectorError = true;
2631
+ }
2632
+ try {
2633
+ detector?.dispose();
2634
+ } catch (disposeError) {
2635
+ console.error("Failed to dispose facemesh detector after estimation error:", disposeError);
2636
+ }
2573
2637
  detector = null;
2574
- // TODO: avoid alert
2575
- console.error("Facemesh estimation failed:", error);
2576
- alert(error);
2638
+ showError(t("faceDetectorError", { defaultValue: "Face detector error" }), error);
2577
2639
  }
2578
2640
  return [];
2579
2641
  };
@@ -2808,7 +2870,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2808
2870
  updateStartStopButton();
2809
2871
  };
2810
2872
 
2811
- let showedCameraError = false;
2873
+ // Handle monkey-patched alert() replacement in face-landmarks-detection.min.js
2874
+ // (Hm, could make it throw instead. Then we wouldn't need this.)
2875
+ window._TrackyMouse_faceLandmarksDetectionAlert = (message) => {
2876
+ // TODO: i18n (it's just one message; we could check for the string (or not) and translate it)
2877
+ // const isContextCreationMessage = message === "Failed to create WebGL canvas context when passing video frame.";
2878
+ errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${message}`;
2879
+ errorMessage.hidden = false;
2880
+ };
2881
+
2882
+ const cameraAccessSlowWarningDelayMS = 5000;
2883
+ let cameraAccessSlowWarningTimeoutID;
2812
2884
  useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
2813
2885
  // Phases:
2814
2886
  // 1. "tryPreferredCamera"
@@ -2898,8 +2970,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2898
2970
  delete constraints.video.facingMode;
2899
2971
  constraints.video.deviceId = { exact: deviceIdToTry };
2900
2972
  }
2973
+ clearTimeout(cameraAccessSlowWarningTimeoutID);
2974
+ errorMessage.hidden = true;
2975
+ cameraAccessSlowWarningTimeoutID = setTimeout(() => {
2976
+ errorMessage.textContent = t("video.status.accessTakingLongerThanExpected", { defaultValue: "Accessing the camera is taking longer than expected..." });
2977
+ errorMessage.hidden = false;
2978
+ }, cameraAccessSlowWarningDelayMS);
2901
2979
  console.log("TrackyMouse.useCamera phase", phase, "constraints", constraints);
2902
2980
  navigator.mediaDevices.getUserMedia(constraints).then(async (stream) => {
2981
+ clearTimeout(cameraAccessSlowWarningTimeoutID);
2903
2982
  if (phase === "justGetPermission") {
2904
2983
  for (const track of stream.getTracks()) {
2905
2984
  track.stop();
@@ -2922,6 +3001,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2922
3001
  useCameraButton.hidden = true;
2923
3002
  errorMessage.hidden = true;
2924
3003
  }, async (error) => {
3004
+ clearTimeout(cameraAccessSlowWarningTimeoutID);
2925
3005
  console.log("TrackyMouse.useCamera phase", phase, "error", error);
2926
3006
  if (
2927
3007
  phase === "tryPreferredCamera" &&
@@ -2933,7 +3013,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2933
3013
  }
2934
3014
  if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
2935
3015
  // required track is missing
2936
- errorMessage.textContent = t("video.errors.noCameraFound", { defaultValue: "No camera found. Please make sure you have a camera connected and enabled." });
3016
+ showError(t("video.errors.noCameraFound", { defaultValue: "No camera found. Please make sure you have a camera connected and enabled." }));
2937
3017
  } else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
2938
3018
  // webcam is already in use
2939
3019
  // or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
@@ -2941,18 +3021,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2941
3021
  // (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
2942
3022
  // though care should be given to make it clear it's a list with one item, with something like "(no more cameras detected)" following the list
2943
3023
  // or "1 camera source detected" preceding it)
2944
- errorMessage.textContent = t("video.errors.cameraInUse", { defaultValue: "Webcam is already in use. Please make sure you have no other programs using the camera." });
3024
+ showError(t("video.errors.cameraInUse", { defaultValue: "Webcam is already in use. Please make sure you have no other programs using the camera." }));
2945
3025
  } else if (error.name === "AbortError") {
2946
3026
  // webcam is likely already in use
2947
3027
  // I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
2948
3028
  // Update: it definitely isn't, but I can't say exactly what it means in other cases.
2949
3029
  // Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
2950
3030
  // I really hope that isn't the problem.
2951
- // errorMessage.textContent = "Webcam may already be in use. Please make sure you have no other programs using the camera.";
2952
- errorMessage.textContent = t("video.errors.retryAfterClosingOtherPrograms", { defaultValue: "Please make sure no other programs are using the camera and try again." });
3031
+ // showError("Webcam may already be in use. Please make sure you have no other programs using the camera.");
3032
+ showError(t("video.errors.retryAfterClosingOtherPrograms", { defaultValue: "Please make sure no other programs are using the camera and try again." }));
2953
3033
  // A more honest/helpful message might be:
2954
- // errorMessage.textContent = "Please try again and then make sure no other programs are using the camera and try again again.";
2955
- // errorMessage.textContent = "Please try again before/after making sure no other programs are using the camera.";
3034
+ // showError("Please try again and then make sure no other programs are using the camera and try again again.");
3035
+ // showError("Please try again before/after making sure no other programs are using the camera.");
2956
3036
  // if it were not to be confusing.
2957
3037
  // That is, one could save some time by just hitting the button to try again before trying to figure out of another program is using the camera,
2958
3038
  // because sometimes that's enough.
@@ -2963,36 +3043,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2963
3043
  // either due to the device not being present, or the ID having changed (don't ask me why that can happen but it can)
2964
3044
  // Note: OverconstrainedError has a `constraint` property but not in Firefox so it's not very helpful.
2965
3045
  if (constraints.video.deviceId?.exact) {
2966
- // errorMessage.textContent = "The previously selected camera is not available. Please select a different camera from the dropdown and try again.";
2967
- // errorMessage.textContent = "The previously selected camera is not available. Please mess around with Video > Camera source.";
2968
- // errorMessage.textContent = "The previously selected camera is not available. Try changing Video > Camera source.";
2969
- // errorMessage.textContent = "The previously selected camera is not available. Please select a camera from the \"Camera source\" dropdown in the Video settings and if it doesn't show up, it might after you select Default.";
2970
- errorMessage.textContent = t("video.errors.previouslySelectedUnavailable", { defaultValue: "The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to." });
3046
+ // showError("The previously selected camera is not available. Please select a different camera from the dropdown and try again.");
3047
+ // showError("The previously selected camera is not available. Please mess around with Video > Camera source.");
3048
+ // showError("The previously selected camera is not available. Try changing Video > Camera source.");
3049
+ // showError("The previously selected camera is not available. Please select a camera from the \"Camera source\" dropdown in the Video settings and if it doesn't show up, it might after you select Default.");
3050
+ showError(t("video.errors.previouslySelectedUnavailable", { defaultValue: "The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to." }));
2971
3051
  // It's awkward but that's my best attempt at conveying how you may need to proceed
2972
3052
  // without complicated description of how/why the dropdown might be populated with
2973
3053
  // fake information until a camera stream is successfully opened.
2974
3054
  } else {
2975
- errorMessage.textContent = t("video.errors.unsupportedResolution", { defaultValue: "Webcam does not support the required resolution. Please change your settings." });
3055
+ showError(t("video.errors.unsupportedResolution", { defaultValue: "Webcam does not support the required resolution. Please change your settings." }));
2976
3056
  }
2977
3057
  } else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
2978
3058
  // permission denied in browser
2979
- errorMessage.textContent = t("video.errors.permissionDenied", { defaultValue: "Permission denied. Please enable access to the camera." });
3059
+ showError(t("video.errors.permissionDenied", { defaultValue: "Permission denied. Please enable access to the camera." }));
2980
3060
  } else if (error.name == "TypeError") {
2981
3061
  // empty constraints object
2982
- errorMessage.textContent = `${t("video.errors.accessFailed", { defaultValue: "Something went wrong accessing the camera." })} (${error.name}: ${error.message})`;
3062
+ showError(t("video.errors.accessFailed", { defaultValue: "Something went wrong accessing the camera." }), error);
2983
3063
  } else {
2984
3064
  // other errors
2985
- errorMessage.textContent = `${t("video.errors.accessFailedRetry", { defaultValue: "Something went wrong accessing the camera. Please try again." })} (${error.name}: ${error.message})`;
3065
+ showError(t("video.errors.accessFailedRetry", { defaultValue: "Something went wrong accessing the camera. Please try again." }), error);
2986
3066
  }
2987
- errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${errorMessage.textContent}`;
2988
- errorMessage.hidden = false;
2989
- // Play CSS animation only on retries
2990
- errorMessage.style.animation = "none";
2991
- if (showedCameraError) {
2992
- void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
2993
- errorMessage.style.animation = "";
2994
- }
2995
- showedCameraError = true;
2996
3067
  });
2997
3068
  };
2998
3069
  useDemoFootageButton.onclick = TrackyMouse.useDemoFootage = () => {
@@ -4089,8 +4160,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4089
4160
  pointTracker.draw(debugPointsCtx);
4090
4161
 
4091
4162
  if (update) {
4092
- const screenWidth = window.electronAPI ? screen.width : innerWidth;
4093
- const screenHeight = window.electronAPI ? screen.height : innerHeight;
4163
+ const screenWidth = window.electronAPI ? (virtualDisplayBounds?.width ?? screen.width) : innerWidth;
4164
+ const screenHeight = window.electronAPI ? (virtualDisplayBounds?.height ?? screen.height) : innerHeight;
4165
+ const screenOffsetX = window.electronAPI ? (virtualDisplayBounds?.x ?? 0) : 0;
4166
+ const screenOffsetY = window.electronAPI ? (virtualDisplayBounds?.y ?? 0) : 0;
4094
4167
 
4095
4168
  let [movementX, movementY] = pointTracker.getMovement();
4096
4169
 
@@ -4220,13 +4293,13 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4220
4293
  mouseX -= deltaX * screenWidth;
4221
4294
  mouseY += deltaY * screenHeight;
4222
4295
 
4223
- mouseX = Math.min(Math.max(0, mouseX), screenWidth);
4224
- mouseY = Math.min(Math.max(0, mouseY), screenHeight);
4296
+ mouseX = Math.min(Math.max(screenOffsetX, mouseX), screenOffsetX + screenWidth);
4297
+ mouseY = Math.min(Math.max(screenOffsetY, mouseY), screenOffsetY + screenHeight);
4225
4298
 
4226
4299
  if (mouseNeedsInitPos) {
4227
4300
  // TODO: option to get preexisting mouse position instead of set it to center of screen
4228
- mouseX = screenWidth / 2;
4229
- mouseY = screenHeight / 2;
4301
+ mouseX = screenOffsetX + screenWidth / 2;
4302
+ mouseY = screenOffsetY + screenHeight / 2;
4230
4303
  mouseNeedsInitPos = false;
4231
4304
  }
4232
4305
  if (window.electronAPI) {
@@ -4355,6 +4428,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4355
4428
  _waitForSettingsLoaded() {
4356
4429
  return settingsLoadedPromise;
4357
4430
  },
4431
+ get _facemeshPrediction() {
4432
+ return facemeshPrediction;
4433
+ },
4434
+ get _headTilt() {
4435
+ return headTilt;
4436
+ },
4437
+ get _video() {
4438
+ return cameraVideo;
4439
+ },
4358
4440
  dispose() {
4359
4441
  // TODO: re-structure so that cleanup can succeed even if initialization fails
4360
4442
  // OOP would help with this, by storing references in an object, but it doesn't necessarily
@@ -4449,11 +4531,13 @@ TrackyMouse.init = function (div, opts = {}) {
4449
4531
 
4450
4532
  createInner();
4451
4533
 
4452
- return {
4453
- dispose() {
4454
- inner.dispose();
4455
- },
4456
- };
4534
+ return new Proxy({}, {
4535
+ get(_target, prop) {
4536
+ if (prop in inner) {
4537
+ return inner[prop];
4538
+ }
4539
+ }
4540
+ });
4457
4541
 
4458
4542
  };
4459
4543
 
@@ -4461,21 +4545,24 @@ TrackyMouse.initScreenOverlay = () => {
4461
4545
 
4462
4546
  const template = `
4463
4547
  <div class="tracky-mouse-hide-near-cursor">
4464
- <div class="tracky-mouse-absolute-center">
4465
- <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
4466
- <img src="${TrackyMouse.dependenciesRoot}/images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
4467
- </div>
4468
- <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
4469
- <img src="${TrackyMouse.dependenciesRoot}/images/head-not-found.svg" alt="head not found" width="128" height="128">
4548
+ <div id="tracky-mouse-screen-overlay-work-area">
4549
+ <div class="tracky-mouse-absolute-center">
4550
+ <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
4551
+ <img src="${TrackyMouse.dependenciesRoot}/images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
4552
+ </div>
4553
+ <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
4554
+ <img src="${TrackyMouse.dependenciesRoot}/images/head-not-found.svg" alt="head not found" width="128" height="128">
4555
+ </div>
4470
4556
  </div>
4557
+ <div id="tracky-mouse-screen-overlay-message"></div>
4471
4558
  </div>
4472
- <div id="tracky-mouse-screen-overlay-message"></div>
4473
4559
  </div>
4474
4560
  `;
4475
4561
  const fragment = document.createRange().createContextualFragment(template);
4476
4562
  document.body.appendChild(fragment);
4477
4563
 
4478
4564
  const message = document.getElementById("tracky-mouse-screen-overlay-message");
4565
+ const workAreaContainer = document.getElementById("tracky-mouse-screen-overlay-work-area");
4479
4566
  message.dir = "auto";
4480
4567
 
4481
4568
  const hideNearCursorEls = document.querySelectorAll(".tracky-mouse-hide-near-cursor");
@@ -4522,9 +4609,30 @@ TrackyMouse.initScreenOverlay = () => {
4522
4609
  }
4523
4610
 
4524
4611
  function update(data) {
4525
- const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset, systemMousePosition } = data;
4526
-
4527
- message.style.bottom = `${bottomOffset}px`;
4612
+ const {
4613
+ messageText,
4614
+ isEnabled,
4615
+ isManualTakeback,
4616
+ inputFeedback,
4617
+ workAreaContainerBounds,
4618
+ bottomOffset,
4619
+ systemMousePosition,
4620
+ } = data;
4621
+
4622
+ if (workAreaContainerBounds) {
4623
+ workAreaContainer.style.left = `${workAreaContainerBounds.x}px`;
4624
+ workAreaContainer.style.top = `${workAreaContainerBounds.y}px`;
4625
+ workAreaContainer.style.width = `${workAreaContainerBounds.width}px`;
4626
+ workAreaContainer.style.height = `${workAreaContainerBounds.height}px`;
4627
+ message.style.bottom = "0px";
4628
+ } else {
4629
+ // bottomOffset was a never-released part of an unstable API.
4630
+ // workAreaContainerBounds could be made required, just like bottomOffset was.
4631
+ workAreaContainer.style.left = "0px";
4632
+ workAreaContainer.style.top = "0px";
4633
+ workAreaContainer.style.width = "100%";
4634
+ workAreaContainer.style.height = `calc(100% - ${bottomOffset ?? 0}px)`;
4635
+ }
4528
4636
 
4529
4637
  // Other diagnostics in the future would be stuff like:
4530
4638
  // - head too far away (smaller than a certain size) https://github.com/1j01/tracky-mouse/issues/49