检测鼠标悬停在重叠+透明图像上
detect mouse hover on overlapping + transparent images
我正在制作一个小游戏,用户可以在其中购买物品来装饰他的房子。
我有很多items/images;所以我决定为每个图像使用一个 "matte"(图像)来定义可悬停区域,而不是为每个图像绘制地图区域。
示例:这里是the displayed couch, and its matte。
我 "convert" 遮罩到 canvas 元素中,稍后将检查悬停像素是否透明以检测项目是否悬停。
其次是很多项目重叠,所以我还需要检查哪一层在上面。
我在房子元素上有 mousemove 事件 (jQuery);绑定函数 getObjectsUnderMouse().
基本上,这就是 getObjectsUnderMouse() 的工作原理:
- 获取鼠标坐标
- 获取房屋中的活动(显示)物品
- 过滤那些项目以仅保留鼠标点击canvas边界的项目,知道项目位置和width/height)
- 过滤那些项目,只保留那些鼠标不在透明像素上的项目 (canvas)
- 过滤那些项目以仅保留顶部的项目(z-index)
- 给那个项目一个 mouseon class
我对我的代码非常满意,这是一个相当大的挑战,但在 Chrome.
上运行完美
我遇到的问题是其他地方速度较慢(不是什么大问题),但是;最重要的是,似乎在 ipad 上崩溃了;我需要我的游戏在 ipad 上达到 运行... :/
有谁知道这是为什么或对此有更好的解决方案吗?
a demo of the game, and here's the javascript file 您可以在此处查看 getObjectsUnderMouse()。
欢迎任何建议!
尽管遮罩 canvas 包含您需要进行命中测试的信息,但为每个遮罩保留全尺寸 canvas 会占用大量内存。为每个遮罩保留 canvas 可能会使用比您的 iPad 可以处理的更多资源。
这里有一个大大减少内存使用的方法:
首先,从每个对象中裁剪出任何额外的透明 space。例如,您的沙发是 600x400=240000 像素,但裁剪掉空白 space 会将图像缩小到 612x163=99756 像素。这比原始图像大小节省了 58%。更少的像素意味着更少的遮罩内存。
不是为每个对象保留全尺寸 canvas,而是为每个对象保留一个数组,该数组仅包含该图像中每个像素的不透明度。数组值为 1 表示像素是不透明的(并且是对象的一部分)。数组值为 0 表示像素是透明的(对象的任何部分都不在该像素处)。
然后针对像素阵列进行命中测试,而不是针对遮罩进行命中测试 canvas。
如果您按 z-index 顺序测试数组,您甚至可以分辨出哪个对象在另一个对象之上。
这是示例代码和演示:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
// display which object the mouse is over
var $result=$('#result');
// create an array of target objects
var targets=[];
targets.push({ name:'couch', x:25, y:50, hitArray:[], url:'https://dl.dropboxusercontent.com/u/139992952/multple/couch.png' });
targets.push({ name:'lamp', x:50, y:30, hitArray:[], url:'https://dl.dropboxusercontent.com/u/139992952/multple/lamp.png' });
var imgCount=targets.length;
// load the image associated with each target object
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.image=new Image();
t.image.crossOrigin='anonymous';
t.image.index=i;
t.image.onload=start;
t.image.src=t.url;
}
// this is called when each image is fully loaded
function start(){
// return if all target images are not loaded
if(--imgCount>0){return;}
// make hit arrays for all targets
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.hitArray=makeHitArray(t.image);
}
// resize the canvas back to its original size
canvas.width=cw;
canvas.height=ch;
// draw all targets on the canvas
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.width=t.image.width;
t.height=t.image.height;
ctx.drawImage(t.image,t.x,t.y);
}
// listen for events
$("#canvas").mousemove(function(e){handleMouseMove(e);});
}
// Draw a target image on a canvas
// Get the imageData of that canvas
// Make an array containing the opacity of each pixel on the canvas
// ( 0==pixel is not part of the object, 1==pixel is part of the object)
function makeHitArray(img){
var a=[];
canvas.width=img.width;
canvas.height=img.height;
ctx.drawImage(img,0,0);
var data=ctx.getImageData(0,0,canvas.width,canvas.height).data;
for(var i=0;i<data.length;i+=4){
// if this pixel is mostly opaque push 1 else push 0
a.push(data[i+3]>250?1:0);
}
return(a);
}
function handleMouseMove(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get the mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// Test the mouse position against each object's pixel array
// Report hitting the topmost object if 2+ objects overlap
var hit='Not hovering';
for(var i=0;i<targets.length;i++){
var t=targets[i];
var imgX=mouseX-t.x;
var imgY=mouseY-t.y;
if(imgX<=t.width && imgY<=t.height){
var hitArrayIndex=imgY*t.width+imgX;
if(hitArrayIndex<t.hitArray.length-1){
if(t.hitArray[hitArrayIndex]>0){
hit='Hovering over '+t.name;
}
}
}
}
$result.text(hit);
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id='result'>Move mouse over objects.</h4>
<canvas id="canvas" width=450 height=250></canvas>
我正在制作一个小游戏,用户可以在其中购买物品来装饰他的房子。
我有很多items/images;所以我决定为每个图像使用一个 "matte"(图像)来定义可悬停区域,而不是为每个图像绘制地图区域。
示例:这里是the displayed couch, and its matte。
我 "convert" 遮罩到 canvas 元素中,稍后将检查悬停像素是否透明以检测项目是否悬停。其次是很多项目重叠,所以我还需要检查哪一层在上面。
我在房子元素上有 mousemove 事件 (jQuery);绑定函数 getObjectsUnderMouse().
基本上,这就是 getObjectsUnderMouse() 的工作原理:
- 获取鼠标坐标
- 获取房屋中的活动(显示)物品
- 过滤那些项目以仅保留鼠标点击canvas边界的项目,知道项目位置和width/height)
- 过滤那些项目,只保留那些鼠标不在透明像素上的项目 (canvas)
- 过滤那些项目以仅保留顶部的项目(z-index)
- 给那个项目一个 mouseon class
我对我的代码非常满意,这是一个相当大的挑战,但在 Chrome.
上运行完美我遇到的问题是其他地方速度较慢(不是什么大问题),但是;最重要的是,似乎在 ipad 上崩溃了;我需要我的游戏在 ipad 上达到 运行... :/
有谁知道这是为什么或对此有更好的解决方案吗?
a demo of the game, and here's the javascript file 您可以在此处查看 getObjectsUnderMouse()。
欢迎任何建议!
尽管遮罩 canvas 包含您需要进行命中测试的信息,但为每个遮罩保留全尺寸 canvas 会占用大量内存。为每个遮罩保留 canvas 可能会使用比您的 iPad 可以处理的更多资源。
这里有一个大大减少内存使用的方法:
首先,从每个对象中裁剪出任何额外的透明 space。例如,您的沙发是 600x400=240000 像素,但裁剪掉空白 space 会将图像缩小到 612x163=99756 像素。这比原始图像大小节省了 58%。更少的像素意味着更少的遮罩内存。
不是为每个对象保留全尺寸 canvas,而是为每个对象保留一个数组,该数组仅包含该图像中每个像素的不透明度。数组值为 1 表示像素是不透明的(并且是对象的一部分)。数组值为 0 表示像素是透明的(对象的任何部分都不在该像素处)。
然后针对像素阵列进行命中测试,而不是针对遮罩进行命中测试 canvas。
如果您按 z-index 顺序测试数组,您甚至可以分辨出哪个对象在另一个对象之上。
这是示例代码和演示:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
// display which object the mouse is over
var $result=$('#result');
// create an array of target objects
var targets=[];
targets.push({ name:'couch', x:25, y:50, hitArray:[], url:'https://dl.dropboxusercontent.com/u/139992952/multple/couch.png' });
targets.push({ name:'lamp', x:50, y:30, hitArray:[], url:'https://dl.dropboxusercontent.com/u/139992952/multple/lamp.png' });
var imgCount=targets.length;
// load the image associated with each target object
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.image=new Image();
t.image.crossOrigin='anonymous';
t.image.index=i;
t.image.onload=start;
t.image.src=t.url;
}
// this is called when each image is fully loaded
function start(){
// return if all target images are not loaded
if(--imgCount>0){return;}
// make hit arrays for all targets
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.hitArray=makeHitArray(t.image);
}
// resize the canvas back to its original size
canvas.width=cw;
canvas.height=ch;
// draw all targets on the canvas
for(var i=0;i<targets.length;i++){
var t=targets[i];
t.width=t.image.width;
t.height=t.image.height;
ctx.drawImage(t.image,t.x,t.y);
}
// listen for events
$("#canvas").mousemove(function(e){handleMouseMove(e);});
}
// Draw a target image on a canvas
// Get the imageData of that canvas
// Make an array containing the opacity of each pixel on the canvas
// ( 0==pixel is not part of the object, 1==pixel is part of the object)
function makeHitArray(img){
var a=[];
canvas.width=img.width;
canvas.height=img.height;
ctx.drawImage(img,0,0);
var data=ctx.getImageData(0,0,canvas.width,canvas.height).data;
for(var i=0;i<data.length;i+=4){
// if this pixel is mostly opaque push 1 else push 0
a.push(data[i+3]>250?1:0);
}
return(a);
}
function handleMouseMove(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get the mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// Test the mouse position against each object's pixel array
// Report hitting the topmost object if 2+ objects overlap
var hit='Not hovering';
for(var i=0;i<targets.length;i++){
var t=targets[i];
var imgX=mouseX-t.x;
var imgY=mouseY-t.y;
if(imgX<=t.width && imgY<=t.height){
var hitArrayIndex=imgY*t.width+imgX;
if(hitArrayIndex<t.hitArray.length-1){
if(t.hitArray[hitArrayIndex]>0){
hit='Hovering over '+t.name;
}
}
}
}
$result.text(hit);
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id='result'>Move mouse over objects.</h4>
<canvas id="canvas" width=450 height=250></canvas>