Canvas上绘制动画

作者:annyran    |    发表于:    |    次访问    |   
文章目录
  1. 1. Canvas上绘制精灵图的一帧
  2. 2. Canvas 上让精灵图动起来
  3. 3. RequestAnimationFrame 与setInterval setTimeout 区别
    1. 3.1. RequestAnimationFrame 与setInterval setTimeout 区别
  4. 4. 关于FramePerSecond和deltaTime
    1. 4.1. 程序里dt(deltaTime)的作用?

Canvas上绘制精灵图的一帧

我们知道我们需要调用drawImage 在画布上去绘制一个图片,那如果我们要绘制一个动图我们需要如何实现呢?我们要获取精灵图片对应的那一帧,然后在画布上绘制。每次绘制一帧,我们需要先清空原先画布上的内容

1
void ctx.drawImage(iamge. sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

注意: sx,sy 是指源精灵图片获取的位置, dx,dy 指的是在画布上需要对应的位置,所以我们写一个Sprite 类,这个类一般要描述下面这些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var bird_opt = {
image: image,
startX: 100, //绘制的起始坐标X
startY: 100, //绘制的起始坐标Y
rect: {x:0,y:0,w:86,h:60},
frames: [ //精灵动画每一帧的相关信息
{x:0,y:0,w:86,h:60},
{x:0,y:60,w:86,h:60},
{x:0,y:120,w:86,h:60}
]
};
function Sprite(opt){
var sprite_proto = {};
sprite_proto.current_frame_index = 0;
sprite_proto.image = opt.image;
sprite_proto.rect = opt.rect;
sprite_proto.frames = opt.frames;
sprite_proto.startX = opt.startX;
sprite_proto.startY = opt.startY;
sprite_proto.getNextFrame = function(){
var curFrame = this.frames[this.current_frame_index];
this.current_frame_index++;
if(this.current_frame_index == this.frames.length)
this.current_frame_index = 0;
return curFrame;
}
return sprite_proto;
}

我们再创建一个绘制画布类DrawCanvas, 主要需要两个方法,一个是renderFrame(绘制精灵的某帧), 一个是清空画布clear

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function DrawCanvas(opt){
this.context = opt.context;
this.width = opt.width;
this.height = opt.height;
}
DrawCanvas.prototype.renderFrame = function(sprite){
var frame = sprite.getNextFrame();
this.context.drawImage(
sprite.image,
frame.x,
frame.y,
frame.w,
frame.h,
sprite.startX,
sprite.startY,
sprite.rect.w,
sprite.rect.h
);
}
DrawCanvas.prototype.clear = function(){
this.context.clearRect(0,0,this.width, this.height);
}

结合上面两个类,我们就可以再画布上先绘制精灵图的第一帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var image = new Image();
image.src = '/images/bird.png';
image.onload = function(){
var bird_opt = {
image: image,
startX: 100,
startY: 100,
rect: {x:0,y:0,w:86,h:60},
frames: [
{x:0,y:0,w:86,h:60},
{x:0,y:60,w:86,h:60},
{x:0,y:120,w:86,h:60}
]
},
birdSprite = Sprite(bird_opt),
canvas = document.getElementById('canvas'),
canvas_opt = {
context: canvas.getContext('2d'),
width:canvas.width,
height:canvas.height
};
var myCanvas = new DrawCanvas(canvas_opt);
myCanvas.renderFrame(birdSprite);
};

Canvas 上让精灵图动起来

我们需要绘制我们的精灵图,那么我们需要setInterval清空我们的画布,绘制对应帧数的图片。

1
2
3
4
5
6
7
//canvas上动画的绘制是通过计时器去实现的
setInterval(function(){
myCanvas.clear();
myCanvas.renderFrame(birdSprite);
}, 200);
//但其实做动画的最佳循环是使用requestAnimationFrame,所以我们换种写法

注意:我们创建我们的sprite之前我们需要预先将我们使用的图片下载完,所以我们需要

1
2
3
4
5
var image = new Image();
image.src = '/images/bird.png';
image.onload = function(){
.....
}

所以在下一篇文章里我们会看到相关框架里面会实现preload,预先加载所有要使用的资源然后再开始绘制。以上,就是canvas动画主要的实现原理。如果想要我们的动画在不同方向上再进行移动,即我们只要不断更改我们绘制坐标位置即可。

拓展阅读1:canvas动画优化小议

拓展阅读2:提高 HTML5 画布性能

  • 拓展阅读里面提到我们对动画可以分层,让不同频率的动画绘制在不同的图层上,同时我们对这一次图层的变化与上一次图层的内容进行对比,找出变化的区域,这也是我们称为的脏区域,这样只要更新脏区域的内容,就能提升canvas的性能了。

RequestAnimationFrame 与setInterval setTimeout 区别

我们以前做动画的时候,都知道要动画流畅,需要让页面1s渲染60帧,所以我们的时间间隔会设置为1/60*1000 = 16 ms 。再后来看到的文章都指出,动画不要使用setInterval或者setTimeout 去实现我们想要的效果,而应该使用RAF,那RAF和setTimeout 与 setInterval的区别到底在哪里?

RequestAnimationFrame 与setInterval setTimeout 区别

  • setInterval和setTimeout 在非当前窗口,或者浏览器‘休眠’的时候,即使渲染停止了,但是计时器不会休息,仍然会顽强的跑着!这样会加大浏览器的负担影响页面性能。

  • 第二个不同在于callback队列的不同:

    我们假设这里有3个循环计数器的实现,A是利用RAF的递归调用做的,B是用setInterval + 16ms做的,C是用setTimeout + 16ms + 递归调用实现的。
    然后用这3个计步器来做一个callback的循环调用。同时假设这个callback执行消耗的时间固定为100ms。
    那么我们可以大致猜想一下 B在做这件事情的具体过程:
    B从时间零点开始第一次执行callback,16ms之后发现应该执行第二次了,但是发现第一次还没做完,于是还是得继续第一次没做完的事情,把第二次callback排到自己的一个计划队列里面,等到第一次执行完了再执行队列里的第二次…
    但是恶性循环,32ms之后,第一次的事情还没做完,第3次的任务又来了。。。
    于是,这个任务队列越来越长,越来越长。。。
    而对于A而言每一次任务的完成仍然需要100ms,但是浏览器不会在A没有完成当前任务的时候给它下一次的任务。也就是说A总是花100ms完成一次任务,然后再花100ms完成下一次任务。

结论:A完成10此任务的时间应该是1000ms,但是B和C完成10次任务的总时间应该需要>=1000ms,因为他们还要分心去管理他们的任务队列

关于FramePerSecond和deltaTime

  • FPS: Frame Per Second 1s的帧数
  • dt: Delta Time 1帧与1帧之间的时间间隔
  • 结论: 理论上 1s/FPS = dt
1
2
3
4
5
6
7
8
9
var start = (+new Date), dt, now;
(function loop(){
requestAnimationFrame(function(){
now = (+new Date);
dt = now - start;
start = now;
});
})();
  • 通过FPS与dt可以用来反映动画的流畅程度,那对于程序而言dt 可以干什么呢?

程序里dt(deltaTime)的作用?

dt 在我们控制物体移动时有巨大的作用,举个例子:

假设我们在性能好的机器和性能差一些的机器上同时测试一个游戏。好的机器的FPS稳定在60,差的机器FPS稳定在30。
就像上面Canvas实现的bird动画里面,我们在每帧的update如果都给它的纵向移动距离+1

1
2
> bird.y +=1
>

>

那么在性能好的机子上,1s可以达到60帧,60帧以后bird纵向距离移动了60;但是在差的机子上,1s可能只有30帧,30帧以后bird纵向距离移动了30,这显然并不科学!

所以我们只要

1
2
> bird.y += dt*60
>

>

性能好的机子上dt = 1/60 性能差的机子上dt = 1/30 ,性能好的机子上每帧移动1,性能差的机子上每帧移动2,这样最后的位置是一致的

分享到