Skip to content

Canvas API

基本绘图

js
// 获取绘制上下文
const ctx = canvas.getContext('2d')
// 使用上下文对象ctx完成后续绘图
// 获取绘制上下文
const ctx = canvas.getContext('2d')
// 使用上下文对象ctx完成后续绘图

2d 的 canvas 上下文支持以下图形的绘制

  1. 直线(矩形 API)

ctx.beginPath, ctx.moveTo, ctx.lineTo, ctx.stroke, ctx.closePath

  1. 曲线(椭圆 API)

ctx.arc

  1. 文字

ctx.font, ctx.fillText, ctx.strokeText

  1. 图片

ctx

绘制流程

  1. 生成路径

  2. 绘制

vue
<template>
  <canvas ref="canvasRef" width="600" height="400"></canvas>
</template>

<script setup>
import { ref, onMounted } from 'vue'
const canvasRef = ref()
onMounted(() => {
  const canvas = canvasRef1.value

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

  // 画一个三角形
  ctx.beginPath()
  ctx.moveTo(100, 50)
  ctx.lineTo(300, 100)
  ctx.lineTo(123, 222)
  ctx.closePath()
  ctx.strokeStyle = '#000'
  ctx.stroke()

  // 画一个圆
  ctx.beginPath()
  ctx.arc(200, 300, 100, 0, 2 * Math.PI)
  ctx.strokeStyle = '#f00'
  ctx.stroke()

  // 画文字
  ctx.font = '50px serif'
  ctx.fillText('Hello world', 300, 100)

  // 画图片
  let image = new Image()
  image.src = '/img-example.jpg'
  image.onload = () => {
    ctx.drawImage(
      image,
      350,
      200,
      image.naturalWidth / 4,
      image.naturalHeight / 4
    )
  }
})
</script>
<template>
  <canvas ref="canvasRef" width="600" height="400"></canvas>
</template>

<script setup>
import { ref, onMounted } from 'vue'
const canvasRef = ref()
onMounted(() => {
  const canvas = canvasRef1.value

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

  // 画一个三角形
  ctx.beginPath()
  ctx.moveTo(100, 50)
  ctx.lineTo(300, 100)
  ctx.lineTo(123, 222)
  ctx.closePath()
  ctx.strokeStyle = '#000'
  ctx.stroke()

  // 画一个圆
  ctx.beginPath()
  ctx.arc(200, 300, 100, 0, 2 * Math.PI)
  ctx.strokeStyle = '#f00'
  ctx.stroke()

  // 画文字
  ctx.font = '50px serif'
  ctx.fillText('Hello world', 300, 100)

  // 画图片
  let image = new Image()
  image.src = '/img-example.jpg'
  image.onload = () => {
    ctx.drawImage(
      image,
      350,
      200,
      image.naturalWidth / 4,
      image.naturalHeight / 4
    )
  }
})
</script>

自定义封装方法

源码如下:

vue
<template>
  <canvas
    ref="cvsRef"
    style="width: 700px; height: 800px; background-color: #000"
  ></canvas>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const cvsRef = ref<any>()

const getRandom = (min, max) => {
  return Math.floor(min + Math.random() * (max - min))
}

onMounted(() => {
  const canvas = cvsRef.value
  const ctx: CanvasRenderingContext2D = canvas.getContext('2d')
  canvas.width = 700 * devicePixelRatio
  canvas.height = 800 * devicePixelRatio

  /**
   * 生成一个点
   */
  class Point {
    x: number
    y: number
    r: number
    xSpeed: number
    ySpeed: number
    lastDrawTime: number | null

    constructor() {
      this.r = 2 * devicePixelRatio
      this.x = getRandom(0, canvas.width - this.r / 2)
      this.y = getRandom(0, canvas.height - this.r / 2)
      this.xSpeed = getRandom(-50, 50)
      this.ySpeed = getRandom(-50, 50)
      this.lastDrawTime = null
    }

    draw() {
      if (this.lastDrawTime) {
        // 计算新的坐标
        const duration = (Date.now() - this.lastDrawTime) / 1000
        const xDis = this.xSpeed * duration
        const yDis = this.ySpeed * duration
        let x = this.x + xDis
        let y = this.y + yDis
        if (x > canvas.width - this.r / 2 || x < this.r / 2) {
          this.xSpeed = -this.xSpeed
        }
        if (y > canvas.height - this.r / 2 || y < this.r / 2) {
          this.ySpeed = -this.ySpeed
        }
        this.x = x
        this.y = y
      }
      ctx.beginPath()
      ctx.arc(this.x, this.y, this.r, 2 * Math.PI, 0)
      ctx.fillStyle = `rgb(200,200,200)`
      ctx.fill()
      this.lastDrawTime = Date.now()
    }
  }

  class Graph {
    /**
     * 点的集合
     */
    points: Point[]

    /**
     * 连线最大距离
     */
    maxDis: number = 150 * devicePixelRatio

    constructor(pointNumber = 30) {
      this.points = new Array(pointNumber).fill(0).map(() => new Point())
    }

    drawLine(p1, p2) {
      // 计算2点间距离
      const d = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
      if (d > this.maxDis) return
      ctx.beginPath()
      ctx.moveTo(p1.x, p1.y)
      ctx.lineTo(p2.x, p2.y)
      // 计算连线清晰度
      ctx.strokeStyle = `rgba(200,200,200,${1 - d / this.maxDis})`
      ctx.stroke()
    }

    /**
     * 绘制
     */
    draw() {
      requestAnimationFrame(() => {
        this.draw()
      })
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      for (let i = 0; i < this.points.length; i++) {
        let p1 = this.points[i]
        p1.draw()
        // 进行连线
        for (let j = i + 1; j < this.points.length; j++) {
          const p2 = this.points[j]
          this.drawLine(p1, p2)
        }
      }
    }
  }
  let graph = new Graph(100)
  graph.draw()
})
</script>
<template>
  <canvas
    ref="cvsRef"
    style="width: 700px; height: 800px; background-color: #000"
  ></canvas>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const cvsRef = ref<any>()

const getRandom = (min, max) => {
  return Math.floor(min + Math.random() * (max - min))
}

onMounted(() => {
  const canvas = cvsRef.value
  const ctx: CanvasRenderingContext2D = canvas.getContext('2d')
  canvas.width = 700 * devicePixelRatio
  canvas.height = 800 * devicePixelRatio

  /**
   * 生成一个点
   */
  class Point {
    x: number
    y: number
    r: number
    xSpeed: number
    ySpeed: number
    lastDrawTime: number | null

    constructor() {
      this.r = 2 * devicePixelRatio
      this.x = getRandom(0, canvas.width - this.r / 2)
      this.y = getRandom(0, canvas.height - this.r / 2)
      this.xSpeed = getRandom(-50, 50)
      this.ySpeed = getRandom(-50, 50)
      this.lastDrawTime = null
    }

    draw() {
      if (this.lastDrawTime) {
        // 计算新的坐标
        const duration = (Date.now() - this.lastDrawTime) / 1000
        const xDis = this.xSpeed * duration
        const yDis = this.ySpeed * duration
        let x = this.x + xDis
        let y = this.y + yDis
        if (x > canvas.width - this.r / 2 || x < this.r / 2) {
          this.xSpeed = -this.xSpeed
        }
        if (y > canvas.height - this.r / 2 || y < this.r / 2) {
          this.ySpeed = -this.ySpeed
        }
        this.x = x
        this.y = y
      }
      ctx.beginPath()
      ctx.arc(this.x, this.y, this.r, 2 * Math.PI, 0)
      ctx.fillStyle = `rgb(200,200,200)`
      ctx.fill()
      this.lastDrawTime = Date.now()
    }
  }

  class Graph {
    /**
     * 点的集合
     */
    points: Point[]

    /**
     * 连线最大距离
     */
    maxDis: number = 150 * devicePixelRatio

    constructor(pointNumber = 30) {
      this.points = new Array(pointNumber).fill(0).map(() => new Point())
    }

    drawLine(p1, p2) {
      // 计算2点间距离
      const d = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
      if (d > this.maxDis) return
      ctx.beginPath()
      ctx.moveTo(p1.x, p1.y)
      ctx.lineTo(p2.x, p2.y)
      // 计算连线清晰度
      ctx.strokeStyle = `rgba(200,200,200,${1 - d / this.maxDis})`
      ctx.stroke()
    }

    /**
     * 绘制
     */
    draw() {
      requestAnimationFrame(() => {
        this.draw()
      })
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      for (let i = 0; i < this.points.length; i++) {
        let p1 = this.points[i]
        p1.draw()
        // 进行连线
        for (let j = i + 1; j < this.points.length; j++) {
          const p2 = this.points[j]
          this.drawLine(p1, p2)
        }
      }
    }
  }
  let graph = new Graph(100)
  graph.draw()
})
</script>

在 canvas 中进行用户交互

源码如下:

vue
<template>
  <canvas ref="cvsRef" style="width: 700px; height: 800px"></canvas>
</template>

<script setup lang="ts">
import { ElementNode } from '@vue/compiler-core'
import { ref, onMounted } from 'vue'

const cvsRef = ref<any>()

const getRandom = (min, max) => {
  return Math.floor(min + Math.random() * (max - min))
}

onMounted(() => {
  const canvas = cvsRef.value as HTMLCanvasElement
  const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
  canvas.width = 700 * devicePixelRatio
  canvas.height = 800 * devicePixelRatio

  const init = () => {
    ctx.strokeStyle = '#000'
    ctx.strokeRect(0, 0, canvas.width, canvas.height)
  }
  init()

  class Rectangle {
    color: string
    startX: number
    startY: number
    endX: number
    endY: number

    constructor(color, startX, startY) {
      this.color = color
      this.startX = startX
      this.startY = startY
      this.endX = startX
      this.endY = startY
    }

    get minX() {
      return Math.min(this.startX, this.endX) * devicePixelRatio
    }

    get maxX() {
      return Math.max(this.startX, this.endX) * devicePixelRatio
    }

    get minY() {
      return Math.min(this.startY, this.endY) * devicePixelRatio
    }

    get maxY() {
      return Math.max(this.startY, this.endY) * devicePixelRatio
    }

    draw() {
      ctx.beginPath()
      ctx.moveTo(this.minX, this.minY)
      ctx.lineTo(this.maxX, this.minY)
      ctx.lineTo(this.maxX, this.maxY)
      ctx.lineTo(this.minX, this.maxY)
      ctx.lineTo(this.minX, this.minY)
      ctx.lineCap = 'square'
      ctx.fillStyle = this.color
      ctx.fill()
      ctx.strokeStyle = '#fff'
      ctx.lineWidth = 3
      ctx.stroke()
    }

    isInside(x, y) {
      x *= devicePixelRatio
      y *= devicePixelRatio
      return (
        x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
      )
    }
  }

  const shapes: Rectangle[] = []

  const draw = () => {
    requestAnimationFrame(draw)
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    for (const shape of shapes) {
      shape.draw()
    }
  }

  const getShape = (x, y) => {
    for (let i = shapes.length - 1; i >= 0; i--) {
      const shape = shapes[i]
      if (shape.isInside(x, y)) {
        return shape
      }
    }
    return null
  }

  canvas.onmousedown = e => {
    const rect = canvas.getBoundingClientRect()
    const clickX = e.clientX - rect.left
    const clickY = e.clientY - rect.top
    const existShape = getShape(clickX, clickY)
    if (existShape) {
      const { startX, startY, endX, endY } = existShape
      // 拖动
      window.onmousemove = e => {
        const disX = e.clientX - rect.left - clickX
        const disY = e.clientY - rect.top - clickY
        existShape.startX = startX + disX
        console.log('disX', disX, startX, existShape.startX)
        existShape.endX = endX + disX
        existShape.startY = startY + disY
        existShape.endY = endY + disY
      }
    } else {
      // 新建
      const shape = new Rectangle('#f00', clickX, clickY)
      shapes.push(shape)
      window.onmousemove = e => {
        shape.endX = e.clientX - rect.left
        shape.endY = e.clientY - rect.top
        draw()
      }
    }

    window.onmouseup = e => {
      window.onmousemove = null
      window.onmouseup = null
    }
  }
})
</script>
<template>
  <canvas ref="cvsRef" style="width: 700px; height: 800px"></canvas>
</template>

<script setup lang="ts">
import { ElementNode } from '@vue/compiler-core'
import { ref, onMounted } from 'vue'

const cvsRef = ref<any>()

const getRandom = (min, max) => {
  return Math.floor(min + Math.random() * (max - min))
}

onMounted(() => {
  const canvas = cvsRef.value as HTMLCanvasElement
  const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
  canvas.width = 700 * devicePixelRatio
  canvas.height = 800 * devicePixelRatio

  const init = () => {
    ctx.strokeStyle = '#000'
    ctx.strokeRect(0, 0, canvas.width, canvas.height)
  }
  init()

  class Rectangle {
    color: string
    startX: number
    startY: number
    endX: number
    endY: number

    constructor(color, startX, startY) {
      this.color = color
      this.startX = startX
      this.startY = startY
      this.endX = startX
      this.endY = startY
    }

    get minX() {
      return Math.min(this.startX, this.endX) * devicePixelRatio
    }

    get maxX() {
      return Math.max(this.startX, this.endX) * devicePixelRatio
    }

    get minY() {
      return Math.min(this.startY, this.endY) * devicePixelRatio
    }

    get maxY() {
      return Math.max(this.startY, this.endY) * devicePixelRatio
    }

    draw() {
      ctx.beginPath()
      ctx.moveTo(this.minX, this.minY)
      ctx.lineTo(this.maxX, this.minY)
      ctx.lineTo(this.maxX, this.maxY)
      ctx.lineTo(this.minX, this.maxY)
      ctx.lineTo(this.minX, this.minY)
      ctx.lineCap = 'square'
      ctx.fillStyle = this.color
      ctx.fill()
      ctx.strokeStyle = '#fff'
      ctx.lineWidth = 3
      ctx.stroke()
    }

    isInside(x, y) {
      x *= devicePixelRatio
      y *= devicePixelRatio
      return (
        x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
      )
    }
  }

  const shapes: Rectangle[] = []

  const draw = () => {
    requestAnimationFrame(draw)
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    for (const shape of shapes) {
      shape.draw()
    }
  }

  const getShape = (x, y) => {
    for (let i = shapes.length - 1; i >= 0; i--) {
      const shape = shapes[i]
      if (shape.isInside(x, y)) {
        return shape
      }
    }
    return null
  }

  canvas.onmousedown = e => {
    const rect = canvas.getBoundingClientRect()
    const clickX = e.clientX - rect.left
    const clickY = e.clientY - rect.top
    const existShape = getShape(clickX, clickY)
    if (existShape) {
      const { startX, startY, endX, endY } = existShape
      // 拖动
      window.onmousemove = e => {
        const disX = e.clientX - rect.left - clickX
        const disY = e.clientY - rect.top - clickY
        existShape.startX = startX + disX
        console.log('disX', disX, startX, existShape.startX)
        existShape.endX = endX + disX
        existShape.startY = startY + disY
        existShape.endY = endY + disY
      }
    } else {
      // 新建
      const shape = new Rectangle('#f00', clickX, clickY)
      shapes.push(shape)
      window.onmousemove = e => {
        shape.endX = e.clientX - rect.left
        shape.endY = e.clientY - rect.top
        draw()
      }
    }

    window.onmouseup = e => {
      window.onmousemove = null
      window.onmouseup = null
    }
  }
})
</script>