下面我将利用“矩形”作为一个特例来讲解:
- JavaScript实现canvas动画的基本原理
- 如何创建一个简单的动画控制器
此外,我还写了一个此文的国际版本,较本文,其代码和样例比较多,且为英文撰写。
一、在canvas上绘制一个矩形
首先,通过var ctx = document.getElementById(“canvas”).getContext(“2d”)获取canvas的2d上下文对象,可以把ctx看做是一只神奇的画笔,如果你想绘制一个实体矩形,那么调用它的fillRect(x,y,width,height)方法,如果你想绘制一条直线,可以调用lineTo(x, y)……
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var context = document.getElementById("canvas").getContext("2d");
//Set location coordinates var x = 100; var y = 50; //Set size var width = 50; var height = 80; //Set colors var backColor = "Red"; var edgeColor = "Black"; ctx.fillStyle = backColor; //Set backColor as pen color ctx.fillRect(x,y,width,height); //Draws a filled rectangle ctx.strokeStyle = edgeColor; //Set edgeColor as pen color ctx.strokeRect(x,y,width,height); //Draws a rectangular outline |
二、构建矩形类
在一个动画中,矩形肯定不止有一个。如果想绘制多个矩形,而且它们各自有不同的样式,那么最好的方法就是:将矩形抽象为一个类。
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 |
Rectangle = function(cfg) //cfg is a object of customize parameters
{ //Set default parameters this.width = 50; this.height = 80; this.x = 100; this.y = 50; this.backColor = "Red"; this.edgeColor = "Black"; //Set customize parameters this.setArguments(cfg); } Rectangle.prototype.setArguments = function(cfg) //Set customize parameters { for(var x in cfg) this[x] = cfg[x]; } Rectangle.prototype.draw = function() //Draw method { ctx.fillStyle = this.backColor; //Set backColor as pen color ctx.fillRect(this.x,this.y,this.width,this.height); //Draws a filled rectangle ctx.strokeStyle = this.edgeColor; //Set edgeColor as pen color ctx.strokeRect(this.x,this.y,this.width,this.height); //Draws a rectangular outline } |
三、移动一个矩形
很可惜ctx只能绘制静态的图形。所以需要我们自己编码实现动画效果。先看看下面这两幅图片:
第二张动态的图片是由4张静态图片组成的,由于人眼的视觉残留效应.所以当多张静态的图片快速切换时,我们便看到了动画。将此原理代码化,来实现矩形的移动动画。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function move()
{ moveShape.animationStatus["Move"] = "new"; timer = setInterval(function() //Run once every 24ms { if(moveShape.animationStatus["Move"] == "new") { ctx.clearRect(0,0,600,400); //Clear the canvas moveShape.nextPosition(); //Set new (x,y) moveShape.draw(); //Draw the rectangle } else clearInterval(timer); //Stop setInterval() when it arrives }, 24); } |
四、丢失的红色矩形
根据之前的三步内容,我编写了一个Demo:
在黄色矩形移动时,红色矩形不见了,问题出在哪呢?回到之前第三步代码的第10行moveShape.draw(),由于重绘canvas时,只调用了黄色矩形的draw(),红色矩形自然就消失了。正确的做法是:在第10行后增加一行代码调用红色矩形的draw(),但是这并不是最好的解决方法。如果canvas上有100个矩形呢?难道要增加100行?比较好的方法是:将画板上的所有图形保存到一个ShapeOnCanvas数组里,把第10行代码替换成:
1
2 |
for(var i=0;i<ShapeOnCanvas.length;i++)
ShapeOnCanvas[i].draw(); //Draw all shapes that on canvas |
五、构建动画控制器
通常的情况是,在同一时刻,有些图形在移动、有些图形在淡入、有些图形在旋转……人都是懒惰的,所以我的目标是一行代码就能实现这些功能,当然这行代码肯定是一个函数调用的接口。
函数调用接口:cmd ({ a1,b1,c1, a2,b2,c2, a3,b3,c3, … });
- a* is a string such as “Move”,”Draw”
- b* is an object of shape
- c* is an object of animation parameters such as {aim_x:10,aim_y:10,moveSpeed:2}
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 35 36 37 38 |
cmd = function()
{ command = arguments; //"command" is a long array that save animation commands //"command[i]" is a string such as "Move","Draw" //"command[i+1]" is an object of shape //"command[i+2]" is an object of animation parameters such as {aim_x:10,aim_y:10,moveSpeed:2} for(var i=0; i<command.length; i+=3) //Do the preparation before start animation(refresh canvas) { command[i+1].animationStatus[command[i]] = "new"; //init all animation status as "new" command[i+1].setArguments(command[i+2]); //Set animation parameters ShapeOnCanvas.push(command[i+1]); //Push it into the stack } timer = setInterval(function() //Run once every 24ms { var j = 0; var allStop = true; //"allStop" is the flag of all animations have been stopped for(var j=0; j<command.length; j+=3) if(command[j+1].animationStatus[command[j]] == "new") { switch(command[j]) { case "Draw": command[j+1].draw(); break; case "Move": command[j+1].nextPosition(); break; } allStop = false; } ctx.clearRect(0,0,600,400); //Clear the canvas for(var i=0;i<ShapeOnCanvas.length;i++) ShapeOnCanvas[i].draw(); //Draw all shapes that on canvas if(allStop) clearInterval(timer); //Stop setInterval() when all animations have been stopped }, 24); } |
如果你想扩充其他动画效果,例如添加“旋转”,只需要在switch添加:
1
2 3 |
case "Rotate":
command[j+1].rotate(); //rotate()应该是根据旋转参数设置图形新的坐标位置 break; |
六、串行动画
上一步的动画控制器只能处理并行的动画,现在我们把它扩展一下,实现串行动画。
其最终效果是:如下代码能实现Demo中的动画效果。
1
2 3 4 5 6 7 8 9 |
cmd("Setup");
cmd ( "Draw",staticShape,{}, "Move",moveShape,{aim_x:400,aim_y:300,moveSpeed:3}, "Move",fastMoveShape,{aim_x:100,aim_y:300,moveSpeed:6} ); cmd("Move",staticShape,{aim_x:300,aim_y:100,moveSpeed:5}); cmd("End"); |
解决方案:创建一个cmdQueue队列,按照调用cmd()的顺序,将传入cmd()的参数(一条并行动画命令)入队,启动一个setInterval(),每隔10ms检测一下之前一条并行动画命令是否执行结束,如果结束,则从队列出队下一条动画命令去执行。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
cmd = function()
{ if(arguments[0] == "Setup") //setup animation { this.cmdQueue = new Array(); //"cmdQueue" is a queue to save animation commands //init the rear and front of the cmdQueue this.rear = 0; this.front = 0; cmdRun = false; var me = this; cmdTimer = setInterval(function() //run once every 10ms { //All previous animation commands have been stopped and there are remaining animation commands if(me.cmdRun == false && me.front < me.rear) { if(me.cmdQueue[me.front][0] == "End") clearInterval(cmdTimer); //Stop cmdTimer else { refresh(me.cmdQueue[me.front]); //Run one animation command that dequeue from cmdQueue me.front++; } } }, 10); } else this.cmdQueue[this.rear++] = arguments; //Enqueue animation commands to cmdQueue } refresh = function(command) { cmdRun = true; for(var i=0; i<command.length; i+=3) //Do the preparation before start animation(refresh canvas) { command[i+1].animationStatus[command[i]] = "new"; //init all animation status as "new" command[i+1].setArguments(command[i+2]); //Set animation parameters ShapeOnCanvas.push(command[i+1]); //Push it into the stack } timer = setInterval(function() //Run once every 24ms { var j = 0; var allStop = true; //"allStop" is the flag of all animations have been stopped for(var j=0; j<command.length; j+=3) if(command[j+1].animationStatus[command[j]] == "new") { switch(command[j]) { case "Draw": command[j+1].draw(); break; case "Move": command[j+1].nextPosition(); break; } allStop = false; } ctx.clearRect(0,0,600,400); //Clear the canvas for(var i=0;i<ShapeOnCanvas.length;i++) ShapeOnCanvas[i].draw(); //Draw all shapes that on canvas if(allStop) { cmdRun = false; clearInterval(timer); //Stop setInterval() when all animations have been stopped } }, 24); } |