tracky-mouse 2.0.0 → 2.1.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/package.json +1 -1
- package/tracky-mouse.css +18 -0
- package/tracky-mouse.js +507 -326
package/package.json
CHANGED
package/tracky-mouse.css
CHANGED
|
@@ -268,4 +268,22 @@ body:not(.tracky-mouse-manual-takeback) .tracky-mouse-manual-takeback-indicator
|
|
|
268
268
|
.tracky-mouse-ui a:link,
|
|
269
269
|
.tracky-mouse-ui a:visited {
|
|
270
270
|
color: rgb(135 0 191);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.tracky-mouse-controls details {
|
|
274
|
+
border: 1px solid rgba(0, 0, 0, 0.3);
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
margin-bottom: 8px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.tracky-mouse-controls details summary {
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
user-select: none;
|
|
282
|
+
font-weight: bold;
|
|
283
|
+
background: rgba(135, 0, 191, 0.1);
|
|
284
|
+
padding: 8px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.tracky-mouse-controls details .tracky-mouse-details-body {
|
|
288
|
+
padding: 8px;
|
|
271
289
|
}
|
package/tracky-mouse.js
CHANGED
|
@@ -565,87 +565,129 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
565
565
|
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">Start</button>
|
|
566
566
|
<br>
|
|
567
567
|
<br>
|
|
568
|
-
<
|
|
569
|
-
<
|
|
570
|
-
<
|
|
571
|
-
<
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
<
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
<
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
<
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
<
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
<!--
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
568
|
+
<details>
|
|
569
|
+
<summary>Head Tracking</summary>
|
|
570
|
+
<div class="tracky-mouse-details-body">
|
|
571
|
+
<label class="tracky-mouse-control-row">
|
|
572
|
+
<span class="tracky-mouse-label-text">Horizontal sensitivity</span>
|
|
573
|
+
<span class="tracky-mouse-labeled-slider">
|
|
574
|
+
<input type="range" min="0" max="100" value="25" class="tracky-mouse-sensitivity-x">
|
|
575
|
+
<span class="tracky-mouse-min-label">Slow</span>
|
|
576
|
+
<span class="tracky-mouse-max-label">Fast</span>
|
|
577
|
+
</span>
|
|
578
|
+
</label>
|
|
579
|
+
<label class="tracky-mouse-control-row">
|
|
580
|
+
<span class="tracky-mouse-label-text">Vertical sensitivity</span>
|
|
581
|
+
<span class="tracky-mouse-labeled-slider">
|
|
582
|
+
<input type="range" min="0" max="100" value="50" class="tracky-mouse-sensitivity-y">
|
|
583
|
+
<span class="tracky-mouse-min-label">Slow</span>
|
|
584
|
+
<span class="tracky-mouse-max-label">Fast</span>
|
|
585
|
+
</span>
|
|
586
|
+
</label>
|
|
587
|
+
<!-- <label class="tracky-mouse-control-row">
|
|
588
|
+
<span class="tracky-mouse-label-text">Smoothing</span>
|
|
589
|
+
<span class="tracky-mouse-labeled-slider">
|
|
590
|
+
<input type="range" min="0" max="100" value="50" class="tracky-mouse-smoothing">
|
|
591
|
+
<span class="tracky-mouse-min-label"></span>
|
|
592
|
+
<span class="tracky-mouse-max-label"></span>
|
|
593
|
+
</span>
|
|
594
|
+
</label> -->
|
|
595
|
+
<label class="tracky-mouse-control-row">
|
|
596
|
+
<span class="tracky-mouse-label-text">Acceleration</span>
|
|
597
|
+
<span class="tracky-mouse-labeled-slider">
|
|
598
|
+
<input type="range" min="0" max="100" value="50" class="tracky-mouse-acceleration">
|
|
599
|
+
<!-- TODO: "Linear" could be described as "Fast", and the other "Fast" labels are on the other side. Should it be swapped? What does other software with acceleration control look like? In Windows it's just a checkbox apparently, but it could go as far as a custom curve editor. -->
|
|
600
|
+
<span class="tracky-mouse-min-label">Linear</span>
|
|
601
|
+
<span class="tracky-mouse-max-label">Smooth</span>
|
|
602
|
+
</span>
|
|
603
|
+
</label>
|
|
604
|
+
<label class="tracky-mouse-control-row">
|
|
605
|
+
<span class="tracky-mouse-label-text">Motion threshold</span>
|
|
606
|
+
<span class="tracky-mouse-labeled-slider">
|
|
607
|
+
<input type="range" min="0" max="10" value="0" class="tracky-mouse-min-distance">
|
|
608
|
+
<span class="tracky-mouse-min-label">Free</span>
|
|
609
|
+
<span class="tracky-mouse-max-label">Steady</span>
|
|
610
|
+
</span>
|
|
611
|
+
</label>
|
|
612
|
+
</div>
|
|
613
|
+
</details>
|
|
614
|
+
<!--
|
|
615
|
+
Only dwell clicking is supported by the web library right now.
|
|
616
|
+
Currently it's a separate API (TrackyMouse.initDwellClicking)
|
|
617
|
+
TODO: bring more of desktop app functionality into core
|
|
618
|
+
https://github.com/1j01/tracky-mouse/issues/72
|
|
619
|
+
|
|
620
|
+
Also, the "Swap mouse buttons" setting is likely not useful for
|
|
621
|
+
web apps embedding Tracky Mouse and designed for head trackers,
|
|
622
|
+
since it necessitates mode switching for dwell clicker usage,
|
|
623
|
+
so it may make sense to hide (or not) even if it is supported there in the future.
|
|
624
|
+
The main point of this option is to counteract the system-level mouse button setting,
|
|
625
|
+
which awkwardly affects what mouse button serenade-driver sends; this doesn't affect the web version.
|
|
626
|
+
-->
|
|
627
|
+
<details class="tracky-mouse-desktop-only">
|
|
628
|
+
<summary>Clicking</summary>
|
|
629
|
+
<div class="tracky-mouse-details-body">
|
|
630
|
+
<div class="tracky-mouse-control-row">
|
|
631
|
+
<label for="tracky-mouse-clicking-mode"><span class="tracky-mouse-label-text">Clicking mode:</span></label>
|
|
632
|
+
<select id="tracky-mouse-clicking-mode">
|
|
633
|
+
<option value="dwell">Dwell to click</option>
|
|
634
|
+
<option value="blink">Wink to click</option>
|
|
635
|
+
<option value="open-mouth">Open mouth to click</option>
|
|
636
|
+
<option value="off">Off</option>
|
|
637
|
+
</select>
|
|
638
|
+
</div>
|
|
639
|
+
<br>
|
|
640
|
+
<!-- special interest: jspaint wants label not to use parent-child relationship so that os-gui's 98.css checkbox styles can work -->
|
|
641
|
+
<!-- though this option might not be wanted in jspaint; might be good to hide it in the embedded case, or make it optional -->
|
|
642
|
+
<!-- also TODO: add description of what this is for: on Windows, currently, when buttons are swapped at the system level, it affects serenade-driver's click() -->
|
|
643
|
+
<!-- also this may be seen as a weirdly named/designed option for right-clicking -->
|
|
644
|
+
<!-- btw: label is selected based on 'for' attribute -->
|
|
645
|
+
<div class="tracky-mouse-control-row">
|
|
646
|
+
<input type="checkbox" id="tracky-mouse-swap-mouse-buttons"/>
|
|
647
|
+
<label for="tracky-mouse-swap-mouse-buttons"><span class="tracky-mouse-label-text">Swap mouse buttons</span></label>
|
|
648
|
+
</div>
|
|
649
|
+
<br>
|
|
650
|
+
<label class="tracky-mouse-control-row">
|
|
651
|
+
<!--
|
|
652
|
+
This setting could called "click stabilization", "drag delay", "delay before dragging", "click drag delay", "drag prevention", etc.
|
|
653
|
+
with slider labels "Easy to click -> Easy to drag" or "Easier to click -> Easier to drag" or "Short -> Long"
|
|
654
|
+
This could generalize into "never allow dragging" at the extreme, if it's special cased to jump to infinity
|
|
655
|
+
at the end of the slider, although you shouldn't need to do that to effectively avoid dragging when trying to click,
|
|
656
|
+
and it might complicate the design of the slider labeling.
|
|
657
|
+
-->
|
|
658
|
+
<span class="tracky-mouse-label-text">Delay before dragging </span>
|
|
659
|
+
<span class="tracky-mouse-labeled-slider">
|
|
660
|
+
<input type="range" min="0" max="1000" value="0" class="tracky-mouse-delay-before-dragging">
|
|
661
|
+
<span class="tracky-mouse-min-label">Easy to drag</span>
|
|
662
|
+
<span class="tracky-mouse-max-label">Easy to click</span>
|
|
663
|
+
</span>
|
|
664
|
+
</label>
|
|
665
|
+
</div>
|
|
666
|
+
</details>
|
|
667
|
+
<details>
|
|
668
|
+
<summary>General</summary>
|
|
669
|
+
<div class="tracky-mouse-details-body">
|
|
670
|
+
<!-- special interest: jspaint wants label not to use parent-child relationship so that os-gui's 98.css checkbox styles can work -->
|
|
671
|
+
<!-- opposite, "Start paused", might be clearer, especially if I add a "pause" button -->
|
|
672
|
+
<div class="tracky-mouse-control-row">
|
|
673
|
+
<input type="checkbox" id="tracky-mouse-start-enabled"/>
|
|
674
|
+
<label for="tracky-mouse-start-enabled"><span class="tracky-mouse-label-text">Start enabled</span></label>
|
|
675
|
+
</div>
|
|
676
|
+
<br>
|
|
677
|
+
<!-- special interest: jspaint wants label not to use parent-child relationship so that os-gui's 98.css checkbox styles can work -->
|
|
678
|
+
<div class="tracky-mouse-control-row tracky-mouse-desktop-only">
|
|
679
|
+
<input type="checkbox" id="tracky-mouse-run-at-login"/>
|
|
680
|
+
<label for="tracky-mouse-run-at-login"><span class="tracky-mouse-label-text">Run at login</span></label>
|
|
681
|
+
</div>
|
|
682
|
+
<br class="tracky-mouse-desktop-only">
|
|
683
|
+
<!-- special interest: jspaint wants label not to use parent-child relationship so that os-gui's 98.css checkbox styles can work -->
|
|
684
|
+
<!-- TODO: try moving this to the corner of the camera view, so it's clearer it applies only to the camera view -->
|
|
685
|
+
<div class="tracky-mouse-control-row">
|
|
686
|
+
<input type="checkbox" checked id="tracky-mouse-mirror"/>
|
|
687
|
+
<label for="tracky-mouse-mirror"><span class="tracky-mouse-label-text">Mirror</span></label>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
</details>
|
|
649
691
|
</div>
|
|
650
692
|
<div class="tracky-mouse-canvas-container-container">
|
|
651
693
|
<div class="tracky-mouse-canvas-container">
|
|
@@ -675,6 +717,8 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
675
717
|
var sensitivityXSlider = uiContainer.querySelector(".tracky-mouse-sensitivity-x");
|
|
676
718
|
var sensitivityYSlider = uiContainer.querySelector(".tracky-mouse-sensitivity-y");
|
|
677
719
|
var accelerationSlider = uiContainer.querySelector(".tracky-mouse-acceleration");
|
|
720
|
+
var minDistanceSlider = uiContainer.querySelector(".tracky-mouse-min-distance");
|
|
721
|
+
var delayBeforeDraggingSlider = uiContainer.querySelector(".tracky-mouse-delay-before-dragging");
|
|
678
722
|
var useCameraButton = uiContainer.querySelector(".tracky-mouse-use-camera-button");
|
|
679
723
|
var useDemoFootageButton = uiContainer.querySelector(".tracky-mouse-use-demo-footage-button");
|
|
680
724
|
var errorMessage = uiContainer.querySelector(".tracky-mouse-error-message");
|
|
@@ -692,21 +736,9 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
692
736
|
runAtLoginCheckbox.disabled = !isPackaged;
|
|
693
737
|
});
|
|
694
738
|
} else {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
// It could be implemented for the web version, but if you're designing an app for facial mouse users,
|
|
699
|
-
// you might want to avoid right-clicking altogether.
|
|
700
|
-
swapMouseButtonsCheckbox.parentElement.hidden = true;
|
|
701
|
-
|
|
702
|
-
// Hide clicking mode option if not in desktop app,
|
|
703
|
-
// since dwell clicking in the web version is a separate API (TrackyMouse.initDwellClicking)
|
|
704
|
-
// and wouldn't automatically be controlled by this UI.
|
|
705
|
-
// TODO: bring more of desktop app functionality into core
|
|
706
|
-
clickingModeDropdown.parentElement.hidden = true;
|
|
707
|
-
|
|
708
|
-
// Hide the "run at login" option if we're not in the desktop app.
|
|
709
|
-
runAtLoginCheckbox.parentElement.hidden = true;
|
|
739
|
+
for (const elementToHide of uiContainer.querySelectorAll('.tracky-mouse-desktop-only')) {
|
|
740
|
+
elementToHide.hidden = true;
|
|
741
|
+
}
|
|
710
742
|
}
|
|
711
743
|
|
|
712
744
|
var canvas = uiContainer.querySelector(".tracky-mouse-canvas");
|
|
@@ -735,15 +767,17 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
735
767
|
var maxPoints = 1000;
|
|
736
768
|
var mouseX = 0;
|
|
737
769
|
var mouseY = 0;
|
|
738
|
-
var prevMovementX = 0;
|
|
739
|
-
var prevMovementY = 0;
|
|
740
|
-
var enableTimeTravel = false;
|
|
741
|
-
// var movementXSinceFacemeshUpdate = 0;
|
|
742
|
-
// var movementYSinceFacemeshUpdate = 0;
|
|
743
770
|
var cameraFramesSinceFacemeshUpdate = [];
|
|
744
771
|
var sensitivityX;
|
|
745
772
|
var sensitivityY;
|
|
746
773
|
var acceleration;
|
|
774
|
+
var minDistance;
|
|
775
|
+
var delayBeforeDragging;
|
|
776
|
+
var buttonStates = {
|
|
777
|
+
left: false,
|
|
778
|
+
right: false,
|
|
779
|
+
};
|
|
780
|
+
var lastMouseDownTime = -Infinity;
|
|
747
781
|
var face;
|
|
748
782
|
var faceScore = 0;
|
|
749
783
|
var faceScoreThreshold = 0.5;
|
|
@@ -752,9 +786,9 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
752
786
|
var pointsBasedOnFaceScore = 0;
|
|
753
787
|
var paused = true;
|
|
754
788
|
var mouseNeedsInitPos = true;
|
|
755
|
-
var debugTimeTravel = false;
|
|
756
789
|
var debugAcceleration = false;
|
|
757
790
|
var showDebugText = false;
|
|
791
|
+
var showDebugEyelidContours = false;
|
|
758
792
|
var mirror;
|
|
759
793
|
var startEnabled;
|
|
760
794
|
var runAtLogin;
|
|
@@ -781,27 +815,10 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
781
815
|
var facemeshEstimateFaces;
|
|
782
816
|
var faceInViewConfidenceThreshold = 0.7;
|
|
783
817
|
var pointsBasedOnFaceInViewConfidence = 0;
|
|
818
|
+
var blinkInfo;
|
|
819
|
+
var mouthInfo;
|
|
784
820
|
|
|
785
|
-
|
|
786
|
-
// reducing this makes it much more likely to drop points and thus not work
|
|
787
|
-
// THIS IS DISABLED and using a performance optimization of currentCameraImageData instead of getCameraImageData;
|
|
788
|
-
// (the currentCameraImageData is also scaled differently, to the fixed canvas size instead of using the native camera image size)
|
|
789
|
-
// const frameScaleForWorker = 1;
|
|
790
|
-
|
|
791
|
-
var mainOops;
|
|
792
|
-
var workerSyncedOops;
|
|
793
|
-
|
|
794
|
-
// const frameCanvas = document.createElement("canvas");
|
|
795
|
-
// const frameCtx = frameCanvas.getContext("2d");
|
|
796
|
-
// const getCameraImageData = () => {
|
|
797
|
-
// if (cameraVideo.videoWidth * frameScaleForWorker * cameraVideo.videoHeight * frameScaleForWorker < 1) {
|
|
798
|
-
// return;
|
|
799
|
-
// }
|
|
800
|
-
// frameCanvas.width = cameraVideo.videoWidth * frameScaleForWorker;
|
|
801
|
-
// frameCanvas.height = cameraVideo.videoHeight * frameScaleForWorker;
|
|
802
|
-
// frameCtx.drawImage(cameraVideo, 0, 0, frameCanvas.width, frameCanvas.height);
|
|
803
|
-
// return frameCtx.getImageData(0, 0, frameCanvas.width, frameCanvas.height);
|
|
804
|
-
// };
|
|
821
|
+
var pointTracker;
|
|
805
822
|
|
|
806
823
|
let currentCameraImageData;
|
|
807
824
|
let detector;
|
|
@@ -883,6 +900,14 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
883
900
|
acceleration = settings.globalSettings.headTrackingAcceleration;
|
|
884
901
|
accelerationSlider.value = acceleration * 100;
|
|
885
902
|
}
|
|
903
|
+
if (settings.globalSettings.headTrackingMinDistance !== undefined) {
|
|
904
|
+
minDistance = settings.globalSettings.headTrackingMinDistance;
|
|
905
|
+
minDistanceSlider.value = minDistance;
|
|
906
|
+
}
|
|
907
|
+
if (settings.globalSettings.delayBeforeDragging !== undefined) {
|
|
908
|
+
delayBeforeDragging = settings.globalSettings.delayBeforeDragging;
|
|
909
|
+
delayBeforeDraggingSlider.value = delayBeforeDragging;
|
|
910
|
+
}
|
|
886
911
|
if (settings.globalSettings.startEnabled !== undefined) {
|
|
887
912
|
startEnabled = settings.globalSettings.startEnabled;
|
|
888
913
|
startEnabledCheckbox.checked = startEnabled;
|
|
@@ -912,6 +937,8 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
912
937
|
headTrackingSensitivityX: sensitivityX,
|
|
913
938
|
headTrackingSensitivityY: sensitivityY,
|
|
914
939
|
headTrackingAcceleration: acceleration,
|
|
940
|
+
headTrackingMinDistance: minDistance,
|
|
941
|
+
delayBeforeDragging: delayBeforeDragging,
|
|
915
942
|
// TODO:
|
|
916
943
|
// eyeTrackingSensitivityX,
|
|
917
944
|
// eyeTrackingSensitivityY,
|
|
@@ -969,6 +996,22 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
969
996
|
setOptions({ globalSettings: { headTrackingAcceleration: acceleration } });
|
|
970
997
|
}
|
|
971
998
|
};
|
|
999
|
+
minDistanceSlider.onchange = (event) => {
|
|
1000
|
+
minDistance = minDistanceSlider.value;
|
|
1001
|
+
// HACK: using event argument as a flag to indicate when it's not the initial setup,
|
|
1002
|
+
// to avoid saving the default settings before the actual preferences are loaded.
|
|
1003
|
+
if (event) {
|
|
1004
|
+
setOptions({ globalSettings: { headTrackingMinDistance: minDistance } });
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
delayBeforeDraggingSlider.onchange = (event) => {
|
|
1008
|
+
delayBeforeDragging = delayBeforeDraggingSlider.value;
|
|
1009
|
+
// HACK: using event argument as a flag to indicate when it's not the initial setup,
|
|
1010
|
+
// to avoid saving the default settings before the actual preferences are loaded.
|
|
1011
|
+
if (event) {
|
|
1012
|
+
setOptions({ globalSettings: { delayBeforeDragging: delayBeforeDragging } });
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
972
1015
|
mirrorCheckbox.onchange = (event) => {
|
|
973
1016
|
mirror = mirrorCheckbox.checked;
|
|
974
1017
|
// HACK: using event argument as a flag to indicate when it's not the initial setup,
|
|
@@ -1019,6 +1062,8 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1019
1062
|
sensitivityXSlider.onchange();
|
|
1020
1063
|
sensitivityYSlider.onchange();
|
|
1021
1064
|
accelerationSlider.onchange();
|
|
1065
|
+
minDistanceSlider.onchange();
|
|
1066
|
+
delayBeforeDraggingSlider.onchange();
|
|
1022
1067
|
paused = !startEnabled;
|
|
1023
1068
|
|
|
1024
1069
|
// Handle right click on "swap mouse buttons", so it doesn't leave users stranded right-clicking.
|
|
@@ -1149,8 +1194,6 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1149
1194
|
cameraVideo.height = cameraVideo.videoHeight;
|
|
1150
1195
|
canvas.width = cameraVideo.videoWidth;
|
|
1151
1196
|
canvas.height = cameraVideo.videoHeight;
|
|
1152
|
-
debugFramesCanvas.width = cameraVideo.videoWidth;
|
|
1153
|
-
debugFramesCanvas.height = cameraVideo.videoHeight;
|
|
1154
1197
|
debugPointsCanvas.width = cameraVideo.videoWidth;
|
|
1155
1198
|
debugPointsCanvas.height = cameraVideo.videoHeight;
|
|
1156
1199
|
|
|
@@ -1159,10 +1202,7 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1159
1202
|
canvasContainer.style.aspectRatio = `${cameraVideo.videoWidth} / ${cameraVideo.videoHeight}`;
|
|
1160
1203
|
canvasContainer.style.setProperty('--aspect-ratio', cameraVideo.videoWidth / cameraVideo.videoHeight);
|
|
1161
1204
|
|
|
1162
|
-
|
|
1163
|
-
if (useFacemesh) {
|
|
1164
|
-
workerSyncedOops = new OOPS();
|
|
1165
|
-
}
|
|
1205
|
+
pointTracker = new OOPS();
|
|
1166
1206
|
});
|
|
1167
1207
|
cameraVideo.addEventListener('play', () => {
|
|
1168
1208
|
clmTracker.reset();
|
|
@@ -1187,11 +1227,6 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1187
1227
|
cameraVideo.width = defaultWidth;
|
|
1188
1228
|
cameraVideo.height = defaultHeight;
|
|
1189
1229
|
|
|
1190
|
-
const debugFramesCanvas = document.createElement("canvas");
|
|
1191
|
-
debugFramesCanvas.width = canvas.width;
|
|
1192
|
-
debugFramesCanvas.height = canvas.height;
|
|
1193
|
-
const debugFramesCtx = debugFramesCanvas.getContext("2d");
|
|
1194
|
-
|
|
1195
1230
|
const debugPointsCanvas = document.createElement("canvas");
|
|
1196
1231
|
debugPointsCanvas.width = canvas.width;
|
|
1197
1232
|
debugPointsCanvas.height = canvas.height;
|
|
@@ -1346,18 +1381,19 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1346
1381
|
}
|
|
1347
1382
|
}
|
|
1348
1383
|
|
|
1384
|
+
// FIXME: can't click to add points because canvas is covered by .tracky-mouse-canvas-overlay
|
|
1349
1385
|
canvas.addEventListener('click', (event) => {
|
|
1350
|
-
if (!
|
|
1386
|
+
if (!pointTracker) {
|
|
1351
1387
|
return;
|
|
1352
1388
|
}
|
|
1353
1389
|
const rect = canvas.getBoundingClientRect();
|
|
1354
1390
|
if (mirror) {
|
|
1355
|
-
|
|
1391
|
+
pointTracker.addPoint(
|
|
1356
1392
|
(rect.right - event.clientX) / rect.width * canvas.width,
|
|
1357
1393
|
(event.clientY - rect.top) / rect.height * canvas.height,
|
|
1358
1394
|
);
|
|
1359
1395
|
} else {
|
|
1360
|
-
|
|
1396
|
+
pointTracker.addPoint(
|
|
1361
1397
|
(event.clientX - rect.left) / rect.width * canvas.width,
|
|
1362
1398
|
(event.clientY - rect.top) / rect.height * canvas.height,
|
|
1363
1399
|
);
|
|
@@ -1392,6 +1428,22 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1392
1428
|
oops.addPoint(x, y);
|
|
1393
1429
|
}
|
|
1394
1430
|
|
|
1431
|
+
/** Returns the distance between a point and a line defined by two points, with the sign indicating which side of the line the point is on */
|
|
1432
|
+
function signedDistancePointLine(point, a, b) {
|
|
1433
|
+
const [px, py] = point;
|
|
1434
|
+
const [x1, y1] = a;
|
|
1435
|
+
const [x2, y2] = b;
|
|
1436
|
+
|
|
1437
|
+
const dx = x2 - x1;
|
|
1438
|
+
const dy = y2 - y1;
|
|
1439
|
+
|
|
1440
|
+
// Perpendicular (normal) vector
|
|
1441
|
+
const nx = -dy;
|
|
1442
|
+
const ny = dx;
|
|
1443
|
+
|
|
1444
|
+
return ((px - x1) * nx + (py - y1) * ny) / Math.hypot(nx, ny);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1395
1447
|
function draw(update = true) {
|
|
1396
1448
|
ctx.resetTransform(); // in case there is an error, don't flip constantly back and forth due to mirroring
|
|
1397
1449
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // in case there's no footage
|
|
@@ -1406,7 +1458,7 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1406
1458
|
ctx.drawImage(cameraVideo, 0, 0, canvas.width, canvas.height);
|
|
1407
1459
|
}
|
|
1408
1460
|
|
|
1409
|
-
if (!
|
|
1461
|
+
if (!pointTracker) {
|
|
1410
1462
|
return;
|
|
1411
1463
|
}
|
|
1412
1464
|
|
|
@@ -1461,7 +1513,7 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1461
1513
|
console.warn("Falling back to clmtrackr");
|
|
1462
1514
|
}
|
|
1463
1515
|
// If you've switched desktop sessions, it will presumably fail to get a new webgl context until you've switched back
|
|
1464
|
-
// Is this setInterval useful, vs just starting the worker
|
|
1516
|
+
// Is this setInterval useful, vs just starting the worker?**
|
|
1465
1517
|
// It probably has a faster cycle, with the code as it is now, but maybe not inherently.
|
|
1466
1518
|
// TODO: do the extra getContext() calls add to a GPU process crash limit
|
|
1467
1519
|
// that makes it only able to recover a couple times (outside the electron app)?
|
|
@@ -1479,7 +1531,7 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1479
1531
|
// Once we can create a webgl2 canvas...
|
|
1480
1532
|
document.createElement("canvas").getContext("webgl2");
|
|
1481
1533
|
clearInterval(fallbackTimeoutID);
|
|
1482
|
-
// It's worth trying to re-initialize [a web worker for facemesh]...
|
|
1534
|
+
// It's worth trying to re-initialize [a web worker** for facemesh]...
|
|
1483
1535
|
setTimeout(() => {
|
|
1484
1536
|
console.warn("Re-initializing facemesh");
|
|
1485
1537
|
initFacemesh();
|
|
@@ -1510,25 +1562,12 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1510
1562
|
clearTimeout(fallbackTimeoutID);
|
|
1511
1563
|
|
|
1512
1564
|
if (!facemeshPrediction) {
|
|
1565
|
+
blinkInfo = null;
|
|
1566
|
+
mouthInfo = null;
|
|
1513
1567
|
return;
|
|
1514
1568
|
}
|
|
1515
1569
|
facemeshPrediction.faceInViewConfidence = 0.9999; // TODO: any equivalent in new API?
|
|
1516
1570
|
|
|
1517
|
-
// this applies to facemeshPrediction.annotations as well, which references the same points
|
|
1518
|
-
// facemeshPrediction.scaledMesh.forEach((point) => {
|
|
1519
|
-
// point[0] /= frameScaleForWorker;
|
|
1520
|
-
// point[1] /= frameScaleForWorker;
|
|
1521
|
-
// });
|
|
1522
|
-
|
|
1523
|
-
// time travel latency compensation
|
|
1524
|
-
// keep a history of camera frames since the prediction was requested,
|
|
1525
|
-
// and analyze optical flow of new points over that history
|
|
1526
|
-
|
|
1527
|
-
// mainOops.filterPoints(() => false); // for DEBUG, empty points (could probably also just set pointCount = 0;
|
|
1528
|
-
|
|
1529
|
-
workerSyncedOops.filterPoints(() => false); // empty points (could probably also just set pointCount = 0;
|
|
1530
|
-
|
|
1531
|
-
// const { annotations } = facemeshPrediction;
|
|
1532
1571
|
const getPoint = (index) =>
|
|
1533
1572
|
facemeshPrediction.keypoints[index] ?
|
|
1534
1573
|
[facemeshPrediction.keypoints[index].x, facemeshPrediction.keypoints[index].y] :
|
|
@@ -1586,77 +1625,30 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1586
1625
|
const annotations = Object.fromEntries(Object.entries(MESH_ANNOTATIONS).map(([key, indices]) => {
|
|
1587
1626
|
return [key, indices.map(getPoint)];
|
|
1588
1627
|
}));
|
|
1628
|
+
|
|
1589
1629
|
// nostrils
|
|
1590
|
-
|
|
1591
|
-
|
|
1630
|
+
maybeAddPoint(pointTracker, annotations.noseLeftCorner[0][0], annotations.noseLeftCorner[0][1]);
|
|
1631
|
+
maybeAddPoint(pointTracker, annotations.noseRightCorner[0][0], annotations.noseRightCorner[0][1]);
|
|
1592
1632
|
// midway between eyes
|
|
1593
|
-
|
|
1633
|
+
maybeAddPoint(pointTracker, annotations.midwayBetweenEyes[0][0], annotations.midwayBetweenEyes[0][1]);
|
|
1594
1634
|
// inner eye corners
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
// console.log(workerSyncedOops.pointCount, cameraFramesSinceFacemeshUpdate.length, workerSyncedOops.curXY);
|
|
1599
|
-
if (enableTimeTravel) {
|
|
1600
|
-
debugFramesCtx.clearRect(0, 0, debugFramesCanvas.width, debugFramesCanvas.height);
|
|
1601
|
-
setTimeout(() => {
|
|
1602
|
-
debugPointsCtx.clearRect(0, 0, debugPointsCanvas.width, debugPointsCanvas.height);
|
|
1603
|
-
}, 900);
|
|
1604
|
-
cameraFramesSinceFacemeshUpdate.forEach((imageData, _index) => {
|
|
1605
|
-
/*
|
|
1606
|
-
if (debugTimeTravel) {
|
|
1607
|
-
debugFramesCtx.save();
|
|
1608
|
-
debugFramesCtx.globalAlpha = 0.1;
|
|
1609
|
-
// debugFramesCtx.globalCompositeOperation = index % 2 === 0 ? "xor" : "xor";
|
|
1610
|
-
frameCtx.putImageData(imageData, 0, 0);
|
|
1611
|
-
// debugFramesCtx.putImageData(imageData, 0, 0);
|
|
1612
|
-
debugFramesCtx.drawImage(frameCanvas, 0, 0, canvas.width, canvas.height);
|
|
1613
|
-
debugFramesCtx.restore();
|
|
1614
|
-
debugPointsCtx.fillStyle = "aqua";
|
|
1615
|
-
workerSyncedOops.draw(debugPointsCtx);
|
|
1616
|
-
}
|
|
1617
|
-
*/
|
|
1618
|
-
workerSyncedOops.update(imageData);
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1635
|
+
// maybeAddPoint(pointTracker, annotations.leftEyeLower0[8][0], annotations.leftEyeLower0[8][1]);
|
|
1636
|
+
// maybeAddPoint(pointTracker, annotations.rightEyeLower0[8][0], annotations.rightEyeLower0[8][1]);
|
|
1621
1637
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
const pointOffset = pointIndex * 2;
|
|
1625
|
-
maybeAddPoint(mainOops, workerSyncedOops.curXY[pointOffset], workerSyncedOops.curXY[pointOffset + 1]);
|
|
1626
|
-
}
|
|
1627
|
-
// Don't do this! It's not how this is supposed to work.
|
|
1628
|
-
// mainOops.pointCount = workerSyncedOops.pointCount;
|
|
1629
|
-
// for (var pointIndex = 0; pointIndex < workerSyncedOops.pointCount; pointIndex++) {
|
|
1630
|
-
// const pointOffset = pointIndex * 2;
|
|
1631
|
-
// mainOops.curXY[pointOffset] = workerSyncedOops.curXY[pointOffset];
|
|
1632
|
-
// mainOops.curXY[pointOffset+1] = workerSyncedOops.curXY[pointOffset+1];
|
|
1633
|
-
// mainOops.prevXY[pointOffset] = workerSyncedOops.prevXY[pointOffset];
|
|
1634
|
-
// mainOops.prevXY[pointOffset+1] = workerSyncedOops.prevXY[pointOffset+1];
|
|
1635
|
-
// }
|
|
1636
|
-
|
|
1637
|
-
// naive latency compensation
|
|
1638
|
-
// Note: this applies to facemeshPrediction.annotations as well which references the same point objects
|
|
1639
|
-
// Note: This latency compensation only really works if it's already tracking well
|
|
1640
|
-
// if (prevFaceInViewConfidence > 0.99) {
|
|
1641
|
-
// facemeshPrediction.keypoints.forEach((point) => {
|
|
1642
|
-
// point.x += movementXSinceFacemeshUpdate;
|
|
1643
|
-
// point.y += movementYSinceFacemeshUpdate;
|
|
1644
|
-
// });
|
|
1645
|
-
// }
|
|
1638
|
+
|
|
1639
|
+
// console.log(pointTracker.pointCount, cameraFramesSinceFacemeshUpdate.length, pointTracker.curXY);
|
|
1646
1640
|
|
|
1647
1641
|
pointsBasedOnFaceInViewConfidence = facemeshPrediction.faceInViewConfidence;
|
|
1648
1642
|
|
|
1649
1643
|
// TODO: separate confidence threshold for removing vs adding points?
|
|
1650
1644
|
|
|
1651
1645
|
// cull points to those within useful facial region
|
|
1652
|
-
|
|
1653
|
-
// a complexity would be that points can be removed over time and we need to keep them identified
|
|
1654
|
-
mainOops.filterPoints((pointIndex) => {
|
|
1646
|
+
pointTracker.filterPoints((pointIndex) => {
|
|
1655
1647
|
var pointOffset = pointIndex * 2;
|
|
1656
1648
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
1657
1649
|
var distance = Math.hypot(
|
|
1658
|
-
(annotations.noseTip[0][0] -
|
|
1659
|
-
annotations.noseTip[0][1] -
|
|
1650
|
+
(annotations.noseTip[0][0] - pointTracker.curXY[pointOffset]) * 1.4,
|
|
1651
|
+
annotations.noseTip[0][1] - pointTracker.curXY[pointOffset + 1]
|
|
1660
1652
|
);
|
|
1661
1653
|
var headSize = Math.hypot(
|
|
1662
1654
|
annotations.leftCheek[0][0] - annotations.rightCheek[0][0],
|
|
@@ -1669,12 +1661,12 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1669
1661
|
// distance to outer corners of eyes
|
|
1670
1662
|
distance = Math.min(
|
|
1671
1663
|
Math.hypot(
|
|
1672
|
-
annotations.leftEyeLower0[0][0] -
|
|
1673
|
-
annotations.leftEyeLower0[0][1] -
|
|
1664
|
+
annotations.leftEyeLower0[0][0] - pointTracker.curXY[pointOffset],
|
|
1665
|
+
annotations.leftEyeLower0[0][1] - pointTracker.curXY[pointOffset + 1]
|
|
1674
1666
|
),
|
|
1675
1667
|
Math.hypot(
|
|
1676
|
-
annotations.rightEyeLower0[0][0] -
|
|
1677
|
-
annotations.rightEyeLower0[0][1] -
|
|
1668
|
+
annotations.rightEyeLower0[0][0] - pointTracker.curXY[pointOffset],
|
|
1669
|
+
annotations.rightEyeLower0[0][1] - pointTracker.curXY[pointOffset + 1]
|
|
1678
1670
|
),
|
|
1679
1671
|
);
|
|
1680
1672
|
if (distance < headSize * 0.42) {
|
|
@@ -1683,62 +1675,148 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1683
1675
|
return true;
|
|
1684
1676
|
});
|
|
1685
1677
|
|
|
1678
|
+
let clickButton = -1;
|
|
1686
1679
|
if (clickingMode === "blink") {
|
|
1680
|
+
// Note: currently head tilt matters a lot, but ideally it should not.
|
|
1681
|
+
// - When moving closer to the camera, theoretically the eye size to head size ratio increases.
|
|
1682
|
+
// (if you can hold your eye still, you can test by moving nearer to / further from the camera (or moving the camera))
|
|
1683
|
+
// - When tilting your head left or right, the contour of one closed eyelid becomes more curved* (as it wraps around your head),
|
|
1684
|
+
// while the other stays near center of the visual region of your head and thus stays relatively straight (experiencing less projection distortion).
|
|
1685
|
+
// - When tilting your head down, the contour of a closed eyelid becomes more curved, which can lead to false negatives.
|
|
1686
|
+
// - When tilting your head up, the contour of an open eyelid becomes more straight, which can lead to false positives.
|
|
1687
|
+
// - *This is a geometric explanation, but in practice, facemesh loses the ability to detect
|
|
1688
|
+
// whether the eye is closed when the head is tilted beyond a point.
|
|
1689
|
+
// Enable `showDebugEyelidContours` to see the shapes we're dealing with here.
|
|
1690
|
+
// - Facemesh uses an "attention mesh model", enabled with `refineLandmarks: true`,
|
|
1691
|
+
// which adjusts points near the eyes and lips to be more accurate (and is 100% necessary for this blink detection to work).
|
|
1692
|
+
// This is what we might ideally target to improve blink detection.
|
|
1687
1693
|
// TODO: try variations, e.g.
|
|
1688
|
-
// -
|
|
1689
|
-
// - would using the eye size instead of head size be different? can compare to see how much variation there is in eye size : head size ratio
|
|
1690
|
-
// - as I noted here https://github.com/1j01/tracky-mouse/issues/1#issuecomment-2053931136
|
|
1694
|
+
// - As I noted here: https://github.com/1j01/tracky-mouse/issues/1#issuecomment-2053931136
|
|
1691
1695
|
// sometimes a fully closed eye isn't detected as fully closed, and an eye can be open and detected at a
|
|
1692
|
-
// similar squinty level
|
|
1696
|
+
// similar squinty level; however, if one eye is detected as fully closed, and the other eye is at that squinty level,
|
|
1693
1697
|
// I think it can be assumed that the squinty eye is open, and otherwise, if neither eye is detected as fully closed,
|
|
1694
1698
|
// then a squinty level can be assumed to be closed. So it might make sense to bias the blink detection, taking into account both eyes.
|
|
1695
1699
|
// (When you blink one eye, you naturally squint with the other a bit, but not necessarily as much as the model reports.
|
|
1696
|
-
// I
|
|
1697
|
-
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
//
|
|
1716
|
-
//
|
|
1717
|
-
//
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1700
|
+
// I suspect this physical phenomenon may have biased the model since eye blinking and opposite eye squinting are correlated.)
|
|
1701
|
+
// - Maybe measure several points instead of just the middle or extreme points
|
|
1702
|
+
// - Can we use a 3D version of the facemesh instead of 2D, to help with ignoring head tilt??
|
|
1703
|
+
// That might be the most important improvement...
|
|
1704
|
+
// We can get z by making getPoint return the z value as well, but this is still camera-relative.
|
|
1705
|
+
// We could transform it using some reference points, but do we have to?
|
|
1706
|
+
// https://chuoling.github.io/mediapipe/solutions/face_mesh.html
|
|
1707
|
+
// This mentions a "face pose transformation matrix" which sounds useful...
|
|
1708
|
+
// - Adjust threshold based on head tilt
|
|
1709
|
+
// - When head is tilted up, make it consider eye open with a thinner eye shape.
|
|
1710
|
+
// Out-of-the-box ideas:
|
|
1711
|
+
// - Use a separate model for eye state detection, using images of the eye region as input.
|
|
1712
|
+
// - I've thought about using "Teachable Machine" for this, it's meant to make training models easy, idk if it's still relevant
|
|
1713
|
+
// - Use multiple cameras. Having a camera on either side would allow seeing the eye from a clear angle in at least one camera,
|
|
1714
|
+
// with significant left/right head tilt.
|
|
1715
|
+
// - It might also help to improve tracking accuracy, by averaging two face meshes, if we can get them into the same coordinate space.
|
|
1716
|
+
// - We might want to ditch the point tracking and just use the facemesh points at that point, although it should still
|
|
1717
|
+
// be possible to use point tracking as long as it's tracked separately and averaged, and the cameras are placed symmetrically.
|
|
1718
|
+
// - Use mirrors. Instead of multiple cameras, imagine two mirrors on either side of the user, angled to reflect the user's head into the camera.
|
|
1719
|
+
// - Fiducial markers on the frames of the mirrors could be used to help with the coordinate space transformation.
|
|
1720
|
+
// - Music stands could be used to hold the mirrors, or they could be hung from the ceiling.
|
|
1721
|
+
// - One might worry about breaking mirrors, but sandbags on stand bases or padding on the mirror frames could help to be safe.
|
|
1722
|
+
// - Lighting integrated into the mirror frames would be a bonus; this is a feature of some vanity mirrors.
|
|
1723
|
+
// - Fewer video streams to process, but more video processing steps, so I'm not sure how it would shake out performance-wise.
|
|
1724
|
+
// - If you're hoping for it to improve tracking, remember that the tracking can be janky when the face is cut off,
|
|
1725
|
+
// and the mirrors would introduce more edges.
|
|
1726
|
+
// - The larger the mirror the better, but the more expensive and unwieldy and thus unlikely to be used.
|
|
1727
|
+
// - If you were to try to avoid using results from faces that are cut off,
|
|
1728
|
+
// you would likely be trying to use the same janky tracking results to determine whether the face is cut off.
|
|
1729
|
+
// It *might* work, but it also might be a bit of a chicken-and-egg problem.
|
|
1730
|
+
|
|
1731
|
+
function getEyeMetrics(eyeUpper, eyeLower) {
|
|
1732
|
+
// The lower eye keypoints have the corners
|
|
1733
|
+
const corners = [eyeLower[0], eyeLower[eyeLower.length - 1]];
|
|
1734
|
+
// Excluding the corners isn't really important since their measures will be 0.
|
|
1735
|
+
const otherPoints = eyeUpper.concat(eyeLower).filter(point => !corners.includes(point));
|
|
1736
|
+
let highest = 0;
|
|
1737
|
+
let lowest = 0;
|
|
1738
|
+
for (const point of otherPoints) {
|
|
1739
|
+
const distance = signedDistancePointLine(point, corners[0], corners[1]);
|
|
1740
|
+
if (distance < lowest) {
|
|
1741
|
+
lowest = distance;
|
|
1742
|
+
}
|
|
1743
|
+
if (distance > highest) {
|
|
1744
|
+
highest = distance;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const eyeWidth = Math.hypot(
|
|
1749
|
+
corners[0][0] - corners[1][0],
|
|
1750
|
+
corners[0][1] - corners[1][1]
|
|
1751
|
+
);
|
|
1752
|
+
const eyeHeight = highest - lowest;
|
|
1753
|
+
const eyeAspectRatio = eyeHeight / eyeWidth;
|
|
1754
|
+
return {
|
|
1755
|
+
corners,
|
|
1756
|
+
upperContour: eyeUpper,
|
|
1757
|
+
lowerContour: eyeLower,
|
|
1758
|
+
highest,
|
|
1759
|
+
lowest,
|
|
1760
|
+
eyeAspectRatio,
|
|
1761
|
+
};
|
|
1723
1762
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1763
|
+
|
|
1764
|
+
const leftEye = getEyeMetrics(annotations.leftEyeUpper0, annotations.leftEyeLower0);
|
|
1765
|
+
const rightEye = getEyeMetrics(annotations.rightEyeUpper0, annotations.rightEyeLower0);
|
|
1766
|
+
|
|
1767
|
+
const thresholdHigh = 0.2;
|
|
1768
|
+
const thresholdLow = 0.16;
|
|
1769
|
+
leftEye.open = leftEye.eyeAspectRatio > (blinkInfo?.leftEye.open ? thresholdLow : thresholdHigh);
|
|
1770
|
+
rightEye.open = rightEye.eyeAspectRatio > (blinkInfo?.rightEye.open ? thresholdLow : thresholdHigh);
|
|
1771
|
+
|
|
1772
|
+
// An attempt at biasing the blink detection based on the other eye's state
|
|
1773
|
+
// (I'm not sure if this is the same as the idea I had noted above)
|
|
1774
|
+
// const threshold = 0.16;
|
|
1775
|
+
// const bias = 0.3;
|
|
1776
|
+
// leftEye.open = leftEye.eyeAspectRatio - threshold - ((rightEye.eyeAspectRatio - threshold) * bias) > 0;
|
|
1777
|
+
// rightEye.open = rightEye.eyeAspectRatio - threshold - ((leftEye.eyeAspectRatio - threshold) * bias) > 0;
|
|
1778
|
+
|
|
1779
|
+
// Involuntary blink rejection
|
|
1780
|
+
const blinkRejectDuration = 100; // milliseconds
|
|
1781
|
+
const currentTime = performance.now();
|
|
1782
|
+
// TODO: DRY
|
|
1783
|
+
if (leftEye.open === blinkInfo?.leftEye.open) {
|
|
1784
|
+
leftEye.timeSinceChange = blinkInfo?.leftEye.timeSinceChange ?? currentTime;
|
|
1785
|
+
} else {
|
|
1786
|
+
leftEye.timeSinceChange = currentTime;
|
|
1726
1787
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
if (clickButton !== -1) {
|
|
1732
|
-
// console.log("Would click button", clickButton);
|
|
1733
|
-
window.electronAPI.clickAtCurrentMousePosition(clickButton === 2);
|
|
1788
|
+
if (rightEye.open === blinkInfo?.rightEye.open) {
|
|
1789
|
+
rightEye.timeSinceChange = blinkInfo?.rightEye.timeSinceChange ?? currentTime;
|
|
1790
|
+
} else {
|
|
1791
|
+
rightEye.timeSinceChange = currentTime;
|
|
1734
1792
|
}
|
|
1793
|
+
const timeSinceChange = currentTime - Math.max(leftEye.timeSinceChange, rightEye.timeSinceChange);
|
|
1794
|
+
leftEye.winking = timeSinceChange > blinkRejectDuration && rightEye.open && !leftEye.open;
|
|
1795
|
+
rightEye.winking = timeSinceChange > blinkRejectDuration && leftEye.open && !rightEye.open;
|
|
1796
|
+
|
|
1797
|
+
if (rightEye.winking) {
|
|
1798
|
+
clickButton = 0;
|
|
1799
|
+
} else if (leftEye.winking) {
|
|
1800
|
+
clickButton = 2;
|
|
1801
|
+
}
|
|
1802
|
+
blinkInfo = {
|
|
1803
|
+
leftEye,
|
|
1804
|
+
rightEye
|
|
1805
|
+
};
|
|
1806
|
+
} else {
|
|
1807
|
+
blinkInfo = null;
|
|
1735
1808
|
}
|
|
1736
1809
|
if (clickingMode === "open-mouth") {
|
|
1737
1810
|
// TODO: modifiers with eye closing or eyebrow raising to trigger different buttons
|
|
1738
|
-
// TODO:
|
|
1739
|
-
|
|
1740
|
-
//
|
|
1741
|
-
const
|
|
1811
|
+
// TODO: refactor and move this code (it's too nested)
|
|
1812
|
+
// TODO: headSize is not a perfect measurement; try alternative measurements, e.g.
|
|
1813
|
+
// - mouth width (implies making an "O" mouth shape would be favored over a wide open mouth shape)
|
|
1814
|
+
const mid = Math.floor(annotations.lipsLowerInner.length / 2);
|
|
1815
|
+
const mouthTopBottomPoints = [
|
|
1816
|
+
annotations.lipsUpperInner[mid],
|
|
1817
|
+
annotations.lipsLowerInner[mid]
|
|
1818
|
+
];
|
|
1819
|
+
const mouthTopBottomDistance = Math.hypot(
|
|
1742
1820
|
annotations.lipsUpperInner[mid][0] - annotations.lipsLowerInner[mid][0],
|
|
1743
1821
|
annotations.lipsUpperInner[mid][1] - annotations.lipsLowerInner[mid][1]
|
|
1744
1822
|
);
|
|
@@ -1746,26 +1824,45 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1746
1824
|
annotations.leftCheek[0][0] - annotations.rightCheek[0][0],
|
|
1747
1825
|
annotations.leftCheek[0][1] - annotations.rightCheek[0][1]
|
|
1748
1826
|
);
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
// and prevent clicking until both eyes are open again
|
|
1754
|
-
// ideally keeping the mouse button held
|
|
1755
|
-
let clickButton = -1;
|
|
1827
|
+
const thresholdHigh = headSize * 0.15;
|
|
1828
|
+
const thresholdLow = headSize * 0.1;
|
|
1829
|
+
// console.log("mouthTopBottomDistance", mouthTopBottomDistance, "threshold", threshold);
|
|
1830
|
+
const mouthOpen = mouthTopBottomDistance > (mouthInfo?.mouthOpen ? thresholdLow : thresholdHigh);
|
|
1756
1831
|
if (mouthOpen) {
|
|
1757
1832
|
clickButton = 0;
|
|
1758
1833
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1834
|
+
mouthInfo = {
|
|
1835
|
+
mouthOpen,
|
|
1836
|
+
mouthTopBottomPoints,
|
|
1837
|
+
corners: [annotations.lipsUpperInner[0], annotations.lipsUpperInner[annotations.lipsUpperInner.length - 1]],
|
|
1838
|
+
mouthOpenDistance: mouthTopBottomDistance / headSize,
|
|
1839
|
+
};
|
|
1840
|
+
} else {
|
|
1841
|
+
mouthInfo = null;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// TODO: implement these clicking modes for the web library version
|
|
1845
|
+
// and unhide the "Clicking mode" setting in the UI
|
|
1846
|
+
// https://github.com/1j01/tracky-mouse/issues/72
|
|
1847
|
+
if ((clickButton === 0) !== buttonStates.left) {
|
|
1848
|
+
window.electronAPI?.setMouseButtonState(false, clickButton === 0);
|
|
1849
|
+
buttonStates.left = clickButton === 0;
|
|
1850
|
+
if ((clickButton === 0)) {
|
|
1851
|
+
lastMouseDownTime = performance.now();
|
|
1852
|
+
} else {
|
|
1853
|
+
// Limit "Delay Before Dragging" effect to the duration of a click.
|
|
1854
|
+
// TODO: consider how this affects releasing a mouse button if two are pressed (not currently possible)
|
|
1855
|
+
// TODO: rename variable, maybe change it to store a cool-down timer? but that would need more state management just for concept clarity
|
|
1856
|
+
lastMouseDownTime = -Infinity; // sorry, making this variable a misnomer
|
|
1761
1857
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
if (clickButton
|
|
1767
|
-
|
|
1768
|
-
|
|
1858
|
+
}
|
|
1859
|
+
if ((clickButton === 2) !== buttonStates.right) {
|
|
1860
|
+
window.electronAPI?.setMouseButtonState(true, clickButton === 2);
|
|
1861
|
+
buttonStates.right = clickButton === 2;
|
|
1862
|
+
if ((clickButton === 2)) {
|
|
1863
|
+
lastMouseDownTime = performance.now();
|
|
1864
|
+
} else {
|
|
1865
|
+
lastMouseDownTime = -Infinity; // sorry, making this variable a misnomer
|
|
1769
1866
|
}
|
|
1770
1867
|
}
|
|
1771
1868
|
}, () => {
|
|
@@ -1774,11 +1871,15 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1774
1871
|
});
|
|
1775
1872
|
}
|
|
1776
1873
|
}
|
|
1777
|
-
|
|
1874
|
+
pointTracker.update(imageData);
|
|
1778
1875
|
}
|
|
1779
1876
|
|
|
1780
1877
|
if (window.electronAPI) {
|
|
1781
|
-
window.electronAPI.
|
|
1878
|
+
window.electronAPI.updateInputFeedback({
|
|
1879
|
+
headNotFound: !face && !facemeshPrediction,
|
|
1880
|
+
blinkInfo,
|
|
1881
|
+
mouthInfo,
|
|
1882
|
+
});
|
|
1782
1883
|
}
|
|
1783
1884
|
|
|
1784
1885
|
if (facemeshPrediction) {
|
|
@@ -1786,17 +1887,10 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1786
1887
|
|
|
1787
1888
|
const bad = facemeshPrediction.faceInViewConfidence < faceInViewConfidenceThreshold;
|
|
1788
1889
|
ctx.fillStyle = bad ? 'rgb(255,255,0)' : 'rgb(130,255,50)';
|
|
1789
|
-
if (!bad ||
|
|
1890
|
+
if (!bad || pointTracker.pointCount < 3 || facemeshPrediction.faceInViewConfidence > pointsBasedOnFaceInViewConfidence + 0.05) {
|
|
1790
1891
|
if (bad) {
|
|
1791
1892
|
ctx.fillStyle = 'rgba(255,0,255)';
|
|
1792
1893
|
}
|
|
1793
|
-
if (update && useFacemesh) {
|
|
1794
|
-
// this should just be visual, since we only add/remove points based on the facemesh data when receiving it
|
|
1795
|
-
facemeshPrediction.keypoints.forEach((point) => {
|
|
1796
|
-
point.x += prevMovementX;
|
|
1797
|
-
point.y += prevMovementY;
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
1894
|
facemeshPrediction.keypoints.forEach(({ x, y }) => {
|
|
1801
1895
|
ctx.fillRect(x, y, 1, 1);
|
|
1802
1896
|
});
|
|
@@ -1807,10 +1901,92 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1807
1901
|
}
|
|
1808
1902
|
}
|
|
1809
1903
|
|
|
1904
|
+
if (clickingMode === "blink" && blinkInfo) {
|
|
1905
|
+
ctx.save();
|
|
1906
|
+
ctx.lineWidth = 2;
|
|
1907
|
+
const drawEye = (eye) => {
|
|
1908
|
+
ctx.strokeStyle = eye.winking ? "red" : eye.open ? "cyan" : "yellow";
|
|
1909
|
+
ctx.beginPath();
|
|
1910
|
+
ctx.moveTo(eye.corners[0][0], eye.corners[0][1]);
|
|
1911
|
+
ctx.lineTo(eye.corners[1][0], eye.corners[1][1]);
|
|
1912
|
+
ctx.stroke();
|
|
1913
|
+
// draw extents as a rectangle
|
|
1914
|
+
ctx.save();
|
|
1915
|
+
ctx.translate(eye.corners[0][0], eye.corners[0][1]);
|
|
1916
|
+
ctx.rotate(Math.atan2(eye.corners[1][1] - eye.corners[0][1], eye.corners[1][0] - eye.corners[0][0]));
|
|
1917
|
+
ctx.beginPath();
|
|
1918
|
+
ctx.rect(0, eye.lowest, Math.hypot(eye.corners[1][0] - eye.corners[0][0], eye.corners[1][1] - eye.corners[0][1]), eye.highest - eye.lowest);
|
|
1919
|
+
ctx.stroke();
|
|
1920
|
+
ctx.restore();
|
|
1921
|
+
// Zoom in and show the eyelid contour SHAPE, for qualitative debugging
|
|
1922
|
+
// This helps to show that the facemesh model doesn't really know whether your eye is open or closed beyond a certain head angle.
|
|
1923
|
+
// Therefore there's not much we can do using the eyelid contour to improve blink detection.
|
|
1924
|
+
// We might be able to tease a little more accuracy out of it using surrounding points in some clever way, 3D information, etc.
|
|
1925
|
+
// but fundamentally, garbage in, garbage out.
|
|
1926
|
+
if (showDebugEyelidContours) {
|
|
1927
|
+
const eyeCenter = [(eye.corners[0][0] + eye.corners[1][0]) / 2, (eye.corners[0][1] + eye.corners[1][1]) / 2];
|
|
1928
|
+
ctx.save();
|
|
1929
|
+
ctx.translate(eyeCenter[0], eyeCenter[1]);
|
|
1930
|
+
ctx.scale(5, 5);
|
|
1931
|
+
ctx.translate(-eyeCenter[0], -eyeCenter[1]);
|
|
1932
|
+
ctx.strokeStyle = "green";
|
|
1933
|
+
ctx.beginPath();
|
|
1934
|
+
for (const contour of [eye.upperContour, eye.lowerContour]) {
|
|
1935
|
+
for (let i = 0; i < contour.length; i++) {
|
|
1936
|
+
const [x, y] = contour[i];
|
|
1937
|
+
if (i === 0) {
|
|
1938
|
+
ctx.moveTo(x, y);
|
|
1939
|
+
} else {
|
|
1940
|
+
ctx.lineTo(x, y);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
ctx.lineWidth = 2 / 5;
|
|
1945
|
+
ctx.stroke();
|
|
1946
|
+
ctx.restore();
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
drawEye(blinkInfo.leftEye);
|
|
1950
|
+
drawEye(blinkInfo.rightEye);
|
|
1951
|
+
ctx.restore();
|
|
1952
|
+
}
|
|
1953
|
+
if (clickingMode === "open-mouth" && mouthInfo) {
|
|
1954
|
+
ctx.save();
|
|
1955
|
+
ctx.lineWidth = 2;
|
|
1956
|
+
ctx.strokeStyle = mouthInfo.mouthOpen ? "red" : "cyan";
|
|
1957
|
+
ctx.beginPath();
|
|
1958
|
+
// ctx.moveTo(mouthInfo.mouthTopBottomPoints[0][0], mouthInfo.mouthTopBottomPoints[0][1]);
|
|
1959
|
+
// ctx.lineTo(mouthInfo.corners[0][0], mouthInfo.corners[0][1]);
|
|
1960
|
+
// ctx.lineTo(mouthInfo.mouthTopBottomPoints[1][0], mouthInfo.mouthTopBottomPoints[1][1]);
|
|
1961
|
+
// ctx.lineTo(mouthInfo.corners[1][0], mouthInfo.corners[1][1]);
|
|
1962
|
+
// ctx.closePath();
|
|
1963
|
+
const mouthCenter = [
|
|
1964
|
+
(mouthInfo.corners[0][0] + mouthInfo.corners[1][0]) / 2,
|
|
1965
|
+
(mouthInfo.corners[0][1] + mouthInfo.corners[1][1]) / 2
|
|
1966
|
+
];
|
|
1967
|
+
const extents = mouthInfo.mouthTopBottomPoints.map(point => signedDistancePointLine(point, mouthInfo.corners[0], mouthInfo.corners[1]));
|
|
1968
|
+
// Draw as two lines rather than a rectangle (or ellipse) to indicate that it's not using aspect ratio of the mouth currently
|
|
1969
|
+
// const highest = Math.max(...extents);
|
|
1970
|
+
// const lowest = Math.min(...extents);
|
|
1971
|
+
// const mouthWidth = Math.hypot(mouthInfo.corners[1][0] - mouthInfo.corners[0][0], mouthInfo.corners[1][1] - mouthInfo.corners[0][1]);
|
|
1972
|
+
const mouthWidth = 50;
|
|
1973
|
+
ctx.translate(mouthCenter[0], mouthCenter[1]);
|
|
1974
|
+
ctx.rotate(Math.atan2(mouthInfo.corners[1][1] - mouthInfo.corners[0][1], mouthInfo.corners[1][0] - mouthInfo.corners[0][0]));
|
|
1975
|
+
ctx.beginPath();
|
|
1976
|
+
// ctx.rect(-mouthWidth / 2, lowest, mouthWidth, highest - lowest);
|
|
1977
|
+
for (const extent of extents) {
|
|
1978
|
+
ctx.moveTo(-mouthWidth / 2, extent);
|
|
1979
|
+
ctx.lineTo(mouthWidth / 2, extent);
|
|
1980
|
+
}
|
|
1981
|
+
ctx.stroke();
|
|
1982
|
+
ctx.restore();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
|
|
1810
1986
|
if (face) {
|
|
1811
1987
|
const bad = faceScore < faceScoreThreshold;
|
|
1812
1988
|
ctx.strokeStyle = bad ? 'rgb(255,255,0)' : 'rgb(130,255,50)';
|
|
1813
|
-
if (!bad ||
|
|
1989
|
+
if (!bad || pointTracker.pointCount < 2 || faceScore > pointsBasedOnFaceScore + 0.05) {
|
|
1814
1990
|
if (bad) {
|
|
1815
1991
|
ctx.strokeStyle = 'rgba(255,0,255)';
|
|
1816
1992
|
}
|
|
@@ -1818,21 +1994,21 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1818
1994
|
pointsBasedOnFaceScore = faceScore;
|
|
1819
1995
|
|
|
1820
1996
|
// nostrils
|
|
1821
|
-
maybeAddPoint(
|
|
1822
|
-
maybeAddPoint(
|
|
1997
|
+
maybeAddPoint(pointTracker, face[42][0], face[42][1]);
|
|
1998
|
+
maybeAddPoint(pointTracker, face[43][0], face[43][1]);
|
|
1823
1999
|
// inner eye corners
|
|
1824
|
-
// maybeAddPoint(
|
|
1825
|
-
// maybeAddPoint(
|
|
2000
|
+
// maybeAddPoint(pointTracker, face[25][0], face[25][1]);
|
|
2001
|
+
// maybeAddPoint(pointTracker, face[30][0], face[30][1]);
|
|
1826
2002
|
|
|
1827
2003
|
// TODO: separate confidence threshold for removing vs adding points?
|
|
1828
2004
|
|
|
1829
2005
|
// cull points to those within useful facial region
|
|
1830
|
-
|
|
2006
|
+
pointTracker.filterPoints((pointIndex) => {
|
|
1831
2007
|
var pointOffset = pointIndex * 2;
|
|
1832
2008
|
// distance from tip of nose (stretched so make an ellipse taller than wide)
|
|
1833
2009
|
var distance = Math.hypot(
|
|
1834
|
-
(face[62][0] -
|
|
1835
|
-
face[62][1] -
|
|
2010
|
+
(face[62][0] - pointTracker.curXY[pointOffset]) * 1.4,
|
|
2011
|
+
face[62][1] - pointTracker.curXY[pointOffset + 1]
|
|
1836
2012
|
);
|
|
1837
2013
|
// distance based on outer eye corners
|
|
1838
2014
|
var headSize = Math.hypot(
|
|
@@ -1854,20 +2030,16 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1854
2030
|
clmTracker.draw(canvas, undefined, undefined, true);
|
|
1855
2031
|
}
|
|
1856
2032
|
}
|
|
1857
|
-
if (debugTimeTravel) {
|
|
1858
|
-
ctx.save();
|
|
1859
|
-
ctx.globalAlpha = 0.8;
|
|
1860
|
-
ctx.drawImage(debugFramesCanvas, 0, 0);
|
|
1861
|
-
ctx.restore();
|
|
1862
|
-
ctx.drawImage(debugPointsCanvas, 0, 0);
|
|
1863
|
-
}
|
|
1864
2033
|
ctx.fillStyle = "lime";
|
|
1865
|
-
|
|
2034
|
+
pointTracker.draw(ctx);
|
|
1866
2035
|
debugPointsCtx.fillStyle = "green";
|
|
1867
|
-
|
|
2036
|
+
pointTracker.draw(debugPointsCtx);
|
|
1868
2037
|
|
|
1869
2038
|
if (update) {
|
|
1870
|
-
|
|
2039
|
+
const screenWidth = window.electronAPI ? screen.width : innerWidth;
|
|
2040
|
+
const screenHeight = window.electronAPI ? screen.height : innerHeight;
|
|
2041
|
+
|
|
2042
|
+
var [movementX, movementY] = pointTracker.getMovement();
|
|
1871
2043
|
|
|
1872
2044
|
// Acceleration curves add a lot of stability,
|
|
1873
2045
|
// letting you focus on a specific point without jitter, but still move quickly.
|
|
@@ -1880,6 +2052,37 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1880
2052
|
var deltaX = accelerate(movementX * sensitivityX, distance);
|
|
1881
2053
|
var deltaY = accelerate(movementY * sensitivityY, distance);
|
|
1882
2054
|
|
|
2055
|
+
// Mimicking eViacam's "Motion Threshold" implementation
|
|
2056
|
+
// https://github.com/cmauri/eviacam/blob/a4032ed9c59def5399a93e74f5ea84513d2f42b1/wxutil/mousecontrol.cpp#L310-L312
|
|
2057
|
+
// (a threshold on instantaneous Manhattan distance, or in other words, x and y speed, separately)
|
|
2058
|
+
// - It's applied after acceleration, following eViacam's lead,
|
|
2059
|
+
// which makes sense in order to have the setting's unit make sense as "pixels",
|
|
2060
|
+
// rather than "pixels before applying a function",
|
|
2061
|
+
// to say nothing of the qualitative differences there might be in reordering the operations.
|
|
2062
|
+
// - Note that it causes jumps which are increasingly noticeable as the setting is increased.
|
|
2063
|
+
// - TODO: consider a "leash" behavior, or a hybrid perhaps
|
|
2064
|
+
// Note that a leash behavior might be less responsive to direction changes,
|
|
2065
|
+
// and might not achieve the goal of stability unless you move back slightly,
|
|
2066
|
+
// since if you've just pulled the leash left for instance, pulling it left
|
|
2067
|
+
// will move it no matter how small, which might turn a click into a drag (if the "Delay Before Dragging" setting doesn't prevent it).
|
|
2068
|
+
// You have to be in the center of the leash region for it to provide stability.
|
|
2069
|
+
// I'm not sure what a hybrid would look like; it might make more sense as two
|
|
2070
|
+
// separate settings, "motion threshold" and "leash distance".
|
|
2071
|
+
if (Math.abs(deltaX * screenWidth) < minDistance) {
|
|
2072
|
+
deltaX = 0;
|
|
2073
|
+
}
|
|
2074
|
+
if (Math.abs(deltaY * screenHeight) < minDistance) {
|
|
2075
|
+
deltaY = 0;
|
|
2076
|
+
}
|
|
2077
|
+
// Avoid dragging when trying to click by ignoring movement for a short time after a mouse down.
|
|
2078
|
+
// This applied previously also to release, to help with double clicks,
|
|
2079
|
+
// but this felt bad, and I find personally that I can still do double clicks without that help.
|
|
2080
|
+
const timeSinceMouseDown = performance.now() - lastMouseDownTime;
|
|
2081
|
+
if (timeSinceMouseDown < delayBeforeDragging) {
|
|
2082
|
+
deltaX = 0;
|
|
2083
|
+
deltaY = 0;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1883
2086
|
if (debugAcceleration) {
|
|
1884
2087
|
const graphWidth = 200;
|
|
1885
2088
|
const graphHeight = 150;
|
|
@@ -1912,9 +2115,6 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1912
2115
|
}
|
|
1913
2116
|
|
|
1914
2117
|
if (!paused) {
|
|
1915
|
-
const screenWidth = window.electronAPI ? screen.width : innerWidth;
|
|
1916
|
-
const screenHeight = window.electronAPI ? screen.height : innerHeight;
|
|
1917
|
-
|
|
1918
2118
|
mouseX -= deltaX * screenWidth;
|
|
1919
2119
|
mouseY += deltaY * screenHeight;
|
|
1920
2120
|
|
|
@@ -1939,25 +2139,6 @@ TrackyMouse.init = function (div, { statsJs = false } = {}) {
|
|
|
1939
2139
|
TrackyMouse.onPointerMove(mouseX, mouseY);
|
|
1940
2140
|
}
|
|
1941
2141
|
}
|
|
1942
|
-
prevMovementX = movementX;
|
|
1943
|
-
prevMovementY = movementY;
|
|
1944
|
-
// movementXSinceFacemeshUpdate += movementX;
|
|
1945
|
-
// movementYSinceFacemeshUpdate += movementY;
|
|
1946
|
-
/*
|
|
1947
|
-
if (enableTimeTravel) {
|
|
1948
|
-
if (facemeshEstimating) {
|
|
1949
|
-
const imageData = getCameraImageData();
|
|
1950
|
-
if (imageData) {
|
|
1951
|
-
cameraFramesSinceFacemeshUpdate.push(imageData);
|
|
1952
|
-
}
|
|
1953
|
-
// limit this buffer size in case something goes wrong
|
|
1954
|
-
if (cameraFramesSinceFacemeshUpdate.length > 500) {
|
|
1955
|
-
// maybe just clear it entirely, because a partial buffer might not be useful
|
|
1956
|
-
cameraFramesSinceFacemeshUpdate.length = 0;
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
*/
|
|
1961
2142
|
}
|
|
1962
2143
|
ctx.restore();
|
|
1963
2144
|
|