本文给大家分享一下使用原生canvas 实现一个简易的画板,带有画笔、橡皮、撤销、恢复、保存、水印等功能,附带源码
预览
源码奉上
开始制作画布
首先是dom结构,背景透明的canvas
<div class="canvas-box">
<div class="cursor"></div>
<canvas id="canvas" style="background-color: rgba(0,0,0,0)"></canvas>
</div>
<div class="option">
// ...一些按钮选项
</div>
<input type="file" name="file" accept="image/jpeg,image/png,image/webp" style="display: none">
.cursor 这个盒子是自定义的画笔头,随着画笔或者橡皮的大小而改变,大概长这样:
js部分
那些获取dom的操作就省略掉了,介绍一下功能的具体实现思路:
- 画笔
使用ctx.moveTo()、ctx.lineTo()、ctx.Stroke()几个方法实现,先将画笔移动到画布某个坐标,然后使用笔画上。
画笔逻辑:鼠标点击 -> 开始绘画 -> 鼠标移动 -> 画笔连线 -> 鼠标抬起 -> 结束绘画
canvas.onmousedown = e => {
// 只允许左键
if (e.button) return
if (!isPencil && !isEraser) return false
isDrawingLine = true
ctx.beginPath()
ctx.globalCompositeOperation = isEraser ? 'destination-out' : 'source-over'
ctx.strokeStyle = isEraser ? '#fff' : colors
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.lineWidth = isEraser ? eraser : pencil
ctx.moveTo(e.offsetX, e.offsetY)
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
// 鼠标移动时不断触发 lineTo stroke就行了
canvas.onmousemove = (e) => {
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
}
}
// 鼠标抬起
canvas.onmouseup = () => {
if (!isPencil && !isEraser) return false
isDrawingLine = false
canvas.onmousemove= null
// 使用了橡皮时,必须存在历史记录,否则不做任何事
if (isEraser && !history.length) return false
addOneHistory(lineToAlpha())
}
- 橡皮
橡皮其实就是特殊的画笔,它特殊在这个属性上:ctx.globalCompositeOperation,画笔是source-over,后续覆盖的意思,把它设置为destination-out模式,就是橡皮了,差不多是重叠覆盖区域清空的意思。
- 撤销、恢复
肯定是用一个索引和数组保存当前画布状态了,数组保存的是画布的当前所有像素信息,通过ctx.getImageData()方法获取,拿到画布像素后保存下来,通过索引取出,随后使用ctx.putImageData()恢复画布即可完成撤销和恢复的操作。
// 撤销
undo.onclick = () => {
historyIdx--
if (historyIdx <= -1) {
historyIdx = -1
ctx.clearRect(...canvasArea)
} else {
ctx.putImageData(history[historyIdx], 0, 0)
}
}
// 恢复
redo.onclick = () => {
historyIdx++
if (historyIdx > history.length - 1) historyIdx = history.length - 1
else ctx.putImageData(history[historyIdx], 0, 0)
}
- 水印
网页全屏水印的实现其实也是差不多,配合MutationObserver api可以做到网页全屏水印常驻不被销毁,这里不介绍了,主要使用到的方法就是ctx.fillText(),也是炒鸡简单的api。
当然,水印可以有旋转角度,设置ctx.rotate。
透明度、文字大小等等,设置ctx.font , ctx.fillStyle
// 计算要绘制的水印坐标,受偏移量 间距等影响
waterMark.onclick = () => {
let [x, y] = gap
let [,, w, h] = canvasArea
const xLine = Math.ceil((w - offsetX) / x)
const yLine = Math.ceil((h - offsetY) / y)
waterMarkOpacityRange.oninput()
ctx.font = `500 ${fontSizeRange.valueAsNumber}px sans-serif`
for (let i = 0; i <= xLine; i++) {
const x0 = x * i + offsetX
for (let j = 0; j <= yLine; j++) {
drawWaterMark(x0, y * j + offsetY)
}
}
}
// 根据坐标绘制水印
// 每次绘制一个水印时要ctx.save() ctx.restore(),不然画布坐标系什么的会乱套
const drawWaterMark = (x, y) => {
ctx.save()
ctx.translate(x, y)
ctx.rotate(rotate / 180 * Math.PI)
ctx.fillText(text, -ctx.measureText(text).width / 2, 0, maxWidth)
ctx.restore()
}
- 保存
canvas导出图片base64格式的api canvas.toDataUrl(),这个是很常用的方法。可以用来判断网页是否支持webp格式、图片格式转换、压缩等。
得到base64后,就是点击下载了
// js 点击下载文件
save.onclick = () => {
const a = document.createElement('a')
a.href = canvas.toDataURL(`image/${imgType}`, quality)
a.download = `save_${Date.now()}`
document.body.append(a)
a.click()
a.remove()
}
最后注意一下,当鼠标移出画布外甚至浏览器外抬起时,要结束绘画,可以通过监听全局鼠标抬起事件解决:
// 鼠标抬起时,发现不处于画布内,调用画笔结束
// isDrawingLine 是否正在划线,即是否使用 画笔 或者橡皮正处于鼠标拖动而没有抬起的状态
const handleLeaveCanvas = (e) => {
if (!isDrawingLine) return
if (!container.contains(e.target)) canvas.onmouseup()
}
window.addEventListener('mouseup', handleLeaveCanvas)