h5 vue利用canvas实现手机签名并且可旋转功能

本文介绍了如何使用HTML5的Canvas元素在手机端实现签名功能,包括清除签名、图片翻转及将签名转化为Base64编码上传后台。代码示例详细展示了从触摸事件处理到图片处理的完整流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

功能描述:

利用canvas实现手机签名转化成图片( 包含清除,由于手机全屏展示 所以还添加了图片翻转功能)生成base64地址 上传后台
在这里插入图片描述

功能展示:

代码展示:

html:

<template>
  <div style="height:100%">
    <div class="content">
      <div class="van-tabContent">
        <div class="tip">
          <span>请在虚线内进行签名</span>
          <div class="rotate"
               @click="rotateImg('testImg','right')"
               v-if="isShowSub">
            <span>旋转图片</span>
          </div>
        </div>
        <img :src="url"
             alt=""
             id="testImg"
             style="display:none">
        <div class="signature"
             @touchmove.prevent>
          <div class="boardBox"
               ref="boardBox">
            <canvas ref="board"
                    id="canvas"
                    @touchstart="mStart"
                    @touchmove="mMove"
                    @touchend="mEnd"></canvas>
          </div>
        </div>
      </div>
      <div class="bar">
        <button round
                    class="clear-btn"
                    @click="clearcanvas">清除</button>
        <button round
                    class="disabled-btn"
                    disabled
                    v-if="!isShowSub">提交</button>
        <button round
                    class="sub-btn"
                    v-else
                    @click="subMitPic">提交</button>
      </div>
    </div>
  </div>
</template>

js:

<script>
export default {
  data () {
    return {
      familysignatureurl: "",
      basedata: "",
      ctx: null,
      point: {
        x: 0,
        y: 0,
      },
      moving: false, // 是否正在绘制中且移动
      isShowSub: false,//是否可点击提交
      url: "",
    }
  },
  mounted () {
    let board = this.$refs.board; // 获取DOM
    board.width = this.$refs.boardBox.offsetWidth; // 设置画布宽
    board.height = this.$refs.boardBox.offsetHeight; // 设置画布高
    this.ctx = board.getContext("2d"); // 二维绘图
    this.ctx.strokeStyle = "#000"; // 颜色
    this.ctx.lineWidth = 3; // 线条宽度
  },
  methods: {
    // 触摸(开始)
    mStart (e) {
      let x = e.touches[0].clientX - e.target.offsetLeft,
        y = e.touches[0].clientY - e.target.offsetTop; // 获取触摸点在画板(canvas)的坐标
      this.point.x = x;
      this.point.y = y;
      this.ctx.beginPath();
      this.moving = true;
    },
    // 滑动中...
    mMove (e) {
      if (this.moving) {
        let x = e.touches[0].clientX - e.target.offsetLeft,
          y = e.touches[0].clientY - e.target.offsetTop; // 获取触摸点在画板(canvas)的坐标
        this.ctx.moveTo(this.point.x, this.point.y); // 把路径移动到画布中的指定点,不创建线条(起始点)
        this.ctx.lineTo(x, y); // 添加一个新点,然后创建从该点到画布中最后指定点的线条,不创建线条
        this.ctx.stroke(); // 绘制
        (this.point.x = x), (this.point.y = y); // 重置点坐标为上一个坐标
      }
    },
    // 滑动结束
    mEnd () {
      if (this.moving) {
        this.ctx.closePath(); // 停止绘制
        this.moving = false; // 关闭绘制开关
        this.isShowSub = true
        document.getElementById("canvas").toDataURL("image/png");
        document.getElementById("canvas").toBlob(async (blobObj) => {
          var file = new File([blobObj], "pic.png", {
            type: blobObj.type,
            lastModified: Date.now(),
          });
          this.getCanvasPic(file)
        });
      }
    },
    //获取canvas图片
    getCanvasPic (file) {
      const _this = this
      var oReader = new FileReader();
      oReader.readAsDataURL(file);
      oReader.onload = function (e) {
        let image = new Image()
        // image.setAttribute('crossOrigin', 'anonymous')
        image.src = e.target.result
        image.onload = function () {
          let canvas = document.createElement('canvas')
          canvas.width = image.width
          canvas.height = image.height
          let context = canvas.getContext('2d')
          context.drawImage(image, 0, 0, image.width, image.height)
          // 将canvas的透明背景设置成白色
          var imageData = context.getImageData(
            0,
            0,
            canvas.width,
            canvas.height
          );
          for (var i = 0; i < imageData.data.length; i += 4) {
            // 当该像素是透明的,则设置成白色
            if (imageData.data[i + 3] == 0) {
              imageData.data[i] = 255;
              imageData.data[i + 1] = 255;
              imageData.data[i + 2] = 255;
              imageData.data[i + 3] = 255;
            }
          }
          context.putImageData(imageData, 0, 0);
          let url = canvas.toDataURL('image/jpg')
          _this.url = url//绘制的图片base64地址
          _this.basedata = url
          console.log(_this.basedata, 'this.basedata')
        }
      }
    },
    //旋转图片
    rotateImg (pid, direction) {
      //最小与最大旋转方向,图片旋转4次后回到原方向  
      var min_step = 0;
      var max_step = 3;
      var img = document.getElementById(pid);
      if (img == null) return;
      //img的高度和宽度不能在img元素隐藏后获取,否则会出错  
      var height = img.height;
      var width = img.width;
      var step = img.getAttribute('step');
      if (step == null) {
        step = min_step;
      }
      if (direction == 'right') {
        step++;
        //旋转到原位置,即超过最大值  
        step > max_step && (step = min_step);
      } else {
        step--;
        step < min_step && (step = max_step);
      }
      img.setAttribute('step', step);
      var canvas = document.getElementById("canvas");
      if (canvas == null) {
        img.style.display = 'none';
        canvas = document.createElement('canvas');
        canvas.setAttribute('id', 'pic_' + pid);
        img.parentNode.appendChild(canvas);
      }
      //旋转角度以弧度值为参数  
      var degree = step * 90 * Math.PI / 180;
      var ctx = canvas.getContext('2d');
      switch (step) {
        case 0:
          canvas.width = width;
          canvas.height = height;
          ctx.drawImage(img, 0, 0);
          let url = canvas.toDataURL('image/jpg')
          this.basedata = url
          break;
        case 1:
          canvas.width = height;
          canvas.height = width;
          ctx.rotate(degree);
          ctx.drawImage(img, 0, -height);
          let url1 = canvas.toDataURL('image/jpg')
          this.basedata = url1
          break;
        case 2:
          canvas.width = width;
          canvas.height = height;
          ctx.rotate(degree);
          ctx.drawImage(img, -width, -height);
          let url2 = canvas.toDataURL('image/jpg')
          this.basedata = url2
          break;
        case 3:
          canvas.width = height;
          canvas.height = width;
          ctx.rotate(degree);
          ctx.drawImage(img, -width, 0);
          let url3 = canvas.toDataURL('image/jpg')
          this.basedata = url3
          break;
      }
    },
    //base64转Blob
    base64ToBlob (base64Data) {
      let arr = base64Data.split(","),
        fileType = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        l = bstr.length,
        u8Arr = new Uint8Array(l);

      while (l--) {
        u8Arr[l] = bstr.charCodeAt(l);
      }
      return new Blob([u8Arr], {
        type: fileType,
      });
    },
    //上传图片
    async subMitPic () {
      //转成file文件
      let blobObj = this.base64ToBlob(this.basedata);
      let file = new File([blobObj], "pic.png", {
        type: blobObj.type,
        lastModified: Date.now(),
      });
      let newformData = new FormData()
      newformData.append('file', file)
      newformData.append('isCompress', "1")
      console.log(newformData.get('file'), newformData.get('isCompress'), 'formData')
      //此处为发送请求给后台获取图片路径
      let res = await this.upload(newformData);
      if (res.code === 200) {
          //可在此处处理上传图片以后的业务操作
          //   this.familysignatureurl = res.data[0]
      //   console.log(this.familysignatureurl);//此处打印的为绘画的图片url
      }
    },
    upload (formData) {
      return postAxiosData('/venus/oss/upload', formData)
    },
    //清除画布
    clearcanvas () {
      var c = document.getElementById("canvas");
      var cxt = c.getContext("2d");
      c.height = c.height;
      this.ctx.lineWidth = 3;
      this.isShowSub = false
    },
  },
}
</script>

css:

<style lang="scss" scoped>
.content {
  height: 100%;
  background-color: #f3f3f3;
  .van-tabContent {
    padding: 15px 15px;
    .tip {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 18px;
      font-size: 13px;
      font-weight: 400;
      line-height: 22px;
      color: #9d9d9d;
      opacity: 1;
      .rotate {
        color: #5e6fdb;
        img {
          width: 18px;
          height: 18px;
          vertical-align: top;
          margin-right: 5px;
        }
      }
    }
    .signature {
      width: 100%;
      height: 400px;
      border: 1px dashed #c1c1c1;
      margin: 11px 0 14px;
    }
    .boardBox {
      height: 100%;
      background: #ffffff;
    }
  }
  .bar {
    display: flex;
    justify-content: space-between;
    padding: 10px 15px 16px;
    position: fixed;
    bottom: 0px;
    width: 100%;
    background-color: #f3f3f3;
    .clear-btn,
    .sub-btn,
    .disabled-btn {
      width: 50%;
    }
    .clear-btn {
      border: 1px solid #5e6fdb;
      font-size: 17px;
      font-weight: 400;
      line-height: 24px;
      color: #5e6fdb;
      opacity: 1;
    }
    .sub-btn,
    .disabled-btn {
      background: #c1c1c1;
      font-size: 17px;
      font-weight: 400;
      line-height: 22px;
      color: #ffffff;
      opacity: 1;
      margin-left: 15px;
    }
    .sub-btn {
      background-color: #5e6fdb;
    }
  }
}
</style>

本文参考:

https://blog.youkuaiyun.com/weixin_50329459/article/details/108218091?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242

https://blog.51cto.com/qings/997998

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值