重学Canvas, 做个钟

工作中用到canvas还是比较少,还是很久很久以前学html的时候学过,都忘记了,所以这会就打算重新学习学习。 下面会先复习+学习一遍 canvas 的特性,支持的 API, 然后做一个可爱的canvas 时钟:

link
src

c

一、<canvas> 元素

1.1 canvas 元素属性

1.2 非闭合元素,以及后备(兜底)处理

canvas 不同与 img 元素, 必须提供闭合标签。

对于一些比较老旧的浏览器,可以提供一个子元素,用以兜底。 当浏览器不支持canvas的时候,就会渲染这个子元素。

<canvas>not support canvas</canvas>
copy success

1.3 canvas 的解耦渲染

canvas 可以通过 OffscreenCanvas API 去渲染,它是和 document 解耦的。好处就是 worker thread 可以处理canvas的渲染,所有canvas操作就不会阻塞主线程的页面渲染了。 即便是处理复杂的渲染逻辑,页面上其他的元素也不会丢失响应性。 详细的去看 OffscreenCanvas API 。

二、canvas 坐标系统

Canvas grid with a blue square demonstrating coordinates and axes.

和浏览器的坐标系统一样, 左上角坐标点即坐标原点 (0,0), x,y 就是定义的画布宽高,如果没有特别设定,那么一个单位尺寸,就是一个像素。

三、 绘制

canvas 和 svg 不同的是, canvas 仅仅支持三种主要的绘制图形: 矩形(rectangle)路径(paths) 。 所有其他的图形只能通过 路径 去组合实现。

2.1 填充和描边

2d 画板中,着色有两种方式,分别是 填充描边,顾名思义,分别是填充封闭区域的颜色,和为轮廓线着色。

填充着色使用 fill(fillStyle), 其中 fillStyle 指定了填充的样式。 描边着色使用 stroke(strokeStyle), 其中 strokeStyle 制定了描边的样式。 fillStyle 以及 strokeStyle 都可以是字符串/渐变对象/图案对象, 它们的默认值都是 #00000, 支持任意的 CSS 颜色格式。

当设定了 fillStylestrokeStyle, 后续的绘制样式都会按照这个样式去绘制,除非再次修改。

可以在 const ctx = HTMLCanvasElement.getContext('2d') 的上下文对象上访问到这些属性的默认值。 在设定和绘制的时候可以如下操作:

下方代码中,用到了path绘制,后文中会详细介绍。

const ctx = document.querySelector('canvas').getContext('2d')

ctx.strokeStyle = 'red'
ctx.fillStyle = '#00ff00'
ctx.lineWidth = 10

ctx.beginPath()// 新建一个 path 路径
ctx.moveTo(0, 0)// 移动画笔到(0,0)坐标
ctx.lineTo(100, 0)// 绘制直线从上一个点为起点,到(100,0) 坐标
ctx.lineTo(100, 100)// 绘制直线从上一个点为起点,到(100,100) 坐标
ctx.lineTo(0, 100)// 绘制直线从上一个点为起点,到(0,100) 坐标
ctx.closePath()// 结束 path 路径,并自动连接起始点构建封闭图形

ctx.fill()// 填充着色
ctx.stroke()// 轮廓
copy success

以上代码绘制如下图形:

image-20240607153932826

绘制结果中,边框之所以不同,是由于我们设定了线宽为10像素,在绘制的时候,实际计算方式是,以绘制中心线为基准,两侧各5各像素。上边和左边边框看上去只有一半,是因为贴合画布边缘,导致绘制中心线外侧被剪切掉了。要解决这个问题,应该将绘制向画布右下角移动一点距离。

2.2 绘制 path 路径

path 就是一个点的集合,中间用线段进行连接,在 2d 上下文中可以通过 路径 创建更复杂的形状或者线条。

要绘制一个 path, 大致步骤如下:

  1. 调用 beginPath() 方法创建路径
  2. 用各种方法创建所需路径
  3. 调用 closePath() 方法闭合路径
  4. 着色

①首先,需要调用上下文对象的 beginPath() 方法,该方法表示要开始绘制新路径。 接下来的任何绘制命令都将会应用到当前路径上。 接下来,有很多种 ②创建路径 方法:

③创建路径之后, 可以使用 closePath() 方法,它将绘制一条返回起点的线,使得路径所绘制的区域构成一个封闭区域。如果就是画线不需要闭合,就不用调这个方法。 路径创建完毕之后,就可以进行 ④着色处理了(fill()/stroke())

clip() 方法

有时候我们需要一个新的剪切区域用来作画, 例如将一张图片绘制在一个不规则图形上, 这时候就需要用到 clip 方法了。

示例:

其中弧形(扇形)的绘制进一步步骤大致如下:

image-20240607171052119

arc 弧度绘制相关方法的绘制辅助理解:
image-20240607174016360

2.3 绘制矩形

矩形是 唯一 可以直接绘制的形状,矩形的绘制有三个 API:

示例:

可以看到,矩形的绘制,不需要像 path 路径绘制那样,先 beginPath, 绘制完毕后还要 closePath。 路径绘制中的 fill 方法也被 fillRect() 方法替换, stroke() 方法则被 strokeRect() 方法替换。

2.4 绘制文本

canvas 中绘制文本,可以通过两个方法:

这两个方法都接收 4 个参数: 要绘制的字符串,x坐标,y坐标,可选最大像素宽度

这两个方法的绘制效果被以下上下文对象属性所影响:

文本的绘制方式非常的丰富,可以实现各种效果,以下是一些特性示例:

示例:

关于 fillText()strokeText() 方法的第四个参数 maxWith ,指定了文字渲染的最大宽度,文字会被 "挤" 在 这个宽度内。以下是一些简单演示:

measureText() 文字大小确定辅助方法

关于文字的自适应绘制, 文字绘制在canvas中是一项很复杂的工作,有时候我们有固定宽度的容器,我们希望文字在不被压缩的情况下,自适应的被绘制在该容器中。

canvas 提供了一个有意思的方法,该方法专门用于辅助文字大小的确定。 measureText(),它仅返回一个对象属性width, 表示以当前的字体样式绘制将会占据的像素宽度。 以下是一个示例。

我们希望将 "你好啊!兄嘚!" 这段文字合适的放置在不同大小的容器。

2.5 变换

canvas 支持所有常见的变形变换效果,常见的变换方法如下:

以下是一些变换的示例:

示例:

2.6 绘制图像

canvas 中,可以通过 drawImage() 方法绘制图像, 该方法接收 3 组不同的参数:

  1. 简单绘制 HTMLImageElement 元素到指定坐标:ctx.drawImage(img, x, y)

    const img = document.querySelector('img')
    ctx.drawImage(img, 10, 10)// 将 img 元素绘制到 (10, 10) 坐标位置
    copy success
  2. 将图像绘制到指定位置,并缩放:ctx.drawImage(img, x, y, width, height)

    const img = document.querySelector('img')
    ctx.drawImage(img, 10, 10, 20, 20)// 将 img 元素绘制到 (10, 10) 坐标位置, 目标宽/高都是 20 个像素
    copy success
  3. 把图像的一部分绘制到指定区域:drawImage(img, 源图像x坐标, 源图像y坐标, 源图像width, 源图像height, 目标区域x坐标, 目标区域y坐标, 目标区域width, 目标区域height)

    const img = document.querySelector('img')
    context.drawImage(image, 0, 0, 100, 100, 0, 0, 200, 200)// 将图像以 (0, 0)坐标开始,100 宽高大小,绘制到画布 (0, 0)坐标位置,宽/高 200/200 像素区域
    copy success

    image-20240608134946035

    利用这个特性,我们可以做点有意思的事情,比如,做个n宫格图:

四、效果

4.1 阴影

canvas 上下文对象中有一些属性用以控制阴影:

示例:

image-20240608160353625

const ctx = document.querySelector('canvas').getContext('2d')

ctx.strokeStyle = 'red'
ctx.lineWidth = 3

ctx.shadowColor = 'white'
ctx.shadowOffsetX = 1
ctx.shadowOffsetY = 20
ctx.shadowBlur = 5

ctx.moveTo(0, 0)// 移动画笔到(0,0)坐标
ctx.beginPath()// 新建一个 path 路径
ctx.arc(0, 0, 200, 0, Math.PI / 3)
ctx.stroke()// 轮廓
copy success

4.2 渐变

canvas 中支持通用的渐变,包括线性,径向,锥形渐变

4.2.1 线性渐变 createLinearGradient()

canvas 中渐变需要通过 createLinearGradient() 方法返回的CanvasGradient 实例进行创建, 然后作为样式传递给着色函数。该方法接收四个参数, 分别是: createLinearGradient(起点x坐标,起点y坐标,终点x坐标,终点y坐标)。 该方法调用会返回一个指定创建的 CanvasGradient 实例对象。

有了 gradient 对象后,需要使用 addColorStop() 方法创建渐变色标点, 这个方法接收两个参数,分别是addColorStop(色标位置(0-1), CSS 颜色表示字符串)

4.2.2 径向渐变 createRadialGradient()

径向渐变对象的创建需要 6 个参数,分别是:createRadialGradient(起点圆心x, 起点圆心y, 起点圆半径, 终点圆心x, 终点圆心y, 终点圆半径,)

同样的,径向渐变对象也是通过 addColorStop() 方法创建渐变色标点。

4.2.4 锥形渐变 createConicGradient()

锥形渐变对象 的创建需要三个参数, 分别是:createConicGradient(渐变开始的角度,中心x, 中心y)

同样的,径向渐变对象也是通过 addColorStop() 方法创建渐变色标点。

特别值得注意的一点是, canvas中的渐变是通过先在画布上绘制一个渐变区域, 然后为某个形状进行着色的区域才会显示出来。 可以理解为,是先绘制渐变效果,然后套用到对应图形区域。

示例:

4.3 图案 pattern

canvas中,可以用 createPattern() 这个方法创建重复性的图案。它接收两个参数,分别是:createPattern(img数据源,绘制规则), 其中数据源可以是HTMLImgElement, 也可以是另一个canvas,甚至可以是一个 video 元素。 绘制规则和css background-repeat 规则一样,可取值:"repeat"、"repeat-x"、"repeat-y"和"no-repeat"

示例

4.4 合成

2D 上下文中绘制的所有内容都会应用两个属性:globalAlpha 和 globalComposition Operation,
其中,globalAlpha 属性是一个范围在 0~1 的值(包括 0 和 1),用于指定所有绘制内容的透明度,默认值为 0。

这些属性会最终影响相叠加元素的最终绘制结果, 内容比较繁多,用的比较少,可以作为了解。 建议直接看 MDN

五、图像数据

canvas 2d 上下文中,有一个强大的能力,值得单独说明。 可以通过 getImageData() 方法获取原始图像的数据。 该方法接收 4 个参数,分别是:getImageData(选取点x,选取点y, 宽度,高度), 其中选取点是数据源的坐标系。

该方法将会返回一个 ImageData 的实例对象,这个对象中,包含了三个属性,width, height, datadata 属性是一个包含了图像的原始像素信息的 Uint8ClampedArray 数字数组。 每个像素在 data 数组中都由 4 个值表示,分别代表 红、绿、蓝,以及透明度。

image-20240608182654124

与get 方法对应的, 还有一个 putImageData 方法, 它用于将之前通过 getImageData 获取的像素数据重新绘制回canvas画布上。它支持的参数列表如下:

ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
copy success

这两个方法让我们具备了直接操作像素点的能力,可以有很多高深的应用。

下面是一个图片效果调整的实现demo: