## 坐标旋转和斜面反弹 坐标旋转,顾名思义,就是说围绕着某个点旋转坐标系。这一章就来介绍一下如何实现坐标旋转和坐标旋转的作用。 内容如下: - 坐标旋转 - 斜面反弹 **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); ```