canvas动画包教不包会:坐标旋转和斜面反弹
- 坐标旋转
- 斜面反弹
vr = 0.1; //角度增量
angle = 0;
radius = 100;
centerX = 0;
centerY = 0;
object.x = centerX + Math.cos(angle) * radius;
object.y = centerY + Math.sin(angle) * radius;
angle += vr;
var dx = ball.x - centerX;
var dy = ball.y - centerY;
var angle = Math.atan2(dy,dx);
var radius = Math.sqrt(dx * dx + dy * dy);
(x,y)
围绕着一个点(x2,y2)
旋转,而我们只知道物体的坐标和点的坐标,那如何计算旋转后物体的坐标呢?下面有一个很适合这种场景的公式:x1 = (x - x2) * cos(rotation) - (y - y2) * sin(rotation);
y1 = (y - y2) * cos(rotation) + (x - x2) * sin(rotation);
(x-x2)
、(y-y2)
是物体相对于旋转点的坐标,rotation
是旋转角度(旋转量,指当前角度和旋转后的角度的差值),x1
、y1
是物体旋转后的位置坐标。/*物体当前的坐标*/
x = radius * cos(angle);
y = radius * sin(angle);
/*物体旋转rotation后的坐标*/
x1 = radius * cos(angle + rotation);
y1 = radius * sin(angle + rotation);
cos(a + b) = cos(a) * cos(b) - sin(a) * sin(b);
sin(a + b) = sin(a) * cos(b) + cos(a) * sin(b);
x1 = radius * cos(angle) * cos(rotation) - radius * sin(angle) *sin(rotation);
y1 = radius * sin(angle) * cos(rotation) + radius * cos(angle) * sin(rotation);
x1 = x * cos(rotation) - y * sin(rotation);
y1 = y * cos(rotation) + x * sin(rotation);
注意:这里的x、y是相对于旋转点的x、y坐标,也就是上面的(x-x2)、(y-y2),而不是相对于坐标系的坐标。
使用这个公式,我们不需要知道起始角度和旋转后的角度,只需要知道旋转角度即可。
(1)旋转单个物体
有了公式,当然要实践一下,我们先来试试旋转单个物体
这里的vr依旧是0.05
,然后计算这个角度的正弦和余弦值,然后根据小球相对于中心点的位置计算出x1、y1,接着利用公式计算出小球旋转后的坐标。
sin = Math.sin(angle);
cos = Math.cos(angle);
var x1 = ball.x - centerX;
var y1 = ball.y - centerY;
ball.x = centerX + (x1 * cos - y1 * sin);
ball.y = centerY + (y1 * cos + x1 * sin);
还是要强制一句,这个公式传入的x、y是物体相对于旋转点的坐标,不是旋转点的坐标,也不是物体的坐标。
你可能会疑惑,这不是跟第一个例子的效果一样吗?为什么要用这个公式呢?不要急,接着看下面的旋转多个物体,看完后你就会明白这条公式的好处了。
(2)旋转多个物体
假如要旋转多个物体,我们将小球保存在变量balles的数组中,旋转代码如下:
balles.forEach(function(ball){
var dx = ball.x - centerX;
var dy = ball.y - centerY;
var angle = Math.atan2(dy,dx);
var dist = Math.sqrt(dx * dx + dy * dy);
angle += vr;
ball.x = centerX + Math.cos(angle) * dist;
ball.y = centerY + Math.sin(angle) * dist;
});
使用高级坐标旋转是这样的:
var cos = Math.cos(vr);
var sin = Math.sin(vr);
balles.forEach(function(ball){
var x1 = ball.x - centerX;
var y1 = ball.y - centerY;
var x2 = x1 * cos - y1 * sin;
var y2 = y2 * cos + x1 * sin;
ball.x = centerX + x2;
ball.y = centerY + y2;
});
我们来对比一下这两种方式,在第一种方式中,每次循环都调用了4次Math函数,也就是说,旋转每一个小球都要调用4次Math函数,而第二种方式,只调用了两次Math函数,而且都位于循环之外,不管增加多少小球,它们都只会执行一次。
实例:
我们用鼠标来控制多个球的旋转速度,如果鼠标位置在canvas的中央,那么它们都静止不动,如果鼠标向左移动,这些小球就沿逆时针方向旋转,如果向右移动,小球就沿顺时针方法越转越快。
2、斜面反弹
前面我们学习了如何让物体反弹,不过都是基于垂直或水平的反弹面,如果是一个斜面,我们该如何反弹呢?
处理斜面反弹,我们要做的是:旋转整个系统使反弹面水平,然后做反弹,最后再旋转回来,这意味着反弹面、物体的坐标位置和速度向量都发生了旋转。
图1是小球撞向斜面,向量箭头表示小球的方向
图2中,整个场景旋转了,反弹面处于水平位置,就像前面碰撞示例中的底部障碍一样。在这里,速度向量也随着整个场景向右旋转了。
图3中,我们就可以实现反弹了,也就是改变y轴上的速度
图4中,就是整个场景旋转回到最初的角度。
什么,你还看不明白,那我再给你画个图吧:
斜面和小球的旋转都是相对于(x,y)。
经历了上图,你应该明白,如果还不明白,请自己画图看看,画出每一步。
2.1 旋转起来
为了斜面反弹的真实性,我们需要创建一个斜面,在canvas中,我们只需画一条斜线,这样我们就可以看到小球在哪里反弹了。
相信画直线对你来说不难,下面创建一个Line类:
function Line(x1, y1, x2, y2) {
this.x = 0;
this.y = 0;
this.x1 = (x1 === undefined) ? 0 : x1;
this.y1 = (y1 === undefined) ? 0 : y1;
this.x2 = (x2 === undefined) ? 0 : x2;
this.y2 = (y2 === undefined) ? 0 : y2;
this.rotation = 0;
this.scaleX = 1;
this.scaleY = 1;
this.lineWidth = 1;
};
/*绘制直线*/
Line.prototype.draw = function(context) {
context.save();
context.translate(this.x, this.y); //平移
context.rotate(this.rotation); // 旋转
context.scale(this.scaleX, this.scaleY);
context.lineWidth = this.lineWidth;
context.beginPath();
context.moveTo(this.x1, this.y1);
context.lineTo(this.x2, this.y2);
context.closePath();
context.stroke();
context.restore();
};
先看实例(点击一下按钮看看):
在上面的例子中,我创建的小球是随机位置的,不过都位于斜线的上方。
一开始,我们首先声明ball、line、gravity和bounce,然后初始化ball和line的位置,接着计算直线旋转角度的cos和sin值
line = new Line(0, 0, 300, 0);
line.x = 50;
line.y = 200;
line.rotation = (10 * Math.PI / 180); //设置线的倾斜角度
cos = Math.cos(line.rotation);
sin = Math.sin(line.rotation);
接下来,用小球的位置减去直线的位置(50,100),就会得到小球相对于直线的位置:
var x1 = ball.x - line.x;
var y1 = ball.y - line.y;
完成了上面这些,我们现在可以开始旋转,获取旋转后的位置和速度:
var x2 = x1 * cos + y1 * sin;
var y2 = y1 * cos - x1 * sin;
如果你够仔细,可能你也发现了,这里的代码好像和坐标旋转公式有点区别:
x1 = x * cos(rotation) - y * sin(rotation);
y1 = y * cos(rotation) + x * sin(rotation);
加号变减号,减号变加号了,写错了吗?其实没有,这是因为现在直线的斜度是10,那要将它旋转成水平的话,就不是旋转10,而是-10才对:
sin(-10) = - sin(10)
cos(-10) = cos(10)
当你旋转后获得相对于直线的坐标和速度后,你就可以使用位置x2
、y2
和速度vx1
、vy1
来执行反弹了,根据什么来判断球碰撞直线呢?用y2
,因为此时y2
是相对直线的位置的,所以“底边”就是line自己,也就是0,还要考虑小球的大小,需要判断y2
是否大于0-ball.radius
:
if(y2 > -ball.radius) {
y2 = -ball.radius;
vy1 *= bounce;
};
最后,你还要将整个系统旋转归位,计算原始角度的正余弦值:
x1 = x2 * cos - y2 * sin;
y1 = y2 * cos + x2 * sin;
求得ball实例的绝对位置:
ball.x = line.x + x1;
ball.y = line.y + y1;
2.2 优化代码
在上面的例子中,有些代码在反弹之前是没必要执行的,所以我们可以将它们放到if语句中:
if(y2 > -ball.radius) {
var x2 = x1 * cos + y1 * sin;
var vx1 = ball.vx * cos + ball.vy * sin;
var vy1 = ball.vy * cos - ball.vx * sin;
y2 = -ball.radius;
vy1 *= bounce;
//旋转回来,计算坐标和速度
x1 = x2 * cos - y2 * sin;
y1 = y2 * cos + x2 * sin;
ball.vx = vx1 * cos - vy1 * sin;
ball.vy = vy1 * cos + vx1 * sin;
ball.x = line.x + x1;
ball.y = line.y + y1;
};
2.3 修复“不从边缘落下”的问题
如果你试过上面的例子,现在你也看到了,即使小球到了直线的边缘,它还是会沿着直线方向滚动,这不科学,原因在于我们是模拟,并不是真实的碰撞,小球并不知道线的起点和终点在哪里。
2.3.1 碰撞检测
在前面的碰撞检测中,我们介绍过一个方法tool.intersects()
,可用来检测直线的边界框是否与小球的边界框重叠。
当然,我们还需要获得直线的边界框,这里给Line类添加一个方法getBound:
Line.prototype.getBound = function() {
if(this.rotation === 0) {
var minX = Math.min(this.x1, this.x2);
var minY = Math.min(this.y1, this.y2);
var maxX = Math.max(this.x1, this.x2);
var maxY = Math.max(this.y1, this.y2);
return {
x: this.x + minX,
y: this.y + minY,
width: maxX - minX,
height: maxY - minY
};
} else {
//基于坐标系原点旋转
var sin = Math.sin(this.rotation);
var cos = Math.cos(this.rotation);
var x1r = cos * this.x1 + sin * this.y1;
var x2r = cos * this.x2 + sin * this.y2;
var y1r = cos * this.y1 + sin * this.x1;
var y2r = cos * this.y2 + sin * this.x2;
return {
x: this.x + Math.min(x1r, x2r),
y: this.y + Math.min(y1r, y2r),
width: Math.max(x1r, x2r) - Math.min(x1r, x2r),
height: Math.max(y1r, y2r) - Math.min(y1r, y2r)
};
}
};
返回一个包含有x
、y
、width
和height
属性的矩形对象。
使用如下:
if(tool.intersects(ball.getBound(), line.getBound()){
}
还有一个更精确的方法。
2.3.2 边界检查
var bounds = line.getBound();
if(ball.x + ball.radius > bounds.x && ball.x - ball.radius <bounds.x + bounds.width){
//执行反弹
}
如上代码所示,如果小球的边界框小于bounds.x
(左边缘),或者大于bounds.x+bounds.width
(右边缘),就说明它已经从线段上掉落了。
注意:因为小球的圆心是中心点,左边框和上边框就是圆心位置减去小球的半径,有边框和下边框就是圆心位置加上小球的半径。
2.4 多个斜面反弹
要实现多个斜面反弹其实也不难,只需要创建多个斜面并循环即可。
实例:
上面的例子中,我们已经实现了多个斜面反弹,可似乎有一个问题,当小球从第二个斜面掉落时,并没有掉落到第三个斜面上,而是在半空中就反弹回去了,这是为什么呢?下面我们就来修复这个问题。
2.5 修复“线下”的问题
在上面的检测碰撞时,首先要判断小球是否在直线附近,然后进行坐标旋转,得到旋转后的位置和速度,接着,判断小球旋转后的纵坐标y2是否越过了直线,如果超过了,则执行反弹。
if(y2 > -ball.radius){}
上面的代码也是导致2.4中例子没有掉落到下面的原因,因为当小球从第二个斜面掉落下,却是落到了第一个斜面的下面,也就会触发第一个斜面和小球的反弹,这不是我们想要的,如何解决呢?先看下图:
左边小球在y轴上的速度大于它与直线的相对距离,这表示它刚刚从直线上穿越下来;右边小球的速度向量小于它与直线的相对距离,这表示,它在这一帧和上一帧都位于线下,因此它此时只是在线下运动,所以我们需要的是在小球穿过直线的那一瞬间才执行反弹。
也就是:比较vy1
和y2
,仅当vy1
大于y2
时才执行反弹:
if(y2 > -ball.radius && y2 < vy1) {}
看看修复后的例子:
总结
这一章,我们介绍了坐标旋转和斜面反弹,其中不遗余力的分析了坐标旋转公式,并且修复了“不从边缘落下”和“线下”两个问题,一定要掌握坐标旋转,后面我们还将多处用到。
下一章:撞球物理
附录
重要公式:
(1)坐标旋转
x1 = x * Math.cos(rotation) - y * Math.sin(rotation);
y1 = y * Math.cos(rotation) + x * Math.sin(rotation);
(2)反向坐标旋转
x1 = x * Math.cos(rotation) + y * Math.sin(rotation);
y1 = y * Math.cos(rotation) - x * Math.sin(rotation);
更多建议: