canvas : 搞个涂鸦板玩玩吧

吐槽君 分类:javascript

前言

最近接了一个项目,里面有一个涂鸦板的模块,刚刚好最近一直在面试,就顺便拿这个项目来进行知识点复习

明确功能

先看一下成品图。这里因为主题的问题下面被截断了一点,但是原来的主题实在不好看,所以就将就一下吧。
在线地址

image.png

那从上图我们大概可以知道有下面这些功能需要实现

  • 支持涂鸦
  • 支持修改画笔颜色
  • 支持修改画笔大小
  • 支持上一步
  • 支持下一步
  • 支持添加背景图片
  • 支持生成图片

初始化

上面已经明确了我们需要完成一个什么样的涂鸦板了,那现在就先来初始化一个模版吧

<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" alt="">
</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
}
},
}

一顿操作之后,页面展示应该如图:

image.png
当然现在的控制台应该很多报错,因为我们还没有将对应的函数等添加到方法中,接下来就开始完善各种功能,在开始之前,先将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添加了mousedownmouseupmousemovemouseout,它们分别对应鼠标的按下,抬起,移动,移出元素,那我们就根据四个事件来完善涂鸦的功能。

鼠标按下时

知道了绘制是从一点到另外一点的路径之后,那当我们开始绘制的时候,需要一个起点,而这个起点其实就是鼠标按下时候的坐标点,那就得先拿到鼠标的坐标点啦,先看一下代码吧。

// 在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()

  • 获取鼠标按下或者移动的时候的坐标点

在这里得先来了解一下最基本的获取坐标的知识。
看一下点击或者移动的时候,获取到的当前对象

image.png
在这个里面我们需要先了解一下几个属性值表示的意思

  • clienX/Y: 当鼠标事件发生时,鼠标相对于浏览器的X或Y轴距离
  • offsetX/Y:当鼠标事件发生时,鼠标相对于事件源X或Y轴的位置
  • screenX/Y:当鼠标事件发生时,鼠标相对于显示器屏幕X或Y轴的位置

用图示就是

image.png
还有一点就是,在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,答应我,点个?再走好吗

回复

我来回复
  • 暂无回复内容