jca*_*lly 12 javascript algorithm graphics html5-canvas
我已经构建了一个画布库,用于管理某些工作项目的形状场景.每个形状都是一个具有与之关联的绘图方法的对象.在刷新画布期间,绘制堆栈上的每个形状.形状可能具有典型的鼠标事件绑定,这些事件都围绕画布自己的DOM鼠标事件.
我发现了一些野外技术,用于检测各个形状上的鼠标悬停,每种技术都有效,但有一些相当严重的警告.
清除的鬼画布用于绘制单独的形状.然后我存储了鬼画布的副本getImageData()
.可以想象,当有许多点绑定鼠标事件时,这会占用大量内存(960x800画布上的100个可点击形状在内存中约为300MB).
为了回避内存问题,我开始遍历像素数据并仅将地址存储到具有非零alpha的像素.这对于减少内存效果很好,但却大大增加了CPU负载.我只迭代每个第四个索引(RGBA),并且任何具有非零alpha的像素地址都存储为哈希键,以便在鼠标移动期间进行快速查找.它仍然会使Linux上的移动浏览器和Firefox超载10秒以上.
我读到了一种技术,其中所有形状将被绘制到一个鬼画布上,使用颜色来区分每个像素拥有的形状.我对这个想法非常满意,因为它理论上应该可以区分数百万个形状.
不幸的是,这被抗锯齿打破了,在大多数画布实现上都无法禁用.每个模糊边缘都会创建数十种颜色,这些颜色可以被安全地忽略,除了它们/它们可以混合/重叠形状边缘.当有人在形状边界上穿过鼠标时,我想要发生的最后一件事就是为与AA混合中出现的颜色相关的不相关形状发射半随机鼠标悬停事件.
我知道这对于视频游戏开发者来说不是一个新问题,必须有快速算法来处理这类事情.如果有人知道一种算法可以解决(逼真地)数百种形状而不占用CPU超过几秒钟或大幅浪费RAM消耗,我将非常感激.
有关鼠标悬停检测的另外两个Stack Overflow主题,这两个主题都讨论了这个主题,但它们只是我描述的3种方法. 检测HTML画布中某些点的鼠标悬停?和 鼠标悬停圈HTML5画布.
编辑:2011/10/21
我测试了另一种更动态且不需要存储任何东西的方法,但它被Firefox中的性能问题所摧毁.该方法基本上是循环形状和:1)在鼠标下清除1x1像素,2)绘制形状,3)在鼠标下获得1x1像素.令人惊讶的是,这在Chrome和IE中非常有效,但在Firefox下却很糟糕.
显然,如果您只想要一个小的像素区域,Chrome和IE就能够进行优化,但Firefox似乎根本不会根据所需的像素区域进行优化.也许在内部它获得整个画布,然后返回你的像素区域.
代码和原始输出:http://pastebin.com/aW3xr2eB.
如果我正确理解了这个问题,你想要检测鼠标何时进入/离开画布上的形状,对吗?
如果是这样,那么您可以使用简单的几何计算,这比在像素数据上循环更简单,更快.您的渲染算法已经包含所有可见形状的列表,因此您可以知道每个形状的位置,尺寸和类型.
假设你有某种形状列表,类似于@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;
}
Run Code Online (Sandbox Code Playgroud)
因此,只需调用trackHoverShape
canvas内部的mousemove
事件,它将跟踪当前鼠标下的形状.
我希望这有帮助.
来自评论:
就我个人而言,我会转而使用 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 来实现,用于自动处理数据存储(例如绘图方法、尺寸……)。