JavaScript动画基础:canvas绘制简单动画
动画是将静止的画面变为动态的艺术.实现由静止到动态,主要是靠人眼的视觉残留效应。视觉残留也叫视觉暂留现象,物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1~0.4秒左右的图像,这种现象被称为视觉暂留现象。利用人的这种视觉生理特性可制作出具有高度想象力和表现力的动画影片。
电影的拍摄和放映就是视觉残留效应的具体应用。
大家可能看过组成电影的实际胶片。从表面上看,它们像一堆画面串在一条塑料胶片上。每一个画面称为一帧,代表电影中的一个时间片段。这些帧的内容总比前一帧有稍微的变化,这样,当电影胶片在投影机上放映时就产生了运动的错觉:每一帧都很短并且很快被另一个帧所代替,这样就产生了运动。
通过循环绘制各帧的图像就可以实现动画的效果。
在Canvas画布中制作动画相对来说很简单,实际上就是绘制帧(图形或图像)、擦除、重绘的过程。也就是说,在Canvas中模拟一个动画过程就是每隔一定时间绘制图形并且清除图形,通过定时循环操作实现。
1.定时循环操作的三个函数
对于动画,需要在一段时间内渲染不同的帧,各帧间隔一定的时间在画布中依次被绘制。为完成定时循环操作帧,可以利用etInterval()、setTimeout()和requestAnimationFrame()这三个函数之一。
(1)setTimeout()方法。
setTimeout() 方法是HTML DOM Window对象的一个方法,它用于在指定的毫秒数后调用函数或计算表达式。其调用格式为:
setTimeout(code,millisec);
其中,参数code表示要调用的函数或要执行的代码串,millisec表示在执行代码前需等待的毫秒数。
例如,setTimeout(“draw()”,1000)表示延时1秒后执行函数draw中的代码。
编写如下的HTML文件。
<!DOCTYPE html>
<html>
<head>
<title>setTimeout方法的应用</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
function draw(x,y,len,color)
{
ctx.fillStyle = color;
ctx.fillRect(x,y,len,len);
}
setTimeout("draw(10,10,100,'red')",1000);
setTimeout("draw(110,110,200,'blue')",5000);
</script>
</body>
</html>
在浏览器中打开保存这段HTML代码的html文件,则等待1秒后,会绘制一个边长为100的红色正方形,再等待5秒,绘制一个边长为200的蓝色正方形。
通过这个例子可以知道:(1)setTimeout()方法可以用于延时;(2)setTimeout()方法只执行code一次。如果要多次调用,则需要让code 自身再次调用 setTimeout()。
为产生动画效果,显然得让setTimeout()方法多次执行。修改上面的HTML代码如下。
<!DOCTYPE html>
<html>
<head>
<title>setTimeout方法的应用</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var i=0;
function move()
{
ctx.fillStyle = 'red';
ctx.fillRect(i,i,50,50);
i++;
if (i==350)
{
i=0;
ctx.clearRect(0,0,400,400);
}
setTimeout("move()",10);
}
move();
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到一个简单的箭头伸出动画,如图1所示。
图1 简单的动画
(2)setInterval() 方法。
setInterval()也是HTML DOM Window对象的一个方法,它可按照指定的周期(以毫秒计)来调用函数或计算表达式。其调用格式为:
setInterval(code,millisec);
其中,参数code表示要调用的函数或要执行的代码串, millisec表示周期性执行或调用 code 之间的时间间隔(以毫秒计)。
setInterval() 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的参数。
clearInterval() 方法可取消由 setInterval() 设置的 timeout。其调用形式为:
clearInterval(id_of_setinterval);
其中参数id_of_setinterval必须是由 setInterval() 返回的 ID 值。
若用setInterval() 方法实现图1所示的动画,则编写的HTML文件如下。
<!DOCTYPE html>
<html>
<head>
<title>setInterval()方法的应用</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var i=0;
function move()
{
ctx.fillStyle = 'red';
ctx.fillRect(i,i,50,50);
i++;
if (i==350)
{
i=0;
ctx.clearRect(0,0,400,400);
}
}
setInterval("move()",10);
</script>
</body>
</html>
(3)requestAnimationFrame()方法。
requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。
编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能保证不同的动画效果显得更平滑流畅;另一方面,循环间隔还要足够长,这样才能保证浏览器有能力渲染产生的变化。大多数显示器的刷新频率是60Hz,相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过了这个频率,用户体验也不会有提升。
因此,最平滑动画的最佳循环间隔是1000ms/60,约等于17ms。以这个循环间隔重绘的动画是平滑的,因为这个速度最接近浏览器的最高限速。为了适应17ms的循环间隔,多重动画可能需要加以节制,以便不会完成得太快。
虽然setTimeout()方法和setInterval()方法均可完成定时循环操作,但setTimeout()和setInterval() 都不十分精确。为它们传入的第二个参数millisec,实际上只是指定了把动画代码添加到浏览器UI线程队列以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务执行完成后再执行。如果UI线程繁忙,比如忙于处理用户操作,那么即使把代码加入队列也不会立即执行。
确定什么时候绘制下一帧是保证动画平滑的关键。然而,面对不十分精确的 setTimeout()和setInterval(),开发人员至今都没有办法确保浏览器按时绘制下一帧。因此,采用setTimeout()和setInterval(),即使优化了循环间隔,可能仍然只能接近想要的效果。
引入requestAnimationFrame()方法的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用requestAnimationFrame()方法,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。
requestAnimationFrame的优势在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。
不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。其调用格式为:
requestID = window.requestAnimationFrame(callback);
目前,主流浏览器(Firefox 23 / IE 10 / Chrome / Safari)都支持这个方法。可以用下面的方法,检查浏览器是否支持requestAnimationFrame。如果不支持,则自行模拟部署该方法。
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame。
与 setTimeout() 和 setInterval() 方法不同,requestAnimationFrame( )不需要调用者指定帧速率,浏览器会自行决定最佳的帧效率。也就是说浏览器页面每次要重绘,就会通知requestAnimationFrame。如果浏览器绘制间隔是16.7ms,它就按这个间隔绘制;如果浏览器绘制间隔是10ms,它就按10ms绘制。这样就不会存在过度绘制的问题,动画不会丢帧。
另外,使用requestAnimationFrame()方法,一旦页面不处于浏览器的当前标签,就会自动停止刷新。例如,页面最小化了,页面是不会进行重绘的,requestAnimationFrame自然也不会触发(因为没有通知)。页面绘制全部停止,资源高效利用,节省了CPU、GPU和电力。
和setTimeout类似,requestAnimationFrame的回调函数只能被调用一次,并不能被重复调用(这点和setInterval不同)。因此,使用requestAnimationFrame的时候,同样需要反复调用它。
由于setTimeout可以自定义调用时间, requestAnimationFrame的调用时间则是跟着系统的刷新频率走的,所以在实现动画的时候,setTimeout比requestAnimationFrame更加灵活, requestAnimationFrame比setTimeout表现效果更加优秀。
若用requestAnimationFrame() 方法实现图1所示的动画,则编写的HTML文件如下。
<!DOCTYPE html>
<html>
<head>
<title>requestAnimationFrame方法的应用</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var i=0;
function move()
{
ctx.fillStyle = 'blue';
ctx.fillRect(i,i,50,50);
i++;
if (i==350)
{
i=0;
ctx.clearRect(0,0,400,400);
}
requestAnimationFrame(move);
}
move();
</script>
</body>
</html>
2.绘制简单图形实现动画
图1的动画就是从左上角坐标位置(0,0)开始,绘制一个边长为50的红色正方形,之后每隔10毫秒后将左上角坐标位置的水平和垂直坐标均增加1,再绘制一个正方形,从而得到一个简单的箭头伸出动画效果。
通过在画布中绘制简单图形,达到时间间隔后,擦除(有时候也可暂时不擦除)前次绘制的图形,重新绘制一个位置或大小略有变化的图形,这样就可得到动画效果。
例1 向中心交汇的箭头。
仿照图1动画思想略作变化,编写如下的HTML代码。
<!DOCTYPE html>
<html>
<head>
<title>向中心交汇的箭头</title>
<script type="text/javascript">
var i=0;
function draw(id)
{
var canvas = document.getElementById(id);
ctx = canvas.getContext('2d');
setInterval(painting,10);
}
function painting()
{
ctx.fillStyle = "green";
ctx.fillRect(i,i,10,10);
ctx.fillRect(400-i,400-i,10,10);
ctx.fillRect(i,400-i,10,10);
ctx.fillRect(400-i,i,10,10);
i++;
if (i==200)
{
ctx.clearRect(0,0,400,400);
i=0;
}
}
</script>
</head>
<body onload="draw('myCanvas')">
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图2所示的动画。
图2 向中心交汇的箭头
例2 逐层向里绘制的圆。
<!DOCTYPE html>
<html>
<head>
<title>层层向内画的圆</title>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var flag=1;
var i=0;
var r=180;
function animate() {
window.requestAnimationFrame(animate);
draw();
}
function draw() {
var dig=Math.PI/120;
var x = Math.sin(i*dig)*r+200;
var y = Math.cos(i*dig)*r+200;
context.fillStyle = flag ? 'rgb(10,255,255)' : 'rgb(255,100,0)';
context.beginPath();
context.arc(x, y, 3, 0, Math.PI*2, true);
context.closePath();
context.fill();
i++;
if (i>240) {
i=0;
r=r-20;
flag = !flag;
if (r<=0) {
context.clearRect(0,0,canvas.width,canvas.height);
r=180;
}
}
}
animate();
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图3所示的动画。
图3 层层向内画的圆
3.通过图形变换实现动画效果
在Canvas中,可以绘制一个基本图形,然后通过对这个基本图形使用平移、缩放和旋转等图形变换的方法实现动画效果。
例3 放大缩小的五角星。
<!DOCTYPE html>
<html>
<head>
<title>放大缩小的五角星</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var x=200;
var y=200;
var radius=30;
var rot=0;
var dr=5;
function draw()
{
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(x,y);
ctx.rotate(rot/180*Math.PI);
ctx.scale(radius,radius);
ctx.beginPath();
for(var i=0;i<5;i++) // 绘制标准五角星
{
ctx.lineTo(Math.cos((18+i*72)/180*Math.PI),-Math.sin((18+i*72)/180*Math.PI));
ctx.lineTo(Math.cos((54+i*72)/180*Math.PI)*0.5,-Math.sin((54+i*72)/180*Math.PI)*0.5);
}
ctx.closePath();
ctx.fillStyle="red";
ctx.fill();
ctx.restore();
radius+=dr;
if (radius>200 || radius<30)
dr=-dr;
}
setInterval("draw()",60);
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图4所示的动画。这个动画效果的变化核心是语句“ctx.scale(radius,radius);”在起作用。
图4 放大缩小的五角星
若将上面程序段中,radius固定取值120,再修改变化语句
radius+=dr;
if (radius>200 || radius<30)
dr=-dr;
为
rot=(rot+2)%360;
则五角星会进行旋转,呈现出如图5所示的动画效果。
图5 旋转的五角星
4.遮罩动画
利用Canvas API提供的裁切方法clip(),可以用来实现遮罩动画。
例4 图片圆形展开后收缩。
<!DOCTYPE html>
<head>
<title>圆形展开后收缩</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var r=10;
var dr=5;
var canvas=document.getElementById('myCanvas');
var ctx=canvas.getContext('2d');
var image = new Image();
image.src = 'aaa.jpg';
image.onload=function(){
ctx.drawImage(image,0,0);
}
setInterval("draw()",100);
function draw()
{
ctx.clearRect(0,0,400,400);
ctx.save();
ctx.beginPath();
ctx.arc(200,200,r,0,Math.PI*2,true);
ctx.closePath();
ctx.fillStyle="white";
ctx.fill();
ctx.clip();
ctx.drawImage(image,0,0);
ctx.restore();
r=r+dr;
if (r>280) dr=-5;
else if (r<=0) dr=5;
}
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图6所示的动画。
图6 圆形展开后收缩
5.多个同类物体同时运动实现动画
有时候设计动画时,画布中会有多个同类物体按各自的规律进行运动,这时将各物体抽象为对象数组比较方便处理。
例3中我们通过图形变换的方法实现了五角星的放大和旋转。下面我们绘制60个五角星在画布上进行移动的动画效果。
例5 60个五角星随机移动。
为了描述60个五角星,抽象一个星星对象Star。为该对象定义五角星中心位置坐标(x,y)、五角星外接圆半径radius、水平方向移动速度speedX、垂直方向移动速度speedY和五角星旋转角度deg等6个属性。具体定义如下:
function Star()
{
this.x = randomNum(30,canvas.width-30);
this.y = randomNum(30,canvas.height-30);
this.radius=randomNum(8,12);
this.speedX = randomNum(-5,5);
this.speedY=randomNum(-5,5);
this.deg = randomNum(0,180);
}
为五角星对象定义两个方法,一个是update方法,更新五角星的坐标位置(x,y)并进行边界碰撞检查;一个方法是draw方法,按属性设置绘制出五角星。具体定义如下:
Star.prototype.update = function()
{
this.x += this.speedX;
this.y += this.speedY;
if (this.x-this.radius<=0)
{
this.speedX=-this.speedX;
this.x=this.radius;
}
if (this.x+this.radius>canvas.width)
{
this.speedX=-this.speedX;
this.x=canvas.width-this.radius;
}
if (this.y-this.radius<=0)
{
this.speedY=-this.speedY;
this.y=this.radius;
}
if (this.y+this.radius>canvas.height)
{
this.speedY=-this.speedY;
this.y=canvas.height-this.radius;
}
}
Star.prototype.draw = function()
{
ctx.beginPath();
for (var i = 0; i < 5; i ++)
{
ctx.lineTo( Math.cos( (18 + i*72 - this.deg)/180 * Math.PI) *this.radius + this.x,
-Math.sin( (18 + i*72 - this.deg)/180 * Math.PI) * this.radius + this.y)
ctx.lineTo( Math.cos( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2+ this.x,
-Math.sin( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2 + this.y)
}
ctx.closePath();
ctx.lineWidth = 3;
ctx.fillStyle = "#ff0000";
ctx.strokeStyle = "#ffff00";
ctx.lineJoin = "round";
ctx.fill();
ctx.stroke();
}
定义好五角星对象后,定义一个数组stars,保存60个五角星并设置动画过程。编写完整的HTML文件如下。
<!DOCTYPE html>
<html>
<head>
<title>满天都是小星星</title>
</head>
<body>
<canvas id="myCanvas" width="500" height="400" style="border:3px double #996633;">
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
function Star()
{
this.x = randomNum(30,canvas.width-30);
this.y = randomNum(30,canvas.height-30);
this.radius=randomNum(8,12);
this.speedX = randomNum(-5,5);
this.speedY=randomNum(-5,5);
this.deg = randomNum(0,180);
}
Star.prototype.update = function()
{
this.x += this.speedX;
this.y += this.speedY;
if (this.x-this.radius<=0)
{
this.speedX=-this.speedX;
this.x=this.radius;
}
if (this.x+this.radius>canvas.width)
{
this.speedX=-this.speedX;
this.x=canvas.width-this.radius;
}
if (this.y-this.radius<=0)
{
this.speedY=-this.speedY;
this.y=this.radius;
}
if (this.y+this.radius>canvas.height)
{
this.speedY=-this.speedY;
this.y=canvas.height-this.radius;
}
}
Star.prototype.draw = function()
{
ctx.beginPath();
for (var i = 0; i < 5; i ++)
{
ctx.lineTo( Math.cos( (18 + i*72 - this.deg)/180 * Math.PI) *this.radius + this.x,
-Math.sin( (18 + i*72 - this.deg)/180 * Math.PI) * this.radius + this.y)
ctx.lineTo( Math.cos( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2+ this.x,
-Math.sin( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2 + this.y)
}
ctx.closePath();
ctx.lineWidth = 1;
ctx.fillStyle = "white";
ctx.strokeStyle = "#ffff00";
ctx.lineJoin = "round";
ctx.fill();
ctx.stroke();
}
function randomNum(min,max)
{
return Math.floor(Math.random()*(max-min+1)+min);
}
var stars = [];
for (var i = 0; i < 60; i++)
{
stars.push(new Star());
if (stars[i].speedX==0 && stars[i].speedY==0)
stars[i].speedX=stars[i].speedY=1;
}
function move()
{
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle="blue";
ctx.fillRect(0,0,canvas.width,canvas.height);
for (var i = 0; i <60; i++)
{
stars[i].draw();
stars[i].update();
}
}
setInterval("move()",10);
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图7所示的动画。
图7 星星在运动
例6 下雪了。
简单模拟下雪场景,编写如下的HTML文件。在屏幕中最多有100片雪花,每片雪花绘制一个小圆表示,从画布顶端开始往下落。由于动画过程一直在循环,因此每当一片雪花落出画布之外后,随机为其赋予水平坐标、置垂直坐标置为0、并随机设置其圆半径和下落速度,表示这是一片新雪花。这样,用一个具有100个元素的数组即可保存屏幕中下落雪花的信息。
<!DOCTYPE html>
<html>
<head>
<title>下雪了</title>
</head>
<body>
<canvas id="myCanvas" width="300" height="300" style="border:3px double #996633;">
</canvas>
<script>
var canvas=document.getElementById('myCanvas');
var ctx=canvas.getContext('2d');
var particles = [];
function loop()
{
createParticles();
downParticles();
drawParticles();
window.requestAnimationFrame(loop);
}
window.requestAnimationFrame(loop);
function createParticles()
{
if(particles.length <100)
{
particles.push({
x: Math.random()*canvas.width,
y: 0,
speed: 2+Math.random()*3,
radius: 3+Math.random()*4,
});
}
}
function downParticles()
{
for(var i in particles)
{
var part = particles[i];
part.y += part.speed;
if(part.y > canvas.height)
{
part.x=Math.random()*canvas.width;
part.y=0;
part.speed=2+Math.random()*3;
part.radius=3+Math.random()*4;
}
}
}
function drawParticles()
{
ctx.fillStyle = "black";
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var i in particles)
{
var part = particles[i];
ctx.beginPath();
ctx.arc(part.x,part.y, part.radius, 0, Math.PI*2);
ctx.closePath();
ctx.fillStyle = "white";
ctx.fill();
}
}
</script>
</body>
</html>
在浏览器中打开包含这段HTML代码的html文件,可以在浏览器窗口中看到如图8所示的动画。
图8 下雪了