← 返回信息流
AI 资讯Hacker News·3 天前

Crashing cars and improving hover detection

AI 深度解读

快速移动鼠标导致悬停失效?游戏引擎的碰撞检测给出了答案

背景

在 Web 开发中,处理元素悬停(Hover)状态是一项常见但容易被忽视的任务。无论是菜单项、颜色选择器的色块,还是网格布局中的方块,用户通常期望鼠标经过时能立即触发视觉反馈。

然而,一个普遍存在的现象是:当用户快速滑动鼠标时,光标可能会“跳过”某些元素,导致这些元素的悬停状态未被触发。这种现象在现实中并不存在——我们的手或手指在桌面上移动是连续且不间断的,但在数字界面中,由于指针位置是离散采样的,快速移动会导致事件丢失。

这篇文章深入探讨了这一问题的根源,并指出其本质与游戏开发中著名的“隧道效应”(Tunnelling)问题相同。通过引入游戏引擎中用于检测物体碰撞的连续运动检测算法,我们可以构建出即使在极速滑动下也能完美响应的悬停检测机制。

核心内容

离散采样与悬停失效

CSS 选择器(如 :hover)、Motion 事件(如 onHoverStart)以及 JavaScript 事件(如 pointerenter)都受到“跳过元素”问题的困扰。

根本原因在于指针位置的采样是离散的,而非连续的。浏览器通过事件流将指针位置以一系列点的形式传递给代码。

  • 慢速移动:指针事件之间的间隔较小,事件点更密集,因此每个元素至少被一个事件点覆盖,悬停状态正常触发。
  • 快速移动:指针事件之间的间隔变大。如果两个事件点之间的空隙足够大,位于该空隙中的元素就会被完全跳过,导致悬停状态丢失。

游戏开发中的“隧道效应”

这个问题在物理引擎和游戏开发中被称为隧道效应(Tunnelling)

想象一辆车高速驶向一堵薄墙:

  1. 帧 A:车在墙前,未发生重叠。
  2. 帧 B:车已经穿过了墙,出现在墙的另一侧。

如果引擎只在每帧检查一次物体的位置(离散检测),它会发现帧 A 没有碰撞,帧 B 也没有碰撞(因为车已经过去了)。结果就是车“穿墙而过”,引擎无法检测到碰撞。

在指针悬停的场景中,快速移动的指针就是那辆高速行驶的车,而悬停目标元素就是那堵薄墙。传统的 pointerenter:hover 只检查当前帧指针所在的点是否在元素内,这正是离散检测的缺陷。

解决方案:从点到线的检测

游戏引擎解决隧道效应的方法是:不再问“当前帧物体在哪里?”,而是问“自上一帧以来,物体走过的路径是什么?这条路径是否与任何物体相交?”

简而言之,从测试一个点转变为测试一条线

对于指针事件,这条线很容易计算:

  1. 获取当前帧的指针位置。
  2. 获取上一帧的指针位置。
  3. 连接这两点形成一条线段。
  4. 检查这条线段是否与目标元素相交,而不是仅检查当前点是否在元素内。

技术实现细节

文章以 Motion 库为例,展示了如何实现这一机制:

  1. 读取指针位置: 使用 usePointerPosition Hook 获取指针位置。Motion 的 motion values 具有记忆功能,可以通过 pointer.x.get() 获取当前位置,通过 pointer.x.getPrevious() 获取上一帧位置。

  2. 测量元素边界: 使用浏览器原生 API element.getBoundingClientRect() 获取目标元素的边界框(Bounding Box),该坐标也是相对于视口的。为了性能,建议在 Motion 的 frame.read 阶段进行测量,以避免布局抖动。

  3. 几何测试:Slab 方法: 为了高效判断线段是否与矩形相交,文章介绍了 Slab 方法( slab method)

    • 原理:将矩形视为两个无限长条带(Slab)的重叠区域。
      • X 轴条带:由矩形的左边缘和右边缘定义。
      • Y 轴条带:由矩形的上边缘和下边缘定义。
    • 计算
      • 计算线段进入和离开 X 轴条带的比例(0 到 1 之间)。
      • 计算线段进入和离开 Y 轴条带的比例。
      • 如果线段完全在条带外(进入比例 > 1 或 离开比例 < 0),则不相交。
    • 判定相交
      • 线段进入矩形框的时刻 = max(enterX, enterY)
      • 线段离开矩形框的时刻 = min(exitX, exitY)
      • 如果 enter < exit,则线段与矩形相交。

这意味着,即使鼠标快速划过,只要线段穿过了元素的边界框,悬停状态就会被正确触发。

关键要点

  • 问题本质:快速移动鼠标导致悬停失效,是因为指针位置是离散采样的,高速移动会在事件点之间产生“空隙”,导致元素被跳过。
  • 类比游戏开发:这与游戏物理引擎中的“隧道效应”(Tunnelling)完全相同,即高速物体在帧间检测中穿墙而过。
  • 核心思路:将检测对象从“当前点”扩展为“上一帧到当前帧的线段”。
  • 算法选择:使用 Slab 方法 进行高效的几何相交测试。该方法将矩形分解为 X 和 Y 方向的无限条带,通过计算线段进入和离开条带的比例来判断相交。
  • 实现优势:相比复杂的几何库,Slab 方法计算成本极低,适合在每一帧中执行。
  • 代码实践:利用 Motion 库的 usePointerPosition 获取连续的位置数据,结合 getBoundingClientRect 和自定义的几何计算逻辑,可以替代默认的 pointerenter 事件,实现无缝的悬停体验。

意义与影响

这一技术解读不仅解决了一个具体的 UI 交互痛点,更展示了跨领域知识迁移的价值。Web 前端开发往往局限于 DOM 事件模型,而游戏引擎经过数十年优化,已经解决了大量高性能实时交互问题。

通过引入连续碰撞检测(Continuous Collision Detection, CCD)的思想,开发者可以创造出更加流畅、响应灵敏的用户界面。这对于构建复杂的交互式数据可视化、游戏化 Web 应用或高精度工具类应用尤为重要。它提醒我们,在处理高频、高速的用户输入时,传统的离散事件处理可能不足以满足需求,需要引入更底层的数学和物理模拟思维来优化体验。

查看原文 →motion.dev