在《每周一点canvas动画》——用户交互这一章我们介绍了用户交互事件,包括:鼠标事件
,触摸事件
和键盘事件
。但是,到目前为止,我们除了键盘事件,对于另外两个事件几乎没有做过任何相应的应用。那么,在这一章,我们将要学习如何让canvas中的物体对用户的动作做出反应,并且学习如何拖拽和抛扔
一个物体。不用担心,用到的都是我们学过的知识。本节主要内容:
物体捕获
捕获验证
物体的拖拽
1.物体捕获
鼠标是个简单,却效率极高的装置。它简单来说只做两件事:移动监测和按钮点击。然后,电脑会根据鼠标得到的信息来做很多事情,比如:位置追踪,点击事件发生时光标的位置,决定鼠标移动的速度,识别双击事件等等。仔细想想,所有的这些是不是都可以归结于鼠标的点击和移动。
现在,好好回忆一下,我们平时玩游戏的时候是怎样移动一个物体的。当然,如果没玩过的话,就别想了。它的基本流程如下:
click -> move -> release
整个流程对应的事件如下:
mousedown:当按下鼠标按钮时发生
mouseup:当鼠标按钮抬起时发生
mousemove:当你移动鼠标时发生
OK!现在我们知道了,移动一个物体分为哪几步(是不是和想起了把大象装进冰箱分为那几步)。接下来还有一个重要的事情:怎样捕获一个物体呢。我们已经知道了如何移动一个物体,可是如果你都没有要移动的对象,那不是白搭嘛!简单来说就是怎样判定我们的鼠标真的点击到了物体上,你知道了不行,必须得让我们的电脑知道才行。
1.1 外接矩形判别法
这里为了便于理解,我做了一个倍详细的图。灰色区域代表canvas, 蓝色的代表rect, 我们已知的是rect的坐标(这里以左上角作为坐标基准)和宽高。当鼠标点击在蓝色区域,即rect上就代表我们捕获这个物体。那么怎样算是鼠标落在这块区域上呢?看下面的几个条件:
1.mouse.x > rect.x
2.mouse.x < rect.x + rect.width
3.mouse.y > rect.y
4.mouse.y < rect.y + rect.height
我们列出了四个条件,仔细对比图片看看。是不是只要所有条件同时满足就可以判定我们的鼠标落在了rect上。所以,如果你要检测的物体是矩形,或者趋近于矩形都可以使用这种方法。
1.2 外接圆判别法
我们可以看到,如果当物体变成圆形,再使用上面的方法去判定鼠标是否落在ball上,就不会那么精确了。当然,不是不行,就是不够精确,当球体很小的时候还是可以接受的。
对于球体,还有另一种方法,那就是判定鼠标距离球心的距离。如果距离小于球体半径就可以判断鼠标落在了球体上,反之则没有。所以,如果你要检测的物体趋近于圆,就可以使用这种方法。
鼠标距球心的距离
dx = mouse.x - ball.x;
dy = mouse.y - ball.y;
dist = Math.sqrt(dx*dx + dy*dy)
if(dist < ball.radius){
//碰上了
}
1.3 多边形和不规则图形
要是我们要捕获的对象是不规则的,那就比较难了,我们需要用到分离轴定理(SAT)和最小平移向量(MTV),听起来就很高深啊。自己去了解一下吧!其实挺容易的,这里就不介绍了。
在本章中为了简单,我们的球体也是使用外接矩形法。我们需要在ball.js
文件中增加如下代码:
Ball.prototype.getBounds = function(){
return {
x: this.x - this.radius,
y: this.y - this.radius,
width: this.radius*2,
height: this.radius*2
};
}
这里定义了一个方法getBounds()
,该方法返回一个对象,包含物体的坐标(以左上角为基准)和宽高,就跟上图中表示的一样。同时我们也需要在工具函数文件utils.js
中增加一个新方法:
utils.containsPoint = function(rect, x, y){
return !(x<rect.x || x>rect.x + rect.width ||
y<rect.y || y>rect.y + rect.height);
}
注意,该方法返回的是个布尔值,如果落在区域内,那么返回的是!(0 || 0 || 0 || 0)
也就是true,如果有任意一个条件不满足,那么久返回false,表示不在区域内。该方法传入三个参数,
rect: 表示要检测的物体
x,y: 鼠标坐标
2.捕获验证
为了防止步子迈的太大扯着蛋!我们先做个简单的效果:
当在鼠标没有点击在ball
上时,打印出canvas
和当前的活动状态,当鼠标点击在球体上时打印出ball
和鼠标的运动状态。具体代码如下:
<canvas id="canvas" width="400" height="400" style="background:#000;">
your browser not support canvas
</canvas>
<textarea name="textarea" id="txt" cols="30" rows="10"></textarea>
<script src="../js/utils.js"></script>
<script src="../js/ball.js"></script>
<script>
window.onload = function(){
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
log = document.getElementById('txt');
//传入canvas,获取鼠标坐标
var mouse = utils.captureMouse(canvas);
var ball = new Ball(20,"red");
ball.x = canvas.width/2;
ball.y = canvas.height/2;
ball.draw(context);
//定义状态函数
function state(wrd){
/*注意这里用到了我们前面新加的代码
ball.getBounds(): 返回一个对象包含球的坐标,宽高.并作为参数传入
utils.containsPoint(): 判断点击区域,返回布尔值
*/
if(utils.containsPoint(ball.getBounds(), mouse.x, mouse.y)){
log.value = "in ball : "+ wrd;
}else{
log.value = "canvas : " + wrd;
}
}
// 为canvas添加mousedown事件
canvas.addEventListener('mousedown', function(event){
state("mousedown");
//mouseup
canvas.addEventListener('mouseup', function(event){
state("mouseup");
},false);
//mousemove
canvas.addEventListener('mousemove', function(event){
state("mousemove");
}, false);
}, false);
}
</script>
注意这里,我们将mouseup
,mousemove
定义在了mousedown
事件内部。也就是说如果mousedown事件不触发,就无法触发另外两个事件。想想我们移动一个物体的基本流程,也的确合乎情理。
touch
事件与鼠标点击事件很相似,这里我就不给代码了,具体的文件可以在文章开头看到。
3.物体的拖拽
有了前面的基础,要移动一个物体算什么难事,我们先上效果图:
具体代码如下:
<canvas id="canvas" width="400" height="400" style="background:#000;">
your browser not support canvas
</canvas>
<script src="../js/utils.js"></script>
<script src="../js/ball.js"></script>
<script>
window.onload = function(){
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
mouse = utils.captureMouse(canvas),
ball = new Ball(20, "red");
ball.x = canvas.width/2;
ball.y = canvas.height/2;
var w = 0, h = 0;
//为canvas设置监听事件
canvas.addEventListener('mousedown', function(event){
//判断是否点击在球体上
if(utils.containsPoint(ball.getBounds(), mouse.x, mouse.y)){
w = mouse.x - ball.x;
h = mouse.y - ball.y;
canvas.addEventListener('mouseup', onMouseUp, false);
canvas.addEventListener('mousemove', onMouseMove, false);
}
}, false);
//mouseup事件执行函数
function onMouseUp(event){
canvas.removeEventListener('mouseup', onMouseUp, false);
canvas.removeEventListener('mousemove', onMouseMove, false);
}
//mousemove事件执行函数
function onMouseMove(event){
ball.x = mouse.x - w;
ball.y = mouse.y - h;
}
//动画循环
(function dramFrame(){
window.requestAnimationFrame(dramFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}())
}
</script>
这部分代码,其实只是在对上部分代码的改进。我们在canvas上设置了mousedown
事件,当鼠标点击在canvas上时,就会触发。此时注意我们的判定条件,如果为true
,就会执行if中的语句。
if中,我们为canvas添加了mousemove
和mouseup
事件,mousemove
事件的触发是在鼠标点击在了ball之后,移动鼠标才触发的,他执行的函数是onMouseMove
。mouseup
是当鼠标抬起是触发的,执行函数是onMouseUp
。
在onMouseMove
函数中我们重新设置了ball的位置。onMouseUp
函数中移除监听事件。
注意,这里有两个变量w
和h
,这两个变量的作用是修正ball的位置的。为什么这样说呢?因为当我们点击球体的时候,不一定点击的就是球体的球心。这时移动物体,如果没有这个修正值,小球就会出现一个快速的偏移,让鼠标位于小球的球心,感觉很别扭。所以,在点击时,我们就把鼠标与球心之间的坐标差值计算出来,在移动的时候给个修正就不会出现快速偏移的现象了。
我说了这么多,要想深刻体会,还是自己动手尝试一下吧!下一节我们介绍如何捕获运动的物体,敬请期待!