移动端下业务需要全屏手写签名,需求:兼容横竖屏,无论横竖屏下,签字图都需要是横向显示: 插件SignaturePad

一、思路分析:

1 . 移动H5虽可以判断横竖屏,但考虑到多设备访问问题(微信下,有的手机需要用户开启横屏模式才可以旋转,一般人不知道怎么设置),所以强制横屏显示只能放弃。

2 . 无法强制横屏,那么我们是否可以通过CSS将页面旋转达到视觉横屏效果呢? 答案当然是可以,通过 transform: rotate(90deg) 即可实现。

3 . 通过transform可以将页面旋转,但是会面临新的问题:canvas画布旋转后会导致落笔精度丢失,如果你是手写canvas可以通过计算X,Y调整落笔,由于笔者采用插件,所以只能另寻思路。

4 . 由于canvas画布旋转精度丢失特殊性,所以我们为它包裹一层div,这样旋转父容器,canvas画布不变,保持原有布局即可。


二、实现方式:

方式两种:区别在于用户感知

1.  通过window.resize或window.orientation判断横屏竖屏状态,设置对应宽高 - 无感知
2.  监听window.resize, 重新加载reload - 有感知,页面会刷新

查看源码 - JUMP TO GITHUB DEMO



三、踩坑记录:

1 . 移动端屏幕旋转,canvas触控点失灵,签名位置错乱

解决方案不唯一,

首先canvas的父容器(class='recruit-canvas')固定定位在body下,默认宽高继承body,100%;

其次,定位canvas采用定位,脱离文档流,动态设定宽高即可(宽高设置无效看踩坑三)

2 . 安卓微信下,网页无法旋转,IOS下网页旋转正常

ios手机权限高于微信,所以微信**旋转方向**默认开启跟随系统

android手机下,微信默认关闭旋转跟随系统,打开设置步骤:我的-设置-通用-开启横屏模式(跟随系统旋转)

3 . canvas赋值宽高无效

canvas.style.width/height 与 canvas.width/height 设置不同,

单独设置canvas.width/height前提需要在html标签中初始化默认width,height属性;
单独设置canvas.style.width/height,需要在后面添加'px'单位,否则失效;

备注:实际测试中Google chrome,发现不设置属性依然可以设置canvas.width/height,可能与兼容性有关

4 . canvas旋转后精度丢失问题

canvas利用css transform:rotate()旋转会导致签名画图落笔精度丢失

解决方法包裹一层父容器,旋转父容器,canvas动态赋值width,height


四、附上源码:

<template>
  <div id="recruitBox" class='recruit-canvas'>
    <div class="canvas-box" ref="canvasRef">
      <canvas ref="canvasMapRef" id="canvas-map" width="100" height="100"></canvas>
    </div>

    <div class="btn-box flex-row">
      <span class="del-btn" @click="clearCanvasHandle">清除</span>
      <span class="sure-btn" @click="makeCanvasHandle">确认</span>
    </div>
  </div>
</template>

<script>
import SignaturePad from 'signature_pad'

export default {
  name: 'MineRecruitCanvas',
  data () {
    return {
      canvasNode: null
    }
  },
  mounted () {
    this.initalHandle()
    window.addEventListener('resize', this.initalHandle, false)
  },
  methods: {
    initalHandle () {
      const _canvasBox = this.$refs.canvasRef
      const _canvas = this.$refs.canvasMapRef
      if (!_canvasBox || !_canvas) {
        return false
      }

      _canvas.width = _canvasBox.clientWidth
      _canvas.height = _canvasBox.clientHeight

      this.clearCanvasHandle()
      this.canvasNode = new SignaturePad(_canvas, {
        minWidth: 2,
        maxWidth: 2,
        penColor: 'rgb(0, 0, 0)'
      })
    },

    clearCanvasHandle () {
      if (this.canvasNode) {
        this.canvasNode.clear()
        this.canvasImage = null
      }
    },

    makeCanvasHandle () {
      const canvasNode = this.canvasNode
      // 重新初始化画布
      if (!canvasNode) {
        this.initalHandle()
      }

      // 是否签字
      if (canvasNode.isEmpty()) {
        this.$toast('您还没有签名')
        return false
      }

      // 图像旋转二次处理
      let _defaultURL = null
      const _boxWidth = window.innerWidth
      const _boxHeight = window.innerHeight
      const _signImg = canvasNode.toDataURL('image/png', 0.6)
      if (_boxWidth < _boxHeight) {
        this.rotateBase64Img(_signImg, -90, (imgUrlRes) => {
          _defaultURL = imgUrlRes
        })
      } else {
        _defaultURL = _signImg
      }

      // upload ajax
    },

    // 重新生成旋转base64
    rotateBase64Img (src, edg, callback) {
      try {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')

        let imgW = 0 // 图片宽度
        let imgH = 0 // 图片高度
        let size = 0 // canvas初始大小

        if (edg % 90 !== 0) {
          throw new Error('旋转角度必须是90的倍数!')
        }

        (edg < 0) && (edg = (edg % 360) + 360)
        const quadrant = (edg / 90) % 4 // 旋转象限
        const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } // 裁剪坐标

        const image = new Image()
        image.src = src
        image.crossOrigin = 'anonymous'

        image.onload = function () {
          imgW = image.width
          imgH = image.height
          size = imgW > imgH ? imgW : imgH

          canvas.width = size * 2
          canvas.height = size * 2
          switch (quadrant) {
            case 0:
              cutCoor.sx = size
              cutCoor.sy = size
              cutCoor.ex = size + imgW
              cutCoor.ey = size + imgH
              break
            case 1:
              cutCoor.sx = size - imgH
              cutCoor.sy = size
              cutCoor.ex = size
              cutCoor.ey = size + imgW
              break
            case 2:
              cutCoor.sx = size - imgW
              cutCoor.sy = size - imgH
              cutCoor.ex = size
              cutCoor.ey = size
              break
            case 3:
              cutCoor.sx = size
              cutCoor.sy = size - imgW
              cutCoor.ex = size + imgH
              cutCoor.ey = size + imgW
              break
          }

          ctx.translate(size, size)
          ctx.rotate(edg * Math.PI / 180)
          ctx.drawImage(image, 0, 0)

          const imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey)
          if (quadrant % 2 === 0) {
            canvas.width = imgW
            canvas.height = imgH
          } else {
            canvas.width = imgH
            canvas.height = imgW
          }
          ctx.putImageData(imgData, 0, 0)

          if (typeof callback === 'function') {
            callback(canvas.toDataURL('image/png', 0.7))
          }
        }
      } catch (e) {
        console.log(e)
      }
    },
  },
  beforeDestroy () {
    window.removeEventListener('resize', this.initalHandle, false)
  }
}
</script>

<style lang='scss' scoped>
.recruit-canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;

  .btn-box,
  .canvas-box{
    position: absolute;
    top: 50%;
    z-index: 50;
  }

  .btn-box{
    left: -22%;
    text-align: center;
    transform: rotate(90deg);
    -o-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -webkit-transform: rotate(90deg);

    .del-btn,
    .sure-btn {
      display: inline-block;
      width: 100px;
      height: 24px;
      margin: 0 10px;
      line-height: 24px;
      border-radius: 6px;
      background-color: $color-background;
    }

    .del-btn {
      color: $color-theme;
    }

    .sure-btn {
      color: $color-background;
      background: linear-gradient(100deg, #FF4E01 0%, #FFBC01 100%);
    }
  }

  .canvas-box {
    left: 22%;
    height: 80vh;
    width: 70vw;
    overflow: hidden;
    border: 1px dashed #D4D4D4;
    transform: translateY(-50%);
    background-color: $color-background;

    #canvas-map {
      width: 100%;
      height: 100%;
    }
  }
}

@media screen and (orientation: portrait) {
  /*竖屏 css*/
}

@media screen and (orientation: landscape) {
  /*横屏 css*/
  .recruit-canvas {
    .canvas-box {
      top: 20px;
      left: 10%;
      width: 80vw;
      height: 70vh;
      transform: translateY(0);
    }

    .btn-box {
      width: 60%;
      left: 20%;
      top: 86%;
      transform: rotate(0);
    }
  }
}
</style>

文章目录