canvas之常用API总结及注意事项

222 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

最近一段时间做了很多关于canvas的需求,这篇文章就总结一下常用的canvas api以及我在使用它们的过程中所遇到的坑.这里只介绍2d画布的相关内容.

canvas.getContext('2d')

我们在canvas初始化时需要设置canvas大小,这个大小分为css大小和画布大小.不过一般我们都直接设置画布大小,css大小默认就和画布大小一致.

  1. 直接使用已有dom

即直接使用dom中的canvas元素,当你想把canvas内容呈现在屏幕上时可以使用这种方式.

<--  也可以不设置width height 通过js设置 -->
<canvas id="canvas" width="200" height="100"></canvas>
<script>
    const canvas = document.querySelector('#canvas')
    // 获取画布上下文
    const ctx = canvas.getContext('2d')
</script>
  1. 创建canvas

当你不想把canvas的内容呈现在屏幕上,或者说这个canvas用于呈现在屏幕上内容的中间处理过程时可以使用这种方式. 同时这种方式也可以用于制作canvas引擎时的绘制性能优化,这点可以以后再说.

 const canvas = document.createElement('canvas')
 canvas.width = 200
canvas.height = 100
 const ctx = canvas.getContext('2d')

绘制矩形

绘制的矩形可以分为两类:

  1. 填充矩形ctx.fillRect(x, y, w, h)
  • x: 代表矩形左上顶点在画布中的x坐标
  • y: 代表矩形左上顶点在画布中的y坐标
  • w: 矩形的宽度
  • h: 矩形的高度

x,y,w,h都为数字

const canvas = document.querySelector('#canvas')
canvas.width = 400
canvas.height = 300
const ctx = canvas.getContext('2d')
ctx.save()  // 保存当前画布的上下文状态,例如画笔颜色等
ctx.fillStyle = 'pink'
ctx.fillRect(10, 30, 100, 200)   // 绘制了一个100 * 200 的矩形,画笔颜色默认为黑色
ctx.restore() // 恢复画布的上下文状态

image.png

这里的ctx.save()ctx.restore()根据你的需求决定写不写,如果你不想画矩形这个过程改变了你原来画布画笔的状态就可以加上.

  1. 绘制矩形框ctx.strokeRect(x, y, w, h)
ctx.save()  // 保存当前画布的上下文状态,例如画笔颜色等
ctx.strokeStyle = 'red'
ctx.strokeRect(10, 30, 100, 200)   // 绘制了一个100 * 200 的矩形框,画笔颜色默认为黑色
ctx.restore() // 恢复画布的上下文状态

image.png

还可以借助ctx.setLineDash(m,n)绘制虚线框,m表示绘制的长度,n表示间隔的长度

ctx.save()
ctx.strokeStyle = 'pink'
ctx.setLineDash([20, 5]);
ctx.strokeRect(10, 30, 100, 200)

image.png

借助ctx.lineDashOffset还可以绘制颜色交替的矩形框

ctx.save()
ctx.strokeStyle = 'pink'
ctx.setLineDash([20, 5]);
ctx.strokeRect(10, 30, 100, 200)
ctx.lineDashOffset = 5;   // 起始落笔位置往前挪5
ctx.setLineDash([5, 20])
ctx.strokeStyle = '#000'
ctx.strokeRect(10, 30, 100, 200)
ctx.restore()

image.png

绘制图片

使用ctx.drawImage(img, x, y, w, h)即可

ctx.drawImage(img, 0, 0)   // 以画布的(0,0)点为图片的左上角,将img画在画布上,图片宽高为img.width, img.height

ctx.drawImage(img, 0, 0, w, h)   // 以画布的(0,0)点为图片的左上角,将img画在画布上,图片在画布上的宽高是w,h

// 将图片的某一部分(10,10,100,80)画在画布上(0,0,200,160)
ctx.drawImage(img, 10, 10, 100, 80, 0, 0, 200, 160)

将图片绘制在canvas上

const img = new Image()
img.src = './GEM.jpg'
img.onload = () => {
  ctx.drawImage(img, 10, 30, img.width / 4, img.height / 4)
}

image.png

getImageData()和toDataURL()

语法

ctx.getImageData(sx, sy, sw, sh);

返回一个ImageData对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为*(sx, sy)、宽为sw、高为sh

canvas.toDataURL(type, encoderOptions);
// type默认为image/png
// encoderOptions 在指定图片格式为 `image/jpeg` 或 `image/webp` 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 `0.92`。其他参数会被忽略。

跨域问题

这两个API在使用时需要注意跨域问题,如果canvas中绘制了图片,且图片跨域,则要求该图片必须是使用CORS请求得到的,否则将会报错.

const img = new Image()
img.src = 'http://localhost:9000/gem.jpg'   // 当前页面端口为5500
img.onload = () => {
  ctx.drawImage(img, 10, 30, img.width / 4, img.height / 4)
  const imageData = ctx.getImageData(0, 0, 600, 450)
}

image.png

这时我们可以使用crossOrigin属性来解决这个问题

const img = new Image()
img.src = 'http://localhost:9000/gem.jpg'
img.crossOrigin = 'anonymous'   // 此时图片会以CORS的方式请求
img.onload = () => {
  ctx.drawImage(img, 10, 30, img.width / 4, img.height / 4)
  const imageData = ctx.getImageData(0, 0, 600, 450)
  console.log('imageData: ', imageData);
}

如果上面的方法还是不行,那么我们可以用最原始的方式来请求图片,我封装了一个方法:

loadImg(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) {
        return;
      }
      if (xhr.status === 200 || xhr.status === 304) {
        // 创建一个给定blob对象或File对象的url
        const url = URL.createObjectURL(this.response);
        const img = new Image();
        img.onload = function () {
            // 注意释放一个之前已经存在的、通过调用 `URL.createObjectURL()`创建的 URL 对象,
            // 以告诉浏览器不用在内存中保存对原文件对象的引用,从而释放内存
            URL.revokeObjectURL(url);
            resolve(img)
        };
        img.src = url;
      } else {
        reject(Error('image error'));
      }
    };
    xhr.open('GET', url, true);
    // 设置不使用缓存,保证图片是CORS得到的
    xhr.setRequestHeader('Cache-Control', 'no-cache')
    // 设置响应数据类型为blob
    xhr.responseType = 'blob';
    xhr.send();
  });
},

画布位移和旋转

  1. 位移

不会影响画布上原来已有的内容

const canvas = document.querySelector('#canvas')
canvas.width = 300
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 90, 50)
ctx.translate(30, 30)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

image.png

  1. 旋转
ctx.roate(angle)

参数angle为一个数字,单位使用弧度,为正数时画布顺时针旋转. 旋转同样不影响原画布内容。

image.png

const canvas = document.querySelector('#canvas')
canvas.width = 300
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 90, 50)
// 旋转
const angle = 60
ctx.rotate(Math.PI * 60 / 180)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

image.png

  1. 缩放
ctx.scale(x,y)

CanvasRenderingContext2D.scale()  是 Canvas 2D API 根据 x 水平方向和 y 垂直方向,为 canvas 单位添加缩放变换的方法。 默认的,在 canvas 中一个单位实际上就是一个像素。例如,如果我们将 0.5 作为缩放因子,最终的单位会变成 0.5 像素,并且形状的尺寸会变成原来的一半。相似的方式,我们将 2.0 作为缩放因子,将会增大单位尺寸变成两个像素。形状的尺寸将会变成原来的两倍。

const canvas = document.querySelector('#canvas')
canvas.width = 300
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 90, 50)

// 缩放
ctx.scale(2,2)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

image.png

变换矩阵

上述的canvas位移旋转缩放都可以使用变换矩阵来完成,canvas提供了两个相关的API

CanvasRenderingContext2D.transform()  是 Canvas 2D API 使用矩阵多次叠加当前变换的方法,矩阵由方法的参数进行描述。你可以缩放、旋转、移动和倾斜上下文。

CanvasRenderingContext2D.setTransform()  是 Canvas 2D API 使用单位矩阵重新设置(覆盖)当前的变换并调用变换的方法,此变换由方法的变量进行描述。

可以看到这两个API的区别就是,transform在当前变换状态的基础上继续变换,而setTransform则是利用提供的矩阵对当前状态进行覆盖。

ctx.transform

transform(a, b, c, d, dx, dy) 方法是将当前的变换矩阵乘上参数的矩阵

a c dx
b d dy
0 0 1

参数各自代表含义如下:

  • a:水平方向的缩放
  • b:水平方向的倾斜偏移
  • c:竖直方向的倾斜偏移
  • d:竖直方向的缩放
  • dx:水平方向的移动
  • dy:竖直方向的移动

translate/rotate/scale 与 transform的对应关系如下:

translate(x,y)    -------->     transform(1,0,0,1,x,y)

// 旋转a弧度
const cos = Math.cos(a)
const sin = Math.sin(a)
rotate(a)         --------->     transform(cos, sin, -sin, cos, 0, 0)

scale(x,y)       ---------->     transform(x,0,0,y,0,0)

用transform来实现前面的位移旋转和缩放如下:

const canvas = document.querySelector('#canvas')
canvas.width = 300
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 90, 50)

// 位移
// ctx.translate(30, 30)
ctx.transform(1, 0, 0, 1, 30, 30)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

// 旋转
const angle = Math.PI * 60 / 180
// ctx.rotate(angle)
// rotate等价于下面
const cos = Math.cos(angle)
const sin = Math.sin(angle)
ctx.transform(cos, sin, -sin, cos, 0, 0)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

// 缩放
// ctx.scale(2,2)
ctx.transform(2, 0, 0, 2, 0, 0)
ctx.strokeStyle = 'red'
ctx.strokeRect(0, 0, 90, 50)

ctx.setTransform()

setTransform(a, b, c, d, dx, dy) 方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform 方法,即该方法取消当前变形,然后设置为指定的变形。
resetTransform() 重置当前变形为单位矩阵,相当于调用 setTransform(1, 0, 0, 1, 0, 0),即下面的默认矩阵(单位矩阵):

1 0 0
0 1 0
0 0 1

清空画布或清除画布某矩形区域

ctx.clearRect(x, y, w, h)

用于清空参数(x, y, w, h)指定的矩形区域内容,如果参数为(0, 0, canvas.width, canvas.height)就是清空整个画布内容

ctx.isPointInPath()

CanvasRenderingContext2D.isPointInPath()  是 Canvas 2D API 用于判断在当前路径中是否包含检测点的方法。

语法

boolean ctx.isPointInPath(x, y);
boolean ctx.isPointInPath(x, y, fillRule);

boolean ctx.isPointInPath(path, x, y);
boolean ctx.isPointInPath(path, x, y, fillRule);

参数含义:

  1. x: 检测点的 X 坐标

  2. y:检测点的 Y 坐标

  3. fillRule

用来决定点在路径内还是在路径外的算法。 允许的值:

  • nonzero:非零环绕规则,默认的规则。

  • evenodd:奇偶环绕原则

  • pathPath2D应用的路径。

这里我封装一个方法用于判断一个点是否在某矩形范围内:

function _checkInPath(x, y, rect) {
    if (!Array.isArray(rect)) {
      const { x, y, w, h } = rect;
      rect = [x, y, w, h];
    }
    const ctx = this.imageCtx;
    ctx.beginPath();     // 通过清空子路径列表开始一个新路径的方法
    ctx.rect(...rect);
    const result = ctx.isPointInPath(x, y);
    ctx.closePath();  // 将笔点返回到当前子路径起始点的方法。它尝试从当前点到起始点绘制一条直线。如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。
    return result;
}

// 使用
_checkInPath(10, 100, [5, 40, 200, 150])
// 或
_checkInPath(10, 100, { x: 5, y: 40, w: 200, h: 150 })

最后

基本上我常用到的API就是以上这些了,其他API以后再补充。 这篇文章主要是为下一篇做铺垫,下一篇文章打算实现一个canvas图片旋转裁剪模糊和马赛克的类。

如有错误欢迎指出!