canvas实现简易画板,画笔、橡皮、撤销、恢复、保存、水印、重置功能

3,158 阅读2分钟

本文给大家分享一下使用原生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 这个盒子是自定义的画笔头,随着画笔或者橡皮的大小而改变,大概长这样:

image.png

image.png

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)