首页 专题 H5案例 前端导航 UI框架

canvas动画包教不包会:坐标旋转和斜面反弹

阅读 1728 评论 0
坐标旋转,顾名思义,就是说围绕着某个点旋转坐标系。这一章就来介绍一下如何实现坐标旋转和坐标旋转的作用。
内容如下:
  • 坐标旋转
  • 斜面反弹

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;

实例:
每次旋转角度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是旋转角度(旋转量,指当前角度和旋转后的角度的差值),x1y1是物体旋转后的位置坐标。

注意:这里采取的依旧是弧度制。

这条公式是不是看的有点糊里糊涂的,不知道怎么来的,下面我们将介绍它是如何得出的。

先看图:

/*物体当前的坐标*/

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的中央,那么它们都静止不动,如果鼠标向左移动,这些小球就沿逆时针方向旋转,如果向右移动,小球就沿顺时针方法越转越快。



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)

当你旋转后获得相对于直线的坐标和速度后,你就可以使用位置x2y2和速度vx1vy1来执行反弹了,根据什么来判断球碰撞直线呢?用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)   

    };   

  }   

};

返回一个包含有xywidthheight属性的矩形对象。

使用如下:

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轴上的速度大于它与直线的相对距离,这表示它刚刚从直线上穿越下来;右边小球的速度向量小于它与直线的相对距离,这表示,它在这一帧和上一帧都位于线下,因此它此时只是在线下运动,所以我们需要的是在小球穿过直线的那一瞬间才执行反弹。


也就是:比较vy1y2,仅当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);


关注”全栈技术杂货铺“

全栈技术杂货铺