前言
平时看到的网页本质上是图片,HTML 和 CSS 经过浏览器渲染流程,最终光栅化为位图。W3C 也陆续提供了更多的标签和 CSS3 属性,以完成更多的设计需求。
但越来越复杂的页面效果在 DOM 结构下很难实现,大量、复杂的 DOM 渲染处理性能堪忧,推出更多的 CSS 属性也难以覆盖需求。
于是 Canvas 诞生了,通过 JS 在 <canvas>
标签形成的画布区域内绘制图形,跳过了 HTML 和 CSS 的渲染,就像直接编写绘制指令一样,性能也更好。
兼容性:IE9+,移动端支持良好,Can I use Canvas?
学习 canvas 离不开线性代数、三角函数的知识,为了实现更多的效果,会需要更多的数学公式,当然,这些公式基本理解、会用就行。
参考:
Canvas-MDN
Canvas 从入门到劝朋友放弃(图解版)
案例+图解带你一文读懂Canvas🔥🔥(2W+字)
Canvas 保姆级教程(上):绘制篇
CanvasStudy
Canvas系列教程(从入门到精通)【详细解读】
canvas 标签
<canvas>
是 HTML5 新增的标签,形成一个画布区域,允许使用 JS 在画布上绘制图形。
与 SVG 对比:
- SVG:基于 XML,矢量图,使用 DOM 操作,适合图形变化较少的场景。
- Canvas:基于 JS,位图,适合图形变化较多的场景。
两种常用 JS API:
- Canvas API:
getContext('2d')
,用于 2D 绘图。 - WebGL API:
getContext('webgl')
,用于 3D 绘图,也能实现 2D 绘图。
我们常说的 Canvas,以及本文所述的,是指使用 Canvas API 在 <canvas>
上绘制 2D 图形。
大小(宽高)
<canvas>
具有两个大小。一是作为 HTML 标签元素的大小,二是作为画布的大小(绘图表面大小)。
默认的画布大小为 300px × 150px,元素大小为其内容大小,所以也是 300px × 150px。
标签的 width 和 height 属性设置的是画布大小,而 CSS 设置的是元素大小。
1 | <style> |
通过 JS 获取和控制大小:
注意:当改变画布大小时,画布会被清空,且上下文对象的属性值会被重置。
1 | const canvas = document.querySelector('canvas'); |
现在绘制个图形,一个位置在 (10, 10) 的 50px × 50px 蓝色正方形。
1 | // 获取画布上下文 |
但实际测量会发现,图形的位置和大小均扩大了一倍,这是因为元素大小是画布大小的两倍,导致画布内容被浏览器自动拉伸了两倍,以填充元素大小。
缩放
当画布大小和元素大小不一致时,画布内容会被缩放以适应元素大小,这会导致图形变形、坐标偏移。
缩放规则:坐标也遵循这一规则。
图形实际大小 = 内容大小 × (元素大小 / 画布大小)
案例:
元素宽度是画布宽度的两倍,而元素高度是画布高度的一半,还是绘制一个 50px × 50px 的正方形。
1 | <style> |
实际测量,正方形的大小为 100px × 25px。
所以,为了避免缩放问题,通常不设置元素大小。
渲染上下文
<canvas>
只提供了一个绘图表面,JS 通过渲染上下文上的方法来操作画布。
使用 getContext(ctxType)
获取渲染上下文:
- 2d:获取二维渲染上下文(CanvasRenderingContext2D)。
- webgl、experimental-webgl:获取 WebGL1(OpenGL ES 2.0) 的三维渲染上下文(WebGLRenderingContext)。
- webgl2、experimental-webgl2:获取 WebGL2(OpenGL ES 3.0) 的三维渲染上下文(WebGL2RenderingContext)。
- bitmaprenderer:创建一个只提供将 canvas 内容替换为指定 ImageBitmap 功能的 ImageBitmapRenderingContext。
画布栅格
Canvas 沿用了计算机图形学的坐标系,原点在左上角,x 轴向右,y 轴向下。
画布被看不见的网格所覆盖,每个网格即是一个逻辑像素。
绘制形状
Canvas 只支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段)。
其他类型的图形都是通过若干条路径组合而成的。
Canvas 提供了许多生成不同路径的方法,如直线、弧、贝塞尔曲线等,这使得绘制复杂图形变得容易。
矩形
仅有三种方法绘制矩形:fillRect(x, y, width, height)
绘制填充矩形。填充色为当前的 fillStylestrokeRect(x, y, width, height)
绘制矩形边框。边框色为当前的 strokeStyle。clearRect(x, y, width, height)
清除指定矩形区域,让清除部分完全透明。
x 与 y 为矩形的左上角坐标。
1 | ctx.fillRect(50, 50, 100, 100); |
clearRect 清除整个画布:
1 | ctx.clearRect(0, 0, canvas.width, canvas.height); |
路径
图形的基本元素是路径。路径是通过各种样式的线段或曲线相连形成的不同形状的点的集合。
使用路径绘制图形的步骤:
beginPath()
清空路径列表,即开始绘制新路径。moveTo(x, y)
移动笔触到指定的坐标点,作为子路径的起点。- 使用各种路径绘制命令,在路径列表中积累子路径。
- (可选)
closePath()
封闭连续子路径,即使用直线连接起点和终点。多次使用 moveTo 可能会导致子路径不连续,无法闭合。 - 描边
stroke()
或填充fill()
路径区域,绘制积累的子路径。fill 会自动闭合路径。
1 | ctx.beginPath(); // 开始新路径 |
移动笔触 moveTo
moveTo(x, y)
移动笔触到指定的坐标点,不绘制任何内容。
- 设置子路径起点。
- 绘制一些不连续的路径。
就像写字时,先把笔移动到纸上的某个位置,然后开始书写。
直线 lineTo
lineTo(x, y)
从当前笔触位置绘制一条直线到指定的坐标点。
1 | // 一个等腰三角形 |
圆弧 arc
arc(x, y, radius, startAngle, endAngle, anticlockwise?)
绘制圆弧路径。
(x, y)
圆心坐标。radius
半径。- 路径从起始弧度
startAngle
到结束弧度endAngle
。 anticlockwise
可选,是否逆时针绘制,默认为 false。
弧度 = ( Math.PI / 180 ) × 角度
1 | // 从半圆到全圆 |
矩形 rect
rect(x, y, width, height)
绘制矩形路径。
绘制一个左上角坐标为 (x,y),宽高为 width 以及 height 的矩形。笔触会移动到 (x,y)。
不同于直接绘制矩形的三个方法,rect 只是定义了一个矩形的路径。
1 | ctx.beginPath(); |
贝塞尔曲线
贝塞尔曲线通过外部控制点绘制复杂的曲线。
quadraticCurveTo(cp1x, cp1y, x, y)
绘制二次贝塞尔曲线。bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制三次贝塞尔曲线。
当前坐标为起始点,cp* 为外部控制点,(x, y) 为结束点。
一些可视化工具:
Canvas贝塞尔曲线绘制工具
二次贝塞尔曲线调试器
三次贝塞尔曲线调试器
1 | // 二次贝塞尔曲线,聊天气泡 |
Path2D
Path2D
用于声明一个路径,即创建可以保留并重用的路径。
Path2D()
创建一个新的 Path2D 实例对象,拥有所有路径绘制方法。
1 | new Path2D(); |
1 | const p1 = new Path2D(); |
path.addPath(path[, transform])
将 path 添加到当前路径。
transform 是一个 DOMMatrix 对象,用于转换 path 的坐标。
1 | const p1 = new Path2D("M10 10 h 80 v 80 h -80 Z"); // 可以使用 SVG path 初始化路径 |
样式
图形样式
fillStyle
设置图形的填充样式,对应 fill()
方法。strokeStyle
设置图形的描边样式,对应 stroke()
方法。
默认值都为 #000,可以接受色值、渐变对象和图案对象。
globalAlpha
设置绘制透明度。
色值
接受的色值格式和 CSS 差不多,但不能用 linear-gradient 等函数。
1 | ctx.strokeStyle = "blue"; |
渐变对象
渐变分为两种:
- 线性渐变:
createLinearGradient(x0, y0, x1, y1)
创建一个线性渐变对象。渐变的起点 (x0,y0),终点 (x1,y1)。 - 径向渐变:
createRadialGradient(x0, y0, r0, x1, y1, r1)
创建一个径向渐变对象。定义了两个圆,一个以 (x0,y0) 为原点,半径为 r0 的圆,一个以 (x1,y1) 为原点,半径为 r1 的圆
都返回 CanvasGradient 对象,使用 addColorStop(offset, color)
添加颜色。offset 偏移量为 0~1 的值,表示颜色的位置。
1 | ctx.beginPath(); |
图案对象
createPattern(image, repetition)
创建一个图案对象,用于填充图形。文档
- image 图像对象,可以是
<img>
、<video>
、<canvas>
、ImageBitmap、ImageData、Blob、CanvasRenderingContext2D - repetition 重复方式,可选值:repeat、repeat-x、repeat-y、no-repeat,默认为 repeat
1 | const img = new Image(); |
createPattern 不仅可以用于创建图案,还能将图案用作填充或描边,而 drawImage 只能用于将图像绘制到画布上。
阴影 shadow
shadowColor
阴影颜色。shadowBlur
阴影模糊程度,默认为 0 为无模糊。shadowOffsetX
阴影水平偏移量。正值向右。shadowOffsetY
阴影垂直偏移量。正值向下。
1 | ctx.shadowOffsetX = 10; |
线条样式
宽度 lineWidth
lineWidth
设置线条的宽度,单位 px,默认为 1。
1 | for (let i = 0; i < 10; i++) { |
1px 的线条宽度上和 2px 差不多,但却模糊,这个问题在后面有详解
端点 lineCap
lineCap
设置线条的端点样式,可选值:butt(默认)、round、square。
1 | const lineCaps = ["butt", "round", "square"]; |
连接点 lineJoin
lineJoin
设置线条的连接样式,可选值:miter(默认)、bevel、round。
- miter:斜接,尖角。
- bevel:平角。
- round:圆角。
1 | const lineJoins = ["miter", "bevel", "round"]; |
斜接限制 miterLimit
miterLimit
设置斜接的限制比例,为 lineWidth 的倍数。
用于限制 lineJoin 值为 miter 时,两条线的斜接长度。斜接长度是指线条交接处内角顶点到外角顶点的长度。
默认为 10,表示最大斜接长度为线条宽度的 10 倍,0、负数、Infinity 和 NaN 都会被忽略。
角度越小,斜接长度越长,超过限制时 lineJoin 会变成 bevel。
1 | ctx.lineWidth = 10; |
第一个角的斜接长度超过限制,变成了 bevel,后面的都是 miter,但角度越大,斜接长度越小。
虚线 lineDash
setLineDash(segments)
设置虚线样式。segments 是一个数组,表示虚线的线段和间隙的长度。空数组切换回至实线模式。lineDashOffset
设置虚线的起始偏移量。正数向左偏移,负数向右偏移。getLineDash()
获取当前虚线样式。返回 segments 数组。
segments 中,元素的索引 index 为奇数表示线段长度,偶数 index 表示间隙长度。如 [5, 15] 表示 5px 线段,15px 间隙。若 segments 的长度为奇数,则会重复数组元素,如 [5] 表示 [5, 5],即 5px 线段,5px 间隙。
1 | let y = 15; |
利用 lineDashOffset 可以让虚线动起来,即蚂蚁线效果。
1 | // 蚂蚁线 |
绘制文本
fillText(text, x, y [, maxWidth])
在 (x,y) 填充指定的文本,可选最大宽度。strokeText(text, x, y [, maxWidth])
在 (x,y) 绘制文本边框,可选最大宽度。
样式
1、font 字体样式,和 CSS 一样,默认字体是 10px sans-serif。
2、textAlign 文本对齐方式,默认为 start。
1 | ctx.textAlign = "left" || "right" || "center" || "start" || "end"; |
3、textBaseline 文本基线位置,即文本的垂直对齐方式,默认为 alphabetic。
- top,文本基线在文本块的顶部。
- hanging,文本基线是悬挂基线。
- middle,文本基线在文本块的中间。
- alphabetic,文本基线是标准的字母基线。
- ideographic,文字基线是表意字基线;如果字符本身超出了 alphabetic 基线,那么 ideographic 基线位置在字符本身的底部。
- bottom,文本基线在文本块的底部。
1 | const textBaselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom']; |
4、direction 文本方向,默认为 inherit。会影响 textAlign 的表现。
1 | ctx.direction = "ltr" || "rtl" || "inherit"; |
预测量文本宽度
measureText(text)
返回 TextMetrics 对象,包含文本的宽度、像素等信息。不受最大宽度等外部因素影响。
1 | width:基于当前上下文字体,计算内联字符串的宽度。 |
1 | const text = "Abcdefghijklmnop"; |
由于字母倾斜/斜体导致字符的宽度可能超出其预定的宽度,因此 actualBoundingBoxLeft 和 actualBoundingBoxRight 的总和可能会比内联盒子的宽度(width)更大。
绘制图像
步骤:
- 获取图片资源
- 使用
drawImage()
绘制图像。
drawImage
1、基础用法(三个参数)
1 | drawImage(image, x, y); |
2、指定宽高缩放(五个参数)
1 | drawImage(image, x, y, width, height); |
3、裁剪图像(九个参数)
1 | drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); |
获取图像资源
drawImage()
允许任何的画布图像源,如 HTMLImageElement、SVGImageElement、HTMLVideoElement、HTMLCanvasElement、ImageBitmap、OffscreenCanvas 或 VideoFrame。
利用 canvas 绘制视频。
1 | const video = document.querySelector("video"); |