前言

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

欢迎各位 ITer 关注点赞收藏

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

介绍

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

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

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

<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 初始化方法

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

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

vue + canvas 实现涂鸦面板的更多相关文章

  1. vue+canvas踩坑之旅

    let img=new Image(); if(img.complete) { console.log('dd'); } img.src="http://localhost:8888/sta ...

  2. Vue Admin - 基于 Vue & Bulma 后台管理面板

    Vue Admin 是一个基于 Vue 2.0 & Bulma 0.3 的后台管理面板(管理系统),相当于是 Vue 版本的 Bootstrap 管理系统,提供了一组通用的后台界面 UI 和组 ...

  3. 微信小程序-基于canvas画画涂鸦

    代码地址如下:http://www.demodashi.com/demo/14461.html 一.前期准备工作 软件环境:微信开发者工具 官方下载地址:https://mp.weixin.qq.co ...

  4. vue+canvas实现炫酷时钟效果的倒计时插件(已发布到npm的vue2插件,开箱即用)

    前言: 此事例是在vue组件中,使用canvas实现倒计时动画的效果.其实,实现效果的逻辑跟vue没有关系,只要读懂canvas如何实现效果的这部分逻辑就可以了 canvas动画的原理:利用定时器,给 ...

  5. canvas实现涂鸦板

    实现思路:监听鼠标按下.移动.松开事件,将鼠标按下的值赋值给moveTo的x和y值,作为起始位置.在移动事件中,将鼠标距离可视区x和y值赋给lineTo,再将路径闭合.以下是具体的代码 <!DO ...

  6. HTML5 canvas 在线涂鸦

    插件地址 http://bencentra.github.io/jq-signature/ 采用技术 jq-signature.min.js Developed using jQuery 2.1.4. ...

  7. vue + canvas 图片加水印

    思路:将两张图片绘制为一张 目标:输入的文字,绘制到图片上,简单实现图片水印 效果:输入的文字1: ‘你猜猜’ + 图片2 = 图片3(不要看清除水印的按钮,本人垃圾 没实现) 选择图片 html & ...

  8. 转:vue+canvas如何实现b站萌系登录界面

    https://juejin.im/post/5ae802a46fb9a07ac55fec04

  9. vue 二级列表折叠面板

    请求出来的数据是二级列表,需要点击一级列表展开相应的二级列表 data(){ return { info: [ {name:'一级菜单1',lists:[{price:1},{price:2}]}, ...

  10. vue组件:canvas实现图片涂鸦功能

    方案背景 需求 需要对图片进行标注,导出图片. 需要标注N多图片最后同时保存. 需要根据多边形区域数据(区域.颜色.名称)标注. 对应方案 用canvas实现涂鸦.圆形.矩形的绘制,最终生成图片bas ...

随机推荐

  1. Azure DevOps(三)Azure Pipeline 自动化将程序包上传到 Azure Bolb Storage

    一,引言 结合前几篇文章,我们了解到 Azure Pipeline 完美的解决了持续集成,自动编译.同时也兼顾了 Sonarqube 作为代码扫描工具.接下来另外一个问题出现了,Azure DevOp ...

  2. 各种远程工具通过ssh连接服务器

    开头 最近遇到一个新的连接方式,不能使用日常的本地通过账号连接,要通过私钥和公钥的连接方式,然后连接到服务器之后才能连接到数据库.因为之前没试过这种连接方式,所以很多工具有不同的连接方式.所以现在就记 ...

  3. 2022-08-01:以下go语言代码输出什么?A:panic;B:5;C:6;D:编译错误。 package main import ( “fmt“ ) func main() {

    2022-08-01:以下go语言代码输出什么?A:panic:B:5:C:6:D:编译错误. package main import ( "fmt" ) func main() ...

  4. 基于.NetCore开发博客项目 StarBlog - (28) 开发友情链接相关接口

    前言 之前介绍的友情链接功能,只实现了友情链接的展示和管理接口. 还缺失友情链接申请.审核管理.通知,现在把这块功能补全. Model 什么的之前那篇文章都有,本文直接补全逻辑代码~ 详见: 基于.N ...

  5. 资源高效搜索方法,你 Get 到了吗?

      随手转发给好友和朋友圈  编辑:办公小通 百度搜索谁都会,但是搜出来的资料往往良莠不齐.搜索速度是快了,但是还要花大量的时间去筛选. 通过下面的两个最常用的栗子,小通强烈推荐大家用高级搜索. 高级 ...

  6. JavaWeb编程面试题——导航

    引言 面试题==知识点,这里所记录的面试题并不针对于面试者,而是将这些面试题作为技能知识点来看待.不以刷题进大厂为目的,而是以学习为目的.这里的知识点会持续更新,目录也会随时进行调整. 关注公众号:编 ...

  7. 逍遥自在学C语言 | break-循环的中断与跳转

    前言 在C语言中,break语句是一种控制流语句,它用于终止当前所在的循环结构(for.while.do-while)或者switch语句,从而跳出循环或者结束switch语句的执行. 一.人物简介 ...

  8. uniapp主题切换功能的方式终结篇(全平台兼容)

    前面我已经给大家介绍了两种主题切换的方式,每种方式各有自己的优势与缺点,例如"scss变量+vuex"方式兼容好但不好维护与扩展,"scss变量+require" ...

  9. Anaconda3安装(Win_x64)

    一.获取Anaconda3 链接:https://pan.baidu.com/s/14Imqk1KBsB84Mwzebpv2BA?pwd=no2x 提取码:no2x --来自百度网盘超级会员V4的分享 ...

  10. .NET 8 Preview 5发布,了解一下Webcil 是啥

    2023年6月13日 .NET 8 Preview 5,.NET 团队在官方博客发布了系列文章: Announcing .NET 8 Preview 5 ASP.NET Core updates in ...