HTML5Canvas动画原理

下面我将利用“矩形”作为一个特例来讲解:

  • 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);
    }
原文地址:https://www.cnblogs.com/liuguanghuiyes/p/3039328.html