vue + canvas 实现涂鸦面板

前言

专栏分享:vue2源码专栏vue router源码专栏玩具项目专栏,硬核💪推荐🙌

欢迎各位 ITer 关注点赞收藏🌸🌸🌸

此篇文章用于记录柏成从零开发一个canvas涂鸦面板的历程,最终效果如下:

介绍

我们基于 canvas 实现了一款简单的涂鸦面板,用于在网页上进行绘图和创作。其支持以下快捷键:

功能 快捷键
撤销 Ctrl + Z
恢复 Ctrl + Y

我们可以通过 new Board 创建一个空白画板,其接收一个容器作为参数,下面是个基本例子:

html 复制代码
<template>
  <div class="drawing-board">
    <div id="container" ref="container" style="width: 100%; height: 100%"></div>
  </div>
</template>

<script setup>
  import { ref, onMounted } from 'vue'
  import Board from '@/canvas/board.js'

  const container = ref(null)
  onMounted(() => {
    // 创建一个空白画板
    new Board(container.value)
  })
</script>

初始化

Board 的实现是一个类,在 src/canvas/board.js中定义。

new Board(container)时做了什么?我们在构造函数中创建一个 canvas 画布追加到了 container 容器中,并定义了一系列属性,最后执行了 init 初始化方法。

在初始化方法中,我们设置了画笔样式(其实可以动态去设置,让用户选择画笔颜色、粗细、线条样式等,时间有限,未实现此功能);注册监听了鼠标键盘事件,用于绘制画笔轨迹和实现撤销恢复快捷键操作。

javascript 复制代码
export default class BoardCanvas {
  constructor(container) {
    // 容器
    this.container = container
    // canvas画布
    this.canvas = this.createCanvas(container)
    // 绘制工具
    this.ctx = this.canvas.getContext('2d')
    // 起始点位置
    this.startX = 0
    this.stateY = 0
    // 画布历史栈
    this.pathSegmentHistory = []
    this.index = 0

    // 初始化
    this.init()
  }
  // 创建画布
  createCanvas(container) {
    const canvas = document.createElement('canvas')
    canvas.width = container.clientWidth
    canvas.height = container.clientHeight
    canvas.style.display = 'block'
    canvas.style.backgroundColor = 'antiquewhite'
    container.appendChild(canvas)
    return canvas
  }

  // 初始化
  init() {
    this.addPathSegment()
    this.setContext2DStyle()
    // 阻止默认右击事件
    this.canvas.addEventListener('contextmenu', (e) => e.preventDefault())
    // 自定义鼠标按下事件
    this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))
    // 自定义键盘按下事件
    window.document.addEventListener('keydown', this.keydownEvent.bind(this))
  }

  // 设置画笔样式
  setContext2DStyle() {
    this.ctx.strokeStyle = '#EB7347'
    this.ctx.lineWidth = 3
    this.ctx.lineCap = 'round'
    this.ctx.lineJoin = 'round'
  }
}

自定义鼠标事件

我们之前在 init 初始化方法中注册了 onmousedown 鼠标按下事件,需要在此处实现鼠标按下拖拽可以绘制画笔轨迹的逻辑

javascript 复制代码
mousedownEvent(e) {
  const that = this
  const ctx = this.ctx
  ctx.beginPath()
  ctx.moveTo(e.offsetX, e.offsetY)
  ctx.stroke()

  this.canvas.onmousemove = function (e) {
    ctx.lineTo(e.offsetX, e.offsetY)
    ctx.stroke()
  }
  
  this.canvas.onmouseup = this.canvas.onmouseout = function () {
    that.addPathSegment()
    this.onmousemove = null
    this.onmouseup = null
    this.onmouseout = null
  }
}

自定义键盘事件

我们之前在 init 初始化方法中注册了 onkeydown 键盘按下事件,需要在此处实现撤销恢复的逻辑

javascript 复制代码
// 键盘事件
keydownEvent(e) {
  if (!e.ctrlKey) return
  switch (e.keyCode) {
    case 90:
      this.undo()
      break
    case 89:
      this.redo()
      break
  }
}

要实现撤销恢复操作,我们需要一个存储画布快照的栈!这又涉及到两个问题,我们如何获取到当前画布快照?如何根据快照数据恢复画布?

查阅 canvas官方API文档 得知,获取快照 API 为 getImageData;通过快照恢复画布的 API 为 putImageData

javascript 复制代码
/*
 * @name 返回一个 ImageData 对象,其中包含 Canvas 画布部分或完整的像素点信息
 * @param { Number } sx 将要被提取的图像数据矩形区域的左上角 x 坐标
 * @param { Number } sy 将要被提取的图像数据矩形区域的左上角 y 坐标
 * @param { Number } sWidth  将要被提取的图像数据矩形区域的宽度
 * @param { Number } sHeight 将要被提取的图像数据矩形区域的高度
 * @return { Object } 返回一个 ImageData 对象,包含 Canvas 给定的矩形图像像素点信息
 */
context.getImageData(sx, sy, sWidth, sHeight);
 
 /*
 * @name 将给定 ImageData 对象的数据绘制到位图上
 * @param { Object } ImageData 对象,包含 Canvas 给定的矩形图像像素点信息
 * @param { Number } dx 目标 Canvas 中被图像数据替换的起点横坐标
 * @param { Number } dy 目标 Canvas 中被图像数据替换的起点纵坐标
 */
 context.putImageData(ImageData, dx, dy);

我们对保存画布快照的逻辑进行了一次封装,如下:

javascript 复制代码
// 添加路径片段
addPathSegment() {
  const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
  
  // 删除当前索引后的路径片段,然后追加一个新的路径片段,更新索引
  this.pathSegmentHistory.splice(this.index + 1)
  this.pathSegmentHistory.push(data)
  this.index = this.pathSegmentHistory.length - 1
}

我们在构造函数中定义了一个存储画布快照的栈 - pathSegmentHistory;一个指向栈中当前快照的索引 - index

在初始化和绘制一个路径片段结束时都会调用 addPathSegment 方法,用于保存当前画布快照到栈中,并将索引指向栈中的最后一个成员

Tip:在保存快照数据之前,我们会先删除栈中位于索引之后的全部快照数据,目的是执行撤销操作后再绘制轨迹,要清空栈中的多余数据。举个栗子,如果我们先执行3次undo,再执行一次redo,最后绘制一条新的轨迹,则需要先清除栈中的最后两条快照数据,再添加一条新的当前画布快照数据,示意图如下

撤销(undo)

当执行 undo 操作时,我们先将索引前移, 然后取出当前索引指向的快照数据,重新绘制画布

javascript 复制代码
// 撤销
undo() {
  if (this.index <= 0) return
  this.index--
  this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
}

恢复(redo)

当执行 redo 操作时,我们先将索引后移, 然后取出当前索引指向的快照数据,重新绘制画布

javascript 复制代码
// 恢复
redo() {
  if (this.index >= this.pathSegmentHistory.length - 1) return
  this.index++
  this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
}

源码

涂鸦面板demo代码:vue-canvas

相关推荐
come1123413 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志35 分钟前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘1 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js