ZetCode

HTML5 Canvas 中的动画

最后修改于 2023 年 7 月 17 日

在本章中,我们在 HTML5 Canvas 中创建动画。

动画 是连续快速播放的图像,给人以运动的错觉。然而,动画并不仅限于运动。随时间改变对象的背景也被认为是动画。

在 HTML5 Canvas 中创建动画有三个函数:

setInterval 函数每隔指定的毫秒数(delay)重复执行传入的函数。setTimeout 在指定的毫秒数(delay)后执行指定的函数。为了创建动画,setTimeout 会在其执行的函数内部调用。requestAnimationFrame 函数允许浏览器在下次重绘之前调用指定的函数来更新动画。浏览器会进行一些优化。

沿曲线移动

在第一个动画中,一个对象沿曲线移动。

move_along_curve.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas move along curve</title>
<style>
    canvas { border: 1px solid #bbbbbb }
</style>
<script>
    var canvas;
    var ctx;
    var x = 20; 
    var y = 80;
    const DELAY = 30;
    const RADIUS = 10;

    function init() {
        
        canvas = document.getElementById('myCanvas');
        ctx = canvas.getContext('2d');
        
        setInterval(move_ball, DELAY);
    }
    
    function draw() {        
        
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        ctx.fillStyle = "cadetblue";
        ctx.arc(x, y, RADIUS, 0, 2*Math.PI);
        ctx.fill();
    }
    
    function move_ball() {
        
        x += 1;
        
        if (x > canvas.width + RADIUS) {
            x = 0;
        }
        
        y = Math.sin(x/32)*30 + 80;
        draw();
    } 
</script>
</head>

<body onload="init();">
    <canvas id="myCanvas" width="350" height="150">
    </canvas>
</body>
</html> 

该示例将一个圆沿正弦曲线移动。当圆移动到画布的右侧边界之外时,它会重新出现在左侧。

setInterval(move_ball, DELAY);

setInterval 函数会每隔 DELAY 毫秒调用一次 move_ball 函数。

function draw() {        
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.fillStyle = "cadetblue";
    ctx.arc(x, y, RADIUS, 0, 2*Math.PI);
    ctx.fill();
}

draw 方法使用 clearRect 方法清除画布,并绘制一个具有更新后的 x 和 y 坐标的新圆。

function move_ball() {
    
    x += 1;
    
    if (x > canvas.width + RADIUS) {
        x = 0;
    }
    
    y = Math.sin(x/32)*30 + 80;
    draw();
}

move_ball 函数中,我们更新圆心点的 x 和 y 坐标。我们检查球是否已经超过画布的右边缘,然后调用 draw 方法重新绘制画布。

淡出

淡出是一种改变对象状态的动画。它是一种过渡动画。

fading_out.html
<!DOCTYPE html>
<html>
<head>
<style>
    canvas {border: 1px solid #bbbbbb}
</style>
<title>HTML5 canvas fading out</title>
<script>
    var canvas;
    var ctx;
    
    var alpha = 1;
    
    var rx = 20;
    var ry = 20;
    var rw = 120;
    var rh = 80;

    const DELAY = 20;
    
    function init() {
        
        canvas = document.getElementById('myCanvas');
        ctx = canvas.getContext('2d');
        
        canvas.addEventListener("click", onClicked);
        
        ctx.fillRect(rx, ry, rw, rh)
    }
    
    function onClicked(e) {
        var cx = e.x;
        var cy = e.y;
        
        if (cx >= rx && cx <= rx + rw && 
            cy >= ry && cy <= ry + rh) {
            fadeout();
        }
    }
    
    function fadeout() {
    
        if (alpha < 0) {
            canvas.removeEventListener("click", onClicked);
            ctx.globalAlpha = 1;
            ctx.fillStyle = 'white';
            ctx.fillRect(rx, ry, rw, rh);
            return;
        }         
        
        ctx.clearRect(rx, ry, rw, rh);
        ctx.globalAlpha = alpha;
        ctx.fillRect(rx, ry, rw, rh)
 
        alpha -= 0.01;
        
        setTimeout(fadeout, DELAY);
    }
        
</script>
</head>

<body onload="init();">
    <canvas id="myCanvas" width="350" height="250">
    </canvas>
</body>
</html> 

有一个矩形对象。当我们点击矩形时,它开始淡出。

canvas.addEventListener("click", onClicked);

使用 addEventListener 方法向画布添加了一个 click 监听器。收到鼠标点击后,会调用 onClicked 函数。

ctx.fillRect(rx, ry, rw, rh)

最初,画布上会绘制一个具有默认黑色填充的矩形。

function onClicked(e) {
    var cx = e.x;
    var cy = e.y;
    
    if (cx >= rx && cx <= rx + rw && 
        cy >= ry && cy <= ry + rh) {
        fadeout();
    }
}

onClicked 函数内部,我们确定鼠标点击的 x 和 y 坐标。我们将鼠标坐标与矩形的外部边界进行比较,如果它落在矩形的区域内,则调用 fadeout 方法。

if (alpha < 0) {
    canvas.removeEventListener("click", onClicked);
    ctx.globalAlpha = 1;
    ctx.fillStyle = 'white';
    ctx.fillRect(rx, ry, rw, rh);
    return;
}    

当矩形完全透明时,我们删除监听器,并用不透明的白色填充该区域。return 语句会结束 fadeout 函数的递归调用。

ctx.clearRect(rx, ry, rw, rh);
ctx.globalAlpha = alpha;
ctx.fillRect(rx, ry, rw, rh)

矩形的区域被清除,并以更新的 alpha 值填充。

alpha -= 0.01;

alpha 值会减少一小部分。

setTimeout(fadeout, DELAY);

DELAY 毫秒后,fadeout 方法会从其自身内部调用。这种做法称为递归

气泡

以下示例灵感来自 Java 2D 演示。

bubbles.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas bubbles</title>
<style>
    canvas {
        border: 1px solid #bbb;
        background: #000;
    }
</style>
<script>
    var cols = ["blue", "cadetblue", "green", "orange", "red", "yellow",
        "gray", "white"];
        
    const NUMBER_OF_CIRCLES = 35;
    const DELAY = 30;
        
    var maxSize;
    var canvas;
    var ctx;
    var circles;
    
    function Circle(x, y, r, c) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.c = c;
    } 

    function init() {
        
        canvas = document.getElementById('myCanvas');
        ctx = canvas.getContext('2d');
        
        circles = new Array(NUMBER_OF_CIRCLES);
        
        initCircles();
        doStep();
    }
    
    function initCircles() {
        
        var w = canvas.width;
        var h = canvas.height;

        maxSize = w / 10; 

        for (var i = 0; i < circles.length; i++) {

            var rc = getRandomCoordinates();
            var r = Math.floor(maxSize * Math.random());   
            var c = cols[Math.floor(Math.random()*cols.length)]
            
            circles[i] = new Circle(rc[0], rc[1], r, c);
        }
    }        
    
    function doStep() {
        
        for (var i = 0; i < circles.length; i++) {
            
            var c = circles[i];
            c.r += 1;
            
            if (c.r > maxSize) {
 
                var rc = getRandomCoordinates();
                c.x = rc[0];
                c.y = rc[1];
                c.r = 1;
            } 
        }
        
        drawCircles();
        setTimeout(doStep, DELAY);
    }
    
    function getRandomCoordinates() {
        
        var w = canvas.width;
        var h = canvas.height;
        
        var x = Math.floor(Math.random() * (w - (maxSize / 2)));
        var y = Math.floor(Math.random() * (h - (maxSize / 2)));
        
        return [x, y];
    }
    
    function drawCircles() {
        
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        for (var i = 0; i < circles.length; i++) {
            
            ctx.beginPath();
            ctx.lineWidth = 2.5;
            
            var c = circles[i];
            ctx.strokeStyle = c.c;
            ctx.arc(c.x, c.y, c.r, 0, 2*Math.PI);
            ctx.stroke();
        }
    }
</script>
</head>

<body onload="init();">
    <canvas id="myCanvas" width="350" height="250">
    </canvas>
</body>
</html>

在此示例中,屏幕上会随机出现和消失不断变大的彩色气泡。

var cols = ["blue", "cadetblue", "green", "orange", "red", "yellow",
    "gray", "white"];

这些颜色用于绘制气泡。

function Circle(x, y, r, c) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.c = c;
} 

这是 Circle 对象的构造函数。除了 x 和 y 坐标以及半径之外,它还包含用于颜色值的 c 属性。

circles = new Array(NUMBER_OF_CIRCLES);

circles 数组用于保存圆对象。

for (var i = 0; i < circles.length; i++) {

    var rc = getRandomCoordinates();
    var r = Math.floor(maxSize * Math.random());   
    var c = cols[Math.floor(Math.random()*cols.length)]
    
    circles[i] = new Circle(rc[0], rc[1], r, c);
}

circles 数组被填充了圆。我们计算随机坐标、随机初始半径和随机颜色值。

function doStep() {

doStep 代表程序的一个动画周期。

for (var i = 0; i < circles.length; i++) {
    
    var c = circles[i];
    c.r += 1;
    
    if (c.r > maxSize) {

        var rc = getRandomCoordinates();
        c.x = rc[0];
        c.y = rc[1];
        c.r = 1;
    } 
}

我们遍历 circles 数组并增加每个圆的半径。当圆达到最大尺寸时,它会被随机重新定位并最小化。

setTimeout(doStep, DELAY);

使用 setTimeout 方法创建动画。您可能需要调整 DELAY 值以适应您的硬件。

function drawCircles() {
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    for (var i = 0; i < circles.length; i++) {
        
        ctx.beginPath();
        ctx.lineWidth = 2.5;
        
        var c = circles[i];
        ctx.strokeStyle = c.c;
        ctx.arc(c.x, c.y, c.r, 0, 2*Math.PI);
        ctx.stroke();
    }
}

drawCircles 函数清除画布并绘制数组中的所有圆。

星空

以下示例创建了一个星空动画。

starfield.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas star field</title>
<script>
var canvas_w;
var canvas_h;
var canvas;
var ctx;
var layer1;
var layer2;
var layer3;

const DELAY = 20;
const N_STARS = 60;
const SPEED1 = 3;
const SPEED2 = 2;
const SPEED3 = 1;

function init() {
    
    canvas = document.getElementById("myCanvas");
    ctx = canvas.getContext("2d");
    
    canvas_w = canvas.width;
    canvas_h = canvas.height;

    layer1 = new layer(N_STARS, SPEED1, "#ffffff");
    layer2 = new layer(N_STARS, SPEED2, "#dddddd");
    layer3 = new layer(N_STARS, SPEED3, "#999999");

    setTimeout("drawLayers()", DELAY);
}

function star() {
    
    this.x = Math.floor(Math.random()*canvas_w);
    this.y = Math.floor(Math.random()*canvas_h);
    
    this.move = function(speed) {
        
        this.y = this.y + speed;
        
        if (this.y > canvas_h) { 
            
            this.y = 0;
            this.x = Math.floor(Math.random()*canvas_w);
        }
    }
    
    this.draw = function(col) {
        
        ctx.fillStyle = col;
        ctx.fillRect(this.x, this.y , 1, 1);
    }
}

function layer(n, sp, col) {
    
    this.n = n;
    this.sp = sp;
    this.col = col;
    this.stars = new Array(this.n);
    
    for (var i=0; i < this.n; i++) {
        this.stars[i] = new star();
    }
    
    this.moveLayer = function() {
        
        for (var i=0; i < this.n; i++) {
            this.stars[i].move(this.sp);
        }
    }
    
    this.drawLayer = function() {
        
        for (var i=0; i < this.n; i++) {
            this.stars[i].draw(this.col);
        }
    }
}

function drawLayers() {

    ctx.fillStyle = '#000000';      
    ctx.fillRect(0, 0, canvas_w, canvas_h);
    
    layer1.moveLayer();
    layer2.moveLayer();
    layer3.moveLayer();
    
    layer1.drawLayer();
    layer2.drawLayer();
    layer3.drawLayer();
    
    setTimeout("drawLayers()", DELAY);
}

</script>
</head>

<body onload="init();">
    <canvas id="myCanvas" width="800" height="600">
    </canvas>
</body>
</html> 

星空动画是通过创建三个不同的图层来实现的;每个图层包含具有不同速度和颜色阴影的星星(小点)。前景图层中的星星更亮、移动更快,后景中的星星更暗、移动更慢。

layer1 = new layer(N_STARS, SPEED1, "#ffffff");
layer2 = new layer(N_STARS, SPEED2, "#dddddd");
layer3 = new layer(N_STARS, SPEED3, "#999999");

创建了三个星星图层。它们具有不同的速度和颜色阴影。

function star() {
    
    this.x = Math.floor(Math.random()*canvas_w);
    this.y = Math.floor(Math.random()*canvas_h);
...    

创建星星时,会为其赋予随机坐标。

this.move = function(speed) {
    
    this.y = this.y + speed;
    
    if (this.y > canvas_h) { 
        
        this.y = 0;
        this.x = Math.floor(Math.random()*canvas_w);
    }
}

move 方法移动星星;它会增加其 y 坐标。

this.draw = function(col) {
    
    ctx.fillStyle = col;
    ctx.fillRect(this.x, this.y , 1, 1);
}

draw 方法在画布上绘制星星。它使用 fillRect 方法以给定的颜色阴影绘制一个小矩形。

function layer(n, sp, col) {
    
    this.n = n;
    this.sp = sp;
    this.col = col;
    this.stars = new Array(this.n);
...    

图层是具有给定速度和颜色阴影的 n 个星星的集合。星星存储在 stars 数组中。

for (var i=0; i < this.n; i++) {
    this.stars[i] = new star();
}

在创建图层时,stars 数组会被填充星对象。

this.moveLayer = function() {
    
    for (var i=0; i < this.n; i++) {
        this.stars[i].move(this.sp);
    }
}

moveLayer 方法遍历星星数组并调用每个星星的 move 方法。

this.drawLayer = function() {
    
    for (var i=0; i < this.n; i++) {
        this.stars[i].draw(this.col);
    }
}

同样,drawLayer 方法调用每个星星的 draw 方法。

function drawLayers() {

    ctx.fillStyle = '#000000';      
    ctx.fillRect(0, 0, canvas_w, canvas_h);
    
    layer1.moveLayer();
    layer2.moveLayer();
    layer3.moveLayer();
    
    layer1.drawLayer();
    layer2.drawLayer();
    layer3.drawLayer();
    
    setTimeout("drawLayers()", DELAY);
}

drawLayers 函数移动每个图层的星星并在画布上绘制它们。它会在 DELAY 毫秒后调用自身,从而创建动画。

在本章的 HTML5 Canvas 教程中,我们介绍了动画。