jspaint 无障碍化实战:深入解析 Tracky Mouse 头部追踪与驻留点击 API
发布时间:2026/9/27 8:48:17来源:尧图网络
前端桌面应用图像处理【免费下载链接】jspaint Classic MS Paint, ✨Extras项目地址https://gitcode.com/gh_mirrors/js/jspaint点击查看免费下载本篇技术指南以 lib/tracky-mouse/core/README.md 为主干结合仓库内 tracky-mouse.js 源码与其在 eye-gaze-mode.js 中的真实集成实践系统讲解 Tracky Mouse 这一开源头部追踪head tracking与驻留点击dwell clickingAPI 的安装、初始化、配置参数、事件模拟与生命周期管理。读完本文你将掌握如何在自己的 Web 应用中接入面部鼠标能力并理解 jspaint 是如何用一套配置同时支撑绘画、拖拽窗口、表单控件与画布操作的无障碍输入。jspaint 驻留点击eye gaze模式测试快照展示经典画图界面与底部睡眠/眼动模式按钮一、Tracky Mouse 是什么Tracky Mouse 是一个简单、开源、可添加到任意 Web 应用的头部追踪与驻留点击 API仓库内以tracky-mouse包的形式内嵌于 lib/tracky-mouse/core版本为 1.2.0见 package.json。它提供两套能力头部追踪Head Tracking通过普通摄像头识别人脸把头部移动换算成指针坐标驻留点击Dwell Clicking让指针停留在某个元素上一段时间即自动触发点击可独立于头部追踪工作适用于外部指点设备、眼动仪以及可以控制电脑鼠标的 Tracky Mouse 桌面应用。API 允许你精确指定页面上哪些元素可被点击、哪些元素应被拖拽、哪些元素应被视作等价控件例如表单标签与其对应的控件、哪些元素应被忽略。同时内置了完整的用户界面摄像头画面、灵敏度与加速度调节、镜像、按键交换等设置项。在 jspaint 中这套 API 被用于Eye Gaze Mode / Dwell Clicking与Head Tracker两项无障碍功能集成代码集中在 eye-gaze-mode.js相关视觉测试快照保存在cypress/snapshots/visual-tests.spec.js/。二、安装与引入Tracky Mouse 当前是script 标签式的库源码以全局TrackyMouse对象形式暴露见 tracky-mouse.js通过 npm 安装后手动引入npm install tracky-mousescript srcpath/to/tracky-mouse/tracky-mouse.js/script引入后必须告诉库去哪里加载相关的依赖文件注意不要带尾部斜杠TrackyMouse.dependenciesRoot path/to/tracky-mouse;还需要引入与脚本同目录的样式表link relstylesheet hrefpath/to/tracky-mouse/tracky-mouse.cssTrackyMouse.dependenciesRoot设置为安装 tracky-mouse 的目录路径不带尾部斜杠。源码中loadDependencies()会首先对其做replace(/\/$/, )去尾斜杠处理tracky-mouse.js所有依赖脚本、worker、样式资源都基于该路径拼接。在 jspaint 中它被设置为仓库内的实际位置eye-gaze-mode.jsTrackyMouse.dependenciesRoot lib/tracky-mouse/core;三、头部追踪Head TrackingTracky Mouse 让头部追踪的采集变得简单但拿位移数据做什么取决于你的应用。通常你会用这些数据在页面上模拟鼠标/指针事件——目前库并不替你生成事件你需要自行定义回调TrackyMouse.onPointerMove(x, y)完成这一层。完整的起步代码TrackyMouse.loadDependencies().then(function() { TrackyMouse.init(); // Pointer event simulation logic should be built into tracky-mouse in the future. // These simulated events connect the Tracky Mouse head tracker to the Tracky Mouse dwell clicker, // as well as any other pointermove/pointerenter/pointerleave handlers on the page. const getEventOptions ({ x, y }) { return { view: window, // needed so the browser can calculate offsetX/Y from the clientX/Y clientX: x, clientY: y, pointerId: 1234567890, // a special value so other code can detect these simulated events pointerType: mouse, isPrimary: true, }; }; let last_el_over null; TrackyMouse.onPointerMove (x, y) { const target document.elementFromPoint(x, y) || document.body; if (target ! last_el_over) { if (last_el_over) { const event new PointerEvent(pointerleave, Object.assign(getEventOptions({ x, y }), { button: 0, buttons: 1, bubbles: false, cancelable: false, })); last_el_over.dispatchEvent(event); } const event new PointerEvent(pointerenter, Object.assign(getEventOptions({ x, y }), { button: 0, buttons: 1, bubbles: false, cancelable: false, })); target.dispatchEvent(event); last_el_over target; } const event new PointerEvent(pointermove, Object.assign(getEventOptions({ x, y }), { button: 0, buttons: 1, bubbles: true, cancelable: true, })); target.dispatchEvent(event); }; });这段代码做了三件事用document.elementFromPoint找到指针下的元素在元素切换时派发pointerenter/pointerleave持续派发pointermove。pointerId: 1234567890是约定的特殊值让页面上其他代码能识别出这些是模拟事件。view: window是必需的否则浏览器无法根据clientX/Y计算offsetX/Y。TrackyMouse.loadDependencies([options])为头部追踪加载依赖驻留点击不需要调用它。可选参数statsJs可选布尔是否加载 stats.js 做性能监控默认false。返回一个在依赖加载完成后 resolve 的 Promise。从源码看该方法默认注入两个脚本no-eval.js 与 clmtrackr.js当statsJs为真时追加 stats.jstracky-mouse.js。no-eval.js 的作用是给 clmtrackr.js 打上禁用 eval的补丁使库能在禁止unsafe-eval的内容安全策略CSP下运行。注释中还提到可选的 tf.js 与 facemesh 依赖目前在 worker 中按需加载。TrackyMouse.init([element, options])为头部追踪初始化库驻留点击同样不需要。它会创建 UI要么新建一个div classtracky-mouse-ui追加到body要么使用并改造你传入的现有元素。传入的元素应为空的div若它没有tracky-mouse-ui类会被直接加上。可选参数statsJs可选布尔是否包含 stats.js 性能监控器默认false。返回一个带dispose方法的对象调用dispose()可移除 UI、终止 web worker 并释放摄像头流。内置 UI 由源码中的模板生成tracky-mouse.js包含Start/Stop 按钮aria-pressed状态、F9快捷键提示、水平灵敏度滑块0–100默认 25、垂直灵敏度滑块0–100默认 50、加速度滑块0–100默认 50两端标注 Linear/Smooth、Swap mouse buttons、 Start enabled、 Run at login、 Mirror 复选框以及摄像头画布与 Allow Camera Access 按钮。其中 Swap mouse buttons 与 Run at login 仅在桌面应用环境window.electronAPI存在下显示tracky-mouse.js。TrackyMouse.useCamera()请求摄像头权限并启动视频流。这是可选的——也可以让用户自己点击界面上的 Allow Camera Access 大按钮。源码中该函数同时被绑定为按钮的onclicktracky-mouse.js使用navigator.mediaDevices.getUserMedia请求 640×480、facingMode: user的前置摄像头并对NotFoundError、NotReadableError、OverconstrainedError、NotAllowedError、TypeError等失败场景给出用户可读的错误提示。TrackyMouse.onPointerMove(x, y)你需要定义的回调用来模拟指针移动。x、y是当前指针位置像素单位。底层原理双引擎追踪与指针输出从源码结构看头部追踪采用了facemesh 优先、clmtrackr 兜底的双引擎策略facemesh 引擎运行在 facemesh.worker.js 中模型与权重存放在 lib/facemesh含model.json与group1-shard1of1.bin。worker 通过postMessage接收相机帧返回ESTIMATED_FACES预测结果主线程利用鼻翼、双眼间中点等面部关键点计算位移并对结果做头部区域裁剪、剔除眨眼等干扰点tracky-mouse.js。clmtrackr 引擎lib/clmtrackr.js配合 jsfeat-min.js作为 fallback默认关闭 WebGLuseWebGL: false在 facemesh 首次预测超时首帧 20s、之后 2s或 worker 初始化失败时自动接管tracky-mouse.js。位移经过灵敏度与加速度曲线变换后累加到指针坐标并限制在屏幕范围内桌面环境下通过electronAPI.moveMouse()移动系统鼠标Web 环境下则显示一个tracky-mouse-pointer覆盖层元素并在回调存在时调用TrackyMouse.onPointerMove(mouseX, mouseY)tracky-mouse.js。加速度曲线形如(delta / 1) * (Math.abs(delta * 5) ** acceleration)代码注释说明它在稳定与快速移动之间取得平衡让你能聚焦到具体点而不抖动同时仍能快速移动tracky-mouse.js。四、驻留点击Dwell Clicking驻留点击的思路是指针在目标元素上停留一段短时间即自动执行点击用户无需移动手指或按键。它独立于头部追踪可配合眼动仪、外部指点设备或桌面应用使用。TrackyMouse.initDwellClicking(config)启动驻留点击器返回一个对象paused驻留点击是否暂停的 getter/setter用于实现暂停/继续按钮常与dwellClickEvenIfPaused配合dispose清理驻留点击器的方法。搜索关键词disposal, destroy, teardown, cleanup, clean-up, clean up, deinitialize, de-initialize, remove, stop, end。配置项详解配置项必填说明config.targets必填CSS 选择器匹配的元素才会被点击其余一律忽略config.shouldDrag(el)可选返回 true 表示该元素应被拖拽而非简单点击config.noCenter(el)可选返回 true 表示该元素应在任意位置点击而非总是点击中心config.retarget可选{ from, to, withinMargin }对象数组定义指针悬停在其他元素上时动态改悬停/点击目标的规则config.isEquivalentTarget(el1, el2)可选返回 true 表示两个元素应视为同一控件点击任一个效果相同相等的元素永远视为等价即使你返回 falseconfig.dwellClickEvenIfPaused(el)可选返回 true 表示即使驻留点击处于暂停状态也应点击该元素通常用于恢复驻留点击的切换按钮本身config.click({x, y, target})必填对给定目标元素触发一次点击的函数config.beforeDispatch()可选派发 pointer 事件前调用用于在事件处理器之外检测不可信用户手势config.afterDispatch()可选派发 pointer 事件后调用同上config.beforePointerDownDispatch()可选派发pointerdown前调用未来可能与beforeDispatch合并config.afterReleaseDrag()可选拖拽释放后调用未来可能与afterDispatch合并retarget 子项说明from必填被重定向的源元素。可以是 CSS 选择器、一个元素或一个接收当前指针下元素并返回是否应重定向的函数。to必填重定向的目标元素。可以是from元素的祖先或后代的 CSS 选择器、一个元素或一个接收指针下元素并返回目标元素返回null表示忽略该元素的函数。withinMargin可选像素数指针在该范围内即视为悬停在to元素上默认Infinity。isEquivalentTarget用于阻止系统把遮挡元素误判为独立控件而拒绝点击——检测到遮挡时界面会闪烁一个红色方框提示。完整示例以 jspaint 的用法为蓝本// This example is based off of how JS Paint uses the Tracky Mouse API. // Its simplified a bit, but includes various settings. const config { // The elements to click. Anything else is ignored. targets: button:not([disabled]), input, textarea, label, a, details summary, .radio-or-checkbox-wrapper, .drawing-canvas, .window:not(.maximized) .window-titlebar , // Filter for elements to drag. They must be included in the targets first. shouldDrag: (target) ( target.matches(.window-titlebar) || (target.matches(.drawing-canvas) current_tool.supports_drag) ), // Instead of clicking in the center of these elements, click at any point within the element. // This is useful for drag offsets, like for a window titlebar, // and position-based inputs like sliders or color pickers, or a drawing canvas. noCenter: (target) ( target.matches( input[typerange], .drawing-canvas, .window-titlebar ) ), // Nudge hovers near the edges of an element onto the element itself, // to make it easier to click on the element. // More specifically it makes it easier to click on the edge of an element, // useful for a drawing canvas. retarget: [ { from: .canvas-container, to: .drawing-canvas, withinMargin: 50 }, ], // Elements that are equivalent are considered the same control. // This is useful for forms if you want the label of a radio button or checkbox // to be highlighted together with the radio button or checkbox. isEquivalentTarget: (apparent_hover_target, hover_target) ( apparent_hover_target.closest(label) hover_target || apparent_hover_target.closest(.radio-or-checkbox-wrapper) hover_target ), // Allow dwell clicking on a Resume Dwell Clicking button, while paused. dwellClickEvenIfPaused: (target) ( target.matches(.toggle-dwell-clicking-button) ), // Define how to click on an element. click: ({ target, x, y }) { if (target.matches(input[typerange])) { // Special handling for sliders const rect target.getBoundingClientRect(); const vertical target.getAttribute(orient) vertical || (getCurrentRotation(target) ! 0) || rect.height rect.width; const min Number(target.min); const max Number(target.max); target.value ( vertical ? (y - rect.top) / rect.height : (x - rect.left) / rect.width ) * (max - min) min; target.dispatchEvent(new Event(input, { bubbles: true })); target.dispatchEvent(new Event(change, { bubbles: true })); } else { // Normal click target.click(); if (target.matches(input, textarea)) { target.focus(); } } }, // Handle untrusted gestures specially in external code. // Somewhere else, for example, you might do something like: // if (window.untrusted_gesture) { // // show download window // } else { // // show save file dialog with FS Access API // } // Recommended: use event.isTrusted instead, where possible. beforeDispatch: () { window.untrusted_gesture true; }, afterDispatch: () { window.untrusted_gesture false; }, // Some extra hooks for JS Paint, likely to be generalized in the future, // especially beforePointerDownDispatch which could be supplanted by passing an Event to beforeDispatch. beforePointerDownDispatch: () { window.pointers []; }, afterReleaseDrag: () { window.pointers []; }, }; const dwellClicker TrackyMouse.initDwellClicking(config); // dwellClicker.paused !dwellClicker.paused; // toggle // dwellClicker.dispose(); // clean up // Source: https://stackoverflow.com/a/54492696/2624876 function getCurrentRotation(el) { const st window.getComputedStyle(el, null); const tm st.getPropertyValue(-webkit-transform) || st.getPropertyValue(-moz-transform) || st.getPropertyValue(-ms-transform) || st.getPropertyValue(-o-transform) || st.getPropertyValue(transform) || none; if (tm ! none) { const [a, b] tm.split(()[1].split())[0].split(,); return Math.round(Math.atan2(a, b) * (180 / Math.PI)); } return 0; }TrackyMouse.cleanupDwellClicking()已废弃Deprecated应改为调用initDwellClicking()返回对象上的dispose()。该函数会停止所有驻留点击器——源码中它遍历内部维护的dwell_clickers数组并逐个调用dispose()tracky-mouse.js。底层行为从源码看驻留点击如何工作输入与防抖initDwellClicking监听全局pointermove/pointerup/pointercancel/focus/blur/mouseleave/mouseenter事件tracky-mouse.js用 500ms 时间窗内的指针点做滑动平均以此抑制微小抖动。触发时序当最近移动量小于 5px 时开始累积悬停候选指针在候选上停留超过hover_timespan500ms即触发点击——先派发pointerdown若shouldDrag返回 true 则进入拖拽状态dwell_dragging否则派发pointerup并调用config.click移动量超过 60px 会取消候选超过 100px 会释放正在进行的拖拽tracky-mouse.js。安全时间窗源码定义了一组非活跃期常量启动后 1500ms、点击/拖拽释放后 1000ms、驻留指示器出现后 1000ms、检测到遮挡取消点击后 1000ms、页面重新获得焦点后 1000mstracky-mouse.js。这些窗口防止误触——例如启动驻留点击时不会立刻点中指针下的东西。视觉反馈库会向页面注入两类元素tracky-mouse-hover-halo悬停高亮框会仿照目标元素绘制圆角、并在滚动容器内裁剪到可见区域和tracky-mouse-dwell-indicator直径最大 50px 的驻留指示圆环随驻留进度从中心缩放放大[tracky-mouse.js](https://link.gitcode.com/i/f784ad4cac62b84f088574b373d4eb93#L166-L175, L427-L486)。拖拽进行中指示器会切换tracky-mouse-for-release样式提示停住即可释放。遮挡检测每次更新都会用elementFromPoint复查当前候选位置是否被其他元素遮挡若新目标与旧目标不等价isEquivalentTarget返回 false则取消本次驻留点击、进入非活跃期并用 4px 红色虚线框闪烁提示遮挡元素tracky-mouse.js。参数校验initDwellClicking对每个配置项做严格类型校验——targets必须是合法 CSS 选择器、click必须是函数、retarget必须是对象数组且每项的from/to合法、withinMargin必须是数字等非法配置会抛出带具体字段的错误信息tracky-mouse.js。五、在 jspaint 中的集成实践jspaint 把 Tracky Mouse 的两项能力分别绑定到两个 body 类dwell-clicker-mode驻留点击 / Eye Gaze Mode与head-tracker-mode头部追踪通过$G.on(dwell-clicker-toggled)、$G.on(head-tracker-toggled)事件动态启停eye-gaze-mode.js。启用头部追踪时它在一个 OS-GUI 窗口中创建 Tracky Mouse UI设置dependenciesRoot lib/tracky-mouse/core后调用loadDependencies()、init()与useCamera()并在窗口关闭时通过change_url_param(head-tracker, false)同步 URL 状态eye-gaze-mode.js。jspaint 的驻留点击配置要点jspaint 的dwell_clicker_configeye-gaze-mode.js是对 README 示例的完整生产级落地值得对照学习targets 覆盖了整个应用的交互面按钮、输入框、文本框、标签、链接、details summary、翻转旋转窗口的单选、当前颜色、色块按钮、编辑颜色窗口的色板与彩虹/亮度画布、未选中的工具、选择器选项、菜单按钮与菜单项、主画布、选区 canvas、调整手柄、抓取区域、非最大化窗口标题栏、历史记录条目。noCenter 用于位置敏感控件主画布、选区、标题栏、彩虹/亮度画布与input[typerange]都按指针实际位置点击因为画布和滑块的点击位置是有语义的。retarget 解决两个具体痛点把画布容器边缘 50px 内的悬停吸附到主画布上方便点中画布边缘当菜单打开时把悬停在顶层菜单按钮上的目标重定向为null避免误点已激活的菜单eye-gaze-mode.js。shouldDrag 支持窗口拖拽与绘画拖拽窗口标题栏、选区/手柄/抓取区域可拖拽主画布在选中非取色、非填充、非放大镜、非多边形、非曲线的工具时也可拖拽因此用户能靠驻留点击停住直接完成绘画。untrusted_gesture 标记beforeDispatch/afterDispatch把window.untrusted_gesture置 true/false。由于驻留点击派发的是不可信事件浏览器会拒绝某些 API如 FS Access 的文件保存对话框jspaint 借此在别处决定走下载窗口还是保存文件对话框分支且保留event.isTrusted作为更优先的方案。暂停联动头部追踪暂停时会把dwell_clicker.paused同步为 true并禁用页面上的toggle-dwell-clicking暂停按钮而恢复驻留点击按钮本身通过dwellClickEvenIfPaused豁免暂停保证用户永远有一条退路eye-gaze-mode.js。测试佐证仓库的 Cypress 视觉测试为这两个模式保留了真实快照例如 cypress/snapshots/visual-tests.spec.js 下的visual tests -- eye gaze mode.snap.png1000×660从快照可见驻留点击模式下的整体界面菜单栏、工具箱、白色画布、调色盘与状态栏以及左下角的睡眠/眼动模式相关浮动按钮。测试用例位于 cypress/integration/visual-tests.spec.js。六、小结Tracky Mouse 是一个把摄像头 面部识别 指针模拟 驻留判定封装成极简全局 API 的库头部追踪侧只需loadDependencies()、init()、useCamera()三个入口和onPointerMove一个回调驻留点击侧只需initDwellClicking(config)一个入口配合targets、shouldDrag、noCenter、retarget、isEquivalentTarget、dwellClickEvenIfPaused、click等配置即可精确控制页面上的全部交互。jspaint 的 eye-gaze-mode.js 展示了如何把这套 API 落进一个复杂桌面式 Web 应用既要区分点一下与拖一下又要处理滑块、画布、菜单弹出遮挡、暂停恢复与不可信手势等问题。对于任何想为头部追踪、眼动输入或驻留点击用户提供无障碍体验的 Web 应用这套 API 与它的集成模式都值得直接参考。赞分享前端桌面应用图像处理【免费下载链接】jspaint Classic MS Paint, ✨Extras项目地址https://gitcode.com/gh_mirrors/js/jspaint点击查看免费下载相关推荐Gatsby 图片预优化实战用 sharp 批量压缩超大图片缩短构建时间Gatsby 图片预优化实战用 sharp 批量压缩超大图片缩短构建时间 本文是一份针对 Gatsby 构建性能的实用指南当你的项目中存在大量超大尺寸图片前端静态站点Web框架React Native 方向锁定终极指南react-native-orientation-locker 完全解析React Native 方向锁定终极指南react native orientation locker 完全解析 react native orientatMac Mouse Fix无障碍焦点设置优化界面导航的选项Mac Mouse Fix无障碍焦点设置优化界面导航的选项 无障碍功能痛点与解决方案 你是否曾在使用Mac时遇到鼠标操作不便的问题尤其是当系统焦点无法正确响桌面应用系统编程上一篇BetterDiscord自定义CSS终极指南掌握SimpleMarkdownExt实现富文本样式下一篇SwanLab与其他工具对比分析TensorBoard、Weights Biases的差异化优势创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网