经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
vue + canvas 实现涂鸦面板
来源:cnblogs  作者:柏成  时间:2023/8/4 9:02:12  对本文有异议

前言

专栏分享:vue2源码专栏vue router源码专栏玩具项目专栏,硬核 ?? 推荐 ??
欢迎各位 ITer 关注点赞收藏 ??????

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

介绍

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

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

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

  1. <template>
  2. <div class="drawing-board">
  3. <div id="container" ref="container" style="width: 100%; height: 100%"></div>
  4. </div>
  5. </template>
  6. <script setup>
  7. import { ref, onMounted } from 'vue'
  8. import Board from '@/canvas/board.js'
  9. const container = ref(null)
  10. onMounted(() => {
  11. // 创建一个空白画板
  12. new Board(container.value)
  13. })
  14. </script>

初始化

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

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

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

  1. export default class BoardCanvas {
  2. constructor(container) {
  3. // 容器
  4. this.container = container
  5. // canvas画布
  6. this.canvas = this.createCanvas(container)
  7. // 绘制工具
  8. this.ctx = this.canvas.getContext('2d')
  9. // 起始点位置
  10. this.startX = 0
  11. this.stateY = 0
  12. // 画布历史栈
  13. this.pathSegmentHistory = []
  14. this.index = 0
  15. // 初始化
  16. this.init()
  17. }
  18. // 创建画布
  19. createCanvas(container) {
  20. const canvas = document.createElement('canvas')
  21. canvas.width = container.clientWidth
  22. canvas.height = container.clientHeight
  23. canvas.style.display = 'block'
  24. canvas.style.backgroundColor = 'antiquewhite'
  25. container.appendChild(canvas)
  26. return canvas
  27. }
  28. // 初始化
  29. init() {
  30. this.addPathSegment()
  31. this.setContext2DStyle()
  32. // 阻止默认右击事件
  33. this.canvas.addEventListener('contextmenu', (e) => e.preventDefault())
  34. // 自定义鼠标按下事件
  35. this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))
  36. // 自定义键盘按下事件
  37. window.document.addEventListener('keydown', this.keydownEvent.bind(this))
  38. }
  39. // 设置画笔样式
  40. setContext2DStyle() {
  41. this.ctx.strokeStyle = '#EB7347'
  42. this.ctx.lineWidth = 3
  43. this.ctx.lineCap = 'round'
  44. this.ctx.lineJoin = 'round'
  45. }
  46. }

自定义鼠标事件

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

  1. mousedownEvent(e) {
  2. const that = this
  3. const ctx = this.ctx
  4. ctx.beginPath()
  5. ctx.moveTo(e.offsetX, e.offsetY)
  6. ctx.stroke()
  7. this.canvas.onmousemove = function (e) {
  8. ctx.lineTo(e.offsetX, e.offsetY)
  9. ctx.stroke()
  10. }
  11. this.canvas.onmouseup = this.canvas.onmouseout = function () {
  12. that.addPathSegment()
  13. this.onmousemove = null
  14. this.onmouseup = null
  15. this.onmouseout = null
  16. }
  17. }

自定义键盘事件

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

  1. // 键盘事件
  2. keydownEvent(e) {
  3. if (!e.ctrlKey) return
  4. switch (e.keyCode) {
  5. case 90:
  6. this.undo()
  7. break
  8. case 89:
  9. this.redo()
  10. break
  11. }
  12. }

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

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

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

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

  1. // 添加路径片段
  2. addPathSegment() {
  3. const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
  4. // 删除当前索引后的路径片段,然后追加一个新的路径片段,更新索引
  5. this.pathSegmentHistory.splice(this.index + 1)
  6. this.pathSegmentHistory.push(data)
  7. this.index = this.pathSegmentHistory.length - 1
  8. }

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

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

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

撤销(undo)

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

  1. // 撤销
  2. undo() {
  3. if (this.index <= 0) return
  4. this.index--
  5. this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
  6. }

恢复(redo)

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

  1. // 恢复
  2. redo() {
  3. if (this.index >= this.pathSegmentHistory.length - 1) return
  4. this.index++
  5. this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
  6. }

源码

涂鸦面板demo代码:vue-canvas

原文链接:https://www.cnblogs.com/burc/p/17604268.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号