canvas元素绘图

HTML5标准中增加的canvas元素可以进行图形图像的绘制工作,为客户端提供更加丰富的编程体验。本文介绍如何使用JavaScript代码和canvas元素绘制图形、图像、文本,以及如何使用渐变色;最后,会创建一个小球在canvas元素中不断运动、回弹的综合示例。

准备画布

canvas有一些基本的属性,如id属性值可以用于JavaScript代码获取canvas元素,width和height属性指定canvas的宽度的高度。下面的代码会在页面中创建一个canvas元素,并做一些初始化工作。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <canvas id="cv" width="600" height="400"
            style="border:2px solid gray;"></canvas>
</body>
</html>
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    //
    g.closePath();
</script>

代码中创建了id为"cv"的canvas元素,宽度为600像素,高度为400像素,边框宽度2像素,颜色为灰色(gray)。JavaScript代码中,首先,使用document对象的getElementById()方法获取canvas元素,然后,使用canvas元素的getContext("2d")方法获取图形绘制的上下文对象;一组绘制操作使用图形对象的beginPath()开始,由closePath()方法关闭,如果只有一组绘制操作,也可以省略beginPath()和closePath()方法。下面介绍各种图形、文本和图像的绘制。

绘制线段

绘制线段相关的方法包括:

  • moveTo(),定义开始绘制的坐标。
  • lineTo()方法,绘制从开始坐标或上次结束坐标到指定坐标的线段。
  • stroke()方法,执行绘制。

此外,指定线条宽度可以使用lineWidth属性设置,单位为像素;strokeStyle属性指定线条的颜色,可以使用颜色名或CSS中支持的颜色格式;lineCap属性则指定线段两端的线帽风格,其值包括:

  • butt,默认值,线段两端显示为方形。
  • square,线段两端显示为方形,但线帽在指定的线段长度之外。
  • round,线段两端为半圆形,线帽在指定的线段长度之外。

下面的代码,通过两条线段绘制了一条折线。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 5;
    g.lineCap = "round";
    g.moveTo(10, 10);
    g.lineTo(200, 100);
    g.lineTo(500, 50);
    g.strokeStyle = "red";
    g.stroke();
    //
    g.closePath();
</script>

绘制效果如下图所示。

绘制线段

绘制矩形

绘制矩形时,同样可以使用lineWidth属性设置线条宽度,使用strokeStyle属性设置线条颜色,矩形参数则使用rect()方法指定,参数分别是左上角水平坐标和垂直坐标、矩形宽度和高度。下面的代码演示了矩形的绘制。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 5;
    g.rect(60, 60, 200, 100);
    g.strokeStyle = "red";
    g.stroke();
    //
    g.closePath();
</script>

绘制效果如下图所示。

绘制矩形

创建填充矩形参数时使用fillRect()方法,参数分别是左上角水平坐标和垂直坐标、矩形宽度和高度。填充颜色使用fillStyle属性指定。下面的代码会绘制一个线条宽度为10像素的矩形和一个填充矩形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 10;
    g.rect(60, 60, 200, 100);
    g.rect(60, 180, 200, 100);
    g.strokeStyle = "red";
    g.stroke();
    // 填充矩形
    g.fillStyle = "yellow";
    g.fillRect(60, 60, 200, 100);
    //
    g.closePath();
</script>

绘制效果如下图所示。

绘制矩形和填充矩形

请注意观察矩形和填充矩形的绘制特点,上面的图形,矩形和填充矩形使用了相同的参数,而绘制的矩形区域有所不同。这里,矩形的左上角坐标是设置坐标分别减去线宽(lineWidth)的一半,所占区域的宽度是设置宽度加上线宽,所占高度则是设置高度加上线宽;而填充矩形则是按设置的坐标和尺寸绘制,工作中应计算好不同的图形实际绘制区域的位置和尺寸。

绘制多边形

多边形的绘制,可以由多条线段绘制成一个封闭图形来实现,如下面的代码绘制了一个三角形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 3;
    g.moveTo(100, 100);
    g.lineTo(50, 200);
    g.lineTo(100, 300);
    g.lineTo(100, 100);
    g.strokeStyle = "red";
    g.stroke();
    //
    g.closePath();
</script>

绘制封闭图形时,起始坐标与结束坐标应相同,绘制效果如下图所示。

绘制三角形

绘制填充封闭图形时,moveTo()和lineTo()方法只需要指定顶点坐标,并使用fillStyle属性设置填充色,使用fill()方法提交绘制操作。下面的代码演示了相关应用。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 2;
    g.moveTo(100, 100);
    g.lineTo(50, 200);
    g.lineTo(100, 300);
    g.lineTo(100, 100);
    g.strokeStyle = "red";
    g.stroke();
    //
    g.closePath();
    //
    g.beginPath();
    g.moveTo(100, 100);
    g.lineTo(50, 200);
    g.lineTo(100, 300);
    g.fillStyle = "yellow";
    g.fill();
    g.closePath();
    //
    g.beginPath();
    g.moveTo(300, 100);
    g.lineTo(250, 200);
    g.lineTo(300, 300);
    g.fillStyle = "yellow";
    g.fill();
    g.closePath();
</script>

代码执行效果如下图所示。

线段三角形和填充三角形

绘制圆形和弧线

绘制圆形和弧线都使用arc()方法,其参数依次为圆心X坐标、圆心Y坐标、圆的半径、开始角度、结束角度、是否逆时针方向;其中,角度使用π值。如下面代码可以绘制一个圆形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.beginPath();
    // 绘制
    g.lineWidth = 5;
    g.arc(300, 200, 100, 0, 2 * Math.PI);
    g.strokeStyle = "blue";
    g.stroke();
    //
    g.closePath();
</script>

绘制效果如下图所示。

绘制圆形

下图显示了arc()参数设置的示意图,其中,(x,y)为圆心坐标,水平方向向右为0π,向左为1π,垂直方向向下为0.5π,向上为1.5π。

arc()方法参数示意

下面的代码,通过两条弧线组成一个圆形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    g.lineWidth = 5;
    g.arc(300, 200, 100, 0, Math.PI);
    g.strokeStyle = "blue";
    g.stroke();
    g.closePath();
    //
    g.beginPath();
    g.lineWidth = 5;
    g.arc(300, 200, 100, 0, Math.PI, true);
    g.strokeStyle = "red";
    g.stroke();
    g.closePath();
</script>

本例,下面的半圆使用默认的顺时针方向绘制,上面的半圆则使用逆时针方向绘制,结果如下图所示。

绘制弧线

绘制填充圆形时,同样使用fillStyle属性设置填充色,使用fill()方法完成绘制,如下面的代码。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    g.lineWidth = 5;
    g.arc(300, 200, 100, 0, 2 * Math.PI);
    g.fillStyle = "blue";
    g.fill();
    g.closePath();
</script>

绘制效果如下图所示。

绘制填充圆形

arcTo()方法可以定义基于角的两条边内切圆的弧线,参数依次为A点水平坐标、A点垂直坐标、B点水平坐标、B点垂直坐标、内切圆的半径;A点为角的顶点,一条边是经过AB两点的线;另一条边上的点可以使用moveTo()方法指定,这里称为C点,也是弧线的起始点,则另一条边是连接AC两点的线段。arcTo()方法绘制的弧线起点在起始点(C点),然后经过内切圆与线段AC和线段AB的切点。下面的代码演示了arcTo()方法的应用,并绘制了两条线段作为参考。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.lineWidth = 3;
    //
    g.beginPath();
    g.moveTo(50, 50);
    g.lineTo(300,50);
    g.lineTo(50, 200);
    g.strokeStyle = "gray";
    g.stroke();
    g.closePath();
    //
    g.beginPath();
    g.moveTo(50, 50);
    g.arcTo(300, 50, 50, 200, 50);
    g.strokeStyle = "black";
    g.stroke();
    g.closePath();
</script>

绘制效果如下图所示,A、B、C为标注的三个坐标点。

arcTo()方法绘制效果

绘制椭圆

绘制椭圆时使用ellipse()方法,其参数依次是中心X坐标、中心Y坐标、水平方向半径、垂直方向半径、旋转角度、开始角度、结束角度、是否逆时针方向绘制。下面的代码代码绘制了两个椭圆形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.lineWidth = 5;
    g.strokeStyle = "blue";
    //
    g.beginPath();
    g.ellipse(150, 200, 100, 50, 0, 0, 2 * Math.PI);
    g.stroke();
    g.closePath();
    //
    g.beginPath();
    g.ellipse(400, 200, 100, 50, 0.2 * Math.PI, 0, 2 * Math.PI);
    g.stroke();
    g.closePath();
</script>

绘制效果如下图所示。

绘制椭圆形

下面的代码演示了绘制填充椭圆形。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    g.ellipse(150, 200, 100, 50, 0, 0, 2 * Math.PI);
    g.fillStyle = "blue";
    g.fill();
    g.closePath();
</script>

绘制效果如下图所示。

绘制填充椭圆

绘制曲线

首先介绍bezierCurveTo()方法,用于定义贝塞尔曲线的绘制参数。绘制贝塞尔曲线需要定义四个点,绘制起始点使用moveTo()方法指定,bezierCurveTo()方法的6个参数分别指定第一控制点、第二控制点、结束点的坐标。下面的代码演示了贝塞尔曲线的绘制。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.lineWidth = 3;
    //
    g.beginPath();
    g.moveTo(50, 300);
    g.bezierCurveTo(150,100,350,300,500,30);
    g.strokeStyle = "blank";
    g.stroke();
    g.closePath();
    //
    g.strokeStyle = "gray";
    g.beginPath();
    g.moveTo(50, 300);
    g.lineTo(150, 50);
    g.stroke();
    g.closePath();
    //
    g.beginPath();
    g.moveTo(350, 300);
    g.lineTo(500, 30);
    g.stroke();
    g.closePath();
</script>

绘制效果如下图所示。

绘制贝塞尔曲线

quadraticCurveTo()方法用于绘制二次贝塞尔曲线,需要三个点坐标,其中,起始点坐标使用moveTo()方法指定,quadraticCurveTo()方法的4个参数,分别定义控制点坐标和结束点坐标。下面的代码演示了二次贝塞尔曲线的绘制。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.lineWidth = 3;
    //
    g.beginPath();
    g.moveTo(100, 300);
    g.quadraticCurveTo(100,100,350,50);
    g.strokeStyle = "blank";
    g.stroke();
    g.closePath();
    //
    g.strokeStyle = "gray";
    g.beginPath();
    g.moveTo(100, 100);
    g.lineTo(100, 300);
    g.stroke();
    g.closePath();
    //
    g.beginPath();
    g.moveTo(100, 100);
    g.lineTo(350, 50);
    g.stroke();
    g.closePath();
</script>

绘制效果如下图所示。

绘制二次贝塞尔曲线

绘制文本

绘制文本时可以关注如下属性和方法:

  • font属性,设置字体参数,如"bold italic 50px Arial",设置字体为Arial,显示为加粗、斜体和50像素。
  • strokeText()方法,绘制镂空字体的文本,参数分别指定文本内容和绘制区域的左上角坐标。
  • fillText()方法,绘制填充文本,参数分别指定文本内容和绘制区域的左上角坐标。可以使用fillStyle属性设置填充色或填充图像。

下面的代码演示了文本绘制的实现。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    g.lineWidth = 3;
    //
    g.beginPath();
    g.lineWidth = 1;
    g.strokeStyle = "red";
    g.font = "bold 50px Arial";
    g.strokeText("绘制文本ABCabc123",50,50);
    g.closePath();
    //
    g.beginPath();
    g.fillStyle = "blue";
    g.font = "bold 50px Arial";
    g.fillText("绘制文本ABCabc123", 50, 150);
    g.closePath();
</script>

绘制效果如下图所示。

绘制文本

绘制图像

绘制图像是使用drawImage()方法,其完整参数设置如下:

  • 参数一,指定图片对象。
  • 参数二,指定裁剪起始位置的X坐标。
  • 参数三,指定裁剪起始位置的Y坐标。
  • 参数四,指定裁剪的宽度。
  • 参数五,指定裁剪的高度。
  • 参数六,指定绘制位置的左上角X坐标。
  • 参数七,指定绘制位置的左上角Y坐标。
  • 参数八,指定绘制宽度。
  • 参数九,指定绘制高度。

此外,绘制不需要裁剪和缩放的图像时可以只使用三个参数,依次为图像对象、绘制区域左上角水平坐标和垂直坐标;需要指定绘制区域尺寸时,可以在上述三个参数后再指定区域的宽度和高度,绘制的图像会按指定的尺寸缩放。

下面的代码演示了读取、裁剪和绘制/demo/earth.jpg图片的操作。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <canvas id="cv" width="800" height="550"
            style="border:2px solid gray;"></canvas>
</body>
</html>
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    let img = new Image()
    img.src = "/demo/earth.jpg";
    //
    g.beginPath();
    g.drawImage(img, 0, 0);
    g.drawImage(img, 10, 220, 400, 92);
    g.drawImage(img, 200, 0, 400, 195, 10, 330, 400, 195);
    g.closePath();
</script>

本例使用的图像尺寸为800*195像素,代码中修改了canvas元素的尺寸,宽度为800像素,高度为550像素,图像绘制效果如下图所示。

绘制图像

代码中,第一个drawImage()方法绘制了完整的图像。第二个drawImage()方法通过设置宽度和高度指定了绘制区域的尺寸,图像会缩放会显示。第三个drawImage()方法裁剪了图像的一部分,然后绘制到canvas元素中。

此外,图像也可以从页面中的img元素中获取,如下面的代码。

JavaScript
let img = document.getElementById("earth");
g.drawImage(img,0,0);

代码会从id为earth的元素中获取图像并绘制到canvas元素的左上角。

填充图案与渐变色

绘制填充图形或文本时,除了使用颜色填充,还可以使用图像或创建渐变色填充,下面的代码,先来看如何使用图像填充文本。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    let img = new Image()
    img.src = "/demo/moon1.jpg";
    let pattern = g.createPattern(img, "repeat");
    g.fillStyle = pattern;
    g.font = "bold 50px Arial";
    g.fillText("图像填充文本",50,50);
    g.closePath();
</script>

下图中,上面为/demo/moon1.jpg图片,尺寸为800*50像素;下面为填充文本效果。

图像填充文本

canvas支持线性渐变色和放射性渐变色,其中,线性渐变色使用createLinearGradient()方法创建,其参数依次为渐变起始X坐标、起始Y坐标、结束X坐标、结束Y坐标。下面的代码演示了水平方向线性渐变色的应用。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    let lg = g.createLinearGradient(50, 50, 250, 50);
    lg.addColorStop(0, "yellow");
    lg.addColorStop(1, "red");
    g.fillStyle = lg;
    g.rect(50, 50, 200, 100);
    g.fill();
    g.closePath();
</script>

代码显示效果如下图所示。

水平线性渐变

创建渐变色时,还需要在渐变对象中使用addColorStop()方法指定位置和颜色,其中,参数一是0到1的数值,表示颜色的位置,如0表示起始位置,1表示结束位置,0.5表示中间位置。参数二指定颜色,可以使用颜色名称,也可以使用CSS支持的颜色格式,如十六进制颜色值等,如"red"、"#FF0000"都表示红色。

创建水平方向的渐变色时,应注意createLinearGradient()方法中的两个Y坐标应相同,如前例中的代码;创建垂直方向的渐变色时,两个X坐标应相同,如下面的代码。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    let lg = g.createLinearGradient(50, 50, 50, 150);
    lg.addColorStop(0, "yellow");
    lg.addColorStop(1, "#FF0000");
    g.fillStyle = lg;
    g.rect(50, 50, 200, 100);
    g.fill();
    g.closePath();
</script>

显示效果如下图所示。

垂直方向渐变色

下面的代码演示了从左上角到右下角的渐变效果。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    let lg = g.createLinearGradient(50, 50, 250, 150);
    lg.addColorStop(0, "yellow");
    lg.addColorStop(1, "red");
    g.fillStyle = lg;
    g.rect(50, 50, 200, 100);
    g.fill();
    g.closePath();
</script>

显示效果如下图所示。

对角渐变色

createRadialGradient()方法创建放射性渐变图像,其参数依次为第一个圆形圆心的X坐标、Y坐标、半径、第二个圆形圆心的X坐标、Y坐标、半径;定义放射性渐变图像对象后,同样需要使用addColorStop()方法添加颜色。下面的代码演示了放射性渐变图像的实现。

JavaScript
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //
    g.beginPath();
    let rg = g.createRadialGradient(300,200,50,300,200,150);
    rg.addColorStop(0, "yellow");
    rg.addColorStop(1, "green");
    g.fillStyle = rg;
    g.arc(300, 200, 150, 0, 2 * Math.PI);
    g.fill();
    g.closePath();
</script>

绘制效果如下图所示。

放射性渐变色

让图形动起来

下面的代码会创建一个困在屏幕中的小球,小球会在canvas元素区域不停的运动,碰撞到边界时会改变方向回弹。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>困在屏幕里的小球</title>
</head>
<body>
    <canvas id="cv" width="600" height="400"
            style="border:2px solid gray;"></canvas>
    <div>
        <button type="button" onclick="gameState=1;">开始</button>
        <button type="button" onclick="gameState=0;">暂停</button>
    </div>
</body>
</html>
<script>
    let cv = document.getElementById("cv");
    let g = cv.getContext("2d");
    //  屏幕尺寸
    const screenWidth = 600;
    const screenHeight = 400;
    // 小球参数
    let ballRadius = 16;
    let speedX = 10;
    let speedY = 10;
    //
    let curX = 0;
    let curY = 0;
    let nextX = 0;
    let nextY = 0;
    // 游戏状态,1执行,0暂停
    let gameState = 0;

    // 绘制小球
    function drawBall(cx, cy) {
        g.beginPath();
        g.arc(cx, cy, ballRadius, 0, 2 * Math.PI);
        g.fillStyle = "blue";
        g.fill();
        g.closePath();
    }

    // 游戏循环,每帧代码
    function gameLoop() {
        if (gameState === 0) return;
        if (gameState === 1) {
            // 清屏
            g.clearRect(0, 0, screenWidth, screenHeight);
            // 计算小球位置
            nextX = curX + speedX;
            nextY = curY + speedY;
            // 左右边界碰撞检查
            if (nextX < ballRadius) {
                nextX = ballRadius;
                speedX = 0 - speedX;
            } else if (nextX > screenWidth - ballRadius) {
                nextX = screenWidth - ballRadius;
                speedX = 0 - speedX;
            }
            // 上下边界碰撞检查
            if (nextY < ballRadius) {
                nextY = ballRadius;
                speedY = 0 - speedY;
            } else if (nextY > screenHeight - ballRadius) {
                nextY = screenHeight - ballRadius;
                speedY = 0 - speedY;
            }
            // 保存当前坐标
            curX = nextX;
            curY = nextY;
            // 绘制小球
            drawBall(curX, curY);
        }
    }

    // 设置小球开始位置
    curX = Math.floor(Math.random() * 300) + ballRadius;
    curY = Math.floor(Math.random() * 200) + ballRadius;
    drawBall(curX, curY);

    // 开始游戏
    setInterval(gameLoop, 25);
</script>

页面初始画面如下图。请注意,小球的初始位置是随机生成的,所以,每次开始的小球位置并不相同。

困在屏幕中的小球

页面中定义了三个主要元素,分别是:

  • canvas元素,用于显示小球的活动区域。
  • “开始”按钮,点击后小球开始活动,由变量gameState变量设置为1实现。
  • “暂停”按钮,点击后小球暂停活动,由变量gameState变量设置为0实现。

JavaScript代码中,首先定义了一系列的变量和常量,下面分类说明。

cv这是获取了canvas元素对象,g变量则是绘制所需的上下文对象。

screenWidth和screenHeight常量分别定义了小球活动区域的宽度和高度,和canvas元素的宽度和高度相同。

小球参数中,ballRadius变量定义了小球的半径,speedX变量定义了小球水平方向运动的速度,speedY变量定义了小球垂直方向运动的速度。curX和curY变量为小球当前坐标,nextX和nextY变量为小球下一帧到达的坐标,这四个变量会在计算小球位置时使用。

gameState变量表示游戏运行状态,约定1表示正常运行,0表示暂停。

drawBall()函数的功能是在canvas元素中绘制小球,参数为小球圆心的坐标。

gameLoop()函数定义了处理小球运动每一帧的代码,首先,游戏状态(gameState)为0时什么也不做,保持静态。游戏状态(gameState)为1时,会调用clearRect()方法清除canvas元素,为下次绘制小球做准备。clearRect()方法的参数是清除区域的左上角X坐标和Y坐标、清除区域的宽度和高度,代码中设置的参数会清理canvas元素的全部区域。

计算小球的新位置时,首先使用nextX和nextY分别加上水平方向速度(speedX)和垂直方向速度(speedY)计算下次显示的位置。然后会进行四个边界的碰撞测试,当新位置到达边界时,会改变其速度,使用0减去速度,得到其相反的数据,这样,小球就会向相反的方向运动。curX和curY变量会保存小球的新坐标,并调用drawBall()函数绘制小球。

代码的最后,先使用随机数给出小球的初始位置,然后调用drawBall()函数显示小球。启动游戏循环时使用了setInterval()函数,参数一指定游戏的每一帧会执行gameLoop()函数,参数二指定每帧间隔为25毫秒,即每秒40帧。