欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

从零开始学 Canvas 到决定放弃的实战教程(图文解析)

最编程 2024-02-18 11:36:58
...

本文简介

点赞 + 关注 + 收藏 = 学会了


在前端领域,如果只是懂 Vue 或者 React ,未来在职场的竞争力可能会比较弱。

根据我多年在家待业经验来看,前端未来在 数据可视化AI 这两个领域会比较香,而 Canvas 是数据可视化在前端方面的基础技术。

本文就用光的速度将 canvas 给入门了。

01.gif

要入门一个技术,前期最重要是快!所以本文只讲入门内容,能应付简单项目。深入的知识点会在其他文章讲解。



Canvas 是什么?

  • Canvas 中文名叫 “画布”,是 HTML5 新增的一个标签。
  • Canvas 允许开发者通过 JS在这个标签上绘制各种图案。
  • Canvas 拥有多种绘制路径、矩形、圆形、字符以及图片的方法。
  • Canvas 在某些情况下可以 “代替” 图片。
  • Canvas 可用于动画、游戏、数据可视化、图片编辑器、实时视频处理等领域。


Canvas 和 SVG 的区别

Canvas SVG
用JS动态生成元素(一个HTML元素) 用XML描述元素(类似HTML元素那样,可用多个元素来描述一个图形)
位图(受屏幕分辨率影响) 矢量图(不受屏幕分辨率影响)
不支持事件 支持事件
数据发生变化需要重绘 不需要重绘

就上面的描述而言可能有点难懂,你可以打开 AntV 旗下的图形编辑引擎做对比。G6 是使用 canvas 开发的,X6 是使用 svg 开发的。


我的建议是:如果要展示的数据量比较大,比如一条数据就是一个元素节点,那使用 canvas 会比较合适;如果用户操作的交互比较多,而且对清晰度有要求(矢量图),那么使用 svg 会比较合适。



起步

学习前端一定要动手敲代码,然后看效果展示。

起步阶段会用几句代码说明 canvas 如何使用,本例会画一条直线。


画条直线

  1. HTML 中创建 canvas 元素
  2. 通过 js 获取 canvas 标签
  3. canvas 标签中获取到绘图工具
  4. 通过绘图工具,在 canvas 标签上绘制图形

02.png

<!-- 1、创建 canvas 元素 -->
<canvas
  id="c"
  width="300"
  height="200"
  style="border: 1px solid #ccc;"
></canvas>

<script>
  // 2、获取 canvas 对象
  const cnv = document.getElementById('c')

  // 3、获取 canvas 上下文环境对象
  const cxt = cnv.getContext('2d')

  // 4、绘制图形
  cxt.moveTo(100, 100) // 起点坐标 (x, y)
  cxt.lineTo(200, 100) // 终点坐标 (x, y)
  cxt.stroke() // 将起点和终点连接起来
</script>

moveTolineTostroke 方法暂时可以不用管,它们的作用是绘制图形,这些方法在后面会讲到~


注意点

1、默认宽高

canvas默认的 宽度(300px) 和 高度(150px)

如果不在 canvas 上设置宽高,那 canvas 元素的默认宽度是300px,默认高度是150px。


2、设置 canvas 宽高

canvas 元素提供了 widthheight 两个属性,可设置它的宽高。

需要注意的是,这两个属性只需传入数值,不需要传入单位(比如 px 等)。

<canvas width="600" height="400"></canvas>

3、不能通过 CSS 设置画布的宽高

使用 css 设置 canvas 的宽高,会出现 内容被拉伸 的后果!!!

03.png

<style>
  #c {
    width: 400px;
    height: 400px;
    border: 1px solid #ccc;
  }
</style>

<canvas id="c"></canvas>

<script>
  // 1、获取canvas对象
  const cnv = document.getElementById('c')

  // 2、获取canvas上下文环境对象
  const cxt = cnv.getContext('2d')

  // 3、绘制图形
  cxt.moveTo(100, 100) // 起点
  cxt.lineTo(200, 100) // 终点
  cxt.stroke() // 将起点和终点连接起来

  console.log(cnv.width) // 获取 canvas 的宽度,输出:300
  console.log(cnv.height) // 获取 canvas 的高度,输出:150
</script>

canvas 的默认宽度是300px,默认高度是150px。

  1. 如果使用 css 修改 canvas 的宽高(比如本例变成 400px * 400px),那宽度就由 300px 拉伸到 400px,高度由 150px 拉伸到 400px。
  2. 使用 js 获取 canvas 的宽高,此时返回的是 canvas 的默认值。

最后出现的效果如上图所示。


4、线条默认宽度和颜色

线条的默认宽度是 1px ,默认颜色是黑色。

但由于默认情况下 canvas 会将线条的中心点和像素的底部对齐,所以会导致显示效果是 2px 和非纯黑色问题。


5、IE兼容性高

暂时只有 IE 9 以上才支持 canvas 。但好消息是 IE 已经有自己的墓碑了。

如需兼容 IE 7 和 8 ,可以使用 ExplorerCanvas 。但即使是使用了 ExplorerCanvas 仍然会有所限制,比如无法使用 fillText() 方法等。



基础图形

坐标系

在绘制基础图形之前,需要先搞清除 Canvas 使用的坐标系。

Canvas 使用的是 W3C 坐标系 ,也就是遵循我们屏幕、报纸的阅读习惯,从上往下,从左往右。

04.jpg

W3C 坐标系数学直角坐标系X轴 是一样的,只是 Y轴 的反向相反。

W3C 坐标系Y轴 正方向向下。


直线

一条直线

最简单的起步方式是画一条直线。这里所说的 “直线” 是几何学里的 “线段” 的意思。

需要用到这3个方法:

  1. moveTo(x1, y1):起点坐标 (x, y)
  2. lineTo(x2, y2):下一个点的坐标 (x, y)
  3. stroke():将所有坐标用一条线连起来

起步阶段可以先这样理解。

05.png

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 绘制直线
  cxt.moveTo(50, 100) // 起点坐标
  cxt.lineTo(200, 50) // 下一个点的坐标
  cxt.stroke() // 将上面的坐标用一条线连接起来
</script>

上面的代码所呈现的效果,可以看下图解释(手不太聪明,画得不是很标准,希望能看懂)

06.jpg


多条直线

如需画多条直线,可以用会上面那几个方法。

07.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.stroke()

  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.stroke()
</script>

仔细观察一下,为什么两条线的粗细不一样的?

明明使用的方法都是一样的,只是第二条直线的 Y轴 的值是有小数点。

答:默认情况下 canvas 会将线条的中心点和像素的底部对齐,所以会导致显示效果是 2px 和非纯黑色问题。

08.jpg

上图每个格子代表 1px

线的中心点会和画布像素点的底部对齐,所以会线中间是黑色的,但由于一个像素就不能再切割了,所以会有半个像素被染色,就变成了浅灰色。

所以如果你设置的 Y轴 值是一个整数,就会出现上面那种情况。


设置样式

  • lineWidth:线的粗细
  • strokeStyle:线的颜色
  • lineCap:线帽:默认: butt; 圆形: round; 方形: square

09.png

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 绘制直线
  cxt.moveTo(50, 50)
  cxt.lineTo(200, 50)

  // 修改直线的宽度
  cxt.lineWidth = 20

  // 修改直线的颜色
  cxt.strokeStyle = 'pink'

  // 修改直线两端样式
  cxt.lineCap = 'round' // 默认: butt; 圆形: round; 方形: square

  cxt.stroke()
</script>

新开路径

开辟新路径的方法:

  • beginPath()

在绘制多条线段的同时,还要设置线段样式,通常需要开辟新路径。

要不然样式之间会相互污染。

比如这样

10.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 第一条线
  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.lineWidth = 10
  cxt.strokeStyle = 'pink'
  cxt.stroke()

  // 第二条线
  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.stroke()
</script>

如果不想相互污染,需要做2件事:

  1. 使用 beginPath() 方法,重新开一个路径
  2. 设置新线段的样式(必须项)

如果上面2步却了其中1步都会有影响。

只使用 beginPath()

11.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 第一条线
  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.lineWidth = 10
  cxt.strokeStyle = 'pink'
  cxt.stroke()

  // 第二条线
  cxt.beginPath() // 重新开启一个路径
  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.stroke()
</script>

第一条线的样式会影响之后的线。

但如果使用了 beginPath() ,后面的线段不会影响前面的线段。

12.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 第一条线
  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.stroke()

  // 第二条线
  cxt.beginPath() // 重新开启一个路径
  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.lineWidth = 4
  cxt.strokeStyle = 'red'
  cxt.stroke()
</script>

设置新线段的样式,没使用 beginPath() 的情况

这个情况会反过来,后面的线能影响前面的线。

13.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // 第一条线
  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.lineWidth = 10
  cxt.strokeStyle = 'pink'
  cxt.stroke()

  // 第二条线
  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.lineWidth = 4
  cxt.strokeStyle = 'red'
  cxt.stroke()
</script>

正确的做法

在设置 beginPath() 的同时,也各自设置样式。这样就能做到相互不影响了。

14.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(20, 100)
  cxt.lineTo(200, 100)
  cxt.lineWidth = 10
  cxt.strokeStyle = 'pink'
  cxt.stroke()

  cxt.beginPath() // 重新开启一个路径
  cxt.moveTo(20, 120.5)
  cxt.lineTo(200, 120.5)
  cxt.lineWidth = 4
  cxt.strokeStyle = 'red'
  cxt.stroke()
</script>

折线

直线 差不多,都是使用 moveTo()lineTo()stroke() 方法可以绘制折线。

15.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(50, 200)
  cxt.lineTo(100, 50)
  cxt.lineTo(200, 200)
  cxt.lineTo(250, 50)

  cxt.stroke()
</script>

画这种折线,最好在草稿纸上画一个坐标系,自己计算并描绘一下每个点大概在什么什么位置,最后在 canvas 中看看效果。


矩形

根据前面的基础,我们可以 使用线段来描绘矩形,但 canvas 也提供了 rect() 等方法可以直接生成矩形。


使用线段描绘矩形

可以使用前面画线段的方法来绘制矩形

16.png

canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
 const cnv = document.getElementById('c')
 const cxt = cnv.getContext('2d')

 // 绘制矩形
 cxt.moveTo(50, 50)
 cxt.lineTo(200, 50)
 cxt.lineTo(200, 120)
 cxt.lineTo(50, 120)
 cxt.lineTo(50, 50) // 需要闭合,又或者使用 closePath() 方法进行闭合,推荐使用 closePath()

 cxt.stroke()
</script>

上面的代码几个点分别对应下图。

17.jpg


使用 strokeRect() 描边矩形

  • strokeStyle:设置描边的属性(颜色、渐变、图案)
  • strokeRect(x, y, width, height):描边矩形(x和y是矩形左上角起点;width 和 height 是矩形的宽高)
  • strokeStyle 必须写在 strokeRect() 前面,不然样式不生效。

18.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // strokeStyle 属性
  // strokeRect(x, y, width, height) 方法
  cxt.strokeStyle = 'pink'
  cxt.strokeRect(50, 50, 200, 100)
</script>

上面的代码可以这样理解

19.jpg


使用 fillRect() 填充矩形

fillRect()strokeRect() 方法差不多,但 fillRect() 的作用是填充。

需要注意的是,fillStyle 必须写在 fillRect() 之前,不然样式不生效。

20.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  // fillStyle 属性
  // fillRect(x, y, width, height) 方法
  cxt.fillStyle = 'pink'
  cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>

同时使用 strokeRect()fillRect()

同时使用 strokeRect()fillRect() 会产生描边和填充的效果

21.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.strokeStyle = 'red'
  cxt.strokeRect(50, 50, 200, 100) // strokeRect(x, y, width, height)
  cxt.fillStyle = 'yellow'
  cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>

使用 rect() 生成矩形

rect()fillRect() 、strokeRect() 的用法差不多,唯一的区别是:

strokeRect()fillRect() 这两个方法调用后会立即绘制;rect() 方法被调用后,不会立刻绘制矩形,而是需要调用 stroke()fill() 辅助渲染。


22.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.strokeStyle = 'red'
  cxt.fillStyle = 'pink'

  cxt.rect(50, 50, 200, 100) // rect(x, y, width, height)

  cxt.stroke()
  cxt.fill()
</script>

等价公式:

cxt.strokeStyle = 'red',
cxt.rect(50, 50, 200, 100)
cxt.stroke()

// 等价于
cxt.strokeStyle = 'red'
cxt.strokerect(50, 50, 200, 100)


// -----------------------------


cxt.fillStyle = 'hotpink'
cxt.rect(50, 50, 200, 100)
cxt.fill()

// 等价于
cxt.fillStyle = 'yellowgreen'
cxt.fillRect(50, 50, 200, 100)

使用 clearRect() 清空矩形

使用 clearRect() 方法可以清空指定区域。

clearRect(x, y, width, height)

其语法和创建 cxt.rect() 差不多。


23.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.fillStyle = 'pink' // 设置填充颜色
  cxt.fillRect(50, 50, 200, 200) // 填充矩形

  cxt.clearRect(60, 60, 180, 90) // 清空矩形
</script>

清空画布

canvas 画布元素是矩形,所以可以通过下面的代码把整个画布清空掉。

// 省略部分代码

cxt.clearRect(0, 0, cnv.width, cnv.height)

要清空的区域:从画布左上角开始,直到画布的宽和画布的高为止。


多边形

Canvas 要画多边形,需要使用 moveTo()lineTo()closePath()


三角形

虽然三角形是常见图形,但 canvas 并没有提供类似 rect() 的方法来绘制三角形。

需要确定三角形3个点的坐标位置,然后使用 stroke() 或者 fill() 方法生成三角形。


24.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>

  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(50, 50)
  cxt.lineTo(200, 50)
  cxt.lineTo(200, 200)

  // 注意点:如果使用 lineTo 闭合图形,是不能很好闭合拐角位的。
  cxt.lineTo(50, 50) // 闭合

  cxt.stroke()

</script>

注意,默认情况下不会自动从最后一个点连接到起点。最后一步需要设置一下 cxt.lineTo(50, 50) ,让它与 cxt.moveTo(50, 50) 一样。这样可以让路径回到起点,形成一个闭合效果。

但这样做其实是有点问题的,而且也比较麻烦,要记住起始点坐标。

上面的闭合操作,如果遇到设置了 lineWidth 或者 lineJoin 就会有问题,比如:

25.png

// 省略部分代码
cxt.lineWidth = 20

当线段变粗后,起始点和结束点的链接处,拐角就出现“不正常”现象。


如果需要真正闭合,可以使用 closePath() 方法。

26.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(50, 50)
  cxt.lineTo(200, 50)
  cxt.lineTo(200, 200)
  // 手动闭合
  cxt.closePath()

  cxt.lineJoin = 'miter' // 线条连接的样式。miter: 默认; bevel: 斜面; round: 圆角
  cxt.lineWidth = 20
  cxt.stroke()
</script>

使用 cxt.closePath() 可以自动将终点和起始点连接起来,此时看上去就正常多了。


菱形

有一组邻边相等的平行四边形是菱形

27.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.moveTo(150, 50)
  cxt.lineTo(250, 100)
  cxt.lineTo(150, 150)
  cxt.lineTo(50, 100)
  cxt.closePath()
  cxt.stroke()
</script>

要绘制直线类型的图形,在草稿纸上标记出起始点和每个拐角的点,然后再连线即可。相对曲线图形来说,直线图形是比较容易的。


圆形

绘制圆形的方法是 arc()

语法:

arc(x, y, r, sAngle, eAngle,counterclockwise)
  • xy: 圆心坐标
  • r: 半径
  • sAngle: 开始角度
  • eAngle: 结束角度
  • counterclockwise: 绘制方向(true: 逆时针; false: 顺时针),默认 false

28.jpg

开始角度和结束角度,都是以弧度为单位。例如 180°就写成 Math.PI ,360°写成 Math.PI * 2 ,以此类推。

在实际开发中,为了让自己或者别的开发者更容易看懂弧度的数值,1°应该写成 Math.PI / 180

  • 100°: 100 * Math.PI / 180
  • 110°: 110 * Math.PI / 180
  • 241°: 241 * Math.PI / 180

注意:绘制圆形之前,必须先调用 beginPath() 方法!!! 在绘制完成之后,还需要调用 closePath() 方法!!!


29.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.beginPath()
  cxt.arc(150, 150, 80, 0, 360 * Math.PI / 180)
  cxt.closePath()

  cxt.stroke()
</script>

半圆

如果使用 arc() 方法画圆时,没做到刚好绕完一周(360°)就直接闭合路径,就会出现半圆的状态。

30.png

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const cxt = cnv.getContext('2d')

  cxt.beginPath()
  cxt.arc(150, 150, 100, 0, 180 * Math.PI / 180) // 顺时针
  cxt.closePath()

  cxt.stroke()
</script>

上面的代码中,cxt.arc 最后一个参数没传,默认是 false ,所以是顺时针绘制。

31.jpg


如果希望半圆的弧面在上方,可以将 cxt.arc 最后一个参数设置成 true

上一篇: TypeScript全栈开发中最推荐学习的技术架构:TRPC

下一篇: 实操展示:打造史上最惊艳的3D地球——三丰js项目实例