12.24 HTML Canvas动画 - 碰撞反弹
到目前为止我们使用的示例其实都没有边界。也就是说,当形状移动到画布的边界处时,什么都没发生,它们只是消失在我们的视野中,再也看不见了。 这也许是你需要的效果。例如,如果你只是创建一段简短的动画,并且动画在到达边界之前就会停止,或者你希望形状移动到画布之外。 但是,如果你不需要这种行为怎么办?如果你希望形状能够感知周围的环境,或者在边界处反弹回来怎么办呢?这种行为可以避免机械性的动画,使动画更加自然和随机。
在学习如何实现这种行为之前,先用本章讨论的技术编写如下代码:
var canvasWidth = canvas.width(); var canvasHeight = canvas.height(); var playAnimation = true; var startButton = $("#startAnimation"); var stopButton = $("#stopAnimation"); startButton.hide(); startButton.click(function() { $(this).hide(); stopButton.show(); playAnimation = true; animate(); }); stopButton.click(function() { $(this).hide(); startButton.show(); playAnimation = false; }); var Shape = function(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; }; var shapes = new Array(); for (var i = 0; i < 10; i++) { var x = Math.random()*250; var y = Math.random()*250; var width = height = Math.random()*30; shapes.push(new Shape(x, y, width, height)); }; function animate() { context.clearRect(0, 0, canvasWidth, canvasHeight); var shapesLength = shapes.length; for (var i = 0; i < shapesLength; i++) { var tmpShape = shapes[i]; context.fillRect(tmpShape.x, tmpShape.y, tmpShape.width, tmpShape.height); }; if (playAnimation) { setTimeout(animate, 33); }; }; animate();
这些代码建立了一个完整的动画循环,该循环将遍历10个随机生成的形状。代码并没有在视觉上移动任何形状,因为我们没有修改动画循环中形状的属性(如,增加x的值将形状向右移动)。
使形状感知画布边界的过程其实非常简单。假设一个形状在每个循环中向右移动1像素。一旦该形状移动到画布的右边界处(假设是500像素),它将会继续移动,并且x值仍在增加,但我们就无法在画布上看到它了。其实你希望该形状发生的行为是:形状在画布的右边界处反弹回来,就好像边界处有一堵墙一样。为此,你需要检查形状是否超过了画布的右边界,如果已经到达边界处,则反向改变形状运动的方向,这样它就会反弹回来。
图1描述了这种效果的基本概念,运动的球上的数字代表每个动画循环。左边的图显示了没有边界时形状的正常反应,右边的图显示了存在边界时形状会如何反应。
计算一个形状是否超过画布的右边界其实就是检查形状的x位置是否超过了画布的宽度。如果形状的x位置大于画布的宽度,那么形状必然会超出右边界。同样,检查形状是否超过画布的左边界也可以采用这种方法。其中,形状的左边界的位置对应的x值为0。检查形状的x位置是否小于0,就可以确定形状是否位于画布的左边界之外。当然,也可以使用同样的方法检查形状是否位于画布的上边界和下边界。具体做法是,检查y值是否小于上边界0,并检查y值是否大于画布的高度(下边界)。
综合运用这些方法,可以创建一组简单的逻辑:让形状在画布的边界处弹回。第一步是向Shape类中添加一些新属性,它们将用于定义形状是否碰到边界及反弹的路径方向:
this.reverseX=false; this.reverseY=false;
默认情况下,这些属性的值为false,在本示例中,这表明形状将一直向右下方运动。下一步是添加逻辑关系来检查形状是否超出了画布边界。在动画循环的fillRect调用下面插入以下代码:
if (tmpShape.x canvasWidth) { tmpShape.reverseX = true; }; if (tmpShape.y 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轴上也可以执行相同的检查。
由于有了这些相对简单的逻辑检查,你可以使一组形状移动到画布的边界时反弹回来(如图2所示)。甚至可以更改Shape类表示反转方向的属性的默认值,从而改变形状的运动方式。
$(document).ready(function () { var canvas = $("#canvas"); var context = canvas.get(0).getContext("2d"); var canvasWidth = canvas.width(); var canvasHeight = canvas.height(); var playAnimation = false; var startButton = $("#startAnimation"); var stopButton = $("#stopAnimation"); stopButton.hide(); startButton.click(function () { $(this).hide(); stopButton.show(); playAnimation = true; animate(); }); stopButton.click(function () { $(this).hide(); startButton.show(); playAnimation = false; }); // Class that defines new shapes to draw var Shape = function (x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; this.reverseX = true; this.reverseY = false; }; // Array that holds all the shapes to draw var shapes = new Array(); // Setting up some shapes for (var i = 0; i < 10; i++) { var x = Math.random() * 250; var y = Math.random() * 250; var width = height = Math.random() * 30; shapes.push(new Shape(x, y, width, height)); }; // Animation loop that does all the fun stuff function animate() { // Clear context.clearRect(0, 0, canvasWidth, canvasHeight); // Loop through every object var shapesLength = shapes.length; for (var i = 0; i < shapesLength; i++) { var tmpShape = shapes[i]; // Update // Check direction to move shape if (!tmpShape.reverseX) { tmpShape.x += 2; } else { tmpShape.x -= 2; }; if (!tmpShape.reverseY) { tmpShape.y += 2; } else { tmpShape.y -= 2; }; // Draw shape context.fillRect(tmpShape.x, tmpShape.y, tmpShape.width, tmpShape.height); // Check the boundaries if (tmpShape.x canvasWidth) { tmpShape.reverseX = true; }; if (tmpShape.y canvasHeight) { tmpShape.reverseY = true; }; }; if (playAnimation) { // Run the animation loop again in 33 milliseconds setTimeout(animate, 33); }; }; // Start the animation loop animate(); });