• vue + canvas 实现涂鸦面板


    前言

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

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

    介绍

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

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

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

    
    
    

    初始化

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

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

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

    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 鼠标按下事件,需要在此处实现鼠标按下拖拽可以绘制画笔轨迹的逻辑

    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 键盘按下事件,需要在此处实现撤销恢复的逻辑

    // 键盘事件
    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

    /*
     * @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);

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

    // 添加路径片段
    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 操作时,我们先将索引前移, 然后取出当前索引指向的快照数据,重新绘制画布

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

    恢复(redo)

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

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

    源码

    涂鸦面板demo代码:vue-canvas


    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17604268.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    《最新出炉》系列初窥篇-Python+Playwright自动化测试-12-playwright操作iframe-中篇
    手把手教你从 0 到 1 搭建一套 RocketMQ 集群
    Tomcat 集群介绍
    创建运行时类的对象
    【Java】继承练习
    [笔记]Springboot入门《五》之单元测试读取配置
    <Java>JDK内置的常用接口和深浅拷贝
    Java:实现反转一个单链表算法(附完整源码)
    开发者必读:2022年移动应用趋势洞察白皮书
    Oracle数据库从入门到精通系列之十二:段
  • 原文地址:https://www.cnblogs.com/burc/p/17604268.html