Canvas概述

<canvas>是 HTML5 新增的,一个可以使用脚本(通常为JavaScript)在其中绘制图像的 HTML 元素。它可以用来制作照片集或者制作简单(也不是那么简单)的动画,甚至可以进行实时视频处理和渲染。

创建Canvas

<canvas>会创建一个固定大小的画布,会公开一个或多个 渲染上下文(画笔),使用 渲染上下文来绘制和处理要展示的内容。

<canvas id="canvas" width="200" height="200"></canvas>

其中width和height并不是指canvas的真正尺寸,而是指canvas的精度,即将整个画布平分为200*200个像素点,真正指定canvas尺寸大小可以由CSS指定

#canvas {
  width: 500px;
  height: 500px;
}

渲染上下文

let canvas = document.getElementById('canvas') // 获取canvas元素 -> HTMLCanvasElement
let ctx = canvas.getContext('2d') // 获取到canvas上下文(画笔) -> CanvasRenderingContext2D

检测浏览器是否支持

由于canvas是HTML5新出的一个标签,有些老牌浏览器可能不支持,因此需要检测浏览器是否支持canvas,再进行后续操作。

window.addEventListener('load', eventWindowLoaded, false);

function eventWindowLoaded() {
  canvasApp();
}

function canvasSupport(e) {
  return !!e.getContext;
}

function canvasApp() {
  let myCanvas = document.getElementById('myCanvas');
  if (!canvasSupport(myCanvas)) {
    return;
  }
  let ctx = myCanvas.getContext('2d');
  function drawScreen() {
    // 这里开始绘制
  }
  drawScreen();
}

栅格和坐标空间

上面说了,canvas的属性width和height将canvas平均分布200*200个像素点,左上顶点坐标为(0, 0),右下顶点为(200, 200),如图所示:

image-20211120171202641

绘制网格

为了更好的看到效果,刚开始学习的时候可以绘制一个网格帮助理解。

function drawGrid(ctx, color, stepX, stepY) {
  ctx.strokeStyle = color;
  ctx.lineWidth = 0.5;
  for (let i = stepX + 0.5; i < myCanvas.width; i += stepX) {
    ctx.beginPath();
    ctx.moveTo(i, 0);
    ctx.lineTo(i, myCanvas.height);
    ctx.stroke();
  }
  for (let i = stepY + 0.5; i < myCanvas.height; i += stepY) {
    ctx.beginPath();
    ctx.moveTo(0, i);
    ctx.lineTo(myCanvas.width, i);
    ctx.stroke();
  }
}
let myCanvas = document.getElementById('myCanvas');
let ctx = myCanvas.getContext('2d');
drawGrid(ctx, '#f00', 10, 10)

一个基础的绘制canvas的HTML模板如下:

<!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 {
      width: 500px;
      height: 500px;
    }
  </style>
</head>
<body>
  <canvas id="canvas" width="200" height="200"></canvas>
  <script>
    window.addEventListener('load', eventWindowLoaded, false);

    function eventWindowLoaded() {
      canvasApp();
    }

    function canvasSupport(e) {
      return !!e.getContext;
    }

    function canvasApp() {
      let myCanvas = document.getElementById('canvas');
      if (!canvasSupport(myCanvas)) {
        return;
      }
      let ctx = myCanvas.getContext('2d');

      function drawScreen() {
        // 绘制网格
        function drawGrid(ctx, color, stepX, stepY) {
          ctx.strokeStyle = color;
          ctx.lineWidth = 0.5;
          for (let i = stepX + 0.5; i < myCanvas.width; i += stepX) {
            ctx.beginPath();
            ctx.moveTo(i, 0);
            ctx.lineTo(i, myCanvas.height);
            ctx.stroke();
          }
          for (let i = stepY + 0.5; i < myCanvas.height; i += stepY) {
            ctx.beginPath();
            ctx.moveTo(0, i);
            ctx.lineTo(myCanvas.width, i);
            ctx.stroke();
          }
        }
        drawGrid(ctx, '#f00', 10, 10)
      }

      drawScreen();
    }

  </script>
</body>
</html>

效果:

image-20211120171325635

绘制基础图形

canvas只支持一种基本形状——矩形,所有其它形状都是通过一个或多个路径组合而成,甚至是基本的矩形也可以通过路径组合成。 更多的图形可以使用第三方插件 draw2d.js 完成。

image-20211120193551372

绘制矩形

canvas提供三个绘制矩形的方法:
1.fillRect(x, y, width, height) 绘制一个填充的矩形
2.strokeRect(x, y, width, height) 绘制一个矩形的边框
3.clearRect(x, y, widh, height) 清除指定的矩形区域,然后这块区域会变的完全透明。(可以理解为一块矩形橡皮擦)

参数说明:
●x, y:矩形的左上角的坐标。
●width, height:绘制的矩形的宽和高。

填充的默认颜色为黑色。

示例:

ctx.fillRect(10, 10, 100, 100)
ctx.clearRect(20, 20, 50, 50)
ctx.strokeRect(120, 10, 20, 20)

效果如下:

image-20211120193749399

绘制路径

图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。

使用路径绘制图形一般需要以下步骤:

  1. 创建路径起始点
  2. 调用绘制方法去绘制出路径
  3. 把路径封闭
  4. 一旦路径生成,通过描边或填充路径区域来渲染图形。

canvas 绘制路径相关的 API

  1. beginPath() 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
  2. moveTo(x, y) 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。(可以理解为将画笔悬空移动)
  3. lineTo(x, y) 将画笔以画线的形式移动到另一点坐标(x, y)。(可以理解为让画笔在画纸上移动)
  4. closePath() 闭合路径之后,图形绘制命令又重新指向到上下文中
  5. stroke() 通过线条来绘制图形轮廓
  6. fill() 通过填充路径的内容区域生成实心的图形

绘制线段

只需要使用其 moveTo 与 lineTo 即可创建线段,使用 stroke 绘制线条。

ctx.moveTo(50, 50);
ctx.lineTo(50, 100);
ctx.stroke();

效果如下:

image-20211120194040783

绘制图形

结合着 beginPath 和 closePath 可以创建一个封闭路径,使用 stroke 进行描边,使用 fill 进行填充。

如:通过路径绘制一个矩形

ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(50, 100);
ctx.lineTo(100, 100);
ctx.lineTo(100, 50);
ctx.closePath();
ctx.fill();

效果如下:

image-20211120194134536

在绘制图形路径时,一定要先调用beginPath()。beginPath()方法将会清空内存中之前的绘制路径信息。如果不这样做,对于绘制单个图形可能没什么影响,但是在绘制多个图形时,将会导致路径绘制或者颜色填充等操作出现任何意料之外的结果。

绘制虚线

ctx.setLineDash(segments)

setLineDash 接收一个数组,按照数组元素组成 (线长,间距) 的形式,循环调用数组中的所有元素作为线长与间距

ctx.save();
ctx.setLineDash([40,30,20]);
ctx.lineWidth = 4;
ctx.strokeStyle = '#0f0';
ctx.beginPath();
ctx.moveTo(10, 100);
ctx.lineTo(400, 100);
ctx.stroke();
ctx.restore();

效果如下:

image-20211120194240033

原理如下:

image-20211120194302146

绘制样式

添加颜色

给绘制的图形上色,可以使用以下 API :

  1. fillStyle = color 设置图形的填充颜色
  2. strokeStyle = color 设置图形轮廓的颜色
  3. globalAlpha = transparencyValue 这个属性影响到 canvas 里所有图形的透明度,有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0。

globalAlpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效。不过,个人认为使用rgba()设置透明度更加好一些。

注意:

  1. color 可以是表示 css 颜色值的字符串、渐变对象或者图案对象。
  2. 默认情况下,线条和填充颜色都是黑色。
  3. 一旦您设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色,需要重新设置 fillStyle。

如:

for (let i = 0; i < 100; i++){
  for (let j = 0; j < 100; j++){
    for (let k = 0; k < 100; k++) {
      ctx.fillStyle = 'rgb(' +
        Math.floor(255 - 20 * i) + ',' +
        Math.floor(255 - 20 * j) + ',' +
        Math.floor(255 - 20 * k) + ')';
      ctx.fillRect(j * 10, i * 10, 10, 10);
    }
  }
}

效果如下:

image-20211120194443551

另一个例子:

function randomInt(from, to){
  return parseInt(Math.random() * (to - from + 1) + from);
}
for (let i = 0; i < 6; i++){
  for (let j = 0; j < 6; j++){
    ctx.strokeStyle = `rgb(${randomInt(0, 255)},${randomInt(0, 255)},${randomInt(0, 255)})`;
    ctx.strokeRect(j * 50, i * 50, 40, 40);
  }
}

效果如下:

image-20211120194529623

添加样式

  1. lineWidth = value 线宽。只能是正值。默认是1.0
  2. lineCap = type 线条末端样式,允许的值有:
    • butt:线段末端以方形结束
    • round:线段末端以圆形结束
    • square:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域。
  3. lineJoin = type 同一个path内,设定线条与线条间接合处的样式。
    • round 通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。
    • bevel 在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。
    • miter (默认) 通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。
  4. 设置虚线样式: setLineDash 方法接受一个数组,来指定线段与间隙的交替;lineDashOffset属性设置起始偏移量。

示例:设置线条末端样式

ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.lineWidth = 10;
ctx.lineCap = 'round'
ctx.stroke();

效果:

image-20211120194756814

示例:设置线条结合处样式

let lineJoin = ['round', 'bevel', 'miter'];
ctx.lineWidth = 20;
for (let i = 0; i < lineJoin.length; i++){
  ctx.lineJoin = lineJoin[i];
  ctx.beginPath();
  ctx.moveTo(50, 50 + i * 50);
  ctx.lineTo(100, 100 + i * 50);
  ctx.lineTo(150, 50 + i * 50);
  ctx.lineTo(200, 100 + i * 50);
  ctx.lineTo(250, 50 + i * 50);
  ctx.stroke();
}

效果:

image-20211120194913602

示例:设置虚线样式

ctx.setLineDash([20, 5, 10, 5]);  // [实线长度, 间隙长度]
ctx.lineDashOffset = 10;
ctx.strokeRect(50, 50, 100, 100);

效果:

image-20211120194945718

绘制文字

canvas 提供了两种方法来渲染文本:

  1. fillText(text, x, y [, maxWidth]) 在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的.
  2. strokeText(text, x, y [, maxWidth]) 在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.
let text = 'Hello canvas!'
ctx.font = "20px sans-serif"
ctx.fillText(text, 50, 50)
ctx.strokeText(text, 50, 100)

效果:

image-20211120195106238

文本样式

  1. font = value 当前我们用来绘制文本的样式。这个字符串使用和 CSS font属性相同的语法. 默认的字体是 10px sans-serif。
  2. textAlign = value 文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 start。
  3. textBaseline = value 基线对齐选项,可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic。
  4. direction = value 文本方向。可能的值包括:ltr, rtl, inherit。默认值是 inherit。

绘制图片

使用drawImage绘制图像

有以下三种使用方法:

  1. context.drawImage(img,x,y)

  2. context.drawImage(img,x,y,width,height)

  3. context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)

  • 第一参数img可以是一个Image()的实例,也可以是一个的Dom元素。

  • sx, sy 必选,为绘制图像的顶点坐标。

  • sWidth, sHeight 可选,为图片缩放大小。

var img = new Image();   // 创建img元素
img.onload = function(){
    ctx.drawImage(img, 20, 20, 150, 100)
}
img.src = 'https://img.xiaoyulive.top/img/shortcut/096.jpg'; // 设置图片源地址

​ 效果:

image-20211120195456476

如果除img外有8个参数:

  • 前4个是定义图像源的切片位置和大小。
  • 后4个则是定义切片的目标显示位置和大小。

原理图:

image-20211120195637147

绘制圆弧

arc

语法:

arc(x, y, r, startAngle, endAngle, anticlockwise)

  • 以 (x, y) 为圆心,以r为半径,从 startAngle 弧度开始到 endAngle 弧度结束,注意: 单位为弧度。
  • anticlosewise 是布尔值,true 表示逆时针,false 表示顺时针。(默认是顺时针)
  • 0弧度为在一个笛卡尔坐标系中的x轴正方向
  • 通常使用 Math.PI 进行弧度运算,一个 Math.PI 就是 180deg
  • radians=(Math.PI/180)*degrees // 角度转换成弧度1
ctx.beginPath();
ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
ctx.stroke();

效果如下:

image-20211120195813424

可以看到从x轴正方向顺时针绘制出了半径为40的 1/4 圆弧。

arcTo

语法:

arcTo(x1, y1, x2, y2, radius)

根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点。

示例:

ctx.beginPath();
ctx.moveTo(50, 50);
//参数1、2:控制点1坐标   参数3、4:控制点2坐标  参数5:圆弧半径
ctx.arcTo(200, 50, 200, 200, 50);
ctx.lineTo(200, 200)
ctx.stroke();

效果如下:

image-20211120195924327

原理图:

image-20211120195945532

可以理解为:绘制的弧形是由两条切线所决定。

第 1 条切线:起始点和控制点1决定的直线。
第 2 条切线:控制点1 和控制点2决定的直线。

圆弧半径可以回想一下css中border-radius的实现。

贝塞尔曲线

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。

一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。

原理动画

一次贝塞尔曲线:

002.gif

二次贝塞尔曲线:

003.gif

image-20211120200216767

三次贝塞尔曲线:

005.gif

image-20211120200151307

绘制二次贝塞尔曲线

语法:

quadraticCurveTo(cp1x, cp1y, x, y);

参数说明:

  • 参数1和2:控制点坐标
  • 参数3和4:结束点坐标
ctx.beginPath();
let bX = 10, bY = 160; // 起始点
ctx.moveTo(bX, bY);
let cX = 40, cY = 100;  // 控制点
let toX = 180, toY = 180; // 结束点
// 绘制二次贝塞尔曲线
ctx.quadraticCurveTo(cX, cY, toX, toY);
ctx.stroke();

ctx.beginPath();
ctx.rect(bX, bY, 10, 10);
ctx.rect(cX, cY, 10, 10);
ctx.rect(toX, toY, 10, 10);
ctx.fill();

效果:

image-20211120200334488

为了方便理解,将起始点、控制点、结束点都用实心矩形标出。

绘制三次贝塞尔曲线

语法

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);

参数说明:

  • 参数1和2:控制点1的坐标
  • 参数3和4:控制点2的坐标
  • 参数5和6:结束点的坐标
ctx.beginPath();
let bX = 10, bY = 160; // 起始点
ctx.moveTo(bX, bY);
let cX1 = 20, cY1 = 50;  // 控制点1
let cX2 = 60, cY2 = 150;  // 控制点2
let toX = 180, toY = 180; // 结束点
// 绘制二次贝塞尔曲线
ctx.bezierCurveTo(cX1, cY1, cX2, cY2, toX, toY);
ctx.stroke();

ctx.beginPath();
ctx.rect(bX, bY, 10, 10);
ctx.rect(cX1, cY1, 10, 10);
ctx.rect(cX2, cY2, 10, 10);
ctx.rect(toX, toY, 10, 10);
ctx.fill();

效果:

image-20211120200707069

渐变

线性渐变

语法:
createLinearGradient(x1, y1, x2, y2);

使用 createLinearGradient(x1, y1, x2, y2) 可以创建一个线性渐变,线性渐变会从第一个点(x1, y1)扩展到第二个点(x2, y2),即定义了渐变的线长与方向。

语法:

addColorStop(position, endColor);

使用addColorStop可以添加一个颜色节点

  • 第一个参数是0-1之间的一个数值,这个数值指定该颜色进入渐变多长的距离
  • 第二个参数是颜色值

示例:

let x1 = 0;
let y1 = 0;
let x2 = 100;
let y2 = 0;
let linearGradient1 = ctx.createLinearGradient(x1, y1, x2, y2);
linearGradient1.addColorStop(0, 'rgb(255, 0, 0)');
linearGradient1.addColorStop(0.5, 'rgb(0, 0, 255');
linearGradient1.addColorStop(1, 'rgb(0, 0, 0)');
ctx.fillStyle = linearGradient1

ctx.fillRect(10, 10, 100, 50);

效果:

image-20211120200920199

径向渐变

径向渐变是一种圆形的颜色扩展模式,颜色从圆心位置开始向外辐射。
一个径向渐变于两个圆形来定义。每一个圆都有一个圆心和一条半径。

语法:

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

使用createRadialGradient可以创建一个径向渐变,(x1, y1, r1)和(x2, y2, r2)分别为两个圆的圆心坐标和半径。

示例:

let x1 = 100;   // 第一个圆圆心的X坐标
let y1 = 100;   // 第一个圆圆心的Y坐标
let r1 = 30;    // 第一个圆的半径
let x2 = 100;   // 第二个圆圆心的X坐标
let y2 = 100;   // 第二个圆圆心的Y坐标
let r2 = 100;   // 第二个圆的半径
let radialGradient1 = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
radialGradient1.addColorStop(0, 'rgb(0, 0, 255)');
radialGradient1.addColorStop(1, 'rgb(0, 255, 0)');
ctx.fillStyle = radialGradient1

ctx.fillRect(10, 10, 200, 200);

效果:

image-20211120201046818

addColorStop的用法同线性渐变。

如果两个圆形的圆心位置相同,那么径向渐变将是一个完整的圆形。如果两个圆的圆心位置不相同,那么径向渐变看起来就像是一个探照灯发出的光线。

示例:

let x1 = 100;   // 第一个圆圆心的X坐标
let y1 = 100;   // 第一个圆圆心的Y坐标
let r1 = 30;    // 第一个圆的半径
let x2 = 150;   // 第二个圆圆心的X坐标
let y2 = 120;   // 第二个圆圆心的Y坐标
let r2 = 100;   // 第二个圆的半径
let radialGradient1 = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
radialGradient1.addColorStop(0, 'rgb(0, 0, 255)');
radialGradient1.addColorStop(1, 'rgb(0, 255, 0)');
ctx.fillStyle = radialGradient1

ctx.fillRect(10, 10, 200, 200);

效果:

image

绘制复杂图形

正多边形基础知识

正多边形是所有角都相等、并且所有边都相等的简单多边形,简单多边形是指在任何位置都不与自身相交的多边形。

正多边形的特性

正n边形每个内角为(1 - 2 / n) 180或者表示为(n - 2) 180 / n角度。也可以用弧度表示为(n - 2) * π / n 或者(n - 2) / 2n。

正多边形的所有顶点都在同一个外接圆上,每个正多边形都有一个外接圆,这也称为圆内接正多边形。

image-20211120201301026

把一个圆分成相等的一些弧,就可以得到这个圆的内接正多边形(Regular Polygon),这个圆就是正多边形的外接圆。外接圆的圆心叫做这个正多边形的中心,外接圆的半径叫做正多边形的半径(Radius),正多边形每一边所对的圆心角叫做正多边形的中心角(圆心角,Central Angle),中心到正多边形的一边的距离叫做正多边形的边心距(Apothem)。

一个圆的圆周是2π,当边的数目为n时,每一个中心角都是2π / n。半径r、边心距a、边长s 都存在着固定的关系,已知其中的两个,都可由上面的公式求出第三个。

当n接近48这个值时,那这个正多边形也就接近是一个圆了。如下图所示:

image-20211120201337971

正多边形属性

image-20211120201407240

上图描述了正多边形的相关属性:

  • 正多边形的中心点正好是一个正多边形的外接圆的圆心
  • 正多边形每条边的长度都相等,如上图中的x
  • 正多边形的每个内角都相等,如上图中的β
  • 正多边形的每个外角都相等,如上图中的α
  • 正多边形的中心角都相等,如上图中的θ
  • 正多边形的中心点距正多边形的内切圆的半行为r
  • 正多边形的顶点数和边数相等,常用n表示
  • 正多边形中心距正多边形的外接圆(或者正多边中心点距正多边形的顶点)就是正多边形外接圆半径,如上图中的R
  • 正多边形中心点距和每条边的端点构成一个等腰三角形,如上图中的A1。这个三角形的两条边长度相等,刚好是正多边形外接圆半径R,而这个三角形的高,刚好是正多边形内切圆半径r(边心距)

那么在Canvas中要使用CanvasRenderingContext2D对象自带的方法,比如moveTo()和lineTo()绘制多边形,我们就必须知道正多边形属性之间的关系。也就是这些属性之间的三角函数。言外之意,在Canvas中,我们使用moveTo()和lineTo()方法,再配合一些简单的三角函数,就可以绘制出任意边数的多边形。

既然绘制正多边形需要一定的三角函数知道,我们在绘制正多边形之前,先了简单的了解一下这方面的基础。

外角(Exterior Angle)

正多边形的外角是正多边形任意边与相邻边延长直线构成的角:

image-20211120201521124

正多边形所有外角之和等于360°。也就是说,每个外角α = 360° / n。比如n=8,一个正八边形,它的外角α = 360° / n = 360° / 8 = 45。著作权归作者所有。

内角(Interior Angles)

正多边形相邻两条边构成的夹角就是正多边形的一个内角。每个内角都有其相邻的一个外角,它们构成一条直线,也就是说内角加上外角,刚好是180°。也就是内角β = 180° - α。即:β = 180° - 360° /n。上面的公式可以转化为:

β = 180° - 360° / n
  = (n × 180° / n) − (2 × 180° / n)
  = (n − 2) × 180° / n

同样的拿n = 8的正八边形为例:β = (n - 2) × 180° / n = (8 - 2) × 180° / 8,即β = 135°。

image-20211120201620259

中心角

正多边形的中心点与正多边形顶点构成的角,即正多边形每条边对应的夹角称为正多边形的中心角θ。如果正边形有n条边,那么就有n个中心角θ,这样一来θ = 360° / n。

image-20211120201649417

正多边形每个顶点的坐标

通过前面的介绍,我们可以很容易得到正多边形的中心角θ。但在Canvas中要绘制一个正多边形,需要知道正多边形每个顶点的坐标。而这个坐标(xPos, yPos)可以通过三角函数得到。

xPos = cos(θ) * R
yPos = sin(θ) * R

而正多边形所有中心角的和是360°也就是2π,中心角 θ = 2π / n:
假设我们正多边形的中点心是(xCenter, yCenter),这样就可以得到每个顶点坐标位置:

xPos = xCenter + Math.cos(angle) * radius;
yPos = yCenter + Math.sin(angle) * radius;

绘制正多边形

前面花了很长的篇幅来介绍正多边形相关知识点。因为只有了解这些基础,才能更好的绘制正多边形,这也是磨刀不误砍柴工。那么我们接下来看怎么绘制一个正多边形。

我们先来看一个简单的绘制方法,比如封装一个绘制正多边形的函数drawPolygons(),给它传几个参数:

  • ctx:Canvas中绘图环境
  • num:正边形边数
  • radius:正边形外接圆半径
  • arc:是否显示正多边形的外接圆
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')

// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心坐标X点
// @param {Number} yCenter 中心坐标Y点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} alpha 角度
// @param {Boolean} arc 是否显示外圆
function drawPolygons(ctx, xCenter, yCenter, radius, sides, alpha, arc) {
  let radAngle = Math.PI * 2 / sides;
  let radAlpha = (alpha != 'undefined') ? alpha * Math.PI / 180 : 0;
  ctx.save();
  ctx.beginPath();
  let xPos = xCenter + Math.cos(radAlpha) * radius;
  let yPos = yCenter + Math.sin(radAlpha) * radius;
  ctx.moveTo(xPos, yPos);
  for (let i = 1; i <= sides; i++) {
    let rad = radAngle * i + radAlpha;
    xPos = xCenter + Math.cos(rad) * radius;
    yPos = yCenter + Math.sin(rad) * radius;
    ctx.lineTo(xPos, yPos);
  }
  ctx.closePath();
}

// 绘制填充的多边形
// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心点X坐标点
// @param {Number} yCenter 中心点Y坐标点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} alpha 角度
// @param {Boolean} arc 是否显示外圆
function drawFillPolygon(ctx, xCenter, yCenter, radius, sides, style, alpha, arc) {
  drawPolygons(ctx, xCenter, yCenter, radius, sides, alpha, arc);
  ctx.fillStyle = style;
  ctx.fill();
  // 画外接圆
  if (arc) {
    ctx.beginPath();
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, true);
    ctx.stroke();
  }
}

// 绘制描边的多边形
// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心点X坐标点
// @param {Number} yCenter 中心点Y坐标点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} alpha 角度 默认270度
// @param {Boolean} arc 是否显示外圆
function drawStrokePolygon(ctx, xCenter, yCenter, radius, sides, style, alpha, arc) {
  drawPolygons(ctx, xCenter, yCenter, radius, sides, alpha, arc);
  ctx.strokeStyle = style;
  ctx.stroke();
  // 画外接圆
  if (arc) {
    ctx.beginPath();
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, true);
    ctx.stroke();
  }
}

drawFillPolygon(ctx, 50,50,50,5,'#f00',0,false)
drawStrokePolygon(ctx, 100,100,50,5,'#00f',0,false)

效果:

image-20211120201914824

演示:

image-20211120201954267

绘制星形

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')

// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心坐标X点
// @param {Number} yCenter 中心坐标Y点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} sideIndent (0 ~ 1)
// @param {Number} alpha 角度 默认270度
// @param {Boolean} arc 是否显示外圆
function drawStarPolygons(ctx, xCenter, yCenter, radius, sides, sideIndent, alpha, arc) {
  let sideIndentRadius = radius * (sideIndent || 0.38);
  let radAngle = alpha ? alpha * Math.PI / 180 : -Math.PI / 2;
  let radAlpha = Math.PI * 2 / sides / 2;
  ctx.save(); ctx.beginPath();
  let xPos = xCenter + Math.cos(radAngle) * radius;
  let yPos = yCenter + Math.sin(radAngle) * radius;
  ctx.moveTo(xPos, yPos);
  for (let i = 1; i <= sides * 2; i++) {
    let rad = radAlpha * i + radAngle;
    let len = (i % 2) ? sideIndentRadius : radius;
    let xPos = xCenter + Math.cos(rad) * len;
    let yPos = yCenter + Math.sin(rad) * len;
    ctx.lineTo(xPos, yPos);
  }
  ctx.closePath();
}

// 绘制填充的多边形
// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心点X坐标点
// @param {Number} yCenter 中心点Y坐标点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} style 填充样式
// @param {Number} sideIndent (0 ~ 1)
// @param {Number} alpha 角度 默认270度
// @param {Boolean} arc 是否显示外圆
function drawFillStarPolygon(ctx, xCenter, yCenter, radius, sides, style, sideIndent, alpha, arc) {
  drawStarPolygons(ctx, xCenter, yCenter, radius, sides, sideIndent, alpha, arc);
  ctx.fillStyle = style;
  ctx.fill();
  // 画外接圆
  if (arc) {
    ctx.beginPath();
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, true);
    ctx.arc(xCenter, yCenter, radius * sideIndent, 0, 2 * Math.PI, true);
    ctx.stroke();
  }
}

// 绘制描边的多边形
// @param {CanvasRenderingContext2D} ctx
// @param {Number} xCenter 中心点X坐标点
// @param {Number} yCenter 中心点Y坐标点
// @param {Number} radius 外圆半径
// @param {Number} sides 多边形边数
// @param {Number} style 填充样式
// @param {Number} sideIndent (0 ~ 1)
// @param {Number} alpha 角度 默认270度
// @param {Boolean} arc 是否显示外圆
function drawStrokeStarPolygon(ctx, xCenter, yCenter, radius, sides, style, sideIndent, alpha, arc) {
  drawStarPolygons(ctx, xCenter, yCenter, radius, sides, sideIndent, alpha, arc);
  ctx.strokeStyle = style;
  ctx.stroke();
  // 画外接圆
  if (arc) {
    ctx.beginPath();
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, true);
    ctx.arc(xCenter, yCenter, radius * sideIndent, 0, 2 * Math.PI, true);
    ctx.stroke();
  }
}

drawFillStarPolygon(ctx, 50,50,50,5,'#f00',.5,0,false)
drawStrokeStarPolygon(ctx, 100,100,50,5,'#00f',.3,0,false)

效果:

image-20211120202033920

案例:星星

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.fillRect(0,0,300,300);
  for (var i=0;i<3;i++) {
    for (var j=0;j<3;j++) {
      ctx.save();
      ctx.strokeStyle = "#9CFF00";
      ctx.translate(50+j*100,50+i*100);
      drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
      ctx.restore();
    }
  }
}
function drawSpirograph(ctx,R,r,O){
  var x1 = R-O;
  var y1 = 0;
  var i    = 1;
  ctx.beginPath();
  ctx.moveTo(x1,y1);
  do {
    if (i>20000) break;
    var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
    var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
    ctx.lineTo(x2,y2);
    x1 = x2;
    y1 = y2;
    i++;
  } while (x2 != R-O && y2 != 0 );
  ctx.stroke();
}
draw();

效果:

image-20211120202158850

状态保存与恢复

save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。

Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。

save()

Canvas状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存(类似数组的push())。

一个绘画状态包括:

  • 当前应用的变形(即移动,旋转和缩放)
  • strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值
  • 当前的裁切路径(clipping path)

restore()

每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。(类似数组的pop())

ctx.fillStyle = '#f00'
ctx.fillRect(10,10,150,150)
ctx.save()

ctx.fillStyle = '#0f0'
ctx.fillRect(20,20,100,100)

ctx.restore() // 恢复 ctx.fillStyle = '#f00' 的状态
ctx.fillRect(30,30,50,50)

剪裁路径

Canvas中有一个很有用的功能:剪裁路径。

它是Canvas之中由路径所定义的一块区域,浏览器会将所有的绘图操作都限制在本区域内执行。在默认情况下,剪辑区域的大小与Canvas画布大小一致。除非你通过创建路径并调用Canvas绘图环境对象的clip()方法来显式的设定剪辑区域,否则默认的剪辑区域不会影响Canvas之中所绘制的内容。然而,一旦设置好剪辑区域,那么你在Canvas之中绘制的所有内容都将局限在该区域内。这也意味着在剪辑区域以外进行绘制是没有任何效果的。

剪切区域

裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏,这一块区域就是剪切区域 。

设定裁选区之后,无论在Canvas上绘制什么,只有落在裁选区内的那部分才能得以显示,其余都会被遮蔽掉。

Canvas中的clip()方法是裁切区可用于限制图像描绘的区域,具体的用法:

  • 使用Canvas的绘制函数比如,rect()、arc()之类的方法选择好绘图区域
  • 使用clip()函数将该区域(由rect()、arc()方法指定的绘图区域)设定为裁选区

clip() 只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。

如:

ctx.beginPath();
ctx.arc(20,20, 100, 0, Math.PI * 2);
ctx.fillStyle = "pink";
ctx.save();

ctx.clip();
ctx.fillRect(20, 20, 100,100);

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

效果:

image-20211120202440579

剪裁示例

原始图像:在画布上绘制三个圆,分别为 蓝、红、绿,初始的时候不设置剪裁

let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d')

let x = 250
let y = 150
let radius = 50
let startAngle = 0
let endAngle = 360
let anticlockwise = true

// 第一个圆: 蓝色
context.fillStyle='#00f';
context.beginPath();
context.arc(x, y, radius, Math.PI / 180 * startAngle, Math.PI / 180 * endAngle, anticlockwise);
context.fill();

// 第二个圆: 红色
context.fillStyle='#f00';
context.beginPath();
context.arc(x - 40, y + 50, radius, Math.PI / 180 * startAngle, Math.PI / 180 * endAngle, anticlockwise);
context.fill();

// 第三个圆: 绿色
context.fillStyle='#0f0';
context.beginPath();
context.arc(x + 40, y + 50, radius, Math.PI / 180 * startAngle, Math.PI / 180 * endAngle, anticlockwise);
context.fill();

image-20211120202513386

将第一个圆设置为剪裁区域

context.fillStyle='#00f';
context.beginPath();
context.arc(x, y, radius, Math.PI / 180 * startAngle, Math.PI / 180 * endAngle, anticlockwise);
context.fill();
context.clip();

image-20211120202601728

将第二个圆设置为剪裁区域

context.fillStyle='#f00';
context.beginPath();
context.arc(x - 40, y + 50, radius, Math.PI / 180 * startAngle, Math.PI / 180 * endAngle, anticlockwise);
context.fill();
context.clip();

image-20211120202629039

取消裁切区

当使用裁切区clip()进行绘图后,可能需要取消该裁选区或者重新定义裁切区。在Canvas中,可以通过save()函数和restore()函数来实现——在构建裁切区之前保存状态,完成裁切区内的绘图之后进行状态读取。

图像剪裁

前面说过,drawImage 如果有9个参数,image参数后的四个参数可以指定剪裁区域

drawImage(image,sx,sy,sw,sh,dx,dy,dw,dh)

如:

let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let image = new Image();
window.onload = function (e) {
  canvas.width = 640;
  canvas.height = 337;
  image.src = 'http://localhost/bg.jpg';
  image.onload = function () {
    context.drawImage(image, 50,50,200,200,100,100,200,200);
  }
}

上面的代码,表示将原图从左上角50,50处开始截取,截取200,200的宽和高,绘制到canvas中100,100,开始200,200大小的画布上,此时效果如下:

image-20211120203134252使用剪裁实现缩放

其实使用剪裁也可以实现图片缩放,只需将裁剪宽高设置为整个图片的宽高即可,但是图像可能会发生伸缩现象

let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let image = new Image();
window.onload = function (e) {
  canvas.width = 640;
  canvas.height = 337;
  image.src = 'http://localhost/bg.jpg';
  image.onload = function () {
    context.drawImage(image, 0, 0, image.width, image.height, 100, 100, 200, 200);
  }
}

效果:

006

图像缩放

原理图:

image-20211120203438129

绿色边框为图片(img)区域,蓝色边框为canvas区域

根据上图可以得出:

dx = canvas.width / 2 - img.width / 2;
dy = canvas.height / 2 - img.height / 2;

根据 drawImage 的特性,指定起始点坐标和宽高即可实现图像缩放,配合着input[type=range]即可实现图片的缩放,这种缩放得到的是一个等比缩放的图像。

<div style='padding: 2em; border: 1px solid black'>
	<canvas id="canvas"></canvas>
	<p><input type="range" id="scale_range" min="0.5" max="3.0" value="1.0" step="0.02" style='width:100%'/></p>
</div>
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var slide = document.getElementById("scale_range");
var image = new Image();
window.onload = function (e) {
  canvas.width = 640;
  canvas.height = 337;
  var scale = slide.value; // 获得初始的缩放值
  image.src = 'http://localhost/bg.jpg';
  image.onload = function () {
    // 当图片完全加载完成,在进行绘制
    drawScaleImage(scale);
    // 为slide添加鼠标移动的事件,每次鼠标在该slide上移动的时候更具新的value重新绘制image
    slide.onmousemove = function () {
      scale = slide.value; // 获得当前的缩放值
      drawScaleImage(scale); // 根据新的scale重新绘制image
    }
  }
}

function drawScaleImage(scale) {
  // 获得缩放以后的图片的宽和高
  var imageWidth = canvas.width * scale;
  var imageHeight = canvas.height * scale;

  var dx = canvas.width / 2 - imageWidth / 2;
  var dy = canvas.height / 2 - imageHeight / 2;

  // 每次在绘制新的image之前先清除当前canvas
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.drawImage(image, dx, dy, imageWidth, imageHeight);
}

效果:

image-20211120204822023

图像合成

合成是指如何精细控制画布上对象的透明度和分层效果。在默认情况之下,如果在Canvas之中将某个物体(源)绘制在另一个物体(目标)之上,那么浏览器就会简单地把源特体的图像叠放在目标物体图像上面。

控制图像合成操作
在 Canvas 中有两个属性 globalAlpha 和 globalCompositeOperation 来控制图像合成操作:

  • globalAlpha:设置图像的透明度。globalAlpha属性默认值为1,表示完全不透明,并且可以设置从0(完全透明)到1(完全不透明)。这个值必须设置在图形绘制之前
  • globalCompositeOperation:该属性的值在globalAlpha以及所有变换都生效后控制在当前Canvas位图中绘制图形

例子:

ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 200, 200);

ctx.globalCompositeOperation = "source-over"; //全局合成操作
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 200, 200);

图像合成类型

ctx.globalCompositeOperation = type

在 Canvas 中 globalCompositeOperation 属性的值总共有26种类型

常见的有以下几种:

  1. source-over (default) 这是默认设置,新图像会覆盖在原有图像。
  2. source-in 仅仅会出现新图像与原来图像重叠的部分,其他区域都变成透明的。(包括其他的老图像区域也会透明)
  3. source-out 仅仅显示新图像与老图像没有重叠的部分,其余部分全部透明。(老图像也不显示)
  4. source-atop 新图像仅仅显示与老图像重叠区域。老图像仍然可以显示。
  5. destination-over 新图像会在老图像的下面。
  6. destination-in 仅仅新老图像重叠部分的老图像被显示,其他区域全部透明。
  7. destination-out 仅仅老图像与新图像没有重叠的部分。 注意显示的是老图像的部分区域。
  8. destination-atop 老图像仅仅仅仅显示重叠部分,新图像会显示在老图像的下面。
  9. lighter 新老图像都显示,但是重叠区域的颜色做加处理。
  10. darken 保留重叠部分最黑的像素。(每个颜色位进行比较,得到最小的)。
  11. lighten 保证重叠部分最量的像素。(每个颜色位进行比较,得到最大的)。
  12. xor 重叠部分会变成透明。
  13. copy 只有新图像会被保留,其余的全部被清除(边透明)。

图像特效

相片底片

/*
 * @param {object} img 要实现反相的图片
 */
function createRevertPic(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  var c = ctx.getImageData(0, 0, img.width, img.height);
  // chrome浏览器报错,ie浏览器报安全错误信息,原因往下看
  for (var i = 0; i < c.height; ++i) {
    for (var j = 0; j < c.width; ++j) {
      var x = i * 4 * c.width + 4 * j,  // imagedata读取的像素数据存储在data属性里,是从上到下,从左到右的,每个像素需要占用4位数据,分别是r,g,b,alpha透明通道
        r = c.data[x],
        g = c.data[x + 1],
        b = c.data[x + 2];
      //图片反相:
      c.data[x] = 255 - r;
      c.data[x + 1] = 255 - g;
      c.data[x + 2] = 255 - b;
      c.data[x + 3] = 255;    // 透明度设置为150,0表示完全透明
    }
  }
  ctx.putImageData(c, 0, 0);
  return canvas.toDataURL(); // 返回canvas图片数据url
}

window.onload = function () {
  var canvas = document.getElementById("canvas");
  var targetCanvas = document.getElementById("targetCanvas");
  var context = canvas.getContext("2d");
  var ctx = targetCanvas.getContext("2d");
  var image = new Image();
  image.src = 'http://localhost/bg.jpg';
  image.onload = function () {
    context.drawImage(image, 0, 0, canvas.width, canvas.height);

    var img = new Image();
    img.src = createRevertPic(image)
    img.onload = function () {
      ctx.drawImage(img, 0, 0, targetCanvas.width, targetCanvas.height);
    }
  }
}

效果:

008

如果将 ctx.putImageData(c, 0, 0) 改为 ctx.putImageData(c, 0, 0, 50, 50, img.width/2, img.height/2) 将会看到剪裁效果,效果如下:

009

灰度图像

/*
 * @param {object} img 要实现灰度的图片
 */
function createGreyPic(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  var c = ctx.getImageData(0, 0, img.width, img.height);
  var pxData = c.data;

  for (var i = 0; i < c.height; ++i) {
    for (var j = 0; j < c.width; ++j) {
      var x = i * 4 * c.width + 4 * j,
        r = c.data[x],
        g = c.data[x + 1],
        b = c.data[x + 2];

      var grey = r * 0.3 + g * 0.59 + b * 0.11; // 转换函数

      // 灰度图片
      c.data[x] = grey;
      c.data[x + 1] = grey;
      c.data[x + 2] = grey;
      c.data[x + 3] = 255;
    }
  }
  ctx.putImageData(c, 0, 0);
  return canvas.toDataURL();
}

window.onload = function () {
  var canvas = document.getElementById("canvas");
  var targetCanvas = document.getElementById("targetCanvas");
  var context = canvas.getContext("2d");
  var ctx = targetCanvas.getContext("2d");
  var image = new Image();
  image.src = 'http://localhost/bg.jpg';
  image.onload = function () {
    context.drawImage(image, 0, 0, canvas.width, canvas.height);

    var img = new Image();
    img.src = createGreyPic(image)
    img.onload = function () {
      ctx.drawImage(img, 0, 0, targetCanvas.width, targetCanvas.height);
    }
  }
}

效果如下:

010

注意到,相比图像反向,只是更改了公式: grey = r 0.3 + g 0.59 + b * 0.11;

其实图像处理就是各像素点之间的函数映射,不同的图像处理公式对应不同的结果而已。

可以根据这些公式,制作各种不同的图像处理滤镜。

添加水印

使用drawImage甚至可以接受另一个canvas的dom作为参数,因此可以在此基础上得到很多奇妙的效果,比如为图像添加水印。

比如:

#water {
  display: none;
}
<canvas id="canvas"></canvas>
<canvas id="water" width="200" height="40"></canvas>
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var water = document.getElementById("water");
var ctx = water.getContext("2d");
var image = new Image();

window.onload = function (e) {
  canvas.width = 640;
  canvas.height = 337;
  image.src = 'https://img.xiaoyulive.top/img/date/20180913/005.jpg';

  ctx.fillStyle='#fff';
  ctx.font = "20px sans-serif"
  ctx.fillText('小昱版权所有', 0, water.height / 2);

  image.onload = function () {
    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    context.drawImage(water, canvas.width - water.width, canvas.height - water.height, water.width, water.height);
  }
}

效果:

下载-jpg

图像像素点的处理

使用getImageData获取图像数据

getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。

比如获取整个画布的所有像素点数据

window.addEventListener('load', eventWindowLoaded, false);

function eventWindowLoaded () {
  let canvas = document.getElementById('canvasOne')
  let ctx = canvas.getContext('2d')
  draw(ctx, canvas)
}

function draw(context, canvas) {
  let w = canvas.width
  let h = canvas.height
  context.fillStyle='#0f0'
  context.fillRect(50, 50, 100, 100)
  let imgData = context.getImageData(0, 0, w, h)
}

可以看到控制台打印出的 imgData 长这样

image-20211120205706947

以看到,imgData.width 和 imgData.height 都为100,则有 100*100 个像素点

其中 imgData.data 是一个 Uint8ClampedArray,简单理解为一个数组就行

每个像素点的值

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

  • R - 红色 (0-255)
  • G - 绿色 (0-255)
  • B - 蓝色 (0-255)
  • A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)

这些值以数组形式存在,并存储于 ImageData 对象的 data 属性中。

比如获取第一个像素点的值

let red = imgData.data[0];
let green = imgData.data[1];
let blue = imgData.data[2];
let alpha = imgData.data[3];
console.log(red, green, blue, alpha)

可以看到 imgData.data 每四个元素组成一个像素点,上面说了,我们获取到 100100 个像素点,则整个 imgData.data 数组包括 100100*4 个元素,也就是 40000 个元素。

因此可以使用循环获取到每个像素点的值,针对这些像素点进行数字图像处理。

使用putImageData绘制图像数据
putImageData() 方法将图像数据(从指定的 ImageData 对象)放回画布上。

putImageData 可以有三个参数,也可以有七个参数

putImageData(imgData, x, y)
putImageData(imgData, x, y, dx, dy, dw, dh)

其中第一个参数imgData规定要放回画布的 ImageData 对象。

  • x 和 y 为 ImageData 对象左上角坐标 (x, y),以像素计。
  • 后四个参数为剪裁区域,规定在画布上放置图像的位置 (dx, dy),宽高分别为 dw,dh,以像素计。

比如将一个图像反转:

window.addEventListener('load', eventWindowLoaded, false);

function eventWindowLoaded () {
  let canvas = document.getElementById('canvasOne')
  let ctx = canvas.getContext('2d')
  draw(ctx, canvas)
}

function draw(context, canvas) {
  let w = canvas.width
  let h = canvas.height

  let lg = context.createLinearGradient(0, 0, 200, 100)
  lg.addColorStop(0, '#f00')
  lg.addColorStop(.5, '#0f0')
  lg.addColorStop(1, '#00f')

  context.fillStyle = lg
  context.fillRect(50, 50, 100, 100)

  let imgData = context.getImageData(50, 50, 100, 100)
  console.log(imgData)

  for (let i = 0; i < imgData.data.length; i += 4) {
    imgData.data[i] = 255-imgData.data[i];
    imgData.data[i+1] = 255-imgData.data[i+1];
    imgData.data[i+2] = 255-imgData.data[i+2];
    imgData.data[i+3] = 255;
  }
  context.putImageData(imgData, 150, 150);
}

效果:

image-20211120205917385

使用createImageData创建图像数据

createImageData() 方法创建新的空白 ImageData 对象。新对象的默认像素值 transparent black。

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

  • R - 红色 (0-255)
  • G - 绿色 (0-255)
  • B - 蓝色 (0-255)
  • A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)

因此,transparent black 表示 (0,0,0,0)。

color/alpha 以数组形式存在,并且既然数组包含了每个像素的四条信息,数组的大小是 ImageData 对象的四倍。(获得数组大小有更简单的办法,就是使用 ImageDataObject.data.length)

比如绘制一个随机杂色:

var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var imgData = ctx.createImageData(100, 100);
for (var i = 0; i < imgData.data.length; i += 4) {
  imgData.data[i] = (i * 2 + Math.random() * 1000) % 256;
  imgData.data[i + 1] = (i * 3 + Math.random() * 500) % 256;
  imgData.data[i + 2] = (i * 4 + Math.random() * 200) % 256;
  imgData.data[i + 3] = 255;
}
ctx.putImageData(imgData, 10, 10);

效果:

image-20211120210018930

捕捉视频图像

使用drawImage可以接受一个video的dom作为参数:

<video src="http://xiaoyulive.oss-cn-beijing.aliyuncs.com/imgs/mov_bbb.mp4" controls></video>
<canvas id="canvas" width="200" height="200"></canvas>
<script>
  let canvas = document.getElementById('canvas')
  let video = document.querySelector('video')
  let ctx = canvas.getContext('2d')
  let i = undefined
  video.addEventListener('play', function() {
    i = window.setInterval(function() {
      ctx.drawImage(video, 0, 0, 270, 135)
    }, 20);
  }, false);
  video.addEventListener('pause', function() {
    window.clearInterval(i);
  }, false);
  video.addEventListener('ended', function() {
    clearInterval(i);
  }, false);
</script>

通过这种方式,我们可以实现很多自定义的视频播放功能。首先隐藏源video标签,然后在canvas中添加各种控制控件、弹幕、滤镜等等。这样做还能防止视频被很容易地窃取。

向原型中添加绘制方法

绘制圆点线

在 Canvas 中没有直接绘制圆点(dotted)线的 API,需要的时候可以自行扩展 CanvasRenderingContext2D 原型,进行绘制圆点线。

let canvasPrototype = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
canvasPrototype.dottedLine = function (x1, y1, x2, y2, interval) {
  if (!interval) {
    interval = 5;
  }
  let isHorizontal = true;
  if (x1 == x2) {
    isHorizontal = false;
  }
  let len = isHorizontal ? x2 - x1 : y2 - y1;
  this.moveTo(x1, y1);
  let progress = 0;
  while (len > progress) {
    progress += interval;
    if (progress > len) {
      progress = len;
    } if (isHorizontal) {
      this.moveTo(x1 + progress, y1);
      this.arc(x1 + progress, y1, 1, 0, Math.PI * 2, true);
      this.fill();
    } else {
      this.moveTo(x1, y1 + progress);
      this.arc(x1, y1 + progress, 1, 0, Math.PI * 2, true);
      this.fill();
    }
  }
}

调用:

// 默认绘制圆点线
context.dottedLine(10, 100, 200, 200);
// 指定圆点间距
context.dottedLine(10, 100, 200, 200, 10);

效果:

image-20211120210341980

Canvas 坐标系

在了解Canvas坐标系之前,先看看我们比较熟悉的一些坐标系统。

笛卡坐标系

笛卡坐标系(Cartesian Coordinate system),这个坐标系统也称为直角坐标系,是一种正交坐标系。

二维的直角坐标系是由两条相互垂直、0点重合的数轴构成的。在平面内,任何一点的坐标是根据数轴上对应的点的坐标设定的。在平面内,任何一点与坐标的对应关系,类似于数轴上点与坐标的对应关系。

image-20211120210438281

可以看到,每个点都有一双与之关联的值。这些被称为坐标点,通常表示为(x,y)。x位于水平轴上,y位于垂直轴上。其中(0,0)点是坐标原点。x轴从原点向右方向为正值,反之为负值,y轴从原点向上为正值,反之为负值。

Web 坐标系统

在Web页面中,或者说我们的浏览器中也有一个坐标系统。只是他和我们数学中的坐标系统不一样。Web的坐标系统的原点是在屏幕(浏览器屏幕)的左上角。

image-20211120210517285

它有两个坐标轴,x轴(水平轴)和y轴(垂直轴)。两轴的交汇点(左上角)为坐标原点(0,0)。原点沿x轴向右是正值,原点沿y轴向下是正值。

Canvas 坐标系统

在Canvas中有2D和3D之分,可以通过getContext(‘2d’)让Canvas得到一个2D环境。言外之意,它还有一个3D环境。这样一来,在Canvas中坐标系统也是有分的。

Canvas 2D 坐标系统

在Canvas中2D环境中其坐标系统和Web的坐标系统是一致的。坐标原点(0,0)在 画布的的左上角。同样的分为x和y两个轴。x轴向右为正值,y轴向下为正值。同样在canvas中,是没有办法直接看到。但同样,在canvas中使用负坐标不会导致canvas不能使用,只不过会移到canvas画布的外面。

比如我们在画布中绘制一个矩形

ctx.fillStyle = '#f36';
ctx.fillRect(15,15,20,20);

image-20211120210602394

Canvas 3D 坐标系统

3D坐标系统多了一个z轴,用来描述深度。比如说一个物体在绘制时,在屏幕之内或之外多远的距离。这里简单的介绍一下3D坐标系统。

如图所示,x轴从左向右在水平方向延展,y轴纵向延展,z轴的正值从屏幕中穿出。如果你熟悉2D坐标系统的概念,那么过渡到3D坐标系统会相当直观容易。
image-20211120210632345

绘制2D坐标系

var myCanvas = document.getElementById('canvas')
var ctx = myCanvas.getContext('2d')
drawScreen()
function drawScreen() {
  // 横线与竖线的是距
  var dx = 50;
  var dy = 50;
  // 初始坐标原点
  var x = 0;
  var y = 0;
  var w = myCanvas.width;
  var h = myCanvas.height;
  // 单个步长所表示的长度
  var xy = 10;
  ctx.lineWidth = .1;
  ctx.strokeStyle = 'rgba(100,100,100,.4)'
  // 画横线
  while (y < h) {
    y += dy;
    ctx.moveTo(x, y);
    ctx.lineTo(w, y);
    ctx.stroke();
    //横坐标的数字
    ctx.fillText(xy, x, y - 2);
    xy += 10;
  }
  // 画竖线
  y = 0;
  xy = 10;
  while (x < w) {
    x = x + dx;
    ctx.moveTo(x, y);
    ctx.lineTo(x, h);
    ctx.stroke(); //纵坐标的数字
    ctx.fillText(xy, x + 2, 10);
    xy += 10;
  }
}

效果:

image-20211120210723185

坐标变换原理

在使用Canvas坐标变换之前,需要先了解Canvas的坐标系统。详细请参考 Canvas 坐标系

Canvas 的坐标系统并不是一尘不变的。可以对 Canvas 坐标系统进行移动、旋转和缩放等操作。而这些操作被称为坐标变换。

image-20211120210917041

坐标变换包括: translate、rotate、scale。

平移 translate

translate(x, y);

用来移动 canvas 的原点到指定的位置(坐标变换)

image-20211120210952385

translate方法接受两个参数:

  • x 是左右偏移量
  • y 是上下偏移量

在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又如果你是在一个循环中做位移但没有保存和恢复canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。

比如通过 translate 绘制重复的图形:

var canvas = document.getElementById('canvas')

window.addEventListener('load', draw(canvas), false);

function draw(canvas) {
  var context = canvas.getContext('2d')
  for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 5; j++) {
      context.save();
      context.fillStyle=`rgb(${i*40}, ${j*30}, ${(i + j)*20})`;
      context.translate(i * 100, j * 100);
      context.fillRect(10, 10, 80, 50);
      context.restore();
    }
  }
}

效果:

image-20211120211220525

旋转 rotate

rotate(angle);

旋转坐标轴, 旋转的中心是 坐标原点。

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

image-20211120211306856

比如通过 translate 绘制旋转的图形:

let canvas = document.getElementById('canvas')

window.addEventListener('load', draw(canvas), false);

function draw(canvas) {
  let context = canvas.getContext('2d')
  let w = canvas.width
  let h = canvas.height

  context.save();
  context.translate(w / 2, h / 2);

  let index = 0
  for (let i = 0; i < 360; i = i + 45) {
    index ++
    context.rotate(Math.PI/180 * i)
    context.fillStyle = `rgb(${index * 10}, ${index * 20}, ${index * 50})`
    context.fillRect(0,0,20,100)
  }

  context.restore();
}

效果如下:

image-20211120211359755

再比如:

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.translate(75,75);  // 移动原点到(75,75)处

  for (var i=1;i<6;i++){ // 里往外画5圈圆
    ctx.save(); // 先保存状态
    ctx.fillStyle = 'rgb('+ (50*i) +','+ (255-20*i) +',' + (30*i) + ')'; // 圆的颜色

    // 下面实现效果是:通过旋转画板,在x轴上画圆。这样的好处是方便计算,所有圆在x轴上实现,通过旋转画板来画所有圆。
    for (var j=0; j<i*6; j++) {
      ctx.rotate(Math.PI*2/(i*6)); // 顺时针旋转Math.PI*2/(i*6)度
      ctx.beginPath();
      ctx.arc(0,i*12.5,5,0,Math.PI*2,true); // 在(0,12.5*i)处画圆,半径为5px,画360度。
      ctx.fill();
    }

    ctx.restore(); // 还原到保存前的状态
  }
}
draw();

效果如下:

image-20211120211448201

在很多实际场景中,我们对某个图形元素做旋转,默认情况之下,其旋转都会围绕Canvas坐标系统原点(0,0)进行旋转。但实际上,我们需要围绕元素中心点来做旋转。在CSS中,我们有一个transform-origin属性可以修改原点。但在Canvas中,就需要借助Canvas的坐标变换中的translate()方法来修改元素的原点,也就是将原点移动到元素的中心位置。

比如我们绘制一个矩形:

var x = 100;
var y = 100;
var width = 100;
var height = 100;
ctx.strokeRect(x, y, width, height);

效果如下:

image-20211120211535706

时,我们可以将坐标中心移到矩形的中心,即

ctx.translate(x + width / 2; y + height / 2)

此时再针对矩形做旋转即可:

image-20211120211608142

拉伸 scale

scale(x, y)

我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。

scale方法接受两个参数。x,y分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩 小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。

举例说,如果 canvas 的 1 单位就是 1 个像素。我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。

image-20211120211757539

ctx.save()

ctx.translate(120, 120)

ctx.strokeStyle = '#f00'
ctx.strokeRect(0,0,50,100)

ctx.scale(.5, .5)
ctx.fillStyle = '#0f0'
ctx.fillRect(0,0,50,100)

ctx.restore()

效果如下:

image-20211120211831985

同样的,缩放中心默认也是坐标原点,需要配合 translate 实现围绕元素的中心进行旋转。

只要在缩放、旋转或者组合旋转缩放前将原点平移到形状的中心,都可以得到想要的效果。记住,任何形状的中心点都是半宽的x值和半高的y值。这需要使用边界框理论找到中心点。

坐标变换中的缩放可以用于实现很多不同的效果,比如说,在绘制了某个图形后,可以调用ctx.scale(-1, 1)来绘制其水平镜像或者调用ctx.scale(1, -1)来绘制其垂直镜像。

let canvas = document.getElementById('canvas')

window.addEventListener('load', draw(canvas), false);

function draw(canvas) {
  let ctx = canvas.getContext('2d')
  ctx.save();
  ctx.strokeStyle = '#f36';
  ctx.beginPath();
  ctx.moveTo(100, 100);
  ctx.lineTo(150, 150);
  ctx.lineTo(100, 200);
  ctx.closePath();
  ctx.stroke();
  ctx.translate(350, 0);
  ctx.strokeStyle = 'lime';
  ctx.beginPath();
  ctx.moveTo(-175, 0.5);
  ctx.lineTo(-175, 300.5);
  ctx.stroke();
  ctx.scale(-1, 1);
  ctx.save();
  ctx.strokeStyle = '#000';
  ctx.beginPath();
  ctx.moveTo(100, 100);
  ctx.lineTo(150, 150);
  ctx.lineTo(100, 200);
  ctx.closePath();
  ctx.stroke();
}

效果如下:

image-20211120211924527