通过canvas画板学习PointerEvent、MouseEvent和TouchEvent

2023-09-25 15:11:55 浏览数 (1)

最近想开发个草稿纸功能, 所以学习了下canvas实现简单的画板功能, 但是我们知道在PC端我们可以用MouseEvent来监听我们的鼠标点按相关操作, 移动端可以使用TouchEvent来监听我们手指触摸相关操作, 所以我们做画板的时候要想兼顾鼠标点按和手指触摸就得写两套逻辑. 但是别忘了, 还存在PointerEventer, 它可以监听鼠标, 手指触摸以及触摸笔, 支持多点触控, 它还有个特殊的参数, 即压感, 在压感屏上可以获取获取压感笔的压感值, 只要根据压感值, 我们可以控制笔画的粗细

1. PointerEvent、MouseEvent和TouchEvent相对应的事件

PointerEvent

MouseEvent

TouchEvent

poninterdown

mousedown

touchstart

pointermove

mousemove

touchmove

pointerup

mouseup

touchend

pointerleave

mouseleave

-

pointercancel

-

touchcancel

2. 做个画板

1) 共用的部分

先写出公共的画板部分代码, 后面只会写不同的事件监听部分代码

代码语言:javascript复制
  <style>
    body {
      padding: 0;
      margin: 0;
      height: 100vh;
      overflow: hidden;
    }
    .canvas-block {
      position: relative;
      width: 100%;
      height: 200px;
    }
    #canvas {
      position: absolute;
      top: 0;
      left: 0;
    }
    #canvas {
      z-index: 1;
    }
  </style>

  <div class="canvas-block">
    <canvas id="canvas" width="400" height="200"></canvas>
  </div>


  <script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    let width = canvas.width
    let height = canvas.height
    if (window.devicePixelRatio) {
      canvas.style.width = width   'px'
      canvas.style.height = height   'px'
      canvas.height = height * window.devicePixelRatio
      canvas.width = width * window.devicePixelRatio
      ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
    }
    canvas.getContext('2d').imageSmoothingEnabled = true
    let lineWidth = 3
    let lineColor = '#fff'
    let painting = false
</script>

2) 使用MouseEvent监听事件

https://codepen.io/klren0312/pen/BapMBvE

代码语言:javascript复制
// 落笔
 canvas.onmousedown = e => {
      painting = true
      const event = e || window.event
      ctx.lineCap = 'round'
      ctx.lineJoin = 'round'
      const x = event.offsetX
      const y = event.offsetY
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.lineWidth = lineWidth
      ctx.strokeStyle = lineColor
    }
    // 移动
    canvas.onmousemove = e => {
      if (!painting) {
        return
      }
      const event = e || window.event
      const x = event.offsetX
      const y = event.offsetY
      ctx.lineTo(x, y)
      ctx.stroke()
    }
    // 抬笔
    canvas.onmouseup = () => {
      if (!painting) {
        return false
      }
      painting = false
      ctx.closePath()
    }

    // 离开画板
    canvas.onmouseleave = () => {
      if (!painting) {
        return false
      }
      painting = false
      ctx.closePath()
    }

效果

image.png

3) 使用TouchEvent

https://codepen.io/klren0312/pen/YzNBKMj

代码语言:javascript复制
// 手指按下
canvas.ontouchstart = e => {
      console.log(e.touches)
      painting = true
      const event = e.touches[0]
      ctx.lineCap = 'round'
      ctx.lineJoin = 'round'
      const x = event.pageX
      const y = event.pageY
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.lineWidth = lineWidth
      ctx.strokeStyle = lineColor
    }
    // 手指移动
    canvas.ontouchmove = e => {
      if (!painting) {
        return
      }
      const event = e.touches[0]
      const x = event.pageX
      const y = event.pageY
      ctx.lineTo(x, y)
      ctx.stroke()
    }
    // 手指抬起
    canvas.ontouchend = () => {
      if (!painting) {
        return false
      }
      painting = false
      ctx.closePath()
    }
    // 手指离开区域
    ontouchcancel = () => {
      if (!painting) {
        return false
      }
      painting = false
      ctx.closePath()
    }

效果

image.png

3) 使用PointerEvent

这是这次的重点, 所以新开个项目写 源码地址: klren0312/drawboard: drawboard with pressure ( 可以使用压感笔的画板 ) (github.com) 示例地址: https://klren0312.github.io/drawboard/

注意点:

  1. 在移动端, 会出现触摸页面时, 产生页面滚动, 浏览器缩放等事件, 这时候需要给画布设置touch-action: none;样式, 来设置当触控事件发生在元素上时,不进行任何操作
  2. 因为我们需要实时根据压感来设置笔画粗细, 所以在每一次移动都作为一个路径的起始和结束, 当然这样的话我们需要记录每次移动的最终坐标, 在pointermove事件再次触发的时候, 将坐标移动到上一次结束的坐标处, 这样保证了笔画的连续性
代码语言:javascript复制
 /**
     * @description 开始
     * @param {PointerEvent} e 事件
     */
    function startDraw(e: PointerEvent) {
      painting = true
      pointerId = e.pointerId
      ctx.lineCap = 'round'
      ctx.lineJoin = 'round'
      doDraw(e, true)
    }
    /**
     * @description 移动
     * @param {PointerEvent} e 事件
     */
    function moveDraw(e: PointerEvent) {
      if (!painting && e.pointerId !== pointerId) {
        return
      } else {
        doDraw(e)
      }
    }
    function cancelDraw(e: PointerEvent) {
      console.log('cancel')
      pointerId = -1
      if (!painting) {
        return false
      }
      painting = false
      historyList.push(ctx.getImageData(0, 0, canvas.width, canvas.height))
      ctx.closePath()
    }
    /**
     * 绘画
     * @param {PointerEvent} e 事件
     * @param {Boolean} isStart 是否是起始点
     */
    function doDraw(e, isStart = false) {
      if (isPen.value && e.pointerType !== 'pen') {
        return
      }
      const event: PointerEvent = e || window.event
      const x: number = event.offsetX
      const y: number = event.offsetY
      ctx.lineWidth = getLineWidth(event)
      ctx.strokeStyle = lineColor.value
      ctx.beginPath()
      if (!isStart) {
        ctx.moveTo(startXY.x, startXY.y)
        ctx.lineTo(x, y)
      } else {
        ctx.moveTo(x, y)
      }
      ctx.closePath()
      ctx.stroke()
      startXY = {
        x: x,
        y: y
      }
    }
    /**
     * @description 计算线宽
     * @param {PointerEvent} e 事件
     * @return {number} 线宽
     */
    function getLineWidth(e: PointerEvent): number {
      switch (e.pointerType) {
        case 'touch': {
          if (e.width < 10 && e.height < 10) {
            return (e.width   e.height) * 2   10;
          } else {
            return (e.width   e.height - 40) / 2;
          }
        }
        case 'pen': return e.pressure * 8;
        default: return (e.pressure) ? e.pressure * 8 : 4;
      }
    }

可以看到上面的代码 , 我们在每次完整的笔画绘制结束, 会向historyList数组里保存一个图片数据.

代码语言:javascript复制
historyList.push(ctx.getImageData(0, 0, canvas.width, canvas.height))

这么做主要是为了实现笔画的回退操作, 我们可以在点击回退按钮时, 取出上一步保存的图片绘制到canvas中, 达到此功能

清屏的功能就相对简单, 可以使用canvas的clearRect来将画布清空

代码语言:javascript复制
ctx.clearRect(0, 0, canvas.width, canvas.height)

效果

image.png

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent https://developer.mozilla.org/zh-CN/docs/Web/API/Pointer_events https://developer.mozilla.org/zh-CN/docs/Web/CSS/touch-action https://zh.javascript.info/pointer-events https://github.com/klren0312/daliy_knowledge/issues/372

0 人点赞