【问题标题】:Three mouse detection techniques for HTML5 canvas, none adequateHTML5画布的三种鼠标检测技术,没有一个足够
【发布时间】:2011-10-17 20:55:19
【问题描述】:

我已经构建了一个画布库,用于管理一些工作项目的形状场景。每个形状都是一个对象,具有与之关联的绘图方法。在刷新画布期间,将绘制堆栈上的每个形状。一个形状可能绑定了典型的鼠标事件,这些事件都包裹在画布自己的 DOM 鼠标事件周围。

我在野外发现了一些用于检测单个形状上的鼠标悬停的技术,每种技术都有效,但有一些非常严重的警告。

  1. 清除的幽灵画布用于自行绘制单个形状。然后我用getImageData() 存储一个幽灵画布的副本。正如您可以想象的那样,当有很多点与鼠标事件绑定时,这会占用大量内存(960x800 画布上的 100 个可点击形状在内存中约为 300MB)。

  2. 为了回避内存问题,我开始循环遍历像素数据并仅将地址存储到具有非零 alpha 的像素。这对于减少内存非常有效,但会显着增加 CPU 负载。我只在每 4 个索引 (RGBA) 上进行迭代,并且任何具有非零 alpha 的像素地址都存储为哈希键,以便在鼠标移动期间快速查找。它仍然会使 Linux 上的移动浏览器和 Firefox 过载 10 多秒。

  3. 我读到了一种技术,可以使用颜色将所有形状绘制到一个幻影画布上,以区分哪个形状拥有每个像素。我对这个想法非常满意,因为理论上它应该能够区分数百万种形状。

    不幸的是,这被抗锯齿破坏了,在大多数画布实现中都无法禁用它。每个模糊边缘都会创建数十种颜色,这些颜色可以安全地忽略,除非/它们可以混合/与重叠的形状边缘。当有人将鼠标越过形状边界时,我最不想发生的事情是为与由于 AA 混合而出现的颜色相关的不相关形状触发半随机鼠标悬停事件。

我知道这对视频游戏开发者来说并不是一个新问题,而且必须有快速算法来解决这类问题。如果有人知道一种算法可以解决(实际上)数百个形状,而不会占用 CPU 超过几秒钟或显着增加 RAM 消耗,我将非常感激。

还有两个关于鼠标悬停检测的 Stack Overflow 主题,它们都讨论了这个主题,但它们仅比我描述的 3 种方法更进一步。 Detect mouseover of certain points within an HTML canvas?mouseover circle HTML5 canvas.

编辑:2011/10/21

我测试了另一种更动态且不需要存储任何内容的方法,但它因 Firefox 中的性能问题而瘫痪。该方法基本上是循环形状和:1)在鼠标下清除1x1像素,2)绘制形状,3)在鼠标下获得1x1像素。令人惊讶的是,这在 Chrome 和 IE 中运行良好,但在 Firefox 下却很糟糕。

如果您只想要一个小像素区域,显然 Chrome 和 IE 能够进行优化,但 Firefox 似乎根本没有根据所需的像素区域进行优化。也许在内部它会获取整个画布,然后返回您的像素区域。

此处的代码和原始输出:http://pastebin.com/aW3xr2eB

【问题讨论】:

  • 就我个人而言,我只想改用 SVG。它更多的是它的目的。但是,可能值得查看 EaselJS (easeljs.com) 源代码。有一个方法Stage.getObjectUnderPoint(),他们的演示似乎工作得很好。

标签: javascript algorithm graphics html5-canvas


【解决方案1】:

如果我正确理解了这个问题,您想检测鼠标何时进入/离开画布上的形状,对吗?

如果是这样,那么您可以使用简单的几何计算,这比循环遍历像素数据更简单、更快。您的渲染算法已经拥有所有可见形状的列表,因此您知道每个形状的位置、尺寸和类型。

假设您有某种形状列表,类似于 @Benjammmin' 所描述的,您可以遍历可见形状并进行点内多边形检查:

// Track which shape is currently under the mouse cursor, and raise
// mouse enter/leave events
function trackHoverShape(mousePos) {
    var shape;
    for (var i = 0, len = visibleShapes.length; i < len; i++) {
        shape = visibleShapes[i];
        switch (shape.type ) {
            case 'arc':
                if (pointInCircle(mousePos, shape) &&
                    _currentHoverShape !== shape) {
                        raiseEvent(_currentHoverShape, 'mouseleave');
                        _currentHoverShape = shape;
                        raiseEvent(_currentHoverShape, 'mouseenter');
                    return;
                }
                break;
            case 'rect':
                if (pointInRect(mousePos, shape) &&
                    _currentHoverShape !== shape) {
                       raiseEvent(_currentHoverShape, 'mouseleave');
                       _currentHoverShape = shape;
                       raiseEvent(_currentHoverShape, 'mouseenter');
                }
                break;
        }
    }
}

function raiseEvent(shape, eventName) {
    var handler = shape.events[eventName];
    if (handler)
        handler();
}

// Check if the distance between the point and the shape's
// center is greater than the circle's radius. (Pythagorean theroem)
function pointInCircle(point, shape) {
    var distX = Math.abs(point.x - shape.center.x),
        distY = Math.abs(point.y - shape.center.y),
        dist = Math.sqrt(distX * distX + distY * distY);
    return dist < shape.radius;
}

所以,只需在画布内调用trackHoverShape 事件mousemove,它就会跟踪当前鼠标下的形状。

我希望这会有所帮助。

【讨论】:

  • 我总体上支持这一点。但是,如果shape 具有pointInside(x,y)pointInside(point) 方法,则可以替换switch 语句。然后将更简单地添加形状类型、更改它们等。
  • @sqykly:好电话。使用“虚拟”isPointInside() 方法会是更好的解决方案。
【解决方案2】:

来自评论:

我个人会改用 SVG。它更像是它本来的样子 为。不过可能值得一看EaselJS 资源。有一个方法 Stage.getObjectUnderPoint(),以及他们的演示 这似乎工作得很好。

我最终查看了源代码,该库使用了您的第一种方法 - 为每个对象单独隐藏画布。

想到的一个想法是尝试创建某种内容感知算法来检测抗锯齿像素及其所属的形状。我很快打消了这个想法。

不过,我确实还有另一种理论。似乎没有办法使用幽灵画布,但也许有一种方法可以仅在需要时生成它们。

请注意,以下想法都是理论性的,未经测试。我可能忽略了一些东西,这意味着这种方法不起作用。

在绘制对象的同时,存储绘制该对象的方法。然后,使用绘制对象的方法,您可以计算出该对象的粗略边界框。单击画布时,循环遍历画布上的所有对象,并提取边界框与点截取的对象。对于每个提取的对象,使用该对象的方法引用将它们分别绘制到幻影画布上。确定鼠标是否位于非白色像素上,清除画布,然后重复。

例如,假设我已经绘制了两个对象。我将以可读的方式存储绘制矩形和圆形的方法。

  • circ = ['beginPath', ['arc', 75, 75, 10], 'closePath', 'fill']
  • rect = ['beginPath', ['rect', 150, 5, 30, 40], 'closePath', 'fill']

(您可能希望缩小保存的数据,或使用其他语法,例如 SVG 语法)

由于我是第一次绘制这些圆圈,我还会记下尺寸值并使用它们来确定边界框(注意:您需要补偿笔画宽度) .

  • circ = {left: 65, top: 65, right: 85, bottom: 85}
  • rect = {left: 150, top: 5, right: 180, bottom: 45}

画布上发生了点击事件。鼠标指向{x: 70, y: 80}

循环遍历这两个对象,我们发现鼠标坐标落在圆圈范围内。因此,我们将圆形对象标记为可能的碰撞候选对象。

分析圆的绘制方法,我们可以在幽灵画布上重新创建它,然后测试鼠标坐标是否落在非白色像素上。

确定有没有后,我们可以清除幽灵画布,准备在上面绘制更多对象。

如您所见,这消除了存储 960 x 800 x 100 像素的需要,并且最多只存储 960 x 800 x2

这个想法最好实现为某种API,用于自动处理数据存储(例如绘图方法、尺寸...)。

【讨论】:

  • 谢谢。幽灵画布很好,我没有问题。我喜欢采用 (x,y) 并为每种形状返回 true 或 false 的快速边界函数的想法。我正在寻找类似算法的资源,或者我可能在高级图形编程课程中学到的其他新颖解决方案。描边和实心圆、矩形和直线看起来很简单,但复杂的形状,如区域、切片圆(饼图段)或 alpha 透明 PNG 会更复杂。必须有比循环遍历每个像素更好的方法。
  • 不客气。在计算边界框时,这个想法并不完全精确 - 刚好 能够包含形状而周围没有太多空白。切片圆的边界框可以像普通圆一样计算,透明的 PNG 可以只依赖图像的原始尺寸。由于边界框的想法是简单地过滤在幻影画布上绘制不必要的形状,因此如果边界框包含额外的空白,它仍然可以工作。 过滤级别只受影响,而不是碰撞的可靠性。
  • 当然,边界框的想法甚至不是必需的。这是一种优化技术 - 不必每次单击都将场景中当前的每个对象重新绘制到幻影画布中,它只会重新绘制潜在的碰撞候选对象。
  • 将其构建为某种 API 的想法是,它可以为您处理形状的复杂性。当然,形状越复杂,重绘的速度就越慢。对于更复杂的形状,您也许可以让它们存储一个专用的幽灵画布,但这是您必须决定和优化的东西。
猜你喜欢
  • 2023-04-04
  • 2013-01-02
  • 2014-08-27
  • 1970-01-01
  • 1970-01-01
  • 2012-09-06
  • 2011-09-24
  • 2021-04-12
相关资源
最近更新 更多