前言
最近接了一个项目,里面有一个涂鸦板的模块,刚刚好最近一直在面试,就顺便拿这个项目来进行知识点复习
明确功能
先看一下成品图。这里因为主题的问题下面被截断了一点,但是原来的主题实在不好看,所以就将就一下吧。
那从上图我们大概可以知道有下面这些功能需要实现
- 支持涂鸦
- 支持修改画笔颜色
- 支持修改画笔大小
- 支持上一步
- 支持下一步
- 支持添加背景图片
- 支持生成图片
初始化
上面已经明确了我们需要完成一个什么样的涂鸦板了,那现在就先来初始化一个模版吧
<template>
<div class="container">
<!-- 画板-->
<div class="canvas-container">
<h3>画板</h3>
<canvas
:width="760"
:height="610"
ref="myPalette"
class="palette"
@mousedown="handleDownCanvas"
@mouseup="handleOverMove"
@mousemove="handleMove"
@mouseout="handleOverMove"
/>
<img style="margin-left: 30px" :src="image" >
</div>
<div>
鼠标坐标x: {{movex}}y:{{movey}}
</div>
<div class="container-item">
<button class="button-item" @click="handlePre">上一步</button>
<button class="button-item" @click="handleNext">下一步</button>
<button class="button-item" @click="handleSetImg">选择图片</button>
<button class="button-item" @click="createImage">生成图片</button>
</div>
<div class="container-item">
<h4>画笔颜色</h4>
<span
class="color-item"
v-for="(item,index) in colors"
:style="{'background':item}"
@click="handleSetColor(item)"
:key="index"
/>
</div>
<div class="container-item">
<h4>画笔大小</h4>
<div class="size-item" v-for="(item,index) in size" :key="index" @click="handleSetSize(item.size)">{{item.name}}</div>
</div>
</div>
</template>
import mixin from "./mixin"
export default {
name: "palette",
mixins:[mixin],
data(){
return{
// 画笔颜色
colors:[
'#f1d506','#0924de','#08e31e','#f32f15','#cccccc','#5ab639'
],
size:[
{name:"小",size:1},
{name:"中",size:2},
{name:"大",size:3}
],
// canvas对象
context: {},
// 保存绘画的路径
lines:[],
// 是否开始绘制
canvasMoveUse: false,
// 画笔的设置
config:{
lineWidth:1, // 线条的宽度
shadowBlur:1, // 阴影模糊的程度
shadowColor:"#f1d506", // 阴影的颜色
strokeStyle:"#f10649" // 笔触的颜色
},
preHandle:[], // 上一步
nextHandle:[], // 下一步
movex:0,
movey:0,
image:null
}
},
}
一顿操作之后,页面展示应该如图:
当然现在的控制台应该很多报错,因为我们还没有将对应的函数等添加到方法中,接下来就开始完善各种功能,在开始之前,先将canvas
添加到data中,方法我们之后进行调用
export default {
...
mounted() {
this.init()
},
methods:{
init(){
const canvas = this.$refs.myPalette
this.context = canvas.getContext("2d")
}
}
}
开发功能模块
涂鸦功能实现
因为接下来的功能都是得在能涂鸦的情况下实现,所以最开始就得先实现这个最基础的功能啦。
在开始之前,首先得明确一下,canvas
是如何做这个绘画的功能的呢?我们知道,当我们开始画图的时候,通常是从某一点到另外一点的线条,那也就表明了,其实我们做的涂鸦功能,也是从某一点(x,y)到另外一点(x,u)路径的绘制,知道了这个之后,就可以开始我们的操作了。
看一下初始化的代码,我们已经给canvas
添加了mousedown
,mouseup
,mousemove
,mouseout
,它们分别对应鼠标的按下
,抬起
,移动
,移出元素
,那我们就根据四个事件来完善涂鸦的功能。
鼠标按下时
知道了绘制是从一点到另外一点的路径之后,那当我们开始绘制的时候,需要一个起点,而这个起点其实就是鼠标按下时候的坐标点,那就得先拿到鼠标的坐标点啦,先看一下代码吧。
// 在canvas中按下鼠标
handleDownCanvas(e){
// 是否可以开始移动绘制
this.canvasMoveUse = true
// 获取当前鼠标按下的位置
const {canvasX,canvasY} = this.getEventXY(e)
// 重置画笔配置
this.handleSetConfig()
// 清除子路径
this.context.beginPath()
// 记录起点
this.context.moveTo(canvasX, canvasY)
// 参数的值 x y width height
const pre = this.context.getImageData(0, 0, 700, 600)
// 记录当前操作,便于后续的撤销操作
this.preHandle.push(pre)
// 重新绘画之后清除所有下一步
this.nextHandle = []
},
然后逐步来说明一下每个模块代码的作用
canvasMoveUse
- 这个变量主要的作用就是用来决定是否要开始绘制路径
getEventXY()
- 获取鼠标按下或者移动的时候的坐标点
在这里得先来了解一下最基本的获取坐标的知识。 看一下点击或者移动的时候,获取到的当前对象
在这个里面我们需要先了解一下几个属性值表示的意思
- clienX/Y: 当鼠标事件发生时,鼠标相对于
浏览器
的X或Y轴距离 - offsetX/Y:当鼠标事件发生时,鼠标相对于
事件源
X或Y轴的位置 - screenX/Y:当鼠标事件发生时,鼠标相对于
显示器屏幕
X或Y轴的位置
用图示就是
还有一点就是,在PC端获取坐标点跟在手机端获取的方式有些差异,但是目前这个涂鸦板只考虑PC,所以手机端就暂时不说,有兴趣可以百度一下
了解完这些之后再来看一下获取鼠标坐标点的函数,就会清晰很多了
getEventXY(e){
// 默认获取pc
let canvasX = e.offsetX
let canvasY = e.offsetY
this.movex = canvasX
this.movey = canvasY
// 使用手机的时候
if(!this.isPC()){
canvasX = e.changedTouches[0].offsetX
canvasY = e.changedTouches[0].offsetY
}
return {canvasX,canvasY}
},
完成之后,在点击移动之后,下面的鼠标坐标也会出现相应的坐标点。
handleSetConfig
- 设置画笔,设置为
config
中的参数,而config
的颜色默认的设置为颜色阴影数组中的第一个
beginPath
- 清除绘画的路径,如果不添加这个参数,每次按下进行绘制的时候,都会被认为是在同一条路径上进行绘制,那这样的话就会导致路线全部连在一起,所有的颜色都会变成你最后选择的颜色
moveTo
- 设置绘制开始的起点
getImageData
- 生成当前的canvas的图像,记录下来,方便后面进行上一步的操作
鼠标抬起,移出
这两个就没什么特别好说的了,主要就是因为抬起移出的时候,如果不清除掉移动,那就会导致还可以继续进行绘制
// 结束绘画
handleOverMove(){
this.canvasMoveUse = false
},
鼠标移动时
// 移动
handleMove(e){
if (!this.canvasMoveUse) return
// 获取坐标点
const {canvasX,canvasY} = this.getEventXY(e)
// 链接每个点
this.context.lineTo( canvasX ,canvasY)
//绘制已定义的路径
this.context.stroke()
}
这个最主要就是连接点跟点,绘制成线,其他的都是canvas的内容,具体的api调用直接上文档吧
canvas
到这里最基础的涂鸦功能就完成,现在尝试一下绘制,不出意外就没问题啦
支持修改画笔颜色,大小
之前已经有config
这个配置参数了跟handleSetConfig
这个设置画笔的配置函数了,那修改大小跟颜色其实就是修改config
的参数,然后调用一下handleSetConfig
就行了。
// 设置画笔的颜色
handleSetColor(color){
this.config.shadowColor = color // 阴影
this.config.strokeStyle = color // 画笔颜色
this.handleSetConfig()
},
// 设置画笔大小
handleSetSize(size){
this.config.lineWidth = size
this.handleSetConfig()
},
支持上一步,下一步
上一步的功能,其实就是把当前画布上的内容重置为上一次画布上的内容,在完善涂鸦功能的时候已经把当前的画布内容保存下来了。
handleDownCanvas(e){
...
// 参数的值 x y width height
const pre = this.context.getImageData(0, 0, 700, 600)
// 记录当前操作,便于后续的撤销操作
this.preHandle.push(pre)
}
然后完善一下上一步的操作,在这里的时候,因为我们把他压进数组的时候,是先进后出
的概念,所以需要从数组的最底部拿到上一次更新的内容,然后将当前的画布的内容,作为下一步的数据存进nextHandle
数组中,然后更新到画布上就可以了。
// 上一步
handlePre(){
if(!this.preHandle.length) return false
const pre = this.preHandle.pop()
// 这里应该是把当前的canvas保存进下一步
const next = this.context.getImageData(0, 0, 760, 610)
this.nextHandle.push(next)
this.context.putImageData(pre,0, 0)
},
下一步的功能跟上一步是一样的,不同的时候这里需要将当前的画布内容存进上一步
// 下一步
handleNext(){
if(!this.nextHandle.length) return false
const next = this.nextHandle.pop()
const pre = this.context.getImageData(0, 0, 760, 610)
this.preHandle.push(pre)
this.context.putImageData(next,0, 0)
}
这样上一步下一步的功能也就完成了
支持添加背景图片,生成图片
添加背景图片这里有一个麻烦的点,就是添加到画布之后,之前绘画的内容就被覆盖掉了,所以我这里处理的方法是将每次绘制的路径参数都保存了下来,等图片添加完成之后,将之前绘制过的复原回去,这是目前我能想到的方案。
所以得在之前的handleDownCanvas
,handleMove
,handleOverMove
函数中添加一下操作
handleDownCanvas(e){
...
// 按下就保存路径位置
this.lines.push({
x:canvasX,
y:canvasY,
strokeStyle:this.context.strokeStyle,
shadowColor:this.context.shadowColor
})
},
handleMove(e){
// 保存路径位置
this.lines.push({
x:canvasX,
y:canvasY,
strokeStyle:this.context.strokeStyle,
shadowColor:this.context.shadowColor
})
},
handleOverMove(){
// 往记录中添加断点
this.lines.push(null)
}
然后先看一下整体的代码吧,
// 选择图片设置
handleSetImg(){
let input = document.createElement("input")
input.type = 'file'
input.accept = 'image/*'
input.onchange = this.putImageToCanvas
input.click()
},
// 更新到canvas
putImageToCanvas(event){
const e = event.target;
const { files } = e; // 拿到所有的文件
const file = files[0]
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
// console.log('file 转 base64结果:' + reader.result)
let imag = new Image();
imag.src = reader.result
imag.onload = () =>{
const {clientWidth,clientHeight} = this.$refs.myPalette
// 绘制之前还是需要将当前页面添加到上一步
this.preHandle.push(this.context.getImageData(0, 0, 760, 610))
this.context.drawImage(imag,0,0,clientWidth,clientHeight)
// 这里没办法解决画图被覆盖的问题,只能绘制完图片之后将线条绘制回去
this.resetLine()
}
}
},
// 重新绘制之前绘画
resetLine(){
this.context.beginPath();
// 这里是将绘制的记录返回回来,但是这里返回之后,就没法再进行上下了
this.lines.forEach((item,index) => {
// item === null 代表着抬起手指,断开绘制
if (item){
const next_item = this.lines[index+1] || item
this.context.moveTo(item.x,item.y);
this.context.lineTo(next_item.x,next_item.y);
this.context.strokeStyle=item.strokeStyle;
this.context.shadowColor=item.shadowColor
this.context.stroke();
}else{
// 清除子路径
console.log('清除子路径')
this.context.beginPath();
}
})
},
handleSetImg
选择图片,老生常谈的操作了,就没啥好说了
putImageToCanvas
这里是将file
类型转换为base64,因为不这样做的话,图片加载不出来,然后再进行原来路径的绘制,在这里同样需要把当前的画布内容添加到上一步
resetLine
重新绘制之前绘画,这里需要注意的点就是因为在绘制的时候会有断开的行为,所以在判断到当前的item === null
的时候,直接调用beginPath()
清除子路径操作,然后继续下一步的绘制就行了。
最后就是生成图片
了,这个也没啥好说的,直接上代码吧
// 生成图片
createImage(){
this.image = this.$refs.myPalette.toDataURL("image/png",1)
console.log("生成图片")
},
结束
现在上面说明的功能都已经完成了,具体的代码在github,答应我,点个?再走好吗
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!