HyG Front-end Dev Engineer

canvas 绘图技术与图片处理

2021-07-22
HyG

本文将讲述一些 canvas 相关的绘图技术,其中包括:

  • 绘图 API
  • 图片加载
  • 像素处理

绘图 API

相关 api 可参考,这里不展开详讲。

下面举几个示例

鼠标绘图

代码如下:

<canvas id="mainCanvas" style="background-color: #fff;" width="800" height="400"></canvas>
const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

if (canvas) {
  const context = canvas.getContext('2d')
  if (context) {
    const { offsetLeft, offsetTop } = canvas
    let x
    let y

    const mouseMoveHandler = (e: MouseEvent) => {
      x = e.pageX
      y = e.pageY
      x -= offsetLeft
      y -= offsetTop
      context.lineTo(x, y)
      context.lineCap = 'round'
      context.lineJoin = 'round'
      context.stroke()
    }

    canvas.addEventListener('mousedown', (e) => {
      context.beginPath()
      context.moveTo(e.pageX - offsetLeft, e.pageY - offsetTop)
      canvas.addEventListener('mousemove', mouseMoveHandler)
    })

    canvas.addEventListener('mouseup', () => {
      canvas.removeEventListener('mousemove', mouseMoveHandler)
    })
  }
}

效果如下

demo 链接 https://gaohaoyang.github.io/canvas-practice/09-mouse-draw/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/09-mouse-draw/index.ts

绘制曲线

二次贝塞尔曲线

具体 api 可参考文档 CanvasRenderingContext2D.quadraticCurveTo()

ctx.quadraticCurveTo(cpx, cpy, x, y);

它需要2个点。第一个点是控制点,第二个点是终点。起始点是当前路径最新的点,当创建二次贝赛尔曲线之前,可以使用 moveTo() 方法进行改变。

接下来实现一个 demo,用鼠标位置作为控制点,控制这个二次贝塞尔曲线。

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

if (canvas) {
  const context = canvas.getContext('2d')
  const { offsetLeft, offsetTop } = canvas

  const x0 = 300
  const y0 = 100
  const x1 = 600
  const y1 = 300

  if (context) {
    canvas.addEventListener('mousemove', (e) => {
      context.clearRect(0, 0, canvas.width, canvas.height)
      const x = e.pageX - offsetLeft
      const y = e.pageY - offsetTop

      context.beginPath()
      context.moveTo(x0, y0)
      context.quadraticCurveTo(x, y, x1, y1)
      context.stroke()
    })
  }
}

效果如下

demo 链接 https://gaohaoyang.github.io/canvas-practice/10-quadratic/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/10-quadratic/index.ts

穿过控制点的二次贝塞尔曲线

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

if (canvas) {
  const context = canvas.getContext('2d')
  const { offsetLeft, offsetTop } = canvas

  const x0 = 300
  const y0 = 100
  const x1 = 600
  const y1 = 300

  if (context) {
    canvas.addEventListener('mousemove', (e) => {
      context.clearRect(0, 0, canvas.width, canvas.height)
      const x = e.pageX - offsetLeft
      const y = e.pageY - offsetTop

      const cpx = x * 2 - (x0 + x1) / 2
      const cpy = y * 2 - (y0 + y1) / 2

      context.beginPath()
      context.moveTo(x0, y0)
      context.quadraticCurveTo(cpx, cpy, x1, y1)
      context.stroke()
    })
  }
}

其核心是

const cpx = x * 2 - (x0 + x1) / 2
const cpy = y * 2 - (y0 + y1) / 2

效果如下

demo 链接 https://gaohaoyang.github.io/canvas-practice/11-quadratic-through/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/11-quadratic-through/index.ts

多条曲线

绘制平滑的曲线,用多个点控制

import Ball from '../common/Ball'

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

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

  if (ctx) {
    const points = []
    const num = 4

    for (let i = 0; i < num; i += 1) {
      const ball = new Ball(2)
      const x = Math.random() * canvas.width
      const y = Math.random() * canvas.height
      points.push({
        x,
        y,
      })
      ball.x = x
      ball.y = y
      ball.draw(ctx)
    }

    ctx.beginPath()
    ctx.moveTo(points[0].x, points[0].y)

    for (let i = 1; i < num - 2; i += 1) {
      const xAv = (points[i].x + points[i + 1].x) / 2
      const yAv = (points[i].y + points[i + 1].y) / 2
      ctx.quadraticCurveTo(points[i].x, points[i].y, xAv, yAv)
    }
    ctx.quadraticCurveTo(points[num - 2].x, points[num - 2].y, points[num - 1].x, points[num - 1].y)
    ctx.stroke()
  }
}

xAv, yAv 围边设置为循环中当前点和后续点的x,y坐标的平均值,这样就能绘制一条平滑的曲线了

效果如下

demo 链接 https://gaohaoyang.github.io/canvas-practice/12-multi-quadratic/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/12-multi-quadratic/index.ts

闭合的多条曲线

import Ball from '../common/Ball'

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

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

  if (ctx) {
    const points = []
    const num = 4

    for (let i = 0; i < num; i += 1) {
      const ball = new Ball(2)
      const x = Math.random() * canvas.width
      const y = Math.random() * canvas.height
      points.push({
        x,
        y,
      })
      ball.x = x
      ball.y = y
      ball.draw(ctx)
    }

    const xAv1 = (points[0].x + points[num - 1].x) / 2
    const yAv1 = (points[0].y + points[num - 1].y) / 2

    ctx.beginPath()
    ctx.moveTo(xAv1, yAv1)

    for (let i = 0; i < num - 1; i += 1) {
      const xAv = (points[i].x + points[i + 1].x) / 2
      const yAv = (points[i].y + points[i + 1].y) / 2
      ctx.quadraticCurveTo(points[i].x, points[i].y, xAv, yAv)
    }
    ctx.quadraticCurveTo(points[num - 1].x, points[num - 1].y, xAv1, yAv1)
    ctx.stroke()
  }
}

demo 链接 https://gaohaoyang.github.io/canvas-practice/13-multi-quadratic-close/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/13-multi-quadratic-close/index.ts

图形与填充色

一般来说绘图顺序如下:

  • beginPath 开始绘制
  • moveTo 移动起点
  • lineStyle 线样式
  • fillStyle 填充样式
  • lineTo 或 quadraticCurveTo 等绘制曲线
  • closePath 闭合
  • fill 填充
  • stroke 描边

渐变色

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

createLinearGradient() 方法需要指定四个参数,分别表示渐变线段的开始和结束点。

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

arc() 是 Canvas 2D API 绘制圆弧路径的方法。 圆弧路径的圆心在 (x, y) 位置,半径为 r ,根据anticlockwise (默认为顺时针)指定的方向从 startAngle 开始绘制,到 endAngle 结束。

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

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

  if (ctx) {
    ctx.beginPath()
    const gradient = ctx.createLinearGradient(100, 100, 200, 200)
    gradient.addColorStop(0, '#ff0000')
    gradient.addColorStop(1, '#000000')
    ctx.fillStyle = gradient
    ctx.fillRect(100, 100, 100, 100)

    const gradient2 = ctx.createLinearGradient(200, 200, 300, 300)
    gradient2.addColorStop(0, '#ff0000')
    gradient2.addColorStop(0.6, '#008880')
    gradient2.addColorStop(1, '#000000')
    ctx.fillStyle = gradient2
    ctx.fillRect(200, 200, 100, 100)

    const gradient3 = ctx.createRadialGradient(500, 200, 0, 500, 200, 100)
    gradient3.addColorStop(0, '#000000')
    gradient3.addColorStop(1, '#ff0000')
    ctx.arc(500, 200, 100, 0, 2 * Math.PI)
    ctx.fillStyle = gradient3
    ctx.fill()
  }
}

效果如下:

demo 链接 https://gaohaoyang.github.io/canvas-practice/14-gradient/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/14-gradient/index.ts

图片加载

有些场景,可能需要在 canvas 内绘制一张图片,接下来我们看看绘制图片的方式

绘制图片

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

if (canvas) {
  const ctx = canvas.getContext('2d')
  if (ctx) {
    const img = new Image()
    img.src = 'https://gw.alicdn.com/imgextra/i2/O1CN01gR6ymq1dfV5RmYxYk_!!6000000003763-2-tps-658-411.png'
    img.addEventListener('load', () => {
      ctx.drawImage(img, 0, 0, 658, 329, 0, 0, 800, 400)
    })
  }
}

效果如下

demo 链接 https://gaohaoyang.github.io/canvas-practice/15-image/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/15-image/index.ts

操作像素

接下来我们尝试将上述demo中的所有绿色像素移除,看看是什么效果

const canvas: HTMLCanvasElement | null = document.querySelector('#mainCanvas')

if (canvas) {
  const ctx = canvas.getContext('2d')
  if (ctx) {
    const img = new Image()
    img.src = '../assets/1.png'
    img.addEventListener('load', () => {
      ctx.drawImage(img, 0, 0, 658, 329, 0, 0, canvas.width, canvas.height)

      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
      const pixels = imageData.data
      for (let i = 0; i < pixels.length; i += 4) {
        pixels[i + 1] = 0
      }
      ctx.putImageData(imageData, 0, 0)
    })
  }
}

效果如下:

demo 链接 https://gaohaoyang.github.io/canvas-practice/16-image-pixel/

源码链接 https://github.com/Gaohaoyang/canvas-practice/blob/main/src/16-image-pixel/index.ts

总结

本文虽然没有涉及过多动画,但还是举例介绍了 canvas 2d 绘图的方式,并且介绍了如何加载图片与操作像素。这为后续的动画学习垫定了基础。


Comments

Content