canvas绘制基础知识强化

609 阅读12分钟

基本用法

canvas和普通的html标签用法差不多,canvas拥有width(默认300)和height(默认150)两个属性,有时出现模糊扭曲的情况很可能是忘记设置width和height了,并不是元素的style中的width和height。

<canvas id="tutorial" width="300" height="150"></canvas>

通过元素已经创建好一个固定大小的绘画区域,通过元素的getContext('2d')获得一个CanvasRenderingContext2D的绘画上下文。也可以通过元素是否有getContext方法来判断浏览器是否支持cnavas(都2021年了!)。画布的方向符合浏览器的滚动条属性,左上为 (0,0),向下是无穷大的Y轴,向右是无穷大的X轴。设置元素对应的width和height后坐标中的一格代表画布的一个像素。

const canvas = document.getElementById('tutorial');
if (canvas.getContext) {
  const ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}

image.png
canvas只支持两个基本形状: 矩形和路径(由直线连接的点列表)。所有其他形状都必须通过组合一个或多个路径来创建。

矩形

  • fillRect(x, y, width, height); // 绘制实心矩形,默认黑色
  • strokeRect(x, y, width, height); // 绘制矩形轮廓,默认#7a7a7a
  • clearRect(x, y, width, height); // 清除指定的矩形区域,使其完全透明 x 和 y 指定矩形左上角在画布上的位置(相对于原点)。width和height提供了矩形的大小。
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas</title>
  <style>
    body {
      background: #f5f5f5;
    }

    canvas {
      border: 1px solid #1890ff;
    }
  </style>
</head>

<body>
  <canvas id="tutorial" width="520" height="200"></canvas>
</body>
<script>
  const draw = ctx => {
    ctx.fillRect(10, 10, 300 - 20, 150 - 20);
    ctx.clearRect(20, 20, 300 - 40, (150 - 40) / 2);
    ctx.strokeRect(30, 30, 30, 30);
  }

  const render = () => {
    const canvas = document.getElementById('tutorial');
    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
      draw(ctx);
    } else {
      // canvas-unsupported code here
    }
  }
  window.setTimeout(render, 0);
</script>

</html>

image.png

路径

  • beginPath(); 通过清空子路径列表启动新路径。在内部,路径被存储为子路径(线、弧等)的列表,这些子路径一起形成一个形状。
  • closePath(); // 使路径的尾节点和头节点通过线段连接。
  • moveTo(); // 将新子路径的起始点移动到指定的(x,y)坐标
  • lineTo(); // 用直线将当前子路径中的最后一个点连接到指定的(x,y)坐标
  • quadraticCurveTo(); // 向当前路径添加二次贝塞尔曲线
  • bezierCurveTo(); // 向当前路径添加三次贝塞尔曲线
  • arc(); // 向当前路径添加圆弧
  • arcTo(); // 用给定的控制点和半径向当前路径添加一条弧线,该弧线通过一条直线连接到前一个点
  • ellipse(); // 向当前路径添加椭圆弧
  • stroke(); // 描边,和strokeRect类似
  • fill(); // 填充,和fillRect类似
  1. 调用fill方法时任何打开的形状将自动关闭,下个图形的起点是上个图形的终点,所以不必调用closePath()连接首尾
const draw = (ctx) => {
    ctx.beginPath();
    ctx.moveTo(10, 20);
    ctx.lineTo(10, 80);
    ctx.lineTo(40, 50);
    ctx.fill();
    ctx.lineTo(150, 50);
    ctx.stroke();
    ctx.closePath();
  }

image.png

  1. stroke方法并不会将形状关闭,与fill一样下个图形的起点是上个图形的终点
const draw = (ctx) => {
    ctx.beginPath();
    ctx.moveTo(10, 20);
    ctx.lineTo(10, 80);
    ctx.lineTo(40, 50);
    ctx.stroke();
    ctx.lineTo(150, 50);
    ctx.stroke();
    ctx.closePath();
  }

image.png

  1. 如果形状已经关闭或只有一个点,调用closePath函数不执行任何操作;closePath调用后下个图形的起点为上个图形moveTo的那个点
const draw = (ctx) => {
    ctx.beginPath();
    ctx.moveTo(10, 20);
    ctx.lineTo(10, 80);
    ctx.lineTo(40, 50);
    ctx.stroke();
    ctx.closePath();
    ctx.lineTo(150, 50);
    ctx.stroke();
    ctx.closePath();
  }

image.png

  1. 调用beginPath时会情况列表创建一条新的路径
const draw = (ctx) => {
    ctx.beginPath();
    ctx.moveTo(10, 20);
    ctx.lineTo(10, 80);
    ctx.lineTo(40, 50);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(10, 20);
    ctx.lineTo(150, 50);
    ctx.stroke();
    ctx.closePath();
  }

image.png

通过quadraticCurveTo、bezierCurveTo、arc、arcTo、ellipse可以绘制一些奇奇怪怪的图形

样式属性

样式属性在设置之后会应用到后续的所有图形绘制上,对于不同样式的每个形状,都需要重新分配

  • fillStyle // 填充颜色,默认#000000,rgb(0, 0, 0),rgba(0, 0, 0, 1)
  • strokeStyle // 描边颜色,默认#000000,rgb(0, 0, 0),rgba(0, 0, 0, 1)
  • globalAlpha // 透明度0到1,默认为1
  • lineWidth // 画笔宽度,默认为1
  • lineCap // 线的两端的样式,butt:两端无样式(默认),round:两端加上圆形,square:两端加上二分之一画笔宽度的方形
  • lineJoin // 线条相交的“角”的外观,round:填充圆形,bevel:填充三角形,miter:填充菱形(默认)
  • setLineDash() // 设置波浪线,参数为数组 [ 实线长度 , 虚线长度 ]
  • lineDashOffset // 波浪线的偏移,默认为0
const draw = ctx => {
    ctx.beginPath()
    ctx.lineWidth = 10
    ctx.strokeStyle = '#1890ff'
    ctx.moveTo(0, 20);
    ctx.lineTo(200, 20);
    ctx.setLineDash([20, 10])
    ctx.stroke();

    ctx.beginPath()
    ctx.moveTo(0, 40);
    ctx.lineWidth = 10
    ctx.setLineDash([])
    ctx.lineCap = 'round' // 两端为圆角
    ctx.lineJoin = 'round' // 线段交叉处为圆角
    ctx.globalAlpha = 0.5
    ctx.lineTo(200, 40);
    ctx.lineTo(100, 80);
    ctx.stroke();
    ctx.closePath()
  }

image.png

渐变色

  • 线性渐变: createLinearGradient(x1, y1, x2, y2),参数为起点(x1,y1)和终点(x2,y2)
  • 径向渐变: createRadialGradient(x1, y1, r1, x2, y2, r2),参数为两个圆
  • 角度渐变: createConicGradient(angle, x, y),参数为弧度和位置,部分浏览器需要更改浏览器设置后可用,大多数浏览器不可用,注意兼容性问题 上述函数调用后会生成CanvasGradient对象,通过对象的addColorStop(offset, color)方法生成过渡色,offset取值为0~1,表示渐变的起始位置
  1. 线性渐变
const draw = ctx => {
    const gradient = ctx.createLinearGradient(0, 0, 200, 0);
    gradient.addColorStop(0, 'green');
    gradient.addColorStop(.5, 'white');
    gradient.addColorStop(1, 'pink');
    ctx.fillStyle = gradient;
    ctx.fillRect(10, 10, 200, 100);
  }

image.png

  1. 径向渐变
const draw = ctx => {
    const gradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 70);
    gradient.addColorStop(0, 'green');
    gradient.addColorStop(.5, 'white');
    gradient.addColorStop(1, 'pink');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 200, 200);
  }

image.png

  1. 角度渐变(谷歌浏览器需要在chrome://flags 中设置new-canvas-2d-api为enabled)
const draw = ctx => {
    const gradient = ctx.createConicGradient(0, 100, 100);
    gradient.addColorStop(0, 'green');
    gradient.addColorStop(.5, 'white');
    gradient.addColorStop(1, 'pink');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 200, 200);
  }

image.png

文本

  • fillText(text, x, y [, maxWidth]) // 在指定的(x,y)位置填充指定的text,绘制的最大宽度是可选的
  • strokeText(text, x, y [, maxWidth]) // 在指定的(x,y)位置绘制text边框,绘制的最大宽度是可选的
  • font // 字体属性,与css font属性一样
  • textAlign // 文本对齐选项;值为start(默认), end, left, right, center
  • textBaseline // 基线对齐选项;值为top, hanging, middle, alphabetic(默认), ideographic, bottom
  • direction // 文本方向;值为ltr, rtl, inherit(默认)
const draw = ctx => {
    ctx.strokeStyle = "#1890ff";
    ctx.beginPath();
    ctx.moveTo(0,48);
    ctx.lineTo(500,48);
    ctx.stroke();
    ctx.closePath();
    ctx.font = "48px serif";
    ctx.textBaseline = "middle";
    ctx.strokeText("Hello world", 0, 48);
  }

image.png

阴影

  • shadowOffsetX // 阴影在 X 轴的延伸距离,负值表示阴影会往左延伸,正值则表示会往右延伸,默认为 0
  • shadowOffsetY // 阴影在 Y 轴的延伸距离,负值表示阴影会往上延伸,正值则表示会往下延伸,默认为 0
  • shadowBlur // 阴影模糊程度,默认为 0
  • shadowColor // 阴影颜色,默认为 rgba(0,0,0,0)
const draw = ctx => {
    ctx.fillStyle = "#1890ff";
    ctx.shadowOffsetX = 8;
    ctx.shadowOffsetY = 8;
    ctx.shadowBlur = 2;
    ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
    ctx.font = "32px Times New Roman";
    ctx.fillText("Hello World", 0, 32);
    ctx.fillRect(0, 64, 50, 50)
  }

image.png

图片

  • drawImage(image, x, y)
  • drawImage(image, x, y, width, height)
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 若调用 drawImage 时,图片没装载完,那什么都不会发生(在一些旧的浏览器中可能会抛出异常)。因此应该用load事件来保证不会在加载完毕之前使用这个图片。
  1. 3个参数的图片绘制中, x,y 为距离画布原点的位置,图片大小为载入的图片大小
const draw = ctx => {
    const img = new Image();
    img.onload = function () {
      ctx.drawImage(img, 0, 0);
    }
    img.src = 'https://sf3-ttcdn-tos.pstatp.com/img/mosaic-legacy/3795/3033762272~300x300.image';
  }

image.png

  1. 5个参数的图片绘制中, 其他参数和基本绘制一样,但图片的大小可以自定义及 width,height 可以对载入图片进行拉伸或缩小
 const draw = ctx => {
    const img = new Image();
    img.onload = function () {
      ctx.drawImage(img, 0, 0);
      ctx.drawImage(img, 200, 0, 90, 180);
    }
    img.src = 'https://sf3-ttcdn-tos.pstatp.com/img/mosaic-legacy/3795/3033762272~300x300.image';
  }

image.png

  1. 8个参数的图片绘制中, sx,sy 为在原图上截取图片的位置, sWidth,sHeight 为截取的宽高, dx,dy 为在画布上绘制的位置, dWidth,dHeight 为将截取下来的图片绘制到画布上的大小

image.png

const draw = ctx => {
    const img = new Image();
    img.onload = function () {
      ctx.drawImage(img, 0, 0);
      ctx.drawImage(img, 0, 0, 90, 90, 200, 0, 180, 180);
    }
    img.src = 'https://sf3-ttcdn-tos.pstatp.com/img/mosaic-legacy/3795/3033762272~300x300.image';
  }

image.png

进阶用法

在绘制复杂图形时必不可少的两个方法:

  • save() // 保存 canvas 的所有状态
  • restore() // 恢复 canvas 状态的 canvas 的状态就是当前画面应用的所有样式和变形的一个快照。canvas状态存储在栈中,每当 save 方法被调用后,当前的状态就被推送到栈中保存;每一次调用  restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。一个绘画状态包括:
  1. 当前应用的变形(即移动,旋转和缩放等)
  2. 这些样式属性strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
  3. 当前的裁切路径 仅单独调用方法时不能体现这两个方法的用处,仅仅一个压栈和出栈的过程:
// 仅调用save
const draw = ctx => {
    ctx.fillStyle = "#1890ff";
    ctx.fillRect(0, 0, 150, 150);
    ctx.save();
  }
// 仅调用restore
const draw = ctx => {
    ctx.fillStyle = "#1890ff";
    ctx.fillRect(0, 0, 150, 150);
    ctx.restore();
  }
// 仅调用save和restore
const draw = ctx => {
    ctx.fillStyle = "#1890ff";
    ctx.fillRect(0, 0, 150, 150);
    ctx.save();
    ctx.restore();
  }

/* 三种方式都是相同的结果:填充一个蓝色的矩形 */

image.png

当调用save方法后当前画笔的状态不会被清除,仍然和save前的一样,后续改了状态后,调用restore后会将栈中的状态取出并自动改变当前画笔的状态

const draw = ctx => {
    ctx.fillStyle = "#1890ff"; // 设置画笔颜色为蓝色
    ctx.fillRect(0, 0, 30, 30); // 第一个矩形绘制
    ctx.save(); // 把蓝色(#1890ff)压入栈中

    ctx.fillRect(40, 0, 30, 30); // 第二个矩形绘制

    ctx.fillStyle = "#389E0D"; // 设置画笔颜色为绿色
    ctx.fillRect(80, 0, 30, 30); // 第三个矩形绘制
    ctx.save(); // 把绿色(#389E0D)压入栈中

    ctx.fillStyle = "#F5222D"; // 设置画笔颜色为红色

    ctx.restore(); // 弹出第二次压栈的状态
    ctx.fillRect(120, 0, 30, 30); // 第四个矩形绘制
    ctx.restore(); // 弹出第一次压栈的状态
    ctx.fillRect(160, 0, 30, 30); // 第五个矩形绘制
  }

image.png

缩放

scale(x, y) 方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会缩小图形,如果比1大会放大图形。默认值为1,为实际大小。

const draw = (ctx) => {
    ctx.fillRect(0, 0, 50, 50);
    ctx.scale(2,2);
    ctx.fillRect(50, 0, 50, 50);
  }

image.png

可以看到不仅是目标的长宽发生了缩放,距离原点(0,0)的偏离也出现了相应的缩放。
注意缩放的数值如果为负数,相当于是对X、Y轴做镜像的翻转,且绘制的窗宽的方向也变成了负数

const draw = (ctx) => {
    ctx.fillRect(0, 0, 50, 50); // 宽度为正数,从左往右计算
    ctx.fillRect(100, 50, -50, 50); // 宽度为负数,从右往左计算
    ctx.scale(-1,1); // 原点左侧为0到正无穷,右侧为0到负无穷,Y轴没做缩放
    ctx.fillRect(-50, 100, 50, 50); // 宽度为正是数,从右往左计算
    ctx.fillRect(-50, 150, -50, 50); // 宽度为负数,从左往右计算
  }

image.png

移动

translate(x, y)方法接受两个参数。x 是左右偏移量,y 是上下偏移量。和其他属性一样,一旦设置以后对后续操作都是叠加效应,每次移动是基于上一次移动的基础之上的,并不会一直相对于原点。

const draw = (ctx) => {
    ctx.fillRect(0, 0, 20, 20);
    ctx.translate(30, 0)
    ctx.fillRect(0, 0, 20, 20);
    ctx.translate(0, 30); // 只平移了Y值,而X值仍然是之前的30
    ctx.fillRect(0, 0, 20, 20);
    ctx.translate(30, 0); // 是在之前(30,0)+(0,30)的基础上再加(30,0),相当于相对于原点的位移是(60,30)
    ctx.fillStyle = '#1890ff';
    ctx.fillRect(0, 0, 20, 20);
  }

image.png

可以配合使用save和restore方法来使位移一直相对于原点,更符合我们的计算

const draw = (ctx) => {
    ctx.save(); // 绘制前保存一个原始的状态
    ctx.fillRect(0, 0, 20, 20);
    ctx.restore(); // 绘制后还原到原始的状态

    ctx.save();
    ctx.translate(20, 20)
    ctx.fillRect(0, 0, 20, 20);
    ctx.restore();

    ctx.save();
    ctx.translate(40, 40)
    ctx.fillRect(0, 0, 20, 20);
    ctx.restore();

    ctx.save();
    ctx.translate(60, 60)
    ctx.fillRect(0, 0, 20, 20);
    ctx.restore();
  }

image.png

旋转

rotate(angle)方法只接受一个参数:旋转的角度(angle),它是以原点为中心顺时针方向的角度,以 弧度 为单位的值。

const draw = (ctx) => {
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(100, 0);
    ctx.stroke();

    ctx.save();
    ctx.rotate(2 * Math.PI / 360 * 30)
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(100, 0);
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.rotate(2 * Math.PI / 360 * 60)
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(100, 0);
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.rotate(2 * Math.PI / 360 * 90)
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(100, 0);
    ctx.stroke();
    ctx.restore();
  }

image.png

如果要绕图形的中心旋转,需要通过旋转角度反推计算平移的距离

const draw = (ctx) => {
    ctx.fillRect(0, 0, 40, 40);

    ctx.save();
    ctx.translate(80, 40)
    ctx.globalAlpha = 0.5
    ctx.fillRect(0, 0, 40, 40); // 图形2, 位移之后的参考坐标
    ctx.rotate(2 * Math.PI / 360 * 45)
    ctx.fillStyle = "#1890ff"; // 蓝色
    ctx.fillRect(0, 0, 40, 40);
    ctx.restore();

    // 如果想在图形2的位置中心旋转,需要动态计算水平偏移坐标
    ctx.save();
    ctx.globalAlpha = 0.5
    ctx.fillStyle = "#FFEC3D"; // 黄色
    const R = 20; // 2分之一的边长
    const r = 60; // 旋转角度
    const diagonalR = Math.sqrt(Math.pow(R, 2) + Math.pow(R, 2)) // 2分之一的对角线长
    const translateX = 80 + R - diagonalR * Math.sin(2 * Math.PI / 360 * (45 - r));
    const translateY = 40 + R - diagonalR * Math.cos(2 * Math.PI / 360 * (45 - r));
    ctx.translate(translateX, translateY)
    ctx.rotate(2 * Math.PI / 360 * r)
    ctx.fillRect(0, 0, 2 * R, 2 * R);
    ctx.restore();
  }

image.png

形变

  • transform(a, b, c, d, e, f),是将当前的变形矩阵乘上一个基于自身参数的矩阵,其原理涉及坐标的几何变换,正是矩阵善于处理的事情
    • a(m11): 水平方向的缩放,别名m11
    • b(m12): 竖直方向的倾斜偏移,别名m12
    • c(m21): 水平方向的倾斜偏移,别名m21
    • d(m22): 竖直方向的缩放,别名m22
    • e(dx): 水平方向的移动,别名dx
    • f(dy): 竖直方向的移动,别名dy
  • setTransform(a, b, c, d, e, f),是将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform(a, b, c, d, e, f) 方法;相当于是先取消了当前变形,然后设置为指定的变形,一步完成
  • resetTransform(),重置当前变形为单位矩阵,它和调用 setTransform(1, 0, 0, 1, 0, 0) 一样
  • getTransform(),获取当前变形矩阵参数,得到一个对象(包含 a、b、c、d、e、f 等参数),可以通过这个方法获取参数来处理部分绘制,比如在变形的基础上某个图形不需要缩放,可以把该图形的宽高除上相应的a(m11)、d(m22)