## 坐标旋转和斜面反弹
坐标旋转,顾名思义,就是说围绕着某个点旋转坐标系。这一章就来介绍一下如何实现坐标旋转和坐标旋转的作用。
内容如下:
- 坐标旋转
- 斜面反弹
**1、坐标旋转**
**1.1 简单旋转**
在前面的三角函数一章中的实例“指红针”中,我们已经使用过坐标旋转技术。只需一个中心点,一个物体,还有半径和角度(弧度制),通过增减这个角度,然后用基本的三角函数计算位置,就能使物体围绕着中心点旋转。
初始化参数:
```
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;
```
实例: canvas-demo/rotate.html
每次旋转角度vr设置为0.05,根据上面的公式计算小球旋转后的位置。
如果只知道物体的位置和中心点,如何做旋转呢?其实也不难,我们只需根据两个点来计算出当前角度和半径即可:
```
var dx = ball.x - centerX;
var dy = ball.y - centerY;
var angle = Math.atan2(dy,dx);
var radius = Math.sqrt(dx * dx + dy * dy);
```
得到角度和半径,我们就可以像上面那样旋转了。
上面的方法比较适合单个物体旋转,对于多个物体的旋转,这种方法不是很高效,当然,我们有更好的方法。
**1.2 高级坐标旋转**
如果物体(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是物体旋转后的位置坐标。
注意:这里采取的依旧是弧度制。
这条公式是不是看的有点糊里糊涂的,不知道怎么来的,下面我们将介绍它是如何得出的。
先看图:
![](https://box.kancloud.cn/57bd2b242387b976a8275efea8e2c7de_413x203.jpg)
```
/*物体当前的坐标*/
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和y1的公式展开:
```
x1 = radius * cos(angle) * cos(rotation) - radius * sin(angle) *sin(rotation);
y1 = radius * sin(angle) * cos(rotation) + radius * cos(angle) * sin(rotation);
```
最后将x、y变量代入公式,就会得到最初那条公式:
```
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-demo/rotate3.htmll
我们用鼠标来控制多个球的旋转速度,如果鼠标位置在canvas的中央,那么它们都静止不动,如果鼠标向左移动,这些小球就沿逆时针方向旋转,如果向右移动,小球就沿顺时针方法越转越快。
**2、斜面反弹**
前面我们学习了如何让物体反弹,不过都是基于垂直或水平的反弹面,如果是一个斜面,我们该如何反弹呢?
处理斜面反弹,我们要做的是:旋转整个系统使反弹面水平,然后做反弹,最后再旋转回来,这意味着反弹面、物体的坐标位置和速度向量都发生了旋转。
![](https://box.kancloud.cn/56b61d1512a54ca0ecec016f4cd7411b_424x375.jpg)
图1是小球撞向斜面,向量箭头表示小球的方向
图2中,整个场景旋转了,反弹面处于水平位置,就像前面碰撞示例中的底部障碍一样。在这里,速度向量也随着整个场景向右旋转了。
图3中,我们就可以实现反弹了,也就是改变y轴上的速度
图4中,就是整个场景旋转回到最初的角度。
什么,你还看不明白,那我再给你画个图吧:
![](https://box.kancloud.cn/d7c217cde9c20ba0583430f2a34ef656_1052x424.jpg)
斜面和小球的旋转都是相对于(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();
};
```
先看实例(点击一下按钮看看):canvas-demo/rotateBevel.html
在上面的例子中,我创建的小球是随机位置的,不过都位于斜线的上方。
一开始,我们首先声明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 多个斜面反弹**
要实现多个斜面反弹其实也不难,只需要创建多个斜面并循环即可。
实例:canvas-demo/rotateBevel2.html
上面的例子中,我们已经实现了多个斜面反弹,可似乎有一个问题,当小球从第二个斜面掉落时,并没有掉落到第三个斜面上,而是在半空中就反弹回去了,这是为什么呢?下面我们就来修复这个问题。
**2.5 修复“线下”的问题**
在上面的检测碰撞时,首先要判断小球是否在直线附近,然后进行坐标旋转,得到旋转后的位置和速度,接着,判断小球旋转后的纵坐标y2是否越过了直线,如果超过了,则执行反弹。
```
if(y2 > -ball.radius){}
```
上面的代码也是导致2.4中例子没有掉落到下面的原因,因为当小球从第二个斜面掉落下,却是落到了第一个斜面的下面,也就会触发第一个斜面和小球的反弹,这不是我们想要的,如何解决呢?先看下图:
![](https://box.kancloud.cn/e303916d87127bdfb4d6ea9c443bd5c3_312x248.jpg)
左边小球在y轴上的速度大于它与直线的相对距离,这表示它刚刚从直线上穿越下来;右边小球的速度向量小于它与直线的相对距离,这表示,它在这一帧和上一帧都位于线下,因此它此时只是在线下运动,所以我们需要的是在小球穿过直线的那一瞬间才执行反弹。
也就是:比较vy1和y2,仅当vy1大于y2时才执行反弹:
```
if(y2 > -ball.radius && y2 < vy1) {}
```
看看修复后的例子:canvas-demo/rotateBevel3.html
**总结**
这一章,我们介绍了坐标旋转和斜面反弹,其中不遗余力的分析了坐标旋转公式,并且修复了“不从边缘落下”和“线下”两个问题,一定要掌握坐标旋转,后面我们还将多处用到。
**附录**
重要公式:
(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);
```