一、项目需求
最近接了一个需求,用户手动在画板上进行绘制图案,绘制完成后,将绘制好的作品上传到服务器,涉及到的功能点(这篇文章有点长):
- 项目的运行环境是在手机端,canvas 的尺寸单位是px,需要做好不同设备的适配。
- 选择画笔颜色、大小后,笔触要跟随手指的移动画出图案。
- 画板的橡皮擦功能。
- 画板的一键清除功能。
- 将用户画好的作品上传到服务器。
二、初始化 canvas
canvas的默认宽度是300,高度是150,单位是px。为了避免画布出现扭曲,不要使用css属性来设置canvas的宽高。
我们可以通过设备宽度来计算不同设备下画布的尺寸。一般情况下,UI给的设计稿的宽度都是750px,根据 (设计稿上canvas的宽度/750)=(用户手机上canvas的宽度/用户手机的宽度)这个公式,就可以计算出不同设备上canvas的尺寸了。
小tip:如果想让canvas的图像更清晰,可以先将它的宽高扩大2倍,再使用scale等比缩放。
通过drawImage() 来设置canvas的背景图。语法:drawImage(img,x,y,width,height)
。x,y 表示图像的起点坐标;width,height 表示图片的尺寸。要注意绘制底图的时候一定在确保图片已经加载完成。
结构代码:
<canvas></canvas>
<!--底图-->
<img src="../assets/draw-paopao.png" style="position:fixed;left:9999"/>
js代码:
this.canvas=document.querySelector("canvas");
const clientHeight = document.body.clientHeight
const w = (380 / 750) * clientHeight
const h = (380 / 750) * clientHeight
//先放大 再缩小 图像会更清晰
this.canvas.width = w * 2
this.canvas.height = h * 2
this.canvas.scale = 0.5
if (!this.canvas.getContext) return
this.ctx = this.canvas.getContext('2d')
//设置底板颜色
this.ctx.fillStyle = '#fff'
this.ctx.fillRect(0, 0, w, h)
//设置底图
const floorImg = document.querySelector("img");
ctx.drawImage(floorImg, (57/750)*clientHeight*2, (60/750)*clientHeight*2,(269/750)*clientHeight*2,(271/750)*clientHeight*2)
三、画图功能
关于canvas的基本使用就不做过多的介绍了,想了解更多内容的可以点击这里查看,这个需求里用到的方法主要有下面几个:
- beginPath(),新建一条路径,开始绘制;
- moveTo(x, y),把画笔移动到指定的坐标(x, y),不创建线条;
- lineTo(x, y),添加一个新点,然后在画布中创建从该点到最后指定点的线条;
- stroke(),绘制已定义的路径;
- closePath(),闭合路径,创建从当前点回到起始点的路径;
- lineWidth,设置或返回当前的线条宽度;
- strokeStyle,设置笔触的颜色、渐变或模式;
- lineJoin,设置或返回两条线相交时,所创建的拐角类型;
- lineCap,设置或返回线条的结束端点样式;
扩展:strokeStyle 属性设置或返回用于笔触的颜色、渐变或模式。语法:context.strokeStyle=color|gradient|pattern。在后面的橡皮檫功能里会用到这个属性。
值 | 描述 |
---|---|
color | 指示绘图笔触颜色的 CSS 颜色值。默认值是 #000000。 |
gradient | 用于填充绘图的渐变对象(线性或放射性) |
pattern | 用于创建 pattern 笔触的 pattern 对象 |
示例图:
画图功能实现思路:
- 在 canvas 里所有有关对坐标的操作,都是相对于左上角为原点的。当触发 touchStart 事件时,
beginPath()
新建一条路径,通过获取手指触摸屏幕的坐标点(clientX/clientY),减去canvas距离屏幕左上顶点的距离(left/top),使用moveTo(clientX-left,clientY-top)
,设置路径的起点坐标。 - 当手指移动时,使用
lineTo(clientX-left,clientY-top)
,将手指移动的坐标添加到路径里。然后使用stroke()
绘制出手指移动的轨迹。 - 当触发 touchend 事件时,使用
closePath()
方法闭合路径,这样就可以设置下一次笔触的大小、颜色。同时还要记得移除对touch事件的监听。
这里要注意,因为canvas的尺寸放大了一倍,所以在获取手指坐标的时候也要乘以2。
画图功能代码如下:
this.canvas.addEventListener('touchstart', e => {this.mouseHandler(e)})
mouseHandler(e) {
// 因为canvas 放大了一倍,笔触的坐标要乘以2
const x = (e.touches[0]?.clientX - this.canvasLeft) * 2
const y = (e.touches[0]?.clientY - this.canvasTop) * 2
switch (e.type) {
case 'touchstart':
this.ctx.moveTo(x, y)
this.ctx.beginPath()
this.ctx.lineWidth = 12
this.ctx.strokeStyle = '#c895ff'
this.canvas.addEventListener('touchmove', e => {this.mouseHandler(e)})
this.canvas.addEventListener('touchend', e => {this.mouseHandler(e)})
break
case 'touchmove':
this.ctx.lineTo(x, y)
this.ctx.stroke()
break
case 'touchend':
this.ctx.closePath()
this.updateImg()
this.canvas.removeEventListener('touchmove', e => {this.mouseHandler(e)})
this.canvas.removeEventListener('touchend', e => {this.mouseHandler(e)})
break
}
}
四、橡皮擦功能
如果画布的背景是纯色,可以将笔触的颜色设置为背景色,来达到橡皮擦的效果。不过这个需求里,canvas有一个背景图,就不能使用这种方法了。通过网上查资料,有人说可以用两个canvas,一个用来放背景,一个供用户操作,最后再把两个canvas合并到一起(这个方案我没有尝试过,感兴趣的小伙伴可以自己研究下)。这里我们通过设置 strokeStyle 的值来实现橡皮擦的功能。
上面我们提到 strokeStyle 属性可以设置笔触模式。那么我们可以将笔触的模式设置成跟画布的背景一样, 就能达到橡皮擦的功能了。
这里用到的方法是:createPattern()
,该方法可以在指定的方向内重复指定的元素。元素可以是图片、视频,或者其他 canvas 元素。被重复的元素可用于绘制/填充矩形、圆形或线条等等。这里我们只需要将橡皮擦的模式设置成跟画布的初始状态保持一致就可以了。
语法:context.createPattern(image,"repeat|repeat-x|repeat-y|no-repeat");
在页面上放一个临时的canvas 用来设置橡皮擦,跟用户看到的画布完全一样。我们可以把公共的内容提取出来,就是下面这样:
html代码:
<!--用户看到的画板-->
<canvas ref="board"></canvas>
<!--临时的canvas 用来设置橡皮擦-->
<canvas ref="eraser" style="position:fixed;left:9999"></canvas>
<!--canvas的底图-->
<img ref="floorImg" style="position:fixed;left:9999" src="../assets/draw-paopao.png" />
js 代码:
// 创建 canvas
async createCanvas() {
const { canvas, ctx } = await this.canvasInit('board')
this.canvas = canvas
this.ctx = ctx
this.canvasLeft = this.canvas.getBoundingClientRect().x
this.canvasTop = this.canvas.getBoundingClientRect().y
this.canvas.addEventListener('touchstart', e => {
this.mouseHandler(e)
})
// 创建一个临时的canvas 设置橡皮檫
const pattCanvas = await this.canvasInit('eraser')
this.patt = pattCanvas.ctx.createPattern(pattCanvas.canvas, 'no-repeat')
//当触发touchstart事件时,如果选的是橡皮擦,设置strokeStyle的值为this.patt
//this.ctx.strokeStyle = isEraser ? this.patt : '#000'
},
// 设置底图和背景颜色
canvasInit(ref) {
return new Promise(resolve => {
// 定时器 是为了等底图加载完成
setTimeout(() => {
const canvas = this.$refs[ref]
// 适配不同的机型 计算canvas的尺寸 单位是px,
// 为了让图像更清晰,宽高设置为2倍,再等比缩放
const w = this.toPx(380)
const h = this.toPx(380)
canvas.width = w
canvas.height = h
canvas.scale = 0.5
if (!canvas.getContext) return
const ctx = canvas.getContext('2d')
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, w, h)
// 绘制底图
const floorImg = this.$refs.floorImg
ctx.drawImage(floorImg, this.toPx(57), this.toPx(60),this.toPx(269),this.toPx(271))
resolve({ canvas, ctx })
}, 300)
})
},
// 计算不同手机下的尺寸 因为canvas整体放大了一倍,所以计算坐标时也需要一起放大
function toPx(num){
const clientHeight = document.body.clientHeight
return (num / 750) * clientHeight * 2
},
五、一键清除功能
一键清除,可以理解成重新对canvas进行初始化,所以只需要重新执行上面的 createCanvas() 就可以了。
六、上传画布到服务器
上传图片到服务器时,需要设置请求头的 Content-Type = multipart/form-data
。将canvas转化成file文件,就可以做上传的操作了。
使用toDataURL()
可以将canvas转成base64,再将base64转成file文件,然后就可以像提交form表单一样,将数据上传到服务器了。
/**
* canvas转化为base64
* @param {canvas} canvas 对象
* @param {type} 图片类型,值为'image/png'、'image/jpeg'
*/
export const canvasToBase64 = (canvas,type) => {
return canvas.toDataURL(type)
}
/**
* canvas转化为图片
* @param {canvas} canvas 对象
* @param {type} 图片类型,值为'image/png'、'image/jpeg'
*/
export const canvasToImg = (canvas,type) => {
const image = new Image()
image.src = canvas.toDataURL(type)
return image
}
/**
* base64转化为file
* @param {urlData} base64内容
* @param {fileName} 文件名称
*/
export const base64ToFile = (urlData, fileName) => {
const arr = urlData.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bytes = atob(arr[1])
let n = bytes.length
const ia = new Uint8Array(n)
while (n--) {
ia[n] = bytes.charCodeAt(n)
}
return new File([ia], fileName, { type: mime })
}
七、总结
- 动态计算在不同设备下canvas的尺寸,不要使用css属性来设置canvas的宽高;
- 想让canvas的图像更清晰,可以先将它的宽高扩大,再使用scale等比缩放;
- 通过drawImage() 来绘制图片时,一定要确保图片已经加载完成;
- 绘制一条路径结束后,要使用 closePath() 闭合路径,才能设置下一次笔触的大小和颜色;
- 通过createPattern() 可以设置笔触的图案,来实现带背景的橡皮擦效果;