持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
canvas 制作动画(下)
1. 改变方向
在 canvas 制作动画(上)已经介绍了如何创建动画,但还没有讨论如何控制形状动画的方式。我觉得只有直线动画会让人觉得很枯燥,不知你是否也这样想。
你已经知道了如何让一个形状向右移动(把 x 的值增加),但是如果需要改变运动的速度又该如何实现呢,或者如何改变动画的方向呢?非常简单:只需要增加(或减少)x 和 y 值就可以了。如果你使用与 canvas 制作动画(上) 中示例相同的代码,按以下方式修改tmpShape.x++语句,就可以非常方便地使形状沿着向右的对角线方向运动:
tmpShape.x += 2;
tmpShape.y++;
与前面代码的不同之处在于,上面的代码将 x 值每次增加2,而不是增加1,并将y 值每次增加 1。这样产生的效果是,在每次动画循环中,每个形状向右移动 2 像素,并向下移动1像素,即为向右的对角线方向移动。
或者还可以实现一些非常有趣的效果。例如,在每个动画循环中将 x 和 y 值设为随机值。这样产生的动画效果就具有不可预测性和无序性,形状将表现为不规则的运动形式。但这种方法可以让对象的运动更加生动自然:
tmpShape.x += Math.random() * 4 - 2;
tmpShape.y += Math.random() * 4 - 2;
以上代码的作用是产生一个介于 0 到 4 之间的随机数(Math.random产生一个 0 到 1 之间的数,然后将该数乘以 4),然后减去 2 得到一个介于 -2 到 2 之间的随机数。通过这种方法,形状可以向右运动(x 值为正数)、向左运动(x 值为负数)、向上运动(y 值为正数)和向下运动(y 值为负数)。
如果你在浏览器中尝试该方法,形状将会前后随机运动,并出现摆动现象。
2. 圆周运动
形状不一定始终沿着直线运动。如果你需要的动画效果是沿着圆周运动,例如,沿着圆形轨道运行,该如何实现呢?好消息是,这是完全可以实现的,并且不需要使用太多代码。坏消息是,这里需要使用三角函数的相关知识,可能需要你稍微动一下脑筋。
概念非常简单:将一个形状放在圆周的边缘处(它的周长上),以圆周的任意位置作为起点。但为了简单起见,可以将形状放在周长上角度为0弧度的位置,该位置位于右手边。在每次动画循环中,只需要增加位于圆周上的形状的角度,就可以使形状沿着圆周运动。这非常简单,接下来我们具体讨论如何实现。
2.1 三角函数
需要解决的问题是:如何计算位于圆周上的形状的(x, y)坐标值。其实很简单。当然,只有用正确的方式来考虑需要解决的问题,才会觉得它容易。
在解决问题之前,首先需要知道圆的实际大小。可以选择任意大小的圆周,毕竟,这里只是示例,所以实际大小并不重要。重要的是可以通过半径(从圆心到圆周的长度)来描述圆的大小。如果画出运动轨道所在圆周的半径,那么你会发现形状移动的角度遵循一种模式。如果你认真地看,或者稍微思考一下,也许就会发现这种模式。如果幸运的话,你会发现三角形的边存在一些规律。
圆周中包含了一个三角形。但它有何用处呢?这个三角形能够提供一些准确的信息,帮助你计算形状沿圆周移动到新位置处的(x, y)坐标值。更具体地说,现在得到了一个三角形和两个角度(沿圆周转动的角度和三角形的 90 度直角),接下来可以构造一些基本三角形来计算你需要的值。这也体现了数学的重要作用。但是,在真正解决问题之前,我还要简要解释一下三角函数的原理。
三角函数的基本要点是:如果已知一个三角形的一个角是 90 度,并且已知另外一个角,那么就可以计算三角形的边长之间的比值。然后,可以通过该比值来计算边的长度,边的长度单位是任意的,本示例中边的单位是像素。因此,你需要知道三角形的哪条边是需要计算的长度,因为它们分别对应着不同的三角函数规则。这三条边分别是斜边(最长的边)、邻边(与除直角以外的已知角相邻的边)和对边(与已知角相对的边)。
要计算边之间的比值,需要用3种三角函数:正弦函数(sin)、余弦函数(cos)或正切函数(tan)。正弦函数是对边与斜边的比值,余弦函数是邻边与斜边的比值,正切函数是对边与邻边的比值。通过把三角形中的已知角代人正确的函数,可以计算出所需的比值来。
在此,我们需要知道三角形的邻边和对边的长度,它们分别代表 x 和 y 的位置。要计算这些边的长度,首先需要在对应的三角函数中通过已知角计算比值。在JavaScriptr中,可以使用Math对象来计算这些比值:
const angle = 45;
const adjRatio = Math.cos(angle * (Math.PI / 180)); // 余弦-邻边-斜边
const oppRatio = Math.sin(angle * (Math.PI / 180)); // 正弦-对边-外边
你会注意到,Math对象的cos和sin方法中执行了一些简单的计算过程。这种计算是为了将角从角度转换为弧度,因为 JavaScript 使用的单位是弧度。如果你在开始就使用弧度制,就不需要做任何转换了。
得到这些比值仅仅完成了一半的工作量。另外一半工作才是最终我们需要得到的答案,将这些比值与斜边(因为它是半径,所以长度已知)的长度相比较。最终的答案可以由半径乘以该比值得到,即:
const radius = 50;
const x = radius * adjRatio;
const y = radius * oppRatio;
2.2 运用
既然你能够计算位于圆周上某个角度的形状对应的(x, y)坐标值,那么把这些结果综合应用于当前的示例就非常简单了。第一步是更新Shape类,并向其中添加几个新属性:
const Shape = function (x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.radius = Math.random() * 30;
this.angle = 0;
}
这两个属性用于设置起始角度和计算圆周的随机半径(介于0~30之间)。倒数第二步是使用以下代码替换动画循环中的现有代码,从而更新形状:
const x = tmpShape.x + (tmpShape.radius * Math.cos(tmpShape.angle * (Math.PI / 180)));
const y = tmpshape.y + (tmpShape.radius * Math.sin(tmpShape.angle * (Math.PI / 180)));
tmpShape.angle += 5;
if (tmpShape.angle > 360) {
tmpShape.angle = 0;
}
前两行代码没有什么新内容,它们分别用于计算位于圆周上当前角度的形状所对应的 x 和 y 值,其中圆周是通过半径来定义的。这里的 x 和 y 值能够提供坐标值(假设圆周中心的坐标为(0, 0)),因此,当将 x 和 y 值添加到形状中对应的点(x, y)时,就可以把形状移动到正确的位置。注意,形状对象中定义的点(x, y)现在引用的是圆周的中心——形状围绕它旋转的点,而不是形状的起点。最后几行代码用于在每个动画循环中增加角的度数,如果角度超过 360 度(一个完整的圆),则将角度重新设置为 0 度。
最后,将新的 x 和 y 变量添加到fillRect方法中:
context.fillRect (x, y, tmpShape.width, tmpshape.height);
如果一切运行正常,就可以选择不同的形状,让它们沿着不同的圆周运动。
以下是完整的代码供你参考。
const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
let playAnimation = true;
const startButton = document.getElementById('startAnimation');
const stopButton = document.getElementById('stopAnimation');
startButton.style.display = 'none';
startButton.onclick = function () {
startButton.style.display = 'none';
stopButton.style.display = 'inline-block';
playAnimation = true;
animate();
}
stopButton.onclick = function () {
stopButton.style.display = 'none';
startButton.style.display = 'inline-block';
playAnimation = true;
playAnimation = false;
}
const Shape = function (x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.radius = Math.random() * 30;
this.angle = 0;
}
const shapes = new Array();
for (let i = 0; i < 10; i++) {
let x = Math.random() * 250;
let y = Math.random() * 250;
let width = height = Math.random() * 50;
shapes.push(new Shape(x, y, width, height));
}
function animate() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
const shapesLength = shapes.length;
for (let i = 0; i < shapesLength; i++) {
const tmpShape = shapes[i];
const x = tmpShape.x + (tmpShape.radius * Math.cos(tmpShape.angle * (Math.PI / 180)));
const y = tmpShape.y + (tmpShape.radius * Math.sin(tmpShape.angle * (Math.PI / 180)));
tmpShape.angle += 5;
if (tmpShape.angle > 360) {
tmpShape.angle = 0;
}
context.fillRect(x, y, tmpShape.width, tmpShape.height);
}
if (playAnimation) {
setTimeout(animate, 33);
}
}
animate();
3. 反弹
如果你希望形状能够感知周围的环境,或者在边界处反弹回来怎么办呢?这种行为可以避免机械性的动画,使动画更加自然和随机。
在学习如何实现这种行为之前,先用编写如下代码:
const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
let playAnimation = true;
const startButton = document.getElementById('startAnimation');
const stopButton = document.getElementById('stopAnimation');
startButton.style.display = 'none';
startButton.onclick = function () {
startButton.style.display = 'none';
stopButton.style.display = 'inline-block';
playAnimation = true;
animate();
}
stopButton.onclick = function () {
stopButton.style.display = 'none';
startButton.style.display = 'inline-block';
playAnimation = true;
playAnimation = false;
}
const Shape = function (x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
const shapes = new Array();
for (let i = 0; i < 10; i++) {
let x = Math.random() * 250;
let y = Math.random() * 250;
let width = height = Math.random() * 30;
shapes.push(new Shape(x, y, width, height));
}
function animate() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
const shapesLength = shapes.length;
for (let i = 0; i < shapesLength; i++) {
const tmpShape = shapes[i];
context.fillRect(tmpShape.x, tmpShape.y, tmpShape.width, tmpShape.height);
}
if (playAnimation) {
setTimeout(animate, 33);
}
}
animate();
这些代码建立了一个完整的动画循环,该循环将遍历10个随机生成的形状。实际上,代码并没有在视觉上移动任何形状,因为我们没有修改动画循环中形状的属性(如,增加的值将形状向右移动)。
使形状感知画布边界的过程其实非常简单。假设一个形状在每个循环中向右移动 1像素。一旦该形状移动到画布的右边界处(假设是500像素),它将会继续移动,并且 x 值仍在增加,但我们就无法在画布上看到它了。其实你希望该形状发生的行为是:形状在画布的右边界处反弹回来,就好像边界处有一堵墙一样。为此,你需要检查形状是否超过了画布的右边界,如果已经到达边界处,则反向改变形状运动的方向,这样它就会反弹回来。
计算一个形状是否超过画布的右边界其实就是检查形状的 x 位置是否超过了画布的宽度。如果形状的 x 位置大于画布的宽度,那么形状必然会超出右边界。同样,检查形状是否超过画布的左边界也可以采用这种方法。其中,形状的左边界的位置对应的值为0。检查形状的 x 位置是否小于0,就可以确定形状是否位于画布的左边界之外。当然,也可以使用同样的方法检查形状是否位于画布的上边界和下边界。具体做法是,检查 y 值是否小于上边界 0,并检查 y 是否大于画布的高度(下边界)。
综合运用这些方法,可以创建一组简单的逻辑:让形状在画布的边界处弹回。第一步是向Shape类中添加一些新属性,它们将用于定义形状是否碰到边界及反弹的路径方向:
this.reverseX = false;
this.reverseY = false;
默认情况下,这些属性的值为false,在本示例中,这表明形状将一直向右下方运动。下一步是添加逻辑关系来检查形状是否超出了画布边界。在动画循环的fillRect调用下面插人以下代码:
if (tmpShape.x < 0) {
tmpShape.reverseX = false;
} else if (tmpShape.x + tmpshape.width >> canvasWidth) {
tmpShape.reverseX = true;
}
if (tmpshape.y < 0) {
tmpShape.reverseY = false;
} else if (tmpShape.y + tmpShape.height > canvasHeight) {
tmpShape.reverseY = true;
}
当形状即将到达边界之外时,这些检查将反向改变形状的运动路线。但是,设置布尔值并不能实际改变形状的具体运动方向,因此,需要另外进行一些检查。此时,需要将它们放在fillRect调用的上面:
if (!tmpShape.reverseX) {
tmpShape.x += 2;
} else {
tmpShape.x -= 2;
}
if (!tmpshape.reverseY) {
tmpShape.y += 2;
} else {
tmpShape.y -= 2;
}
如果形状在 x 轴上没有反转,那么这些检查将会使形状向右移动(通过增加 x 位置)。如果形状在 x 轴上反转,那么这些检查将会使形状向左移动(通过减少 x 位置)。同样,在 y 轴上也可以执行相同的检查。
由于有了这些相对简单的逻辑检查,你可以使一组形状移动到画布的边界时反弹回来。甚至可以更改 Shape 类表示反转方向的属性的默认值,从而改变形状的运动方式。