乘风破浪的canvas

626 阅读15分钟

本篇文章主要涉及的内容有

canvas基本了解

什么是canvas?

canvas是HTML5新增的元素,可用于通过javascript中的脚本来绘制图形;例如它可以用于绘制图形,创建动画。canvas最早由Apple引入webkit;

我们可以使用canvas标签来定义一个canvas元素,使用canvas标签时,建议成对出现,不要使用闭合的形式。canvas默认具有宽高,width: 300px;height: 150px;

canvas中的替换内容

canvas标签很容易定义一些替换内容,由于某些较老的浏览器(尤其是IE9之前的IE浏览器),不支持HTML元素canvas,但是在这些浏览器上你应该给用户展示一些替换内容。这非常简单,我们只需要在canvas标签中提供替换文本就可以了,支持canvas的浏览器将会忽略容器中包含的内容,并且只是正常渲染canvas,不支持canvas的浏览器会显示替换内容。

canvas标签中的两个属性

canvas看起来和img标签看起来很像,唯一不同的是它并没有src和alt属性。实际上canvas标签只有两个属性,width和height;这些都是可选的,当没有设置高宽时,canvas会初始化宽度为300px和高度150px。

画布高宽

html属性设置width height 时只影响画布本身不影响画布内容css属性设置width height时不但会影响画布本身的高宽,还会使画布中的内容等比例缩放(缩放参照于画布默认的尺寸)

!!!所以不要通过css设置画布的尺寸

渲染上下文

canvas元素只是创建了一个固定大小的画布,要想在它上面绘制内容,我们需要找到它的渲染上下文; canvas元素中有一个getContext()方法,这个方法是用来获得渲染上下文和它的绘画功能。

getContext只有一个参数,上下文格式

上下文格式类型:

    "2d", 建立一个 CanvasRenderingContext2D 二维渲染上下文。
    "webgl" (或"experimental-webgl") 这将创建一个 WebGLRenderingContext 三维渲染上下文对象。只在实现WebGL 版本1(OpenGL ES 2.0)的浏览器上可用。
    "webgl2" (或 "experimental-webgl2") 这将创建一个 WebGL2RenderingContext 三维渲染上下文对象。只在实现 WebGL 版本2 (OpenGL ES 3.0)的浏览器上可用。
    "bitmaprenderer" 这将创建一个只提供将canvas内容替换为指定ImageBitmap功能的ImageBitmapRenderingContext  。
const canvas = document.querySelector('canvas');
if (canvas.getContext) {
    const ctx = canvas.getContext('2d');
}

绘制矩形

HTML中的元素canvas只支持一种原生(调用一个api)的图形绘制:矩形;所有其他的图形的绘制都至少需要生成一条路径。

绘制矩形

canvas提供三种方法绘制矩形:
    1. 绘制一个填充矩形
        ctx.fillRect(x, y, width, height);
    2. 绘制一个矩形的边框
        ctx.strokeRect(x, y, width, height);
    3. 清除指定矩形的区域,让清除部分完全透明
        ctx.clearRect(x, y, width, height);
        
x,y指定了在canvas画布上所绘制的左上角(相对于原点)的坐标。
widthheight设置绘制矩形的尺寸(存在边框的话,边框会在width上占据一个边框的宽度,高度同理)

strokeRect时,边框像素渲染问题

按理来说渲染出来的边框应该是1px,然而canvas在渲染矩形边框时,边框的宽度是平均分在偏移位置的两侧。

ctx.strokeRect(10, 10, 50, 50);
说明:
    canvas渲染边框在10.59.5之间
    然而浏览器是不会让一个像素只用自己的一半,所有这个边框渲染时在911之间。

解决办法:

1. ctx.strokeRect(10.5, 10.5, 50, 50);

2. 绘制一个透明的矩形进行覆盖
    ctx.strokeRect(10, 10, 50, 50);
    ctx.clearRect(10, 10, 50, 50);

添加样式和颜色

fillStyle 设置图形的填充颜色,默认#000000
strokeStyle 设置图形轮廓的颜色,默认#000000
lineWidth 设置当前绘线的粗细,属性值必须为正值。默认1.0

lineJoin

设定线条与线条间接合处的样式(默认是miter),与填充没有关系 都有哪些值,含义是什么

1. round: 圆角
2. bevel: 斜角
3. miter: 直角

绘制路径

图形的基本元素是路径。路径是通过不同颜色和宽高的线段或曲线相连形成的不同形状的点的集合。

大体绘制的步骤

1. 首先,你需要创建路径起始点
2. 然后使用画图命令去画路径
3. 而后你把路径进行封闭
4. 一旦路径生成,你就能通过描边或者填充路径区域来渲染图形

绘制路径之绘制三角形

画笔上的api
1. beginPath()
    新建一条路径,生成之后,图形绘制命令指向到路径上准备生成路径。
    生成路径的第一步叫做beginPath()。
    本质上,路径是由多个子路径构成,这些子路径都在一个列表中,
    所有的子路径构成图形,而每次这个方法调用之后,列表清空重置。
2. moveTo(x, y)
    将笔触移动到指定坐标 x y上
3. lineTo(x, y)
    将笔触移动到指定坐标 x y上
4. closePath()
    闭合路径之后图形绘制命令又重新指向到上下文
5. stroke()
    需要手动合并路径,要么closePath(),要么lineTo到起始点
6. fill()
    会自动合并路径

画一个描边三角形

    ctx.beginPath();
    ctx.moveTo(100, 100);
    ctx.lineTo(100, 200);
    ctx.lineTo(200, 200);
    ctx.lineTo(100, 100);// 或者 closePath()
    ctx.stroke(); // 描边
    ctx.fill(); // 填充三角形

如何理解beginPath呢?看一个实例,以及效果图 我只是像绘制一个描边三角形和另外一个填充的三角形,但是见效果图,把前面的本只是描边的三角形也重新填充了一遍;

    ctx.strokeStyle = 'pink';
    ctx.lineWidth = 10;
    // ctx.beginPath();
    ctx.moveTo(100, 100);
    ctx.lineTo(100, 200);
    ctx.lineTo(200, 200);
    ctx.closePath();
    ctx.stroke();

    ctx.moveTo(200, 200);
    ctx.lineTo(200, 300);
    ctx.lineTo(300, 300);

    ctx.fill();

绘制路径之绘制矩形

rect(x, y, width, height)

    ctx.rect(100, 100, 100, 100);
    ctx.fill();// 绘制一个填充矩形
    // ctx.stroke(); // 绘制一个描边矩形

lineCap

lineCap 是canvas 2D API 指定如何绘制没一条线段末端的属性。

存在的值
1. butt(默认值) 线段末端以方形结束
2. round 线段末端以圆形结束
3. square 线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域

save

save() 是Canvas 2D API 通过将当前状态入栈中,保存canvas全部状态的方法

保存到栈中的绘制状态有下面部分组成:
    当前的变换矩阵。
    当前的剪切区域
    当前的虚线列表
    以下属性当前的值:
        strokeStyle
        fillStyle
        lineWidth
        lineCap
        lineJoin

restore

save & restore & beginPath套路步骤(多个图形之间完全独立时)

    ctx.save();
        
        样式相关的
    
    ctx.beginPath(); // 每次绘制路径之前都先beginPath一下
        路径
    ctx.restore();

    然后开始绘制图形

看一个简单的小demo结合图解理解save&restore

    ctx.save();
    ctx.fillStyle = 'pink';
    ctx.save();
    ctx.fillStyle = 'deeppink';
    ctx.fillStyle = 'blue';
    ctx.save();
    ctx.fillStyle = 'red';
    ctx.save();
    ctx.fillStyle = 'green';
    ctx.save();
    ctx.restore(); // 绿色
    ctx.restore(); // 红色
    ctx.restore(); // blue
    ctx.restore(); // deeppink
    ctx.restore(); // pink
    
    ctx.fillRect(50, 50, 100, 100); // pink

了解了以上基本的用法,这时候可以插入一个小demo——签名

  const canvas = document.querySelector('canvas');
  if (canvas.getContext) {
    const ctx = canvas.getContext('2d');
    ctx.beginPath();
    canvas.addEventListener('mousedown', function(e) {
      e = e || window.event;
      ctx.moveTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
      document.onmousemove = function(e) {
        e = e || window.event;
        ctx.lineTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
        ctx.stroke();
      }
      document.onmouseup = function(e) {
        e = e || window.event;
        document.onmousedown = document.onmousemove = null;
        if (document.releaseCapture) {
          document.releaseCapture();
        }
      }
    });

绘制曲线

绘制圆形

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

该api描述的是以(x, y)为圆心radius为半径的圆弧,从star开始到endAngle结束,
按照anticlockwise给定的方向(默认是顺时针)生成一个圆弧。

anticlockwise: false(默认顺时针)
x,y  绘制圆弧所在圆上的圆心坐标
radius 半径
startAngle, endAngle: 值是弧度,这些值都是以x轴为基准参数
arcTo绘制圆弧
arcTo(x1, y1, x2, y2, radius)
根据给定的控制点和半径画一段圆弧(至少需要三个控制点)
    ctx.arcTo(50, 50, 100, 50, 50);
    ctx.arcTo(100, 50, 100, 100, 50);

    ctx.stroke();

二次贝塞尔

quadraticCurveTo(cp1x, cp1y, x, y)
绘制二次贝塞尔尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。
// 连接三个控制点,方便看出曲线是如何绘制的
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(100, 50);
    ctx.lineTo(100, 100);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.quadraticCurveTo(100, 50, 100, 100); // 没有指定半径,绘制的圆弧必须过这个控制点

    ctx.stroke();

三次贝塞尔

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x, cp2y为控制点二,x,y为结束点
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(100, 50);
    ctx.lineTo(0, 300);
    ctx.lineTo(200, 200);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.bezierCurveTo(100, 50, 0, 300, 200, 200); // 没有指定半径,绘制的圆弧必须过第一和第三个控制点

    ctx.stroke();

canvas中变换

translate(x, y)

这个方法是用来移动canvas的原点到一个指定的位置。translate方法接受两个参数。x是左右偏移量,y是上下偏移量,并且在canvas中translate是累加的

    ctx.translate(50, 50);
    
    ctx.strokeRect(0, 0, 50, 50);

    ctx.translate(50, 50);
    ctx.strokeRect(0, 0, 50, 50);

从效果图不难看出这个translate方法是改变了画布的原点并且是有累加效果的,如果不想要这个累加这个效果,这时候就需要使用前面提及的save方法和restore方法了,试试看

    ctx.save();
    ctx.translate(50, 50);
    ctx.strokeRect(0, 0, 50, 50);
    ctx.restore();

    ctx.save();
    ctx.strokeStyle = 'red';

    ctx.translate(70, 70);
    ctx.strokeRect(0, 0, 50, 50);

rotate(angle)

这个方法只接受一个参数,旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。旋转的中心点始终是canvas的原地,如果要改变原点,我们可以使用translate方法,并且在canvas中roate也是累加的。

ctx.rotate(Math.PI * 45 / 180);

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

    ctx.rotate(Math.PI * 45 / 180);

    ctx.fillRect(50, 50, 50, 50);
    ctx.rotate(Math.PI * (360 - 45) / 180); // 如果是叠加的让它转回默认的位置
    ctx.fillRect(50, 50, 50, 50); // 这个绘制出来的是效果图中完整矩形

scale(x, y)

scale方法接受两个参数,x,y,分别是横轴和纵轴的缩放因子,它们必须是正值。值比1.0小表示缩小,比1.0大则表示放大,值为1.0什么效果都没有。缩放一般我们用它增减图形在canvas中的像素数目,对形状,位图进行缩小或者放大。在canvas中scale是累加的

css像素是一个抽象单位,它所占据实际尺寸会随着缩放发生变化
放大时,区域物理尺寸没有变,区域内(画布)css像素个数变少,每一个css像素的面积变大
缩小时,区域物理尺寸没有变,区域内(画布)css像素个数变多,每一个css像素的面积变小

变换和旋转以及缩放相结合

    ctx.scale(2, 2);  
    ctx.translate(10, 10);
    ctx.rotate(Math.PI * 10 / 180);

    ctx.fillRect(50, 50, 50, 50); // 也就是说参数指定的像素点的个数

来一个变换相关的实例找找感觉

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    canvas {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      margin: auto;
      background: grey;
    }
  </style>
</head>
<body>
  <canvas width="300" height="300"></canvas>
</body>
<script>
  const canvas = document.querySelector('canvas');
  if (canvas.getContext) {
    const ctx = canvas.getContext('2d');
    let flag = 0;
    let scaleValue = 0;
    let scaleFlag = 1;
    setInterval(function() {
      flag += 2;
      if (scaleValue >= 100) {
        scaleFlag = -2;
      } else if (scaleValue === 0) {
        scaleFlag = 2;
      }
      scaleValue += scaleFlag; // scaleValue值的区间[0, 2]
      ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);

      ctx.save();
      ctx.translate(150, 150);

      ctx.rotate(Math.PI*flag/180);
      ctx.scale(scaleValue/50, scaleValue/50); 
      ctx.fillRect(-50, -50, 100, 100); 
      ctx.restore();

    }, 1000/60);
  }
</script>
</html>

了解了canvas中的变化,这时候可以来写一个简易版的时钟啦

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    
    canvas {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate3d(-50%, -50%, 0);
      background: grey;

    }
  </style>
</head>
<body>
  <canvas width="400" height="400">
    您的浏览器不支持canvas,请使用萌萌哒谷歌:https://www.google.cn/chrome/
  </canvas>
</body>
<script>
  window.onload = function() {
    const canvas = document.querySelector('canvas');
  if (canvas.getContext) {
    const ctx = canvas.getContext('2d');

    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      move();
    }, 1000);
    move();
    function move() {
      ctx.save();
      
      ctx.lineWidth = 8;
      ctx.lineCap = 'round';
      ctx.translate(200, 200);
      ctx.rotate(-90*Math.PI/180);
      ctx.scale(.5, .5);
      ctx.beginPath();
      
      // 外层空心圆盘
      ctx.save();
      ctx.strokeStyle = '#325FA2';
      ctx.lineWidth = 14;
      ctx.beginPath();
      ctx.arc(0,0, 140, 0, Math.PI*2);
      ctx.stroke();
      ctx.restore();

      // 时针刻度
      ctx.save();
      for (let i = 0; i < 12; i++) {
        ctx.rotate(30*Math.PI/180); // canvas中变换是累加的
        ctx.beginPath();
        ctx.moveTo(100, 0);
        ctx.lineTo(120, 0);
        ctx.stroke();
      }
      ctx.restore();

      // 分针刻度
      ctx.save();
      ctx.lineWidth = 4;
      for (let i = 0; i < 60; i++) {
        // 处理时钟的位置不重复画分针刻度
        if (i % 5 !== 0) {
          ctx.beginPath();
          ctx.moveTo(117, 0);
          ctx.lineTo(120, 0);
        }
        ctx.rotate(6*Math.PI/180); // canvas中变换是累加的
        ctx.stroke();
      }
      ctx.restore();

      // 时针 分针 秒针 表座
      const date = new Date();
      const s =  date.getSeconds();
      const m = date.getMinutes() + s / 60;
      let h = date.getHours() + m / 60;
      h > 12 ? h - 12 : h;
      console.log(s, m, h);
      // 时针
      ctx.save();
      ctx.lineWidth = '14';
      ctx.rotate(h*30*Math.PI/180);
      ctx.beginPath();
      ctx.moveTo(-20, 0);
      ctx.lineTo(80, 0);
      ctx.stroke();
      ctx.restore();
      // 分针
      ctx.save();
      ctx.lineWidth = '10';
      ctx.rotate(m*6*Math.PI/180);
      ctx.beginPath();
      ctx.moveTo(-28, 0);
      ctx.lineTo(112, 0);
      ctx.stroke();
      ctx.restore();

      // 秒针 表座
      ctx.save();
      ctx.strokeStyle = '#D40000';
      ctx.fillStyle = '#D40000';
      ctx.lineWidth = '6';
      ctx.rotate(s*6*Math.PI/180);
      ctx.beginPath();
      ctx.moveTo(-30, 0);
      ctx.lineTo(83, 0);
      ctx.stroke();
      // 表座
      ctx.beginPath();
      ctx.arc(0,0,10,0,Math.PI*2);
      ctx.fill();

      ctx.beginPath();
      ctx.arc(96,0,10,0,Math.PI*2);
      ctx.stroke();

      ctx.restore();

      ctx.restore();

      
    }
    
  }

  }
  
</script>
</html>

canvas中使用图片(背景、渐变)

在canvas中插入图片

1.canvas操作图片时,必须要等图片加载完才能操作
2.drawImage(image, x, y, width, height)
    image: 图像源;是imgae对象 或者 canvas 对象
    x, y: 图片在canvas中开始绘制的起始坐标
    width & height: 这两个参数用来控制 当向canvas画入时应该缩放的大小
    const image = new Image();
    image.src = './image.jpg'; 
    image.onload = function() {
      ctx.drawImage(image, 0, 0, image.width, image.height);
    }

在canvas中设置背景

1. createPattern(image, repetition)
    image: 图像源
    repetition: repeat | repeat-x | repeat-y | no-repeat
一般情况下会将createPattern()返回的对象作为fillStyle的值
    const image = new Image();
    image.src = './image.jpg'; 
    image.onload = function() {
      ctx.fillStyle = ctx.createPattern(image, 'repeat');
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

在canvas中使用渐变

线性渐变
1. createLinearGradient(x1, y1, x2, y2)
    x1,y1: 渐变的起点
    x2,y2: 渐变的终点
2. gradient.addColorStop(position, color)
    gradient: createLinearGradient方法的返回值
    position: 必须是0 - 1 之间的数值,表示渐变中颜色所在的相对位置。
    color: 必须是一个有效的css颜色值(如: #fff rgba(255, 255, 255, 1)等)
径向渐变
1. createRadialGradient(x1, y1, r1, x2, y2, r2)
    x1, y1, r1: 定义一个以(x1, y1)为原点,半径为r1的圆
    x2, y2, r2: 定义一个以(x2, y2)为原点,半径为r2的圆

线性渐变

    const gradient = ctx.createLinearGradient(0, 0, 300, 200);
    gradient.addColorStop(0, 'red');
    gradient.addColorStop(.5, 'yellow');
    gradient.addColorStop(1, 'green');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    

径向渐变

    // 两个圆之间是渐变的
    const gradient = ctx.createRadialGradient(150, 150, 50, 150, 150, 150); 
    gradient.addColorStop(0, 'red');
    gradient.addColorStop(.5, 'yellow');
    gradient.addColorStop(1, 'green');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

绘制文本

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

1. fillText(text, x, y): 在指定的(x, y)位置填充指定的文本
2. strokeText(text, x, y): 在指定的(x, y)位置绘制描边文字

还提供api设置文本样式

font = value 
    默认字体是 '10px sans-serif'
    这个字符串使用和 css font属性的语法相同
    注意:font属性在指定时,必须要有字体大小 和 字体 缺一不可,并且只有sans-serif这种字体可用,写错了也默认使用sans-serif
textAlign = value
    设置文本水平对齐方式
    文本对齐选项:
        left: 文本左对齐
        right: 文本右对齐
        center: 文本居中对齐。
            值得注意的是,这里的文本居中是基于你在fillText的时候所给x值,文本的一半在x位置的左边一半内容在右边
textBaseline = value
    设置文本基线的属性
    top|middle|bottom
        文本基线在文本块的顶部|中间|底部
        
measureText: 返回TextMetrics对象,包含文本尺寸信息

            
    const ctx = canvas.getContext('2d');
    ctx.font = '26px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('hello world', 150, 150); 

计算文本的宽度,这个比较有用

    ctx.font = '26px sans-serif';
    ctx.fillText('hello world', 150, 150); 
    const textMetrics = ctx.measureText('hello world');
    console.log(textMetrics);

文本阴影 & 盒模型阴影

1. shadowOffsetX = float 默认为0
    shadowOffsetX用来设定阴影在X轴的延伸距离
2. shadowOffsetY = float 默认为0
    shadowOffsetY用来设定阴影在Y轴的延伸距离
3. shadowOffsetBlur = float 默认为0
    用于设定阴影的模糊程度,与像素数量没有关系,不受变换矩阵的影响
4. shadowOffsetColor = color(必需) 默认全透明的黑色
    ctx.font = '26px sans-serif';
    ctx.shadowOffsetX = 3;
    ctx.shadowOffsetY = 3;
    ctx.shadowBlur = 0.5;
    ctx.shadowColor = 'rgba(0, 0, 0, .6)';
    ctx.fillText('hello world', 150, 150); 

像素操作

到目前为止,我们还尚未了解canvas画布真实像素的原理,事实上,我们可以通过ImageData对象操纵像素数据,直接读取或者将数据数组写入该对象中

获取场景像素数据

getImageData() 获得一个含画布场景像素数据的ImageData对象

ctx.getImageData(sx, sy, sw, sh)
    sx 将要被提取的图形像素数据矩形区域左上角x坐标
    sy 将要被提取的图形像素数据矩形区域左上角y坐标
    sw 将要被提取的图形像素数据矩形区域的宽度
    sh 将要被提取的图形像素数据矩形区域的高度
返回的ImageData对象
    width: 横向上像素点的个数
    height: 纵向上像素点的个数
    data: Unit8ClampeArray类型的一维数组,包含着RGBA格式的整型数据
        R: 0 - 255 (黑色到白色)
        G: 0 - 255 (黑色到白色)
        B: 0 - 255 (黑色到白色)
        A: 0 - 255 (透明到不透明)
        
// 为了方便看出画布,图中画布灰色是通过css设置的
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    console.log(imageData);
    console.log(imageData.data);
    

由此可以看出画布默认是黑色透明的

在场景中写入像素数据

1. putImageData(myImageData, dx, dy)
    dx和dy参数表示在场景内左上角绘制的像素所得到的设备坐标

    ctx.fillRect(0, 0, 50, 50); // 默认绘制的矩形是黑色
    const imageData = ctx.getImageData(0, 0, 50, 50); // 获取场景矩形的像素数据
    
    for (let i = 0; i < imageData.data.length; i++) {
      const r = i*4;
      const g = i*4 + 1;
      const b = i*4 + 2;
      const a = i*4 + 3;
      imageData.data[r] = 100; 
    }
    ctx.putImageData(imageData, 0, 0); // 修改了场景所有像素点G为100变成了酒红色

创建一个ImageData对象

ctx.createImageData(width, height)
    width: ImageData新对象的宽度
    height: ImageData新对象的高度
默认情况下创建的是黑色透明的

像素操作之马赛克

铺垫

单像素操作,定义一个工具函数,给定一个imageData对象和一个坐标获取该坐标点的像素信息

 // 给定偏移量拿像素点的信息
  function getPXInfo (imageData, x, y) {
    const color = [];
    const data = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    const theFirst = (y * width) + x; // 当前像素前面有多少个像素点
    // r
    color[0] = data[theFirst * 4];
    // g
    color[1] = data[theFirst * 4 + 1];
    // b
    color[2] = data[theFirst * 4 + 2];
    //a
    color[3] = data[theFirst * 4 + 3];

    return color;
  }

单像素操作,定义一个工具函数,给定一个坐标和像素信息设置该坐标


    ctx.save();
    ctx.fillStyle = 'pink';

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

    ctx.restore();

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    console.log(getPXInfo(imageData, 100, 0));
    // 将矩形区域的第一行改成了黑色
    for (let i = 0; i < 100; i++) {
      var newImageData = changePXInfo(imageData, i, 0, [0, 0, 0, 255]);
    }
    ctx.putImageData(newImageData, 0, 0);
    // 改变指定位置上的像素点信息
    function changePXInfo (imageData, x, y, color) {
        const data = imageData.data;
        const width = imageData.width;
        const height = imageData.height;
        const theFirst = (y * width) + x; // 当前像素前面有多少个像素点
        // r
        data[theFirst * 4] = color[0];
        // g
        data[theFirst * 4 + 1] = color[1];
        // b
        data[theFirst * 4 + 2] = color[2];
        //a
        data[theFirst * 4 + 3] = color[3];
        return imageData;
      }

马赛克实现

思路
1. 将图片区域划分成若干个马赛克矩形区域
2. 从马赛克矩形区域中随机选择一个像素点,并且将该像素点信息赋予马赛克矩形区域内其他像素点
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
    }
    canvas {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate3d(-50%, -50%, 0);
      /* border: 1px solid; */
      background: grey;

    }
  </style>
</head>
<body>
  <canvas></canvas>
</body>
<script>
  const canvas = document.querySelector('canvas');
  if (canvas.getContext) {
    const ctx = canvas.getContext('2d');

    ctx.save();
    ctx.fillStyle = 'pink';

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

    ctx.restore();
    const image = new Image();
    image.src = './image.jpeg'; 
    image.onload = function() {
      canvas.width = image.width * 2;
      canvas.height = image.height; 
      draw();
    }

    function draw() {
      ctx.drawImage(image, 0, 0, image.width, image.height);
      const oldImageData = ctx.getImageData(0, 0, image.width, image.height);
      // 根据 oldImageData 数据的修改成
      let newImageData = ctx.createImageData(image.width, image.height);
      // 1. 选取马赛克矩形
      // 2. 从马赛克矩形中随机抽出一个像素点的信息(rgba)
      // 3. 将整个马赛克矩形中的像素点信息统一调成随机抽取的那个(2中的)
      const size = 5;
      for (let i = 0; i < oldImageData.width/size;i++) {
        // 列的马赛克矩形
        for (let j = 0; j < oldImageData.height/size;j++) {
          // (i, j) 每一个马赛克矩形的坐标
          /*
            (0, 0)  (0, 0) - (4, 4)    (1, 0)  (5, 0) - (9, 4)
            (0, 1)  (0, 5) - (4, 9)    (1, 1)  (5, 5) - (9, 9)
          */
         // Math.random ---> [0, 1)
         const color = getPXInfo(oldImageData, Math.floor(Math.random()*size + size*i), Math.floor(Math.random()*size + size*j));
         // 循环每一个马赛克矩形  将整个马赛克矩形中的像素点信息统一调成随机抽取的那个(2中的)
         for (let a = 0; a < size; a++) {
            for (let b = 0; b < size; b++) {
              newImageData = setPXInfo(newImageData, size * i + a, size * j + b, color);
            }
         }
        }
      }
      ctx.putImageData(newImageData, image.width, 0);
    }
  }

  // 给定偏移量拿像素点的信息
  function getPXInfo (imageData, x, y) {
    const color = [];
    const data = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    const theFirst = (y * width) + x; // 当前像素前面有多少个像素点
    // r
    color[0] = data[theFirst * 4];
    // g
    color[1] = data[theFirst * 4 + 1];
    // b
    color[2] = data[theFirst * 4 + 2];
    //a
    color[3] = data[theFirst * 4 + 3];

    return color;
  }

  function setPXInfo (imageData, x, y, color) {
    const data = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    const theFirst = (y * width) + x; // 当前像素前面有多少个像素点
    // r
    data[theFirst * 4] = color[0];
    // g
    data[theFirst * 4 + 1] = color[1];
    // b
    data[theFirst * 4 + 2] = color[2];
    //a
    data[theFirst * 4 + 3] = color[3];
    return imageData;
  }
</script>
</html>

部分图解

实现的效果图:

合成

全局透明的的设置

globalAlpha = value(默认值是1)
    这个属性影响到canvas里所有图形的透明的
    有效值范围0(完全透明)到1(完全不透明)
    ctx.save();
    ctx.fillStyle = 'red';
    ctx.globalAlpha = .5; // 把画布上的所有内容都透明度都修改了
    ctx.beginPath();
    ctx.fillRect(0, 0, 100, 100);
    ctx.restore();

覆盖合成

多个图像叠加在一起时,如何展示的操作 source: 新的图像源(后面绘制的图像) destination: 已经绘制过的图形(目标)

ctx.globalCompositeOperation: (属性值不止下方八种,这八种比较常用)
    source-over(默认值): 源在上面,新的图像层级比较高
    source-in: 只留下源与目标的重叠部分(源的那部分)
    source-out: 只留下源超出目标的部分
    source-atop: 砍掉源溢出的部分
    
    destination-over(默认值): 目标在上面,旧的图像层级比较高
    destination-in: 只留下源与目标的重叠部分(目标的那部分)
    destination-out: 只留下目标超过源的部分
    destination-atop: 砍掉目标溢出的部分

    ctx.fillStyle = 'darkred';
    ctx.fillRect(0, 0, 100, 100);

    ctx.globalCompositeOperation = 'source-over';

    ctx.fillStyle = 'black';
    ctx.fillRect(50, 50, 100, 100);

下图是globalCompositeOperation八种值所对应的效果图

刮刮卡的制作

分析结构: 底层是背景图片,上面是canvas

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    html, body {
      height: 100%;
      overflow: hidden;
    }
    #wrap, #wrap ul, ul li {
      height: 100%;
    }
    ul  li {
      background-image: url(down.jpg);
      background-size: 100% 100%;
      background-repeat: no-repeat;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      transition: 1s;
    }
  </style>
</head>
<body>
  <div id='wrap'>
    <canvas></canvas>
    <ul>
      <li></li>
    </ul>
  </div>
  
</body>

<script type="text/javascript">
  window.onload = function() {
    const canvas = document.querySelector('canvas');
    canvas.width = document.documentElement.clientWidth;// 获取视口的宽度
    canvas.height = document.documentElement.clientHeight;// 获取视口的宽度

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

      const image = new Image();
      image.src = './up.jpg';

      image.onload = function() {
        draw();
      }

      function draw() {
        let count = 0;
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.lineWidth = 40;
        canvas.addEventListener('touchstart', function(e) {
          e = e || window.e;
          const touchC = e.changedTouches[0]; // 移动端获取手指列表的第一根手指
          const x = touchC.clientX - canvas.offsetLeft;
          const y = touchC.clientY - canvas.offsetTop;
          ctx.save();
          
          ctx.globalCompositeOperation = 'destination-out'; // 只留下目标操过源的部分
          ctx.beginPath();
          // ctx.arc(x, y, 20, 0, 2*Math.PI);
          ctx.moveTo(x, y);
          ctx.lineTo(x + 1, y + 1);
          ctx.stroke();
          ctx.restore();

        });
        // 手指移动的时候 必须touchstart触发后才会触发
        canvas.addEventListener('touchmove', function(e) {
          e = e || window.event;
          const touchC = e.changedTouches[0]; // 移动端获取手指列表的第一根手指
          const x = touchC.clientX - canvas.offsetLeft;
          const y = touchC.clientY - canvas.offsetTop;

          ctx.save();
          ctx.globalCompositeOperation = 'destination-out'; // 只留下目标操过源的部分
          
          // ctx.arc(x, y, 20, 0, 2*Math.PI);
          ctx.moveTo(x, y);
          ctx.lineTo(x + 1, y + 1);
          ctx.stroke();

          ctx.restore();

        });
        canvas.addEventListener('touchend', function(e) {
          e = e || window.event;
          // 当划掉的内容超过一半的时候,把canvas那一层有过渡的干掉
          const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

          const allPx = imageData.width * imageData.height; // 所有的像素点个数
          // 统计像素点是透明的
          for (let i = 0; i < allPx;i++) {
            if (imageData.data[4*i+3] === 0) {
              count++;
            }
          }
          if (count >= allPx/2) {
            canvas.style.opacity = 0;
          }

        });
        // 当canvas完成过渡时触发
        canvas.addEventListener('transitionend',  function() {
          this.remove();
        });
      }
    }
  }
</script>
</html>

其他

将画布导出为图像(移动端)

toDataURL() canvas元素上的方法
    返回: base64格式的路径
    

事件操作

ctx.isPointInPath(x, y) 判读在当前路径中是否包含检测点
    x 检测点x坐标
    y 检测点y坐标
此方法只作用于最新画出的canvas图像
    ctx.arc(100, 100, 50, 0, 2*Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(200, 200, 50, 0, 2*Math.PI);

    ctx.fill();
    canvas.onclick = function(e) {
      // 点击画布时都触发
      e = e || window.event;
      const x = e.clientX - canvas.offsetLeft;
      const y = e.clientY - canvas.offsetTop;

      if (ctx.isPointInPath(x, y)) {
        alert('点在了最后绘制目标图形上了');
      }