Canvas 2D详解
2021-03-31 00:19:50

在我书的第六章中有一个关于MNIST手写数字的例子,当数据集加载完成之后,用户可以在<canvas/>上输入手写数字,点击「预测」按钮之后,浏览器会弹出经模型预测之后的结果;在我书的第九章和第十章中,分别有关于目标检测和人体姿态检测的案例,当关键点的得分符合一定要求时,会通过<canvas/>将关键部分绘制出来,请看效果:

图1 - MNIST手写数字案例

图2 - 人体姿态检测案例

图3 - 目标检测案例

接下来,我们将在本文中详细讲解一下上述三个模块均用到的一个技术<canvas/>

参考资料

  1. MDN Web Docs:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
  2. 《JavaScript高级程序设计(第三版)》
  3. 《HTML5权威指南》

1.canvas概述

HTML5最受欢迎的功能就是<canvas>元素,Canvas API提供了一个通过JavaScriptHTML<canvas>元素来绘制图形的方式,可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面,该API主要聚焦于2D图形,我们也可以使用WebGL API绘制硬件加速的2D3D图形。

2.基本用法

使用<canvas>大概可以分为两个步骤:

  1. HTML中定义<canvas>标签;
  2. 获取<canvas>对象的上下文并判断getContext()方法是否存在;

了解了使用<canvas>的大概步骤之后,我们首先在页面中定义一个<canvas>标签,请看代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas示例</title>
</head>
<body>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
</body>
</html>

该标签有两个属性——widthheight,当没有设置widthheight的值时,<canvas>标签会初始化大小为300*150px的矩形,我们也可以通过CSS去设置<canvas>元素的大小,但在绘制时图像会伸缩以适应它的尺寸框架,可能会出现扭曲等情况,所以推荐大家使用widthheight属性明确的规定宽度和高度,接下来,请看效果展示:

图4 - 画布展示

在页面中定义了<canvas>标签之后,我们需要获取该标签的绘图上下文,请看示例代码:

var canvas = document.getElementById("canvas")
if (canvas.getContext){
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = "orange"
    ctx.fillRect(10,10,100,100)
}

上述代码中,我们首先通过 document.getElementById()方法获取页面中的<canvas>标签,并通过getContext()方法获取渲染上下文和它的绘画功能,接着我们通过ctx.fillStyle()ctx.fillRect()方法在<canvas>中绘制了一个左上角(xy)坐标为(10,10)且大小为100*100px的绿色正方形,请看效果展示:

图5 - 绘制矩形

3.2D上下文

使用2D绘图上下文提供的方法可以绘制简单的2D图形,比如矩形、弧线和路径。2D上下文的坐标开始于<canvas>元素的左上角,原点坐标是(0,0),所有坐标值都是基于这个原点计算,x值越大表示越靠右,y值越大表示越靠下。

图6 - canvas绘制规则

3.1 填充和描边

2D上下文的两种基本绘图操作是填充和描边。填充就是用指定的样式(颜色、渐变或图像)填充图形;描边就是只在图形的边缘画线。大多数2D上下文操作都会细分为填充和描边两个操作,其操作结果取决于fillStyle属性和strokeStyle属性,其默认值均为#000000,请看代码示例:

var canvas = document.getElementById("canvas")
if (canvas.getContext){
    var ctx = canvas.getContext("2d");
    ctx.fillRect(10,10,100,100)
    ctx.strokeRect(120,10,100,100)

}

上述代码中,我们通过ctx.fillRect()方法和ctx.strokeRect()方法分别绘制了两个矩形,其中通过ctx.fillRect()方法绘制的为填充矩形;通过ctx.strokeRect()方法绘制的为描边矩形,其样式均采用默认样式,请看演示效果:

图7 - 案例展示

3.2 绘制矩形

canvas只支持两种形式的图形绘制:矩形和路径,矩形是唯一一种可以直接在2D上下文中绘制的形状,它提供了三种方法绘制矩形,请看详细介绍:

  1. fillRext(x,y,width,height)

该方法用于绘制一个填充的矩形,可以通过fillStyle属性决定当前矩形的填充样式;

2.strokeRect(x,y,width,height)

该方法用于绘制一个矩形的边框;

3.clearRect(x,y,width,height)

该方法用于清除指定矩形区域,让清除部分完全透明;

上述提供的三个方法中均包含相同的参数,xy指定了在<canvas>画布上所绘制矩形左上角(相对于原点)的坐标,widthheight设置矩形的尺寸,请看演示案例:

var canvas = document.getElementById("canvas")
if (canvas.getContext){
    var ctx = canvas.getContext("2d");
    ctx.fillRect(100,100,300,300)
    ctx.clearRect(150,150,200,200)
    ctx.strokeRect(200,200,100,100)
}

上述代码中,我们在大小为500*500px的画布中绘制了三个矩形,其中第一个矩形左上角坐标为(100,100),大小为300*300;第二个矩形左上角坐标为(150,150),大小为200*200;第三个矩形左上角坐标为(200,200),大小为100*100,请看效果演示:

图8 - 案例展示

3.3 绘制路径

要绘制路径,首先必须调用beginPath()方法,表示要开始绘制新的路径,然后调用以下方法来绘制路径,请看方法介绍:

1.arc(x,y,radius,startAngle,endAngle,anticlockwise)

该方法会绘制一个以(x,y)为圆心,以radius为半径的圆弧,从startAngle开始到endAngle结束,其中,startAngle表示圆弧的起始点(单位:弧度),endAngle表示圆弧的终点(单位:弧度),我们还可以指定anticlockwise的参数值来规定绘制圆弧的方向(默认为顺时针);

2.arcTo(x1,y1,x2,y2,radius)

该方法根据给定的控制点(其中x1,y1表示第一个控制点的坐标,x2,y2表示第二个控制点的坐标,)和半径画一段圆弧,再以直线连接两个控制点;

3.lineTo(x,y)

该方法绘制一条从当前位置到指定x以及y位置的直线;

4.moveTo(x,y)

该方法将笔触移动到指定的坐标x以及y上,当<canvas>初始化或者beginPath()调用之后,我们通常会使用moveTo()函数设置起点;

5.rect(x,y,width,height)

该方法绘制一个左上角坐标为(x,y),宽高为widthheight的矩形;

创建了路径之后,我们可以调用closePath()方法绘制一条连接到路径起点的线条;也可以调用fill()方法并指定fillStyle属性完成填充;另外,也可以通过stroke()方法对路径描边,并通过strokeStyle指定其样式;

接下来,请看演示案例:

var canvas = document.getElementById("canvas")
if (canvas.getContext){
    var ctx = canvas.getContext("2d");
    //绘制一个圆形
    ctx.beginPath()
    ctx.moveTo(250,150)
    ctx.arc(250,150,10,0,2*Math.PI)
    ctx.fillStyle = "aqua"
    ctx.fill()
    ctx.closePath()

    //绘制一条线
    ctx.beginPath()
    ctx.moveTo(150,200)
    ctx.lineTo(350,200)
    ctx.lineWidth = 10
    ctx.strokeStyle = "blue"
    ctx.stroke()
    ctx.closePath()

    //绘制一个矩形
    ctx.beginPath()
    ctx.moveTo(150,250)
    ctx.rect(150,250,200,200)
    ctx.fillStyle = "green"
    ctx.fill()
    ctx.closePath()
}

上述代码中我们分别绘制了圆形、直线、矩形,请看效果展示:

图9 - 案例展示

3.4 绘制文本

canvas提供了两种方法来渲染文本,请看详细介绍:

  1. fillText(text,x,y,[,maxWidth])

该方法在指定的(x,y)位置填充指定的文本,其中maxWidth表示绘制的最大宽度,为可选参数;

2.strokeText(text,x,y,[,maxWidth])

该方法在指定的(x,y)位置绘制文本边框,其中maxWidth表示绘制的最大宽度,为可选参数;

我们可以通过设置fonttextAligntextBaselinedirection的值来对文本进行渲染,请看详细介绍:

  • font:表示文本样式、大小及字体,默认字体是10px sans-serif

  • textAlign:表示文本对齐方式,其值包括left(文本左对齐)、right(文本右对齐)、center(文本居中对齐)、start(文本对齐界线开始的地方,即左对齐指本地从左向右,右对齐指本地从右向左)、end(文本对齐界线结束的地方,即左对齐指本地从左向右,右对齐指本地从右向左),默认值为start

  • textBaseline:表示文本的基线,其值包括top(文本基线在文本块的顶部)、hanging(文本基线是悬挂基线)、middle(文本基线在文本块的中间)、alphabetic(文本基线是标准的字母基线)、ideographicbottom,默认值是 alphabetic

  • direction:此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀;

接下来,请看演示案例:

var canvas = document.getElementById("canvas")
   if (canvas.getContext){
       var ctx = canvas.getContext("2d");
       ctx.font = "bold 30px Arial"
       ctx.textAlign = "center"
       ctx.textBaseline = 'middle'
       ctx.fillText("石璞东",100,100)
       ctx.strokeText("石璞东",200,100)
   }

上述代码中我们通过ctx.fillText()方法和ctx.strokeText()方法绘制了字符串石璞东,并指定了其字体样式、对齐方式等,请看效果演示:

图10 - 案例展示

3.5 绘制图像

可以用drawImage()方法在画布上绘制图像,该方法需要三个、五个或九个参数,请看参数解释:

  1. drawImage(image, x, y)

其中,imageimage或者canvas对象,xy是其在目标canvas里的起始坐标;

2.drawImage(image, x, y, width, height)

该方法多了两个参数:widthheight,这两个参数用来控制当向canvas画入时应该缩放的大小;

3.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

第一个参数和其它的是相同的,都是一个图像或者另一个canvas的引用,前4个是定义图像源的切片位置和大小,后4个则是定义切片的目标显示位置和大小,如图所示:

图11 - 图解参数

接下来,请看演示案例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas示例</title>
    <style>
        img{
            width: 250px;
            height: 250px;
        }
    </style>
</head>
<body>
<img src="author.jpg" id="image">
<button id="btn">绘制图像</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
    var canvas = document.getElementById("canvas")
    var imgEle = document.getElementById("image")
    var oclear = document.getElementById("clear")
    var obtn = document.getElementById("btn")
    var num = 0;
    if (canvas.getContext){
        var ctx = canvas.getContext("2d");
        obtn.onclick = () => {
            if(num == 0){
                ctx.drawImage(imgEle,20,20)
            }else if (num == 1){
                ctx.drawImage(imgEle,20,20,100,100)
            }else{
                ctx.drawImage(imgEle,20,20,300,300,10,10,200,200)
            }
        }
        oclear.onclick = () => {
            ctx.clearRect(0,0,500,500)
            num >= 2 ? num=0:num++;
        }
    }
</script>
</body>
</html>

上述代码中,我们分别指定了当ctx.drawImage()方法分别有3个、5个、9个参数时绘制图像的效果,请看效果展示:

图12 - 案例展示

读者在每次绘制完成当前状态下的图像之后,需要点击「清除画布」按钮清除当前画布。

实际上,我们还可以将video元素作为drawImage()方法的图像来源,请看代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas示例</title>
</head>
<body>
<video src="Lenet.mp4" id="video" controls width="360" height="240" preload="auto"></video>
<button id="btn">绘制图像</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
    var canvas = document.getElementById("canvas")
    var videoEle = document.getElementById("video")
    var oclear = document.getElementById("clear")
    var obtn = document.getElementById("btn")
    if (canvas.getContext){
        var ctx = canvas.getContext("2d");
        obtn.onclick = () => {
            ctx.drawImage(videoEle,0,0,360,360)
        }
        oclear.onclick = () => {
            ctx.clearRect(0,0,500,500)
        }
    }
</script>
</body>
</html>

上述代码中,我们可以通过点击「绘制图像」按钮将当前帧图像绘制在canvas中;我们还可以在视频中绘制一个矩形,请看代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas示例</title>
</head>
<body>
<video src="Lenet.mp4" id="video" controls width="360" height="240" preload="auto"></video>
<button id="btn">开始预测</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
    var canvas = document.getElementById("canvas")
    var videoEle = document.getElementById("video")
    var oclear = document.getElementById("clear")
    var obtn = document.getElementById("btn")

    if (canvas.getContext){
        var ctx = canvas.getContext("2d");
        var width = 180;
        var height = 150;
        ctx.lineWidth = 5;
        ctx.strokeStyle = "aqua"
        obtn.onclick = () => {
            ctx.drawImage(videoEle,0,0,360,360)
            ctx.strokeRect(30,90,width,height)
            ctx.font = "20px bold Arial"
            ctx.fillStyle ="blue"
            ctx.fillText("computer",100,85)
        }
        oclear.onclick = () => {
            ctx.clearRect(0,0,500,500)
        }
    }
</script>
</body>
</html>

上述代码中,我们在视频中绘制了一个矩形,并用矩形框框出了视频在0:01秒时电脑出现的位置,请看效果演示:

图13 - 案例展示

4.你画我猜(MNIST手写数字版)

讲解完成了<canvas>的基础知识之后,我们来看看本书中用到的<canvas>的相关内容,在你画我猜(MNIST手写数字版)这个案例中,我们先在页面中定义一个<canvas>标签,当用户按下鼠标并拖动时,可以在画布上画出拖动的轨迹,请看代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>你画我猜(MNIST手写数字版)canvas示例</title>
</head>
<body>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<button id="clear">清除画布</button>
<script>
    var canvas = document.getElementById("canvas")
    var oclear = document.getElementById("clear")
    if (canvas.getContext){
        var ctx = canvas.getContext("2d");
        canvas.onmousemove = (e) => {
            if(e.buttons == 1){
                ctx.fillStyle = "black"
                ctx.fillRect(e.offsetX,e.offsetY,5,5)
            }
        }
        oclear.onclick = () => {
            ctx.clearRect(0,0,500,500)
        }
    }
</script>
</body>
</html>

请看效果演示:

图13 - 案例展示

5.绘制关键点(人体姿态检测、目标识别)

在本书关于目标检测的案例中,我们需要使用手机的摄像头,并实时的框选出摄像头所采集的每一帧数据中的物体,如图3所示,其中涉及到的canvas相关的知识请参考3.5小节,这里不在赘述。

6.文章最后

以上就是本文的所有内容,小伙伴们学会了嘛?快去实践一下吧!更多详情请关注我的更多开源作品:

1. 微信公众号(hahaCoder)

图14 - 微信公众号

2. 微信小程序(hahaAI)

图15 - 微信小程序

3. Github
链接地址:https://github.com/TURBO1002