tracky-mouse 2.2.0 → 2.3.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 CHANGED
@@ -161,6 +161,7 @@ Arguments:
161
161
  - `config.afterDispatch()` (optional): a function to call after a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
162
162
  - `config.beforePointerDownDispatch()` (optional): a function to call before a `pointerdown` event is dispatched. Likely to be merged with `config.beforeDispatch()` in the future.
163
163
  - `config.afterReleaseDrag()` (optional): a function to call after a drag is released. May be merged with `config.afterDispatch()` in the future.
164
+ - `config.isHeld()` (optional): a function that returns true if the next dwell should be a release (triggering `pointerup`). Not needed for basic `config.shouldDrag(el)` usage. Honestly I don't remember what this is for.
164
165
 
165
166
  Returns an object with the following properties:
166
167
  - `paused`: a getter/setter for whether dwell clicking is paused. Use this to implement a pause/resume button, in conjunction with `config.dwellClickEvenIfPaused`.
@@ -257,6 +258,7 @@ const config = {
257
258
  // especially `beforePointerDownDispatch` which could be supplanted by passing an `Event` to `beforeDispatch`.
258
259
  beforePointerDownDispatch: () => { window.pointers = []; },
259
260
  afterReleaseDrag: () => { window.pointers = []; },
261
+ isHeld: () => { return window.pointer_active; },
260
262
  };
261
263
  const dwellClicker = TrackyMouse.initDwellClicking(config);
262
264
  // dwellClicker.paused = !dwellClicker.paused; // toggle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracky-mouse",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Add facial mouse accessibility to JavaScript applications",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -15,6 +15,7 @@
15
15
  "webcam",
16
16
  "head-tracker",
17
17
  "head-tracking",
18
+ "facial-recognition",
18
19
  "face-tracker",
19
20
  "face-tracking",
20
21
  "headmouse",
@@ -24,13 +25,15 @@
24
25
  "eye-tracking",
25
26
  "eye-gaze",
26
27
  "accessibility",
28
+ "assistive-technology",
27
29
  "cursor",
28
30
  "pointer",
29
31
  "pointing",
30
32
  "input-method",
31
33
  "hands-free",
32
34
  "handsfree",
33
- "desktop-automation"
35
+ "desktop-automation",
36
+ "telekinesis"
34
37
  ],
35
38
  "repository": {
36
39
  "type": "git",
package/tracky-mouse.css CHANGED
@@ -108,7 +108,7 @@
108
108
 
109
109
  .tracky-mouse-ui .tracky-mouse-labeled-slider {
110
110
  display: inline-flex;
111
- position: relative;
111
+ flex-direction: column;
112
112
  margin-bottom: 18px;
113
113
  margin-top: 5px;
114
114
  flex: 1;
@@ -119,20 +119,16 @@
119
119
  flex: 1;
120
120
  }
121
121
 
122
- .tracky-mouse-ui .tracky-mouse-labeled-slider .tracky-mouse-min-label,
123
- .tracky-mouse-ui .tracky-mouse-labeled-slider .tracky-mouse-max-label {
122
+ .tracky-mouse-ui .tracky-mouse-labeled-slider .tracky-mouse-slider-labels {
124
123
  opacity: 0.8;
125
- position: absolute;
126
- bottom: -12px;
127
124
  pointer-events: none;
128
- }
129
-
130
- .tracky-mouse-ui .tracky-mouse-labeled-slider .tracky-mouse-min-label {
125
+ display: flex;
126
+ width: 100%;
131
127
  left: 0;
132
- }
133
-
134
- .tracky-mouse-ui .tracky-mouse-labeled-slider .tracky-mouse-max-label {
135
128
  right: 0;
129
+ justify-content: space-between;
130
+ /* If they come this close, wrap the slider label text */
131
+ gap: 10px;
136
132
  }
137
133
 
138
134
  .tracky-mouse-canvas-container-container {
@@ -274,6 +270,10 @@ body:not(.tracky-mouse-manual-takeback) .tracky-mouse-manual-takeback-indicator
274
270
  color: rgb(135 0 191);
275
271
  }
276
272
 
273
+ .tracky-mouse-controls {
274
+ overflow: auto;
275
+ }
276
+
277
277
  .tracky-mouse-controls details {
278
278
  border: 1px solid rgba(0, 0, 0, 0.3);
279
279
  border-radius: 4px;
package/tracky-mouse.js CHANGED
@@ -35,16 +35,16 @@ TrackyMouse.loadDependencies = function ({ statsJs = false } = {}) {
35
35
  return Promise.all(scriptFiles.map(loadScript));
36
36
  };
37
37
 
38
- const is_selector_valid = ((dummy_element) =>
38
+ const isSelectorValid = ((dummyElement) =>
39
39
  (selector) => {
40
- try { dummy_element.querySelector(selector); } catch { return false; }
40
+ try { dummyElement.querySelector(selector); } catch { return false; }
41
41
  return true;
42
42
  })(document.createDocumentFragment());
43
43
 
44
44
 
45
- const dwell_clickers = [];
45
+ const dwellClickers = [];
46
46
 
47
- const init_dwell_clicking = (config) => {
47
+ const initDwellClicking = (config) => {
48
48
  /*
49
49
  Arguments:
50
50
  - `config.targets` (required): a CSS selector for the elements to click. Anything else will be ignored.
@@ -59,6 +59,8 @@ const init_dwell_clicking = (config) => {
59
59
  - `config.click({x, y, target})` (required): a function to trigger a click on the given target element.
60
60
  - `config.beforeDispatch()` (optional): a function to call before a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
61
61
  - `config.afterDispatch()` (optional): a function to call after a pointer event is dispatched. For detecting un-trusted user gestures, outside of an event handler.
62
+ - `config.beforePointerDownDispatch()` (optional): a function to call before a `pointerdown` event is dispatched. Likely to be merged with `config.beforeDispatch()` in the future.
63
+ - `config.isHeld()` (optional): a function that returns true if the next dwell should be a release (triggering `pointerup`).
62
64
  */
63
65
  if (typeof config !== "object") {
64
66
  throw new Error("configuration object required for initDwellClicking");
@@ -69,7 +71,7 @@ const init_dwell_clicking = (config) => {
69
71
  if (typeof config.targets !== "string") {
70
72
  throw new Error("config.targets must be a string (a CSS selector)");
71
73
  }
72
- if (!is_selector_valid(config.targets)) {
74
+ if (!isSelectorValid(config.targets)) {
73
75
  throw new Error("config.targets is not a valid CSS selector");
74
76
  }
75
77
  if (config.click === undefined) {
@@ -126,89 +128,89 @@ const init_dwell_clicking = (config) => {
126
128
  if (typeof rule.to !== "string" && typeof rule.to !== "function" && !(rule.to instanceof Element) && rule.to !== null) {
127
129
  throw new Error(`config.retarget[${i}].to must be a CSS selector string, an Element, a function, or null`);
128
130
  }
129
- if (typeof rule.from === "string" && !is_selector_valid(rule.from)) {
131
+ if (typeof rule.from === "string" && !isSelectorValid(rule.from)) {
130
132
  throw new Error(`config.retarget[${i}].from is not a valid CSS selector`);
131
133
  }
132
- if (typeof rule.to === "string" && !is_selector_valid(rule.to)) {
134
+ if (typeof rule.to === "string" && !isSelectorValid(rule.to)) {
133
135
  throw new Error(`config.retarget[${i}].to is not a valid CSS selector`);
134
136
  }
135
137
  }
136
138
  }
137
139
 
138
- // tracky_mouse_container.querySelector(".tracky-mouse-canvas").classList.add("inset-deep");
139
-
140
- const circle_radius_max = 50; // dwell indicator size in pixels
141
- const hover_timespan = 500; // how long between the dwell indicator appearing and triggering a click
142
- const averaging_window_timespan = 500;
143
- const inactive_at_startup_timespan = 1500; // (should be at least averaging_window_timespan, but more importantly enough to make it not awkward when enabling dwell clicking)
144
- const inactive_after_release_timespan = 1000; // after click or drag release (from dwell or otherwise)
145
- const inactive_after_hovered_timespan = 1000; // after dwell click indicator appears; does not control the time to finish that dwell click, only to click on something else after this is canceled (but it doesn't control that directly)
146
- const inactive_after_invalid_timespan = 1000; // after a dwell click is canceled due to an element popping up in front, or existing in front at the center of the other element
147
- const inactive_after_focused_timespan = 1000; // after page becomes focused after being unfocused
148
- let recent_points = [];
149
- let inactive_until_time = performance.now();
140
+ // trackyMouseContainer.querySelector(".tracky-mouse-canvas").classList.add("inset-deep");
141
+
142
+ const circleRadiusMax = 50; // dwell indicator size in pixels
143
+ const hoverTimespan = 500; // how long between the dwell indicator appearing and triggering a click
144
+ const averagingWindowTimespan = 500;
145
+ const inactiveAtStartupTimespan = 1500; // (should be at least averagingWindowTimespan, but more importantly enough to make it not awkward when enabling dwell clicking)
146
+ const inactiveAfterReleaseTimespan = 1000; // after click or drag release (from dwell or otherwise)
147
+ const inactiveAfterHoveredTimespan = 1000; // after dwell click indicator appears; does not control the time to finish that dwell click, only to click on something else after this is canceled (but it doesn't control that directly)
148
+ const inactiveAfterInvalidTimespan = 1000; // after a dwell click is canceled due to an element popping up in front, or existing in front at the center of the other element
149
+ const inactiveAfterFocusedTimespan = 1000; // after page becomes focused after being unfocused
150
+ let recentPoints = [];
151
+ let inactiveUntilTime = performance.now();
150
152
  let paused = false;
151
- let hover_candidate;
152
- let dwell_dragging = null;
153
+ let hoverCandidate;
154
+ let dwellDragging = null;
153
155
 
154
- const deactivate_for_at_least = (timespan) => {
155
- inactive_until_time = Math.max(inactive_until_time, performance.now() + timespan);
156
+ const deactivateForAtLeast = (timespan) => {
157
+ inactiveUntilTime = Math.max(inactiveUntilTime, performance.now() + timespan);
156
158
  };
157
- deactivate_for_at_least(inactive_at_startup_timespan);
159
+ deactivateForAtLeast(inactiveAtStartupTimespan);
158
160
 
159
161
  const halo = document.createElement("div");
160
162
  halo.className = "tracky-mouse-hover-halo";
161
163
  halo.style.display = "none";
162
164
  document.body.appendChild(halo);
163
- const dwell_indicator = document.createElement("div");
164
- dwell_indicator.className = "tracky-mouse-dwell-indicator";
165
- dwell_indicator.style.width = `${circle_radius_max}px`;
166
- dwell_indicator.style.height = `${circle_radius_max}px`;
167
- dwell_indicator.style.display = "none";
168
- document.body.appendChild(dwell_indicator);
169
-
170
- const on_pointer_move = (e) => {
171
- recent_points.push({ x: e.clientX, y: e.clientY, time: performance.now() });
165
+ const dwellIndicator = document.createElement("div");
166
+ dwellIndicator.className = "tracky-mouse-dwell-indicator";
167
+ dwellIndicator.style.width = `${circleRadiusMax}px`;
168
+ dwellIndicator.style.height = `${circleRadiusMax}px`;
169
+ dwellIndicator.style.display = "none";
170
+ document.body.appendChild(dwellIndicator);
171
+
172
+ const onPointerMove = (e) => {
173
+ recentPoints.push({ x: e.clientX, y: e.clientY, time: performance.now() });
172
174
  };
173
- const on_pointer_up_or_cancel = (_e) => {
174
- deactivate_for_at_least(inactive_after_release_timespan);
175
- dwell_dragging = null;
175
+ const onPointerUpOrCancel = (_e) => {
176
+ deactivateForAtLeast(inactiveAfterReleaseTimespan);
177
+ dwellDragging = null;
176
178
  };
177
179
 
178
- let page_focused = document.visibilityState === "visible"; // guess/assumption
179
- let mouse_inside_page = true; // assumption
180
- const on_focus = () => {
181
- page_focused = true;
182
- deactivate_for_at_least(inactive_after_focused_timespan);
180
+ let pageFocused = document.visibilityState === "visible"; // guess/assumption
181
+ let mouseInsidePage = true; // assumption
182
+ const onFocus = () => {
183
+ pageFocused = true;
184
+ deactivateForAtLeast(inactiveAfterFocusedTimespan);
183
185
  };
184
- const on_blur = () => {
185
- page_focused = false;
186
+ const onBlur = () => {
187
+ pageFocused = false;
186
188
  };
187
- const on_mouse_leave_page = () => {
188
- mouse_inside_page = false;
189
+ const onMouseLeavePage = () => {
190
+ mouseInsidePage = false;
189
191
  };
190
- const on_mouse_enter_page = () => {
191
- mouse_inside_page = true;
192
+ const onMouseEnterPage = () => {
193
+ mouseInsidePage = true;
192
194
  };
193
195
 
194
- window.addEventListener("pointermove", on_pointer_move);
195
- window.addEventListener("pointerup", on_pointer_up_or_cancel);
196
- window.addEventListener("pointercancel", on_pointer_up_or_cancel);
197
- window.addEventListener("focus", on_focus);
198
- window.addEventListener("blur", on_blur);
199
- document.addEventListener("mouseleave", on_mouse_leave_page);
200
- document.addEventListener("mouseenter", on_mouse_enter_page);
196
+ window.addEventListener("pointermove", onPointerMove);
197
+ window.addEventListener("pointerup", onPointerUpOrCancel);
198
+ window.addEventListener("pointercancel", onPointerUpOrCancel);
199
+ window.addEventListener("focus", onFocus);
200
+ window.addEventListener("blur", onBlur);
201
+ document.addEventListener("mouseleave", onMouseLeavePage);
202
+ document.addEventListener("mouseenter", onMouseEnterPage);
201
203
 
202
- const get_hover_candidate = (clientX, clientY) => {
204
+ const getHoverCandidate = (clientX, clientY) => {
203
205
 
204
- if (!page_focused || !mouse_inside_page) return null;
206
+ if (!pageFocused || !mouseInsidePage) return null;
205
207
 
206
208
  let target = document.elementFromPoint(clientX, clientY);
207
209
  if (!target) {
208
210
  return null;
209
211
  }
210
212
 
211
- let hover_candidate = {
213
+ let hoverCandidate = {
212
214
  x: clientX,
213
215
  y: clientY,
214
216
  time: performance.now(),
@@ -221,33 +223,33 @@ const init_dwell_clicking = (config) => {
221
223
  typeof from === "function" ? from(target) :
222
224
  target.matches(from)
223
225
  ) {
224
- const to_element =
226
+ const toElement =
225
227
  (to instanceof Element || to === null) ? to :
226
228
  typeof to === "function" ? to(target) :
227
229
  (target.closest(to) || target.querySelector(to));
228
- if (to_element === null) {
230
+ if (toElement === null) {
229
231
  return null;
230
- } else if (to_element) {
231
- const to_rect = to_element.getBoundingClientRect();
232
+ } else if (toElement) {
233
+ const toRect = toElement.getBoundingClientRect();
232
234
  if (
233
- hover_candidate.x > to_rect.left - withinMargin &&
234
- hover_candidate.y > to_rect.top - withinMargin &&
235
- hover_candidate.x < to_rect.right + withinMargin &&
236
- hover_candidate.y < to_rect.bottom + withinMargin
235
+ hoverCandidate.x > toRect.left - withinMargin &&
236
+ hoverCandidate.y > toRect.top - withinMargin &&
237
+ hoverCandidate.x < toRect.right + withinMargin &&
238
+ hoverCandidate.y < toRect.bottom + withinMargin
237
239
  ) {
238
- target = to_element;
239
- hover_candidate.x = Math.min(
240
- to_rect.right - 1,
240
+ target = toElement;
241
+ hoverCandidate.x = Math.min(
242
+ toRect.right - 1,
241
243
  Math.max(
242
- to_rect.left,
243
- hover_candidate.x,
244
+ toRect.left,
245
+ hoverCandidate.x,
244
246
  ),
245
247
  );
246
- hover_candidate.y = Math.min(
247
- to_rect.bottom - 1,
248
+ hoverCandidate.y = Math.min(
249
+ toRect.bottom - 1,
248
250
  Math.max(
249
- to_rect.top,
250
- hover_candidate.y,
251
+ toRect.top,
252
+ hoverCandidate.y,
251
253
  ),
252
254
  );
253
255
  retargeted = true;
@@ -267,14 +269,14 @@ const init_dwell_clicking = (config) => {
267
269
  if (!config.noCenter?.(target)) {
268
270
  // Nudge hover previews to the center of buttons and things
269
271
  const rect = target.getBoundingClientRect();
270
- hover_candidate.x = rect.left + rect.width / 2;
271
- hover_candidate.y = rect.top + rect.height / 2;
272
+ hoverCandidate.x = rect.left + rect.width / 2;
273
+ hoverCandidate.y = rect.top + rect.height / 2;
272
274
  }
273
- hover_candidate.target = target;
274
- return hover_candidate;
275
+ hoverCandidate.target = target;
276
+ return hoverCandidate;
275
277
  };
276
278
 
277
- const get_event_options = ({ x, y }) => {
279
+ const getEventOptions = ({ x, y }) => {
278
280
  return {
279
281
  view: window, // needed for offsetX/Y calculation
280
282
  clientX: x,
@@ -287,7 +289,7 @@ const init_dwell_clicking = (config) => {
287
289
  };
288
290
  };
289
291
 
290
- const average_points = (points) => {
292
+ const averagePoints = (points) => {
291
293
  const average = { x: 0, y: 0 };
292
294
  for (const pointer of points) {
293
295
  average.x += pointer.x;
@@ -300,78 +302,78 @@ const init_dwell_clicking = (config) => {
300
302
 
301
303
  const update = () => {
302
304
  const time = performance.now();
303
- recent_points = recent_points.filter((point_record) => time < point_record.time + averaging_window_timespan);
304
- if (recent_points.length) {
305
- const latest_point = recent_points[recent_points.length - 1];
306
- recent_points.push({ x: latest_point.x, y: latest_point.y, time });
307
- const average_point = average_points(recent_points);
305
+ recentPoints = recentPoints.filter((pointRecord) => time < pointRecord.time + averagingWindowTimespan);
306
+ if (recentPoints.length) {
307
+ const latestPoint = recentPoints[recentPoints.length - 1];
308
+ recentPoints.push({ x: latestPoint.x, y: latestPoint.y, time });
309
+ const averagePoint = averagePoints(recentPoints);
308
310
  // debug
309
- // const canvas_point = to_canvas_coords({clientX: average_point.x, clientY: average_point.y});
311
+ // const canvasPoint = toCanvasCoords({clientX: averagePoint.x, clientY: averagePoint.y});
310
312
  // ctx.fillStyle = "red";
311
- // ctx.fillRect(canvas_point.x, canvas_point.y, 10, 10);
312
- const recent_movement_amount = Math.hypot(latest_point.x - average_point.x, latest_point.y - average_point.y);
313
+ // ctx.fillRect(canvasPoint.x, canvasPoint.y, 10, 10);
314
+ const recentMovementAmount = Math.hypot(latestPoint.x - averagePoint.x, latestPoint.y - averagePoint.y);
313
315
 
314
316
  // Invalidate in case an element pops up in front of the element you're hovering over, e.g. a submenu
315
- // (that use case doesn't actually work in jspaint because the menu pops up before the hover_candidate exists)
317
+ // (that use case doesn't actually work in jspaint because the menu pops up before the hoverCandidate exists)
316
318
  // (TODO: disable hovering to open submenus in facial mouse mode in jspaint)
317
319
  // or an element occludes the center of an element you're hovering over, in which case it
318
320
  // could be confusing if it showed a dwell click indicator over a different element than it would click
319
321
  // (but TODO: just move the indicator off center in that case)
320
- if (hover_candidate && !dwell_dragging) {
321
- const apparent_hover_candidate = get_hover_candidate(hover_candidate.x, hover_candidate.y);
322
- const show_occluder_indicator = (occluder) => {
323
- const occluder_indicator = document.createElement("div");
324
- const occluder_rect = occluder.getBoundingClientRect();
325
- const outline_width = 4;
326
- occluder_indicator.style.pointerEvents = "none";
327
- occluder_indicator.style.zIndex = 1000001;
328
- occluder_indicator.style.display = "block";
329
- occluder_indicator.style.position = "fixed";
330
- occluder_indicator.style.left = `${occluder_rect.left + outline_width}px`;
331
- occluder_indicator.style.top = `${occluder_rect.top + outline_width}px`;
332
- occluder_indicator.style.width = `${occluder_rect.width - outline_width * 2}px`;
333
- occluder_indicator.style.height = `${occluder_rect.height - outline_width * 2}px`;
334
- occluder_indicator.style.outline = `${outline_width}px dashed red`;
335
- occluder_indicator.style.boxShadow = `0 0 ${outline_width}px ${outline_width}px maroon`;
336
- document.body.appendChild(occluder_indicator);
322
+ if (hoverCandidate && !dwellDragging) {
323
+ const apparentHoverCandidate = getHoverCandidate(hoverCandidate.x, hoverCandidate.y);
324
+ const showOccluderIndicator = (occluder) => {
325
+ const occluderIndicator = document.createElement("div");
326
+ const occluderRect = occluder.getBoundingClientRect();
327
+ const outlineWidth = 4;
328
+ occluderIndicator.style.pointerEvents = "none";
329
+ occluderIndicator.style.zIndex = 1000001;
330
+ occluderIndicator.style.display = "block";
331
+ occluderIndicator.style.position = "fixed";
332
+ occluderIndicator.style.left = `${occluderRect.left + outlineWidth}px`;
333
+ occluderIndicator.style.top = `${occluderRect.top + outlineWidth}px`;
334
+ occluderIndicator.style.width = `${occluderRect.width - outlineWidth * 2}px`;
335
+ occluderIndicator.style.height = `${occluderRect.height - outlineWidth * 2}px`;
336
+ occluderIndicator.style.outline = `${outlineWidth}px dashed red`;
337
+ occluderIndicator.style.boxShadow = `0 0 ${outlineWidth}px ${outlineWidth}px maroon`;
338
+ document.body.appendChild(occluderIndicator);
337
339
  setTimeout(() => {
338
- occluder_indicator.remove();
339
- }, inactive_after_invalid_timespan * 0.5);
340
+ occluderIndicator.remove();
341
+ }, inactiveAfterInvalidTimespan * 0.5);
340
342
  };
341
- if (apparent_hover_candidate) {
343
+ if (apparentHoverCandidate) {
342
344
  if (
343
- apparent_hover_candidate.target !== hover_candidate.target &&
345
+ apparentHoverCandidate.target !== hoverCandidate.target &&
344
346
  // !retargeted &&
345
347
  !config.isEquivalentTarget?.(
346
- apparent_hover_candidate.target, hover_candidate.target
348
+ apparentHoverCandidate.target, hoverCandidate.target
347
349
  )
348
350
  ) {
349
- hover_candidate = null;
350
- deactivate_for_at_least(inactive_after_invalid_timespan);
351
- show_occluder_indicator(apparent_hover_candidate.target);
351
+ hoverCandidate = null;
352
+ deactivateForAtLeast(inactiveAfterInvalidTimespan);
353
+ showOccluderIndicator(apparentHoverCandidate.target);
352
354
  }
353
355
  } else {
354
- let occluder = document.elementFromPoint(hover_candidate.x, hover_candidate.y);
355
- hover_candidate = null;
356
- deactivate_for_at_least(inactive_after_invalid_timespan);
357
- show_occluder_indicator(occluder || document.body);
356
+ let occluder = document.elementFromPoint(hoverCandidate.x, hoverCandidate.y);
357
+ hoverCandidate = null;
358
+ deactivateForAtLeast(inactiveAfterInvalidTimespan);
359
+ showOccluderIndicator(occluder || document.body);
358
360
  }
359
361
  }
360
362
 
361
- let circle_position = latest_point;
362
- let circle_opacity = 0;
363
- let circle_radius = 0;
364
- if (hover_candidate) {
365
- circle_position = hover_candidate;
366
- circle_opacity = 0.4;
367
- circle_radius =
368
- (hover_candidate.time - time + hover_timespan) / hover_timespan
369
- * circle_radius_max;
370
- if (time > hover_candidate.time + hover_timespan) {
371
- if (config.isHeld?.() || dwell_dragging) {
363
+ let circlePosition = latestPoint;
364
+ let circleOpacity = 0;
365
+ let circleRadius = 0;
366
+ if (hoverCandidate) {
367
+ circlePosition = hoverCandidate;
368
+ circleOpacity = 0.4;
369
+ circleRadius =
370
+ (hoverCandidate.time - time + hoverTimespan) / hoverTimespan
371
+ * circleRadiusMax;
372
+ if (time > hoverCandidate.time + hoverTimespan) {
373
+ if (config.isHeld?.() || dwellDragging) {
372
374
  config.beforeDispatch?.();
373
- hover_candidate.target.dispatchEvent(new PointerEvent("pointerup",
374
- Object.assign(get_event_options(hover_candidate), {
375
+ hoverCandidate.target.dispatchEvent(new PointerEvent("pointerup",
376
+ Object.assign(getEventOptions(hoverCandidate), {
375
377
  button: 0,
376
378
  buttons: 0,
377
379
  })
@@ -380,70 +382,70 @@ const init_dwell_clicking = (config) => {
380
382
  } else {
381
383
  config.beforePointerDownDispatch?.();
382
384
  config.beforeDispatch?.();
383
- hover_candidate.target.dispatchEvent(new PointerEvent("pointerdown",
384
- Object.assign(get_event_options(hover_candidate), {
385
+ hoverCandidate.target.dispatchEvent(new PointerEvent("pointerdown",
386
+ Object.assign(getEventOptions(hoverCandidate), {
385
387
  button: 0,
386
388
  buttons: 1,
387
389
  })
388
390
  ));
389
391
  config.afterDispatch?.();
390
- if (config.shouldDrag?.(hover_candidate.target)) {
391
- dwell_dragging = hover_candidate.target;
392
+ if (config.shouldDrag?.(hoverCandidate.target)) {
393
+ dwellDragging = hoverCandidate.target;
392
394
  } else {
393
395
  config.beforeDispatch?.();
394
- hover_candidate.target.dispatchEvent(new PointerEvent("pointerup",
395
- Object.assign(get_event_options(hover_candidate), {
396
+ hoverCandidate.target.dispatchEvent(new PointerEvent("pointerup",
397
+ Object.assign(getEventOptions(hoverCandidate), {
396
398
  button: 0,
397
399
  buttons: 0,
398
400
  })
399
401
  ));
400
- config.click(hover_candidate);
402
+ config.click(hoverCandidate);
401
403
  config.afterDispatch?.();
402
404
  }
403
405
  }
404
- hover_candidate = null;
405
- deactivate_for_at_least(inactive_after_hovered_timespan);
406
+ hoverCandidate = null;
407
+ deactivateForAtLeast(inactiveAfterHoveredTimespan);
406
408
  }
407
409
  }
408
410
 
409
- if (dwell_dragging) {
410
- dwell_indicator.classList.add("tracky-mouse-for-release");
411
+ if (dwellDragging) {
412
+ dwellIndicator.classList.add("tracky-mouse-for-release");
411
413
  } else {
412
- dwell_indicator.classList.remove("tracky-mouse-for-release");
414
+ dwellIndicator.classList.remove("tracky-mouse-for-release");
413
415
  }
414
- dwell_indicator.style.display = "";
415
- dwell_indicator.style.opacity = circle_opacity;
416
- dwell_indicator.style.transform = `scale(${circle_radius / circle_radius_max})`;
417
- dwell_indicator.style.left = `${circle_position.x - circle_radius_max / 2}px`;
418
- dwell_indicator.style.top = `${circle_position.y - circle_radius_max / 2}px`;
419
-
420
- let halo_target =
421
- dwell_dragging ||
422
- (hover_candidate || get_hover_candidate(latest_point.x, latest_point.y) || {}).target;
423
-
424
- if (halo_target && (!paused || config.dwellClickEvenIfPaused?.(halo_target))) {
425
- let rect = halo_target.getBoundingClientRect();
426
- const computed_style = getComputedStyle(halo_target);
427
- let ancestor = halo_target;
428
- let border_radius_scale = 1; // for border radius mimicry, given parents with transform: scale()
416
+ dwellIndicator.style.display = "";
417
+ dwellIndicator.style.opacity = circleOpacity;
418
+ dwellIndicator.style.transform = `scale(${circleRadius / circleRadiusMax})`;
419
+ dwellIndicator.style.left = `${circlePosition.x - circleRadiusMax / 2}px`;
420
+ dwellIndicator.style.top = `${circlePosition.y - circleRadiusMax / 2}px`;
421
+
422
+ let haloTarget =
423
+ dwellDragging ||
424
+ (hoverCandidate || getHoverCandidate(latestPoint.x, latestPoint.y) || {}).target;
425
+
426
+ if (haloTarget && (!paused || config.dwellClickEvenIfPaused?.(haloTarget))) {
427
+ let rect = haloTarget.getBoundingClientRect();
428
+ const computedStyle = getComputedStyle(haloTarget);
429
+ let ancestor = haloTarget;
430
+ let borderRadiusScale = 1; // for border radius mimicry, given parents with transform: scale()
429
431
  while (ancestor instanceof HTMLElement) {
430
- const ancestor_computed_style = getComputedStyle(ancestor);
431
- if (ancestor_computed_style.transform) {
432
+ const ancestorComputedStyle = getComputedStyle(ancestor);
433
+ if (ancestorComputedStyle.transform) {
432
434
  // Collect scale transforms
433
- const match = ancestor_computed_style.transform.match(/(?:scale|matrix)\((\d+(?:\.\d+)?)/);
435
+ const match = ancestorComputedStyle.transform.match(/(?:scale|matrix)\((\d+(?:\.\d+)?)/);
434
436
  if (match) {
435
- border_radius_scale *= Number(match[1]);
437
+ borderRadiusScale *= Number(match[1]);
436
438
  }
437
439
  }
438
- if (ancestor_computed_style.overflow !== "visible") {
440
+ if (ancestorComputedStyle.overflow !== "visible") {
439
441
  // Clamp to visible region if in scrollable area
440
442
  // This lets you see the hover halo when scrolled to the middle of a large canvas
441
- const scroll_area_rect = ancestor.getBoundingClientRect();
443
+ const scrollAreaRect = ancestor.getBoundingClientRect();
442
444
  rect = {
443
- left: Math.max(rect.left, scroll_area_rect.left),
444
- top: Math.max(rect.top, scroll_area_rect.top),
445
- right: Math.min(rect.right, scroll_area_rect.right),
446
- bottom: Math.min(rect.bottom, scroll_area_rect.bottom),
445
+ left: Math.max(rect.left, scrollAreaRect.left),
446
+ top: Math.max(rect.top, scrollAreaRect.top),
447
+ right: Math.min(rect.right, scrollAreaRect.right),
448
+ bottom: Math.min(rect.bottom, scrollAreaRect.bottom),
447
449
  };
448
450
  rect.width = rect.right - rect.left;
449
451
  rect.height = rect.bottom - rect.top;
@@ -468,40 +470,40 @@ const init_dwell_clicking = (config) => {
468
470
  "borderBottomLeftRadius",
469
471
  ]) {
470
472
  // Unfortunately, getComputedStyle can return percentages, probably other units, probably also "auto"
471
- if (computed_style[prop].endsWith("px")) {
472
- halo.style[prop] = `${parseFloat(computed_style[prop]) * border_radius_scale}px`;
473
+ if (computedStyle[prop].endsWith("px")) {
474
+ halo.style[prop] = `${parseFloat(computedStyle[prop]) * borderRadiusScale}px`;
473
475
  } else {
474
- halo.style[prop] = computed_style[prop];
476
+ halo.style[prop] = computedStyle[prop];
475
477
  }
476
478
  }
477
479
  } else {
478
480
  halo.style.display = "none";
479
481
  }
480
482
 
481
- if (time < inactive_until_time) {
483
+ if (time < inactiveUntilTime) {
482
484
  return;
483
485
  }
484
- if (recent_movement_amount < 5) {
485
- if (!hover_candidate) {
486
- hover_candidate = {
487
- x: average_point.x,
488
- y: average_point.y,
486
+ if (recentMovementAmount < 5) {
487
+ if (!hoverCandidate) {
488
+ hoverCandidate = {
489
+ x: averagePoint.x,
490
+ y: averagePoint.y,
489
491
  time: performance.now(),
490
- target: dwell_dragging || null,
492
+ target: dwellDragging || null,
491
493
  };
492
- if (!dwell_dragging) {
493
- hover_candidate = get_hover_candidate(hover_candidate.x, hover_candidate.y);
494
+ if (!dwellDragging) {
495
+ hoverCandidate = getHoverCandidate(hoverCandidate.x, hoverCandidate.y);
494
496
  }
495
- if (hover_candidate && (paused && !config.dwellClickEvenIfPaused?.(hover_candidate.target))) {
496
- hover_candidate = null;
497
+ if (hoverCandidate && (paused && !config.dwellClickEvenIfPaused?.(hoverCandidate.target))) {
498
+ hoverCandidate = null;
497
499
  }
498
500
  }
499
501
  }
500
- if (recent_movement_amount > 100) {
501
- if (dwell_dragging) {
502
+ if (recentMovementAmount > 100) {
503
+ if (dwellDragging) {
502
504
  config.beforeDispatch?.();
503
505
  window.dispatchEvent(new PointerEvent("pointerup",
504
- Object.assign(get_event_options(average_point), {
506
+ Object.assign(getEventOptions(averagePoint), {
505
507
  button: 0,
506
508
  buttons: 0,
507
509
  })
@@ -510,29 +512,29 @@ const init_dwell_clicking = (config) => {
510
512
  config.afterReleaseDrag?.();
511
513
  }
512
514
  }
513
- if (recent_movement_amount > 60) {
514
- hover_candidate = null;
515
+ if (recentMovementAmount > 60) {
516
+ hoverCandidate = null;
515
517
  }
516
518
  }
517
519
  };
518
- let raf_id;
520
+ let rafId;
519
521
  const animate = () => {
520
- raf_id = requestAnimationFrame(animate);
522
+ rafId = requestAnimationFrame(animate);
521
523
  update();
522
524
  };
523
- raf_id = requestAnimationFrame(animate);
525
+ rafId = requestAnimationFrame(animate);
524
526
 
525
527
  const dispose = () => {
526
- cancelAnimationFrame(raf_id);
528
+ cancelAnimationFrame(rafId);
527
529
  halo.remove();
528
- dwell_indicator.remove();
529
- window.removeEventListener("pointermove", on_pointer_move);
530
- window.removeEventListener("pointerup", on_pointer_up_or_cancel);
531
- window.removeEventListener("pointercancel", on_pointer_up_or_cancel);
532
- window.removeEventListener("focus", on_focus);
533
- window.removeEventListener("blur", on_blur);
534
- document.removeEventListener("mouseleave", on_mouse_leave_page);
535
- document.removeEventListener("mouseenter", on_mouse_enter_page);
530
+ dwellIndicator.remove();
531
+ window.removeEventListener("pointermove", onPointerMove);
532
+ window.removeEventListener("pointerup", onPointerUpOrCancel);
533
+ window.removeEventListener("pointercancel", onPointerUpOrCancel);
534
+ window.removeEventListener("focus", onFocus);
535
+ window.removeEventListener("blur", onBlur);
536
+ document.removeEventListener("mouseleave", onMouseLeavePage);
537
+ document.removeEventListener("mouseenter", onMouseEnterPage);
536
538
  };
537
539
 
538
540
  const dwellClicker = {
@@ -544,16 +546,16 @@ const init_dwell_clicking = (config) => {
544
546
  },
545
547
  dispose,
546
548
  };
547
- dwell_clickers.push(dwellClicker);
549
+ dwellClickers.push(dwellClicker);
548
550
  return dwellClicker;
549
551
  };
550
552
 
551
553
  TrackyMouse.initDwellClicking = function (config) {
552
- return init_dwell_clicking(config);
554
+ return initDwellClicking(config);
553
555
  };
554
556
  TrackyMouse.cleanupDwellClicking = function () {
555
- for (const dwell_clicker of dwell_clickers) {
556
- dwell_clicker.dispose();
557
+ for (const dwellClicker of dwellClickers) {
558
+ dwellClicker.dispose();
557
559
  }
558
560
  };
559
561
 
@@ -972,6 +974,19 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
972
974
  // description: "Makes Tracky Mouse active as soon as it's launched.",
973
975
  // description: "Automatically starts Tracky Mouse as soon as it's run.",
974
976
  },
977
+ {
978
+ // For "experimental" label:
979
+ // - I'm preferring language that doesn't assume a new build is coming soon, fixing everything
980
+ // - I considered adding "⚠︎" but it feels a little too alarming
981
+ // 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
+ // 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>)",
984
+ className: "tracky-mouse-close-eyes-to-toggle",
985
+ key: "closeEyesToToggle",
986
+ type: "checkbox",
987
+ default: false,
988
+ description: "If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds.",
989
+ },
975
990
  {
976
991
  label: "Run at login",
977
992
  className: "tracky-mouse-run-at-login",
@@ -982,6 +997,17 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
982
997
  description: "If enabled, Tracky Mouse will automatically start when you log into your computer.",
983
998
  // description: "Makes Tracky Mouse start automatically when you log into your computer.",
984
999
  },
1000
+ {
1001
+ label: "Check for updates",
1002
+ className: "tracky-mouse-check-for-updates",
1003
+ key: "checkForUpdates",
1004
+ type: "checkbox",
1005
+ default: true,
1006
+ platform: "desktop",
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.",
1010
+ },
985
1011
  ],
986
1012
  },
987
1013
  ];
@@ -1079,8 +1105,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1079
1105
  <span class="tracky-mouse-label-text">${setting.label}</span>
1080
1106
  <span class="tracky-mouse-labeled-slider">
1081
1107
  <input type="range" min="${setting.min}" max="${setting.max}" class="${setting.className}">
1082
- <span class="tracky-mouse-min-label">${setting.labels.min}</span>
1083
- <span class="tracky-mouse-max-label">${setting.labels.max}</span>
1108
+ <span class="tracky-mouse-slider-labels">
1109
+ <span class="tracky-mouse-min-label">${setting.labels.min}</span>
1110
+ <span class="tracky-mouse-max-label">${setting.labels.max}</span>
1111
+ </span>
1084
1112
  </span>
1085
1113
  `;
1086
1114
  } else if (setting.type === "checkbox") {
@@ -1277,6 +1305,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1277
1305
  var mouthInfo;
1278
1306
  var headTilt = { pitch: 0, yaw: 0, roll: 0 };
1279
1307
  var headTiltFilters = { pitch: null, yaw: null, roll: null };
1308
+ var lastTimeWhenAnEyeWasOpen = Infinity; // far future rather than far past so that sleep gesture doesn't trigger initially, skipping the delay
1280
1309
  // ## State related to switching between head trackers
1281
1310
  var useClmTracking = true;
1282
1311
  var showClmTracking = useClmTracking;
@@ -1290,6 +1319,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1290
1319
  right: false,
1291
1320
  middle: false,
1292
1321
  };
1322
+ var mouseButtonUntilMouthCloses = -1;
1293
1323
  var lastMouseDownTime = -Infinity;
1294
1324
  var mouseNeedsInitPos = true;
1295
1325
 
@@ -1563,6 +1593,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1563
1593
  pointsBasedOnFaceScore = 0;
1564
1594
  faceScore = 0;
1565
1595
  faceConvergence = 0;
1596
+ lastTimeWhenAnEyeWasOpen = Infinity; // far future rather than far past so that sleep gesture doesn't trigger initially, skipping the delay
1566
1597
 
1567
1598
  startStopButton.textContent = "Start";
1568
1599
  startStopButton.setAttribute("aria-pressed", "false");
@@ -2257,16 +2288,26 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2257
2288
  const thresholdHigh = 0.25;
2258
2289
  const thresholdLow = 0.15;
2259
2290
  mouth.thresholdMet = mouth.heightRatio > (prevThresholdMet ? thresholdLow : thresholdHigh);
2260
- mouth.active = mouth.thresholdMet;
2261
- // Preserve mouse button state; could be simpler as a separate variable.
2262
- mouth.mouseButton = mouthInfo?.mouseButton ?? -1;
2291
+ mouth.active = mouth.thresholdMet; // TODO: maybe default to false, have this only set externally in gesture handling code
2263
2292
  return mouth;
2264
2293
  }
2265
2294
 
2266
- const prevMouthOpen = mouthInfo?.active;
2295
+ const prevMouthOpen = mouthInfo?.thresholdMet;
2267
2296
 
2268
2297
  blinkInfo = detectBlinks();
2269
2298
  mouthInfo = detectMouthOpen();
2299
+ if (blinkInfo.rightEye.open || blinkInfo.leftEye.open) {
2300
+ lastTimeWhenAnEyeWasOpen = performance.now();
2301
+ }
2302
+ if (performance.now() - lastTimeWhenAnEyeWasOpen > 2000) {
2303
+ if (s.closeEyesToToggle) {
2304
+ paused = !paused;
2305
+ updatePaused();
2306
+ // TODO: handle edge cases
2307
+ // TODO: try to keep variable names meaningful
2308
+ lastTimeWhenAnEyeWasOpen = Infinity;
2309
+ }
2310
+ }
2270
2311
 
2271
2312
  blinkInfo.used = false;
2272
2313
  mouthInfo.used = false;
@@ -2280,6 +2321,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2280
2321
  }
2281
2322
  }
2282
2323
  // TODO: maybe split into a "simple"/mouth-only mode vs "with eye modifiers" mode?
2324
+ // (or just hold out for a full I/O binding system)
2283
2325
  if (s.clickingMode === "open-mouth") {
2284
2326
  mouthInfo.used = true;
2285
2327
  blinkInfo.used = true;
@@ -2289,17 +2331,27 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2289
2331
  // Keep same button held if eye is opened,
2290
2332
  // so you can continue to scroll a webpage without trying to
2291
2333
  // read with one eye closed (for example).
2292
- if (mouthInfo.active && !prevMouthOpen) {
2334
+ if (mouthInfo.thresholdMet && !prevMouthOpen) {
2293
2335
  if (blinkInfo.rightEye.active) {
2294
- mouthInfo.mouseButton = 1;
2336
+ mouseButtonUntilMouthCloses = 1;
2295
2337
  } else if (blinkInfo.leftEye.active) {
2296
- mouthInfo.mouseButton = 2;
2338
+ mouseButtonUntilMouthCloses = 2;
2339
+ } else if (!blinkInfo.rightEye.open && !blinkInfo.leftEye.open) {
2340
+ mouseButtonUntilMouthCloses = -1;
2297
2341
  } else {
2298
- mouthInfo.mouseButton = 0;
2342
+ mouseButtonUntilMouthCloses = 0;
2299
2343
  }
2300
2344
  }
2301
- if (mouthInfo.active) {
2302
- clickButton = mouthInfo.mouseButton;
2345
+ if (mouthInfo.thresholdMet) {
2346
+ clickButton = mouseButtonUntilMouthCloses;
2347
+ if (clickButton === -1) {
2348
+ // Show as passive / not clicking in visuals
2349
+ mouthInfo.active = false;
2350
+ // TODO: show eyes as yellow too regardless of eye state?
2351
+ }
2352
+ // TODO: DRY mapping
2353
+ blinkInfo.rightEye.active = clickButton === 1;
2354
+ blinkInfo.leftEye.active = clickButton === 2;
2303
2355
  }
2304
2356
  }
2305
2357