canvas 实现画布签名

canvas 实现画布签名功能

方法描述

在 Web 开发中,实现画布签名功能是一个常见的需求,例如电子合同签署、在线表单签名等。 这个Signature方法,它可以在画布上实现签名功能,并提供了缩放、旋转、保存等实用方法。

代码概述
Signature 主要实现了以下功能
  • 在 HTML5 画布上绘制签名。
  • 支持缩放、旋转画布。
  • 保存签名为 PNG 或 JPG 格式的图片。
  • 清空画布、清除上一笔签名

Signature 类的原型方法提供了各种操作画布的功能,如缩放、旋转、保存、清空等。每个方法都有明确的功能和参数,方便开发者使用

在这里插入图片描述

使用实例 (这里使用的是vue)

在这里插入代码片
<!-- -->
<template>
  <div class="box">
    <canvas ref="canvas" class="canvas"></canvas>
    <div class="clearBtn" @click="clear"> 全部清除</div>
    <div class="clearBtn" @click="clearLastStroke"> 清除上一笔</div>
  </div> 
</template>
<script>
import Signature from "./signature";
import { Dialog, Toast } from "vant";
let signature = null;
export default {
    mounted() {
      setTimeout(() => {
        signature = new Signature(this.$refs.canvas);
      }, 100);
    },
    methods: {
      // 逐笔删除
      clearLastStroke(){
        // 调用逐笔清除方法
        signature.clearLastStroke();
      },
      clear() {
        signature.clear();        
      },
    },    
};
</script>

构造函数
在这里插入代码片
function Signature(canvas, degree = 0, config = {}) {
  if (!(this instanceof Signature)) return new Signature(canvas, config);
  if (!canvas) return;
  let { width, height } = window.getComputedStyle(canvas, null);
  width = width.replace("px", "");
  height = height.replace("px", "");
  this.canvas = canvas;
  this.context = canvas.getContext("2d");
  this.width = width;
  this.height = height;
  const context = this.context;
  const devicePixelRatio = window.devicePixelRatio || 1;
  if (devicePixelRatio) {
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    canvas.height = height * devicePixelRatio;
    canvas.width = width * devicePixelRatio;
    context.scale(devicePixelRatio, devicePixelRatio);
  } else {
    canvas.width = width;
    canvas.height = height;
  }
  context.lineWidth = 5;
  context.strokeStyle = "black";
  context.lineCap = "round";
  context.lineJoin = "round";
  Object.assign(context, config);
  const { left, top } = canvas.getBoundingClientRect();
  const point = {};
  // 检测是否为鸿蒙系统 NEXT
  const isHarmonyNext = /HarmonyOS/i.test(navigator.userAgent) && /Next/i.test(navigator.userAgent);
  const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent) || isHarmonyNext;
  if (!isMobile) {
    context.shadowBlur = 1;
    context.shadowColor = "black";
  }
  let pressed = false;
  this.hasContent = false; 
  this.paths = []; 
  let currentPath = [];
  this.drawPlaceholder = () => {
    const placeholderText = "请用正楷签名";
    this.context.font = "20px Arial";
    this.context.fillStyle = "#aaabb9";
    this.context.textAlign = "left";
    this.context.fillText(placeholderText, 12, 30);
  };  
    this.drawPlaceholder();
  const paint = signal => {
    if (!this.hasContent) {  
      context.clearRect(0, 0, width, height);
      this.hasContent = true;
    }
    switch (signal) {
      case 1:
        context.beginPath();
        context.moveTo(point.x, point.y);
        currentPath = []; // 开始新的一笔签名,清空当前路径
        currentPath.push({ x: point.x, y: point.y });
      case 2:
        context.lineTo(point.x, point.y);
        context.stroke();
        currentPath.push({ x: point.x, y: point.y });
        break;
      default:
    }
  };
  const create = signal => e => {
    e.preventDefault();
    if (signal === 1) {
      pressed = true;
    }
    if (signal === 1 || pressed) {
      if (isHarmonyNext && e.type.includes('touch')) {// 适配鸿蒙系统 NEXT 触摸事件
        e = e.changedTouches[0];
      } else {
        e = isMobile ? e.touches[0] : e;
      }
      point.x = e.clientX - left;
      point.y = e.clientY - top;
      paint(signal);
      if (signal === 2) {
        this.paths[this.paths.length - 1] = currentPath; 
      }
    }
    if (signal === 1) {
      this.paths.push(currentPath); 
    }
  };
  const start = create(1);
  const move = create(2);
  const requestAnimationFrame = window.requestAnimationFrame;
  const optimizedMove = requestAnimationFrame
    ? e => {
        requestAnimationFrame(() => {
          move(e);
        });
      }: move;
  if (isMobile) {
    canvas.addEventListener("touchstart", start);
    canvas.addEventListener("touchmove", optimizedMove);
  } else {
    canvas.addEventListener("mousedown", start);
    canvas.addEventListener("mousemove", optimizedMove);
    ["mouseup", "mouseleave"].forEach(event => {
      canvas.addEventListener(event, () => {
        pressed = false;
      });
    });
  }
  if (typeof degree === "number") {
    this.degree = degree;
    context.rotate((degree * Math.PI) / 180);
    switch (degree) {
      case -90:
        context.translate(-height, 0);
        break;
      case 90:
        context.translate(0, -width);
        break;
      case -180:
      case 180:
        context.translate(-width, -height);
        break;
      default:
    }
  }
}
export default Signature;
原型方法
在这里插入代码片
Signature.prototype = {
  scale(width, height, canvas = this.canvas) {
    const w = canvas.width;
    const h = canvas.height;
    width = width || w;
    height = height || h;
    if (width !== w || height !== h) {
      const tmpCanvas = document.createElement("canvas");
      const tmpContext = tmpCanvas.getContext("2d");
      tmpCanvas.width = width;
      tmpCanvas.height = height;
      tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
      canvas = tmpCanvas;
    }
    return canvas;
  },
  isSigned() {
    return !this.hasContent;
  },
  rotate(degree, image = this.canvas) {
    degree = ~~degree;
    if (degree !== 0) {
      const maxDegree = 180;
      const minDegree = -90;
      if (degree > maxDegree) {
        degree = maxDegree;
      } else if (degree < minDegree) {
        degree = minDegree;
      }
      const canvas = document.createElement("canvas");
      const context = canvas.getContext("2d");
      const height = image.height;
      const width = image.width;
      const degreePI = (degree * Math.PI) / 180;
      switch (degree) {
        case -90:
          canvas.width = height;
          canvas.height = width;
          context.rotate(degreePI);
          context.drawImage(image, -width, 0);
          break;
        case 90:
          canvas.width = height;
          canvas.height = width;
          context.rotate(degreePI);
          context.drawImage(image, 0, -height);
          break;
        case 180:
          canvas.width = width;
          canvas.height = height;
          context.rotate(degreePI);
          context.drawImage(image, -width, -height);
          break;
        default:
      }
      image = canvas;
    }
    return image;
  },
  getPNGImage(canvas = this.canvas) {
    return canvas.toDataURL("image/png");
  },
  getJPGImage(canvas = this.canvas) {
    return canvas.toDataURL("image/jpeg", 0.5);
  },
  downloadPNGImage(image) {
    const url = image.replace(
      "image/png",
      "image/octet-stream;Content-Disposition:attachment;filename=test.png"
    );
    window.location.href = url;
  },
  dataURLtoBlob(dataURL) {
    const arr = dataURL.split(",");
    const mime = arr[0].match(/:(.*?);/)[1];
    const bStr = atob(arr[1]);
    let n = bStr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bStr.charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime });
  },
  getCanvasDimensions () {
    if ([ -90, 90, ].includes(this.degree)) {
      return { width: this.height, height: this.width };
    }
    return { width: this.width, height: this.height };
  },
  clear () {
    const { width, height } = this.getCanvasDimensions();
    this.context.clearRect(0, 0, width, height);
    this.hasContent = false; 
    // this.paths = []; 
    this.drawPlaceholder();
  },
  upload(blob, url, success, failure) {
    const formData = new FormData();
    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    formData.append("image", blob, "sign");
    xhr.open("POST", url, true);
    xhr.onload = () => {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        success(xhr.responseText);
      } else {
        failure();
      }
    };
    xhr.onerror = e => {
      if (typeof failure === "function") {
        failure(e);
      } else {
        console.log(`upload img error: ${e}`);
      }
    };
    xhr.send(formData);
  },
  clearLastStroke() {
    if (this.paths.length > 0) {
      this.paths.pop(); 
      this.context.clearRect(0, 0, this.width, this.height); 
      this.hasContent = false; 
      for (let path of this.paths) {
        if (path.length > 0) {
          this.context.beginPath();
          this.context.moveTo(path[0].x, path[0].y);
          for (let i = 1; i < path.length; i++) {
            this.context.lineTo(path[i].x, path[i].y);
          }
          this.context.stroke();
          this.hasContent = true; 
        }
      }
    }
    if(!this.hasContent){
      this.drawPlaceholder();
    }
  },
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值