十五、Canvas 详细版

郁子大约 35 分钟约 10511 字笔记渡一教育语言基础董明晨HTML5

(一)概述

  • Canvas 是 HTML5 的一个新标签,相当于一个画布,可以用来绘制丰富的图形,最终渲染在浏览器上
  • Canvas 标签本身不具备绘制图形的能力,配合 JavaScript 提供的 CanvasAPI,才能绘制图形,文本和图像,以及实现动画和交互
  • 支持 2d 绘图,也支持 3d 绘图(WebGL)

1.Canvas绘制的图形是位图

  • 放缩会导致图像失真,所以需要注意放缩比例的控制
  • 可以操作每一个点位的像素,进而实现高度自定义的图形绘制和动画效果
  • 相当于 <img> 引入的图片,可以右键另存

2.Canvas绘制的内容不属于dom元素

  • 通常比dom元素绘制的方式有更高的渲染能力
  • 但也存在一些问题
    • 无法在浏览器查看器中查找
    • 无法支持鼠标监听(但可以通过其他方式实现类似的效果)

3.Canvas应用领域

  • 可视化图表
  • h5游戏制作
  • banner广告

(二)画布与画笔

1.创建画布和画笔

  • 提供HTML标签 <canvas>
  • 创建JS画布:Canvas对象 HTMLCanvasElement
  • 创建JS画笔:Context对象 CanvasRenderingContext2D

2.提供canvas标签的方式

1)方式一:直接定义canvas标签

<canvas id="c1"></canvas>

<script>
  const canvas1 = document.querySelector("#c1");
  const context1 = canvas1.getContext("2d");
</script>

2)方式二:JS创建canvas标签

  • 推荐,vscode有更友好的提示
  • 使用JS方式创建canvas时,canvas对象(HTMLCanvasElement)和context对象(CanvasRenderingContext2D)都是具体的类型,vscode编码开发时,提示更加友好

3.canvas标签的版本检查

  • 绝大多数的浏览器都支持canvas,但少数老版本的浏览器支持不佳(IE9-)

1)使用文本/图片替换canvas

  • 浏览器不支持canvas,会显示标签中的文本或图片内容
<canvas id="c1">您的浏览器版本过低,不支持canvas,请升级浏览器或更换浏览器</canvas>

2)脚本检测

  • 浏览器不支持canvas时,canvas对象没有getContext函数
if (!canvas2.getContext) {
  console.log("您的浏览器版本过低,不支持canvas,请升级浏览器或更换浏览器");
} else {
  // coding...
}

4.画布区域特点

  • canvas是一个 行内元素
  • canvas可以使用 widthheight 设置区域宽高
    • 默认宽高:300*150
  • canvas可以使用 style 样式设置宽高
    • widthheight 设置的效果有所不同

1)坐标系

  • 每一个画布中都有一个坐标系统,画布的左上角为默认的 (0, 0) 原点

2)画布区域

  • 可见的坐标系区域,使用 widthheight 属性控制的区域
  • 这个区域有多大,其包含的坐标系就有多大
    • 画布/坐标系本身可以很大
    • 但是页面上可见区域只会根据属性值展示
    • 超出设定范围的坐标系中的图形将不可见
<!-- 定义400*400的坐标系 -->
<canvas id="c1" width="400" height="400"></canvas>

3)放置区域

  • 使用 style 样式控制的区域大小,可理解为 比例尺
  • 画布区域中绘制的图形,最终会在放置区域中展示
  • 默认情况下,放置区域与画布区域相同
  • 放置区域如果比画布区域大/小,画布中的图形就会按比例放大/缩小(图像可能失真)
<style>
  canvas {
    border: 1px solid #ccc;
    margin-left: 100px;
  }
  #c2 {
    width: 200px;
    height: 200px;
  }
  #c3 {
    width: 600px;
    height: 600px;
  }
</style>

<canvas id="c1" width="400" height="400"></canvas>
<canvas id="c2" width="400" height="400"></canvas>
<canvas id="c3" width="400" height="400"></canvas>

<script>
  {
    const canvas = document.querySelector("#c1");
    const ctx = canvas.getContext("2d");
    ctx.fillRect(100, 100, 100, 100);
  }

  {
    const canvas = document.querySelector("#c2");
    const ctx = canvas.getContext("2d");
    ctx.fillRect(100, 100, 100, 100);
  }

  {
    const canvas = document.querySelector("#c3");
    const ctx = canvas.getContext("2d");
    ctx.fillRect(100, 100, 100, 100);
  }
</script>

(三)绘制图形

1.绘制矩形

  • 可以绘制两种矩形,有三种方式
    • 填充的矩形(实心矩形)
    • 描边的矩形(空心矩形)

1)填充矩形

  • ctx.fillRect(x, y, width, height)
const ctx = canvas.getContext("2d");
ctx.fillRect(100, 100, 200, 100);

2)描边矩形

  • ctx.strokeRect(x, y, width, height)
const ctx = canvas.getContext("2d");
ctx.strokeRect(100, 100, 200, 100);

3)矩形路径

  • ctx.rect(x, y, width, height)
  • 只是提供了一个规划,默认在页面上没有效果
  • 需要配合 ctx.stroke()ctx.fill() 来描边或填充
const ctx = canvas.getContext("2d");
ctx.rect(100, 100, 200, 100);
ctx.stroke();
ctx.fill();
  • 使用 ctx.fillStyle 属性设置填充的颜色
  • 使用 ctx.strokeStyle 属性设置描边颜色
  • 使用 ctx.lineWidth 属性设置描边粗细

注意

一定要在绘制图形之前设置

const ctx = canvas.getContext("2d");
ctx.rect(100, 100, 200, 100);

ctx.fillStyle = "rgba(255, 0, 0, 0.8)";
ctx.strokeStyle = "#00f";
ctx.lineWidth = 10;
ctx.stroke();
ctx.fill();
// 代码至此,已经绘画完毕了

// ctx.fillStyle = 'red'; // 无效果

2.beginPath方法

  • stroke()fill() 默认会对之前所有绘制的路径进行统一处理
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.rect(20, 20, 100, 100);
  ctx.stroke();

  ctx.fillStyle = "#f00";
  ctx.strokeStyle = "#0f0";
  ctx.rect(20, 200, 100, 100);
  ctx.fill();
})();

  • 当需要只对刚刚绘制的图形路径进行处理时,可以使用 ctx.beginPath() 方法
  • 为不同部分的路径设置开关/分组,此时只对紧邻这组路径进行绘制
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.strokeStyle = "#00f";
  ctx.lineWidth = 10;
  ctx.rect(20, 20, 100, 100);
  ctx.stroke();

  ctx.fillStyle = "#f00";
  ctx.strokeStyle = "#0f0";
  ctx.beginPath();
  ctx.rect(20, 200, 100, 100);
  ctx.fill();
  ctx.stroke();
})();

相关信息

  • 一组路径可以有多个图形(线,弧,曲线,矩形等)
  • 使用 fillRect()strokeRect() 不会有影响
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.strokeRect(20, 20, 100, 100);

  ctx.fillStyle = "#f00";
  ctx.fillRect(20, 200, 100, 100);
})();

3.绘制圆角矩形

  • ctx.roundRect(x, y, width, height, r)
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  ctx.roundRect(100, 100, 200, 200, 50);
  ctx.stroke();
})();

1)r 有多种写法,可以实现四个圆角单独设置

  • r: 10 | [10]
  • r: [10, 20] [top-left-and-bottom-right, top-right-and-bottom-left]
  • r: [10, 20, 30] [top-left, top-right-and-bottom-left, bottom-right]
  • r: [10, 20, 30, 40]
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  ctx.roundRect(10, 10, 100, 100, [10]);
  ctx.stroke();

  ctx.roundRect(10, 200, 100, 100, [10, 30]);
  ctx.stroke();

  ctx.roundRect(200, 10, 100, 100, [10, 30, 20]);
  ctx.stroke();

  ctx.roundRect(200, 200, 100, 100, [10, 20, 30, 40]);
  ctx.stroke();
})();

4.绘制直线&折线

1)概念

  • 直线:两点之间的连线
  • 折线:多条直线连接

2)绘制

  • 使用 ctx.moveTo(x, y) 将画笔放置到指定的坐标位置(起始点)
  • 使用 ctx.lineTo(x, y)上一个点 绘制直线路径到指定的点
    • 上一个点可以是moveTo指定的点
    • 也可以是上一次lineTo指定的点
    • 即:可以多个lineTo连续使用,形成折线
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(50, 50);
  ctx.lineTo(250, 50);
  ctx.stroke();

  ctx.beginPath();
  ctx.strokeStyle = "#00f";
  ctx.lineWidth = 10;
  ctx.moveTo(50, 200);
  ctx.lineTo(250, 200);
  ctx.stroke();
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.moveTo(50, 50);
  ctx.lineTo(200, 50);
  ctx.lineTo(50, 200);
  ctx.stroke();
})();

5.线条API

1)ctx.lineWidth 属性

  • 设置线条粗细

2)ctx.lineCap 属性

  • 设置线条端点的样式(连接点、线帽)
属性值含义说明
butt平的默认,没有任何额外的效果
round圆形端点处增加了半圆,视觉效果上直线变长了
square方形端点处增加了矩形,视觉效果上直线变长了
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(50, 10);
  ctx.lineTo(50, 90);
  ctx.moveTo(250, 10);
  ctx.lineTo(250, 90);
  ctx.stroke();

  ctx.lineWidth = 10;
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.lineCap = "butt";
  ctx.moveTo(50, 30);
  ctx.lineTo(250, 30);
  ctx.stroke();

  ctx.beginPath();
  ctx.lineCap = "round";
  ctx.moveTo(50, 50);
  ctx.lineTo(250, 50);
  ctx.stroke();

  ctx.beginPath();
  ctx.lineCap = "square";
  ctx.moveTo(50, 70);
  ctx.lineTo(250, 70);
  ctx.stroke();
})();

3)ctx.lineJoin 属性

  • 设置折线连接处的样式
属性值含义说明
miter尖角默认,尖的
round圆角圆的
bevel斜角平的
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.lineWidth = 10;
  ctx.lineJoin = "miter";
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.moveTo(50, 50);
  ctx.lineTo(150, 150);
  ctx.lineTo(250, 50);
  ctx.stroke();

  ctx.lineWidth = 10;
  ctx.lineJoin = "round";
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.moveTo(50, 100);
  ctx.lineTo(150, 200);
  ctx.lineTo(250, 100);
  ctx.stroke();

  ctx.lineWidth = 10;
  ctx.lineJoin = "bevel";
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.moveTo(50, 150);
  ctx.lineTo(150, 250);
  ctx.lineTo(250, 150);
  ctx.stroke();
})();

4)ctx.miterLimit 属性

  • 限制折线形成的尖角长短
    • 当线条比较粗、折线夹角比较小时,lineJoin设置的miter效果形成的尖会比较长
    • 可以利用该属性来控制尖角的长短
(() => {
  const canvas = document.createElement('canvas');
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext('2d');

  ctx.beginPath();
  ctx.moveTo(20, 100);
  ctx.lineTo(250, 100);
  ctx.stroke();

  ctx.lineWidth = 30;
  ctx.lineJoin = "miter";
  ctx.strokeStyle = '#00f';
  ctx.beginPath();
  ctx.miterLimit = 0;
  ctx.moveTo(50, 50);
  ctx.lineTo(80, 100);
  ctx.lineTo(110, 50);
  ctx.stroke();

  ctx.beginPath();
  ctx.miterLimit = 1;
  ctx.moveTo(150, 50);
  ctx.lineTo(180, 100);
  ctx.lineTo(210, 50);
  ctx.stroke();
})();

5)ctx.setLineDash(array) 方法

  • 设置虚线
  • array 中可以放置多个数值,分别表示 线段的长度线段间留白的长度
  • [10]:线段长度(10)、留白长度(10)
  • [20, 10]:线段长度(20)、留白长度(10)
  • [10, 20, 30]:按照数组的数列,无限的延续下去
    • 线段10 留白20 线段30 留白10 线段20 留白30 线段10 留白20 ......
    • 相当于 [10, 20, 30, 10, 20, 30, 10, 20, 30, 10, 20, 30, ......]
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.lineWidth = 10;
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.setLineDash([20]);
  ctx.moveTo(50, 50);
  ctx.lineTo(250, 50);
  ctx.stroke();

  ctx.beginPath();
  ctx.setLineDash([20, 10]);
  ctx.moveTo(50, 100);
  ctx.lineTo(250, 100);
  ctx.stroke();

  ctx.beginPath();
  ctx.setLineDash([40, 20, 10]);
  ctx.moveTo(50, 150);
  ctx.lineTo(250, 150);
  ctx.stroke();
})();

6)ctx.lineDashOffset 属性

  • 设置虚线起始位置的偏移
    • 正数值,向左偏移
    • 负数值,向右偏移
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(50, 10);
  ctx.lineTo(50, 190);
  ctx.stroke();

  ctx.lineWidth = 10;
  ctx.strokeStyle = "#00f";
  ctx.beginPath();
  ctx.setLineDash([40]);
  ctx.moveTo(50, 50);
  ctx.lineTo(250, 50);
  ctx.stroke();

  ctx.beginPath();
  ctx.setLineDash([40]);
  ctx.lineDashOffset = -20;
  ctx.moveTo(50, 100);
  ctx.lineTo(250, 100);
  ctx.stroke();

  ctx.beginPath();
  ctx.setLineDash([40]);
  ctx.lineDashOffset = 20;
  ctx.moveTo(50, 150);
  ctx.lineTo(250, 150);
  ctx.stroke();
})();

6.清除画布

  • 大多数情况下,当canvas配合js动画实现动画效果时
    • 默认每一次都是在之前的基础上进行绘制
    • 所以应该清除上一次的绘画效果,重新绘制

1)清除画布中的指定矩形区域

  • ctx.clearRect(x, y, width, height)
  • 如果 width 和 height 等于画布宽高,就相当于清除整个画布,否则清除画布的一部分

注意

  • 清除画布的本质是将指定的矩形区域,设置透明度为0,之前的路径依然存在
  • 绘制新路径时需要配合 beginPath(),否则 stroke()fill() 时之前的清除效果会重现
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  // 绘制横线
  ctx.lineWidth = 10;
  ctx.moveTo(0, 100);
  ctx.lineTo(400, 100);
  ctx.stroke();

  // 清除画布
  ctx.clearRect(0, 0, 400, 400);

  // 绘制竖线
  ctx.beginPath();
  ctx.moveTo(100, 0);
  ctx.lineTo(100, 400);
  ctx.stroke();
})();
  • 如果没有 beginPath(),绘制竖线的时候,之前的横线也会出现
  • 如果有 beginPath(),只会绘制竖线,之前的横线不会重新绘制,实现 永久擦除 效果

7.虚线小动画

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(50, 100);
  ctx.lineTo(50, 200);
  ctx.moveTo(250, 100);
  ctx.lineTo(250, 200);
  ctx.stroke();

  ctx.lineWidth = 10;
  ctx.strokeStyle = "#00f";
  ctx.setLineDash([200]);
  ctx.lineDashOffset = 200;

  function move() {
    ctx.clearRect(50, 145, 200, 10);
    ctx.beginPath();
    ctx.lineDashOffset -= 1;
    ctx.moveTo(50, 150);
    ctx.lineTo(250, 150);
    ctx.stroke();

    if (ctx.lineDashOffset == -200) {
      ctx.lineDashOffset = 200;
    }
    requestAnimationFrame(move);
  }
  requestAnimationFrame(move);
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.lineWidth = 5;
  ctx.strokeStyle = "#00f";
  ctx.setLineDash([10]);

  function move() {
    ctx.clearRect(0, 0, 400, 400);
    ctx.beginPath();
    ctx.lineDashOffset += 1;
    ctx.rect(100, 100, 200, 200);
    ctx.stroke();

    requestAnimationFrame(move);
  }
  requestAnimationFrame(move);
})();




















 
 
 
 
 
 
 
 
 
 
 
 
 
 
 














 
 
 
 
 
 
 
 
 
 

8.closePath方法

  • 多个连续线条围合的区域,可以使用 fill() 进行填充
  • 如果需要首尾节点自动闭合,可以使用 ctx.closePath() 方法
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.lineWidth = 10;
  ctx.fillStyle = "#fac";
  ctx.moveTo(100, 100);
  ctx.lineTo(100, 200);
  ctx.lineTo(200, 200);
  // ctx.lineTo(100, 100); // 手动闭合
  ctx.closePath();
  ctx.stroke();
  ctx.fill();
})();

9.绘制圆弧

1)arc(x, y, r, startAngle, endAngle[, dir])

  • x,y:圆点坐标
  • r:半径
  • startAngle:起始点的角度
    • 默认 圆点所在的x轴右侧半径长度的位置 为绘制的起始点,即:0度点/3点钟方向
    • 角度方向是 顺时针
  • endAngle:结束点的角度
  • dir:绘制方向
    • false 顺时针,默认
    • true 逆时针
    • 可缺省

警告

  • 设计圆弧时,用的是角度
  • 传递参数时,传递的是弧度
  • 1弧度:弧长等于半径的圆弧所对应的角度(r)
  • 1(角度) = Math.PI / 180(弧度)
  • 360(角度) = Math.PI * 2
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.arc(100, 100, 50, 0, Math.PI * 2);
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(300, 100, 50, 0, Math.PI, true);
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(100, 300, 50, Math.PI / 2, Math.PI, true);
  ctx.stroke();
})();

2)arcTo(x1, y1, x2, y2, r)

  • 是由3个控制点实现圆弧的绘制
    • 第一个点:moveTo 或上一次图形结束的点,不需要通过参数设置
    • 第二个点:(x1, y1)
    • 第三个点:(x2, y2)
  • 按照 1 -> 2 -> 3 的顺序进行连线,两条线会形成一个夹角
  • 根据r绘制圆弧,保证与两个线条相切
    • 半径越大,圆弧与两条线围合的区域(左下角)越大
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.beginPath();
  ctx.moveTo(100, 100);
  ctx.lineTo(100, 300);
  ctx.lineTo(300, 300);
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(200, 200, 100, 0, Math.PI * 2);
  ctx.stroke();

  ctx.beginPath();
  ctx.lineWidth = 4;
  ctx.strokeStyle = "#00f";
  ctx.moveTo(100, 200);
  ctx.arcTo(100, 300, 200, 300, 100);
  ctx.stroke();
})();

10.绘制椭圆

1)ctx.ellipse(x, y, rx, ry, rotate, startAngle, endAngle[, dir])

  • x, y:圆点坐标
  • rx, ry:x轴半径和y轴半径
  • rotate:x轴旋转角度(顺时针方向)
  • startAngle:起始点角度默认0度(三点钟方向)
  • endAngle:终点角度
  • dir:绘制方向
    • false 顺时针
    • true 逆时针
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  ctx.lineWidth = 4;
  ctx.strokeStyle = "#00f";

  ctx.beginPath();
  ctx.ellipse(100, 100, 100, 50, 0, 0, Math.PI * 2);
  ctx.stroke();

  ctx.beginPath();
  ctx.ellipse(300, 100, 100, 50, 0, 0, Math.PI / 2, true);
  ctx.stroke();

  ctx.beginPath();
  ctx.ellipse(100, 300, 100, 50, Math.PI / 2, 0, Math.PI * 2);
  ctx.stroke();
})();

11.绘制曲线

1)概念

  • canvas提供了一种绘制曲线的方式:贝塞尔曲线
    • 二次贝塞尔曲线
    • 三次贝塞尔曲线

2)原理

  • 有一个起点和终点
  • 在两点中间,有多个控制点
    • 有一个控制点,称为二次贝塞尔曲线
    • 有两个控制点,称为三次贝塞尔曲线
    • 控制点越多,曲线越精细
    • 控制点位置不受起终点影响
  • 起点 -> 经过控制点 -> 终点依次连线(第一批线段)
  • 提供一个参数t,在 [0, 1] 范围内变化
  • 每一个t都存在以下情况:
    • 在任意线段中,从起点到终点,存在一个中间点,使得 前部分线段 / 整条线段 = t
    • 对每条线段的这些点,再一次连接(第二批线段,比第一批少一条)
    • 在第二批线段中,依然存在符合比例t的那些点
    • 重复之前连线、找点的操作
    • 直到找到最后一个点,就是这段贝塞尔曲线在当前比例t时曲线上的点
  • 当t在0-1范围变化时,每次都会有一个这样的点,这些点连接后就形成了贝塞尔曲线

3)绘制二次贝塞尔曲线

  • ctx.quadraticCurveTo(cx1, cy1, ex, ey)
  • cx1, cy1:控制点坐标
  • ex, ey:终点坐标
  • 起点的坐标是moveTo设置,或者是上一次绘图的结尾点
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.fillStyle = "#f00";
  ctx.beginPath();
  ctx.arc(50, 200, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(100, 100, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(250, 200, 4, 0, Math.PI * 2);
  ctx.fill();

  ctx.beginPath();
  ctx.moveTo(50, 200);
  ctx.quadraticCurveTo(100, 100, 250, 200);
  ctx.stroke();
})();

相关信息

移动控制点,可以改变曲线的形状

4)绘制三次贝塞尔曲线

  • bezierCurveTo(cx1, cy1, cx2, cy2, ex, ey)
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.fillStyle = "#f00";
  ctx.beginPath();
  ctx.arc(50, 200, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(100, 100, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(200, 300, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(250, 200, 4, 0, Math.PI * 2);
  ctx.fill();

  ctx.beginPath();
  ctx.moveTo(50, 200);
  ctx.bezierCurveTo(100, 100, 200, 300, 250, 200);
  ctx.stroke();
})();

12.绘制文本

1)ctx.fillText(textStr, x, y[, maxWidth])

  • 绘制填充文本
  • textStr:文本内容
  • x, y:文本位置(坐标)
  • maxWidth:可选,设置文本最大宽度
    • 如果 文本宽度 > 最大宽度 就会放缩,压缩在 maxWidth 范围内

2)ctx.strokeText()

  • 绘制描边文本(镂空)

3)ctx.font

  • 设置文本样式(粗体、斜体、大小、字体)

警告

必须设置字体,否则其他样式无效

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.font = "bold italic 40px sans-serif";
  ctx.fillText("DMC", 200, 200);
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.font = "bold italic 80px sans-serif";
  ctx.strokeText("DMC", 200, 200, 500);
})();

4)ctx.textAlign

  • 设置基于锚点水平位置
  • left
  • center
  • right

5)ctx.textBaseline

  • 设置基于锚点的垂直位置
  • bottom
  • middle
  • top

6)ctx.measureText(text)

  • 对文本相关盒子尺寸进行预估
  • 返回一个对象,包含文本宽度、字符间距、边距等属性值
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.textAlign = "center";
  ctx.font = "80px sans-serif";
  ctx.textBaseline = "middle";
  ctx.strokeText("DMC", 200, 200, 500);

  const obj = ctx.measureText("DMC");
  console.log(obj);

  ctx.beginPath();
  ctx.fillStyle = "#f00";
  ctx.arc(200, 200, 4, 0, Math.PI * 2);
  ctx.fill();
})();

(四)使用图像

  • 在canvas中引入其他图片

1.基本使用

1)图片源

  • Image对象,对应img标签
    • 可以是图片的路径
    • 也可以是图片的base64
  • video对象
  • canvas对象

2)ctx.drawImage(imgSource, x, y)

  • 引入图片
  • x, y:在canvas画布中放置的起始坐标位置
  • 会按照图像原大小展示,超出画布可视区域部分将不可见
ctx.drawImage(img, 0, 0);

3)ctx.drawImage(imgSource, x, y, width, height)

  • width, height:图像展示的大小(缩放处理)
ctx.drawImage(img, 0, 90, 400, 220);

4)ctx.drawImage(imgSource, x1, y1, w1, h1, x2, y2, w2, h2)

  • x1, y1, w1, h1:图像的截图区域,此时基于图像的坐标系
  • x2, y2, w2, h2:画布展示区域
ctx.drawImage(img, 44, 0, 200, img.height, 100, (400 - img.height) / 2, 200, img.height);

警告

使用图片源时,要确保图片加载完成,建议在 img.onload 事件中使用图片源

const img = new Image();
img.src = "../img/01.png";
img.onload = function () {
  ctx.drawImage(img, 0, 0);
};

2.图像与动画

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.src = "../imgs/02.png";
  img.onload = function () {
    let i = 0;
    function show() {
      ctx.clearRect(0, 0, 400, 400);
      ctx.drawImage(img, i * 94, 0, 94, img.height, 0, 0, 94, img.height);
      i++;
      if (i == 5) {
        i = 0;
      }
    }
    setInterval(show, 100);
  };
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.src = "../imgs/02.png";
  img.onload = function () {
    let i = 0;
    let j = 0;
    function show() {
      ctx.clearRect(0, 0, 400, 400);
      ctx.drawImage(img, i * 94, 0, 94, img.height, j * 10, 0, 94, img.height);
      i++;
      j++;
      if (i == 5) {
        i = 0;
      }
      if (j * 10 >= canvas.width) {
        j = 0;
      }
    }
    setInterval(show, 100);
  };
})();

3.视频图像

  • 在视频播放中,抓取当前帧作为图像,引入canvas
<video src="../img/01.mp4" controls width="400" height="400" muted></video>

<script>
  (() => {
    const canvas = document.createElement("canvas");
    canvas.width = 400;
    canvas.height = 400;
    document.body.append(canvas);

    const ctx = canvas.getContext("2d");

    const video = document.querySelector("video");
    video.addEventListener("play", function () {
      draw();
    });

    ctx.arc(200, 200, 150, 0, Math.PI * 2);
    // ctx.filter = "blur(5px)";
    // ctx.filter = 'invert(0.8)';
    // ctx.clip();

    function draw() {
      ctx.clearRect(0, 0, 400, 400);
      ctx.drawImage(video, 0, 0, 400, 400);
      requestAnimationFrame(draw);
    }
  })();
</script>

4.Canvas图像

1)Canvas引入

  • canvas本身也是一个图像
  • 可以作为图像源,引入另一个canvas画布
let canvas1;
(() => {
  canvas1 = document.createElement("canvas");
  canvas1.width = 200;
  canvas1.height = 200;
  document.body.append(canvas1);

  const ctx = canvas1.getContext("2d");

  // 同心圆
  for (let i = 1; i <= 5; i++) {
    ctx.beginPath();
    ctx.arc(100, 100, 20 * i, 0, Math.PI * 2);
    ctx.stroke();
  }

  ctx.beginPath();
  ctx.moveTo(0, 100);
  ctx.lineTo(200, 100);
  ctx.stroke();

  ctx.beginPath();
  ctx.moveTo(100, 0);
  ctx.lineTo(100, 200);
  ctx.stroke();
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.drawImage(canvas1, 0, 0, 100, 100, 150, 150, 100, 100);
})();

2)Canvas下载

  • 右键另存
  • 编程式下载
(() => {
  const btn = document.querySelector("button");
  btn.onclick = function () {
    const url = canvas1.toDataURL();
    const a = document.createElement("a");
    a.href = url;
    a.download = "canvas画布";
    a.click();
  };
})();

相关信息

  • toDataURL() 默认生成 png 格式
  • 可以通过传参指定图片格式 toDataURL('image/jpeg')
  • 如果canvas中的图像来自于其他路径的图像源(img、video)
    • 可能存在同源问题,画布会被污染
  • 解决方案
    • 设置同源策略 img.crossOrigin = 'anonymous';
    • 服务器启动代码,不能使用浏览器直接打开文件
let canvas3;
(() => {
  const canvas = document.createElement("canvas");
  canvas3 = canvas;
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.crossOrigin = "anonymous";
  img.src = "../imgs/01.png";
  img.onload = function () {
    ctx.drawImage(img, 0, 0);
  };
})();

(() => {
  const btn = document.querySelectorAll("button")[1];
  btn.onclick = function () {
    const url = canvas3.toDataURL();
    const a = document.createElement("a");
    a.href = url;
    a.download = "canvas画布";
    a.click();
  };
})();

5.图像像素处理

  • JS中提供了ImageData对象
  • 包含了某一个区域内的像素值
    • imageData.width:横向有多少个像素
    • imageData.height:纵向有多少个像素
    • imageData.data:array,包含区域内所有的像素值(rgba值)
      • array 是一个 一维数组,每4个位置表示一个像素值
      • [r1,g1,b1,a1,r2,g2,b2,a2,r3,g3,b3,a3,...]
  • (x, y) 像素的值为 (imageData.width * 4) * y + x * 4 + 0/1/2/3
    • imageData.width _ 4 _ y:横向像素总的值 * 多少行
    • x * 4:找到 (x, y) 对应的 r, g, b, a 在 data 数组中的位置
    • 0/1/2/3:找到对应的 r/g/b/a 颜色通道值

1)ctx.getImageData(x, y, width, height)

  • 获得画布中指定区域的ImageData对象(像素值)
  • 获得ImageData对象后,就可以通过其获得每一个像素的值,也可以设置每一个像素的值
  • 设置之后默认不会生效,还需要重新设置画布的ImageData

2)ctx.putImageData(imageData, x, y)

  • 重新设置画布指定区域的像素值(灰度设置、反色设置等)
  • (x, y) 是重新设置后图像所在位置,可以设置错位效果
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.src = "../img/01.png";
  img.onload = function () {
    ctx.drawImage(img, 0, 0);

    const imageData = ctx.getImageData(0, 0, 400, 400);
    for (let i = 0; i < imageData.data.length; i += 4) {
      const r = imageData.data[i];
      const g = imageData.data[i + 1];
      const b = imageData.data[i + 2];
      const a = imageData.data[i + 3];

      // 灰度
      const avg = (r + g + b) / 3;
      imageData.data[i] = avg;
      imageData.data[i + 1] = avg;
      imageData.data[i + 2] = avg;

      // 仅对红色通道设置灰度
      // imageData.data[i] = r;
      // imageData.data[i + 1] = r;
      // imageData.data[i + 2] = r;

      // 相反色
      // imageData.data[i] = 255 - r;
      // imageData.data[i + 1] = 255 - g;
      // imageData.data[i + 2] = 255 - b;

      // 仅对红绿差距明显的位置,减少红色增加绿色(对应值互换)
      // if (r - g > 30) {
      //   imageData.data[i] = g;
      //   imageData.data[i + 1] = r;
      // }
    }

    ctx.putImageData(imageData, 0, 0);
  };
})();

相关信息

  • 可以使用外部图像,但要通过ImageData对其进一步处理时,会存在跨域问题
  • 使用服务器模式启动即可
  • 如果还是无效,可以设置 img.cross-origin = "anonymous"

6.图像填充

  • 可以将引入图像作为填充背景、描边背景
  • 图像源可以有多种:img、canvas、video、......

1)ctx.createPattern(imgSource, repetition)

  • 创建一个图案对象,CanvasPattern对象
    • imgSource:图像源
    • repetition:重复机制
      • repeat
      • repeat-x
      • repeat-y
      • no-repeat
      • 类型为 String | null ,不能不传(undefined)

2)设置背景

  • 填充背景:ctx.fillStyle = pattern;
  • 描边背景:ctx.strokeStyle = pattern;
let bgCanvas;

(() => {
  bgCanvas = document.createElement("canvas");
  bgCanvas.width = 30;
  bgCanvas.height = 30;
  document.body.append(bgCanvas);

  const ctx = bgCanvas.getContext("2d");

  // 绘制一个菱形
  ctx.moveTo(0, 15);
  ctx.lineTo(15, 0);
  ctx.lineTo(30, 15);
  ctx.lineTo(15, 30);
  ctx.closePath();
  ctx.fill();
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.src = "../img/03.png";
  img.onload = function () {
    const pattern = ctx.createPattern(img, "repeat");
    const pattern2 = ctx.createPattern(bgCanvas, "");

    ctx.lineWidth = 30;
    ctx.strokeStyle = pattern2;
    ctx.fillStyle = pattern;
    ctx.rect(15, 15, 330, 330);
    ctx.stroke();
    ctx.fill();
  };
})();






























 
 
 
 
 
 
 
 
 
 
 

相关信息

  • 图案平铺的样式都是基于 画布坐标系的原点 开始计算的,不是基于绘制的路径坐标系位置
  • 所以在横向平铺、纵向平铺和不平铺的情况下,有可能画布中央的图形无法显示完整图形效果(可能有裁剪)

(五)图像裁剪

1.ctx.clip()

  • 将clip前给出的路径设置为裁剪路径
  • 之后绘制的图形只会在裁剪路径中展示
  • 对之前绘制的图形没有影响
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img = new Image();
  img.src = "../img/04.png";
  img.onload = function () {
    // 设置裁剪路径
    ctx.beginPath();
    ctx.arc(200, 200, 100, 0, Math.PI * 2);
    ctx.clip();

    // 裁剪的图形
    ctx.drawImage(img, 200 - img.width / 2, 200 - img.height / 2);
  };
})();

2.ctx.clip(rule)

  • nonzero:默认值,非零环绕路径
  • evenodd:奇偶环绕路径

相关信息

在绘制裁剪路径的时候,有些路径区域可能会被重复绘制(包含、相交)

1)非零环绕

  • 顺时针绘制经过路径区域,数量 + 1
  • 逆时针绘制经过路径,数量 - 1
  • 路径区域最终经过的数量为0,就不裁剪(不可见)
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  ctx.strokeRect(100, 100, 200, 200);

  ctx.beginPath();
  // 顺时针
  ctx.arc(200, 200, 100, 0, Math.PI * 2, false);
  // 逆时针
  ctx.arc(200, 200, 80, 0, Math.PI * 2, true);
  ctx.clip("nonzero");

  ctx.fillRect(100, 100, 200, 200);
})();

2)奇偶环绕

  • 不分顺时针和逆时针,只要绘制路径经过区域,数量 + 1
  • 最终奇数裁剪(可见),偶数不裁剪(不可见)
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  ctx.strokeRect(100, 100, 200, 200);

  ctx.beginPath();
  // 顺时针
  ctx.arc(200, 200, 100, 0, Math.PI * 2, false);
  // 顺时针
  ctx.arc(200, 200, 80, 0, Math.PI * 2, false);
  ctx.clip("evenodd");

  ctx.fillRect(100, 100, 200, 200);
})();

(六)图像合成

  • 将前后图形合成一个图形
  • 使用 ctx.globalCompositeOperation 属性设置合成机制
  • 需要在前后两个图形 中间 设置

相关信息

前后图形指的是代码绘制图形的先后顺序

1.路径(形状)合成

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");
  // 前(原)图形
  ctx.beginPath();
  ctx.fillStyle = "#f00";
  ctx.fillRect(50, 50, 200, 200);

  ctx.globalCompositeOperation = "???";

  // 后(新)图形
  ctx.beginPath();
  ctx.fillStyle = "#0f0";
  ctx.fillRect(150, 150, 200, 200);
})();

1)以前面图形为主导

属性说明
source-over默认,前后图形都展示,重叠部分展示 后面 的图形
source-in只展示后面的图形,展示 与前面图形重叠 的部分
source-out只展示后面的图形,展示 与前面图形不重叠 的部分
source-atop展示前面的图形,后面的图形只展示 与前面图形重叠 的部分

2)以后面图形为主导

属性说明
destination-over默认,前后图形都展示,重叠部分展示 前面 的图形
destination-in只展示前面的图形,展示 与后面图形重叠 的部分
destination-out只展示前面的图形,展示 与后面图形不重叠 的部分
destination-atop展示后面的图形,前面的图形只展示 与后面图形重叠 的部分

3)以重叠部分为主导

属性说明
copy后面的图形覆盖前面的图形(前面的图形没了)
xor展示前后两个图形非重叠的部分

2.颜色合成

  • 关注的是颜色的混合
  • 图形的形状没有变化
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const img1 = new Image();
  img1.src = "../imgs/05.png";
  img1.onload = function () {
    ctx.drawImage(img1, 200 - img1.width / 2, 200 - img1.height / 2);

    // 必须放在img1加载之后,或者img2加载之前,确保页面上有了img1之后才能设置合成机制
    ctx.globalCompositeOperation = "multiply";
  };

  const img2 = new Image();
  img2.src = "../imgs/04.png";
  img2.onload = function () {
    ctx.drawImage(img2, 200 - img2.width / 2, 200 - img2.height / 2);
  };
})();













 
 








属性说明
lighter重叠部分的颜色相加
multiply整体偏暗,同一个像素的颜色计算后的颜色值
越接近白色则显示原色,越接近黑色则显示黑色
screen整体偏亮,同一个像素的颜色计算后的颜色值
越接近黑色则显示原色,越接近白色则显示白色
darken整体偏暗,同一个像素的颜色,取暗色的色值(二者取一,无计算)
lighten整体偏亮,同一个像素的颜色,取亮色的色值(二者取一,无计算)

3.刮刮乐效果

<!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>Document</title>
    <style>
      canvas {
        border: 1px solid #ccc;
        margin-right: 5px;
        position: absolute;
      }

      img {
        width: 300px;
        height: 200px;
        background-color: #fdd;
        text-align: center;
        line-height: 200px;
        font-size: 66px;
        position: absolute;
        user-select: none;
      }
    </style>
  </head>

  <body>
    <img src="../imgs/04.png" />
    <script>
      (() => {
        const canvas = document.createElement("canvas");
        canvas.width = 300;
        canvas.height = 200;
        document.body.append(canvas);

        const ctx = canvas.getContext("2d");

        ctx.beginPath();
        ctx.fillStyle = "#ccc";
        ctx.fillRect(0, 0, 300, 200);

        ctx.globalCompositeOperation = "destination-out";

        ctx.beginPath();
        ctx.strokeStyle = "#fff";
        ctx.lineWidth = 20;
        ctx.lineCap = "round";
        ctx.lineJoin = "round";

        canvas.onmousedown = function (e) {
          ctx.moveTo(e.offsetX, e.offsetY);

          canvas.onmousemove = function (e) {
            ctx.lineTo(e.offsetX, e.offsetY);
            ctx.stroke();
          };

          canvas.onmouseup = canvas.onmouseout = function () {
            canvas.onmousemove = null;
            canvas.onmouseout = null;
          };
        };
      })();
    </script>
  </body>
</html>

(七)颜色渐变

1.线性渐变

1)ctx.createLinearGradient(x0, y0, x1, y1)

  • 创建线性渐变对象,CanvasGradient对象
  • (x0, y0) 和 (x1, y1) 是两个点
  • 会按照两点的连线方向渐变(横向、纵向、斜向)

注意

渐变中的两个点是基于坐标系的,需要考虑渐变区域与图形区域的关系

2)gradient.addColorStop(percent, color)

  • 设置渐变过程中每一部分的颜色
    • 填充渐变:ctx.fillStyle = gradient
    • 描边渐变:ctx.strokeStyle = gradient
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const gradient = ctx.createLinearGradient(0, 0, 400, 0);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(0.5, "#0f0");
  gradient.addColorStop(1, "#00f");
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 400, 400);

  const gradient = ctx.createLinearGradient(0, 0, 400, 0);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(0.5, "#0f0");
  gradient.addColorStop(1, "#00f");
  ctx.fillStyle = gradient;
  ctx.fillRect(100, 0, 200, 400);

  const gradient = ctx.createLinearGradient(100, 0, 300, 0);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(0.5, "#0f0");
  gradient.addColorStop(1, "#00f");
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 400, 400);
})();








 




 

 




 

 




 

相关信息

  • 如果渐变区域与图形区域相同,则显示完整渐变效果
  • 如果渐变区域比图形区域大,则图形显示对应区域的渐变效果
  • 如果渐变区域比图形区域小,则图形范围的两侧就是渐变两侧的颜色

2.径向渐变

1)ctx.createRadialGradient(x1, y1, r1, x2, y2, r2)

  • 创建径向渐变对象
  • x1, y1, r1:表示渐变开始的圆
  • x2, y2, r2:表示渐变结束的圆

相关信息

  • 一般都是一个大圆,一个小圆才会有效果
  • 小圆一定要在大圆内,否则会出现意想不到的效果
  • 小圆以里,大圆以外 的范围使用渐变两端的颜色
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const gradient = ctx.createRadialGradient(200, 200, 100, 200, 200, 50);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(1, "#ff0");
  ctx.fillStyle = gradient;

  ctx.arc(200, 200, 200, 0, Math.PI * 2);
  ctx.stroke();
  ctx.fill();
})();

(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const gradient = ctx.createRadialGradient(250, 200, 49, 200, 200, 100);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(1, "#ff0");
  ctx.fillStyle = gradient;

  ctx.arc(200, 200, 200, 0, Math.PI * 2);
  ctx.stroke();
  ctx.fill();
})();








 
 
 
 














 
 
 
 





3.锥形渐变

1)ctx.createConicGradient(angle, x, y)

  • 创建锥形渐变对象,CanvasGradient对象
  • (x, y):圆心点
  • angle:起始角度,默认0°角是三点钟方向
    • angle = 90° 表示从六点钟方向开始旋转
    • angle传参时使用的是 角度对应的弧度值
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  const gradient = ctx.createConicGradient(Math.PI / 2, 200, 200);
  gradient.addColorStop(0, "#f00");
  gradient.addColorStop(0.25, "#ff0");
  gradient.addColorStop(0.5, "#0f0");
  gradient.addColorStop(0.75, "#0ff");
  gradient.addColorStop(1, "#00f");
  ctx.fillStyle = gradient;

  ctx.arc(200, 200, 100, 0, Math.PI * 2);
  ctx.stroke();
  ctx.fill();

  ctx.beginPath();
  ctx.fillStyle = "#fff";
  ctx.arc(200, 200, 60, 0, Math.PI * 2);
  ctx.fill();
})();

(八)图形阴影

1.设置模糊程度

  • ctx.shadowBlur
  • 数字越大越模糊

2.设置阴影颜色

  • ctx.shadowColor
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.shadowColor = "#ff0";
  ctx.shadowBlur = 100;

  ctx.fillStyle = "#f00";
  ctx.arc(200, 200, 100, 0, Math.PI * 2);
  ctx.fill();
})();

3.设置阴影的偏移量

  • ctx.shadowOffsetX
  • ctx.shadowOffsetY
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.shadowColor = "#666";
  ctx.shadowBlur = 15;
  ctx.shadowOffsetX = 8;
  ctx.shadowOffsetY = 8;

  const gradient = ctx.createRadialGradient(170, 170, 30, 200, 200, 100);
  gradient.addColorStop(0, "#d00");
  gradient.addColorStop(1, "#800");

  ctx.fillStyle = gradient;
  ctx.arc(200, 200, 100, 0, Math.PI * 2);
  ctx.fill();
})();

(九)滤镜

  • 对像素点的 rgba 值计算后改变像素的视觉效果
  • ctx.filter 属性,设置一个或多个滤镜

1.设置模糊

  • ctx.filter = "blur(10px)"
  • 值越大,模糊效果越明显
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 300;
  canvas.height = 300;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.filter = "blur(4px)";

  const img = new Image();
  img.src = "../imgs/04.png";
  img.onload = function () {
    ctx.drawImage(img, 0, 0, 300, 300);
  };
})();

2.设置亮度

  • ctx.filter = "brightness(%)"
  • 1不变
  • <1 变暗
  • >1 变亮,稍刺眼
ctx.filter = "brightness(1.5)";

3.设置对比度

  • ctx.filter = "contrast(%)"
  • 1不变
  • <1 颜色接近
  • >1 颜色鲜明、更深
ctx.filter = "contrast(1.5)";

4.设置饱和度

  • ctx.filter = "saturate(%)"
  • 1不变
  • <1 变灰,rgb颜色占比减少
  • >1 颜色鲜明,占比最多的颜色通道值增加,其他通道减少
ctx.filter = "saturate(1.5)";

5.设置灰度

  • ctx.filter = "grayscale(%)"
  • 0不变
  • 1变灰
ctx.filter = "grayscale(1)";

6.设置怀旧风格

  • ctx.filter = "sepia(1)"
  • 0不变
  • 1怀旧风格(深褐色)
ctx.filter = "sepia(1)";

7.设置反色

  • ctx.filter = "invert(1)"
  • 0不变
  • 1颜色取反
  • 0.5灰色
ctx.filter = "invert(1)";

8.设置阴影

  • ctx.filter = "drop-shadow(x, y, blur, color)"
  • x, y, blur 都需要带px单位
ctx.filter = "drop-shadow(10px 10px 10px #acf)";

9.设置色调

  • ctx.filter = "hue-rotate(180deg)"
  • 各像素点的色值在色盘上旋转给定角度后,形成新的色值
ctx.filter = "hue-rotate(180deg)";

10.使用多个滤镜

  • ctx.filter = "hue-rotate(180deg) contrast(0.5)"
ctx.filter = "hue-rotate(180deg) contrast(1.5)";

11.引用svg滤镜

  • ctx.filter = "url(svgFilterID)"

(十)图像变换

  • 对图形进行移动、旋转、放缩、矩阵斜切

1.图形移动

  • 移动不是动画,只是视觉位置上的变化

1)实现图形位置的移动

  • ctx.translate(x, y)
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.translate(100, 100);

  ctx.rect(100, 100, 200, 200);
  ctx.fill();
})();

2)原理

  • 实际移动的并不是指定的图形,而是坐标系
  • 对于之前已经绘制过的图形没有影响
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  // 基于原坐标系绘制的图形
  const ctx = canvas.getContext("2d");

  ctx.fillRect(0, 0, 100, 100);

  ctx.translate(100, 100);

  // 绘制坐标系
  ctx.beginPath();
  ctx.moveTo(-400, 0);
  ctx.lineTo(400, 0);
  ctx.moveTo(0, -400);
  ctx.lineTo(0, 400);
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(0, 0, 6, 0, Math.PI * 2);
  ctx.fill();

  for (let i = -400; i <= 400; i += 10) {
    ctx.beginPath();
    ctx.moveTo(i, -5);
    ctx.lineTo(i, 5);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(-5, i);
    ctx.lineTo(5, i);
    ctx.stroke();
  }

  // 新坐标系中绘制图形
  ctx.fillRect(100, 100, 100, 100);
})();

2.图形放缩

  • 本质是对坐标系横纵坐标的放缩
  • 原来长度是多少像素,放缩后还是多少像素,只是视觉效果变化了

1)ctx.scale(xRatio, yRatio)

  • 设置横纵坐标的放缩比例
  • 0 < ratio < 1 缩小
  • 1 < ratio 放大
  • 负数时,对应坐标轴的方向反转
// 横轴坐标放大2倍
ctx.translate(200, 200);
ctx.scale(-2, 1);

// 纵轴反转 => 构建数学坐标系
ctx.translate(0, 400);
ctx.scale(1, -1);

3.图像旋转

1)ctx.rotate(angle)

  • 设置顺时针旋转的角度
  • 旋转的还是坐标系,不是图形

注意

  • 逻辑上传递的是角度,语法上要求传递是 弧度
  • 弧度 = 角度 * Math.PI / 180
  • 移动与旋转的设置顺序不同,最终的效果也不相同
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  ctx.strokeStyle = "#f00";
  ctx.setLineDash([10]);
  ctx.strokeRect(100, 100, 50, 50);

  // 变化坐标系
  ctx.translate(125, 125);
  ctx.rotate((45 * Math.PI) / 180);

  // 坐标系中绘制图形
  ctx.fillRect(-25, -25, 50, 50);
})();

4.矩阵变换

  • Canvas没有提供斜切方法,可以利用矩阵变换来实现
  • 矩阵变换可以实现所有的图形变换(平移、放缩、旋转、斜切)

1)矩阵变换机制

  • 所谓的变换就是将原坐标按照一定的变换公式(逻辑)变换成一个新坐标
    • (x, y) -> 矩阵 -> (x', y')
  • 使用 齐次坐标系 来进行矩阵变换,可以简化平移计算
  • 传递转换矩阵,实现图形变换
    • 必须是三行三列的矩阵
    • ctx.transform(a, b, c, d, e, f)
|x|	  |a c e|		|x'|
|y|	* |b d f|	=	|y'|
|1|	  |0 0 1|		|1 |

x' = x * a + y * c + 1 * e
y' = x * b + y * d + 1 * f
1 =  x * 0 + y * 0 + 1 * 1

2)矩阵移动

  • 在原有 (x, y) 的基础上,移动指定的数值
/**
 * 横纵各移动100
 * 1 0 100
 * 0 1 100
 * 0 0 1
 */
ctx.transform(1, 0, 0, 1, 100, 100);

3)矩阵放缩

  • 在原有 (x, y) 的基础上,乘上指定的倍率
/**
 * 横向放大至2,纵向缩小至0.5
 * 2 0   0
 * 0 0.5 0
 * 0 0   1
 */
ctx.transform(2, 0, 0, 0.5, 0, 0);

4)矩阵斜切

  • 延x轴或y轴拉扯,使得与x轴或y轴形成一个夹角
    • 延x轴斜切,会产生与y轴的夹角,最终x位置发生变化
    • 延y轴斜切,会产生与x轴的夹角,最终y位置发生变化

/**
 * 实现skewX(30°)
 * 1	tan(30°)  0
 * 0  1		      0
 * 0  0         1
 */
ctx.transform(1, 0, Math.tan((30 * Math.PI) / 180), 1, 0, 0);

5)矩阵旋转

  • 旋转会发生角度的变化
  • 旋转都是基于 x轴正方向,顺时针旋转
  • 旋转角度是基于 原位置 的旋转角度,而不是基于x轴的旋转角度,所以还要考虑原位置与x轴夹角

和角公式

sin(a+b) = sin(a) * cos(b) + cos(a) * sin(b)
cos(a+b) = cos(a) * cos(b) - sin(a) * sin(b)
/**
 * 旋转45°
 * cos(30°)   -sin(30°)   0
 * sin(30°)   cos(30°)    0
 * 0          0           1
 */
ctx.transform(
  Math.cos((45 * Math.PI) / 180),
  Math.sin((45 * Math.PI) / 180), // a b
  -Math.sin((45 * Math.PI) / 180),
  Math.cos((45 * Math.PI) / 180), // c d
  0,
  0, // e f
);

(十一)状态存储与重置

1.绘制状态/上下文

  • 描边样式,填充样式
  • 线条样式
  • 文本样式
  • 裁剪
  • 合成
  • 图像变换

2.存储

  • 每次调用会将之前设置的状态存储起来(存入栈中)
  • 可以调用多次,将多个状态按照顺序 存入栈中
ctx.save();

3.重置

  • 每次调用会重置状态
  • 将当前状态 从栈中移除(删除),恢复到上一次的状态
ctx.restore();

注意

  • save 和 restore 不是必须的
  • 可以手动按照逻辑,恢复上一次的状态
(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 400;
  document.body.append(canvas);

  const ctx = canvas.getContext("2d");

  // 填充一个绿色的矩形
  ctx.save();
  ctx.fillStyle = "#0f0";
  ctx.beginPath();
  ctx.fillRect(0, 0, 100, 100);

  // 填充一个蓝色的圆
  ctx.save();
  ctx.fillStyle = "#00f";
  ctx.beginPath();
  ctx.arc(200, 200, 100, 0, Math.PI * 2);
  ctx.fill();

  // 填充一个绿色的矩形
  // ctx.fillStyle = '#0f0'; // 逻辑上恢复状态
  ctx.restore(); // 通过restore方法恢复状态,移除蓝色圆save的状态,剩余绿色矩形save的状态
  ctx.fillRect(300, 300, 100, 100);
})();

(十二)实战案例

1.动态时钟

  • 表盘
  • 刻度(大刻度、小刻度)
  • 表针(时针、分针、秒针)
    • 表针需要不停的变动
  • 动画绘制
  • 表针角度

2.粒子烟花

1)烟花升空

  • 由多个小球组成一个烟花升空拖尾的效果
  • 每一个小球的半径依次减小
  • 每一个小球的透明度依次变化,上升过程中,透明度整体还会变化
    • 第一瞬间10个小球的透明度分别是:1、0.99、0.98、......
    • 第二瞬间10个小球的透明度分别是:0.5、0.49、0.48、......

2)烟花爆炸

  • 烟花主体消失
  • 绘制一组小颗粒
  • 沿着圆弧扩散

相关信息

  • 有的烟花处于升空状态,有的烟花处于爆炸状态,两个效果可以同时进行
  • 所以需要每一次都重新绘制
  • 涉及对象
    • 烟花对象
    • 小球对象,组成烟花主体
    • 粒子对象,爆炸中的例子
  • 从升空到爆炸
    • 每隔一段时间,升空一个烟花
    • 当屏幕中达到3个烟花时,最开始烟花就可以爆炸了
  • 爆炸的时候,烟花主体消失(不再绘制)
  • 绘制爆炸后产生的粒子(400, 500),每次重新绘制的时候,改变其位置

3.贪吃蛇

1)组成部分

  • 棋盘
  • 蛇(蛇头 + 身体)
  • 食物

2)提供2个画布

  • 一个绘制棋牌(静态)
  • 一个绘制蛇 + 食物(动态)

3)绘制蛇

  • 将蛇设计成一个对象
  • 设计一个矩形对象,表示蛇的组成,表示食物

4)蛇(头)的移动

  • 注意方向
  • 每绘制一个新位置,要将原来位置的图形删除掉

5)生成食物

  • 随机绘制一个矩形
  • 确保在一个空白的位置(没有蛇的位置)
  • 设计
    • 定义一个对象
      • key表示每一个格子的坐标
      • value表示格子的占用状态,0空闲,1占用
    • 每次绘制矩形的时候,将其格子设置为1,清除矩形的时候,将其格子设置为0
    • 生成食物时,随机一个格子位置,判断其状态,1就重新随机,0绘制食物

6)吃食物&身体变化&身体移动

  • 当蛇头坐标与食物坐标相撞时,表示吃到食物
  • 吃到食物后,蛇的身体会边长,视觉效果上看就是后面身体的部分都没有变化,只有原来蛇头的位置加了一块身体,所以只需要在body的头部增加一个矩形,并绘制即可
  • 没有吃到食物,当蛇移动时,视觉效果上,最后一个位置的身体部分消失,在原来蛇头的位置增加了新的身体部分,其余部分没有变化,所以只需要将最后一个矩形移动到body的最前面并绘制即可

4.画图板

1)线条绘制

  • 多个点的连线
  • 鼠标按下是起始点
  • 鼠标移动,产生过程点
  • 鼠标抬起,绘制结束

2)矩形绘制

  • 起始点, 宽高
  • 鼠标按下,获得起始点
  • 鼠标移动,产生过程点
  • 通过两点,可以计算宽高
  • 矩形在绘制过程中,没有抬起鼠标,则还处于选择阶段,矩形没有确定,有一个拖拽的视觉效果
  • 实际上是一个不断绘制的过程,此时存在一个问题
    • 由于矩形会与其他图形产生覆盖,如果删除之前绘制的矩形,也会将之前图形覆盖的部分也删除
    • 为了提高性能,考虑使用2个画布
    • 在第一个画布中,绘制当前的这一个图形,提起鼠标后,图形确定,再将其绘制到第二个画布上

3)圆形绘制

  • 圆心点,半径
  • 鼠标按下,获得起始点
  • 鼠标移动,产生过程点
  • 起始点和过程点,计算半径和圆心点
  • 可能是正圆,也可能是椭圆
  • 圆的拖拽绘制与矩形相同

4)填充

  • 不是对某一个图形进行填充,而是对一块合围区域填充
  • 合围区域可能是有多个图形部分合围而成
  • 难点:如何确定这个合围区域?
    • 可以通过像素操作来实现
    • 以触发填充操作的那个点为基准,获得那个点的rgba值
    • 然后向四周分散,一次找到四周所有的点,与这个rgba比较
      • 完全相等,就实现颜色的变化,继续向四周扩散
      • 不相等,说明已经到了一个边界,就不继续发散了
// 递归(范围有限)
// 改变当前点的颜色
function change(x, y) {
  // 获得要改变这个点的原始颜色 (如何根据坐标点,获得其imageData中的显色位置)
  const i = point2Index(imageData, x, y);
  // 判断这个原始颜色和基准颜色是否相同,相同就改,不相同就结束了
  if (
    baseImageData.data[0] == imageData.data[i] &&
    baseImageData.data[1] == imageData.data[i + 1] &&
    baseImageData.data[2] == imageData.data[i + 2] &&
    baseImageData.data[3] == imageData.data[i + 3]
  ) {
    // 相等,这个位置颜色可以改变
    imageData.data[i] = 255;
    imageData.data[i + 1] = 0;
    imageData.data[i + 2] = 0;
    imageData.data[i + 3] = 255;

    // 继续发散,再检查其四周
    change(x - 1, y);
    change(x + 1, y);
    change(x, y + 1);
    change(x, y - 1);
  } else {
    // 不相等,到达边界
    return;
  }
}
change(this.x, this.y);

5)橡皮擦

  • 与刮刮乐实现过程相似
  • 本质还是画线条
  • 只不过与原图形的合成关系发生了变化

6)综上分析

  • 目前需要2个画布
    • 一个体现绘制过程
    • 一个用来展示绘制结果
  • 需要图形对象
    • 包括多种类型(线条、矩形、圆形、橡皮擦)
上次编辑于: