likes
comments
collection
share

canvas 签名功能实现

作者站长头像
站长
· 阅读数 19

介绍

一个兼容PCH5的签名工具函数实现;这里不依赖框架来实现,方便任何网页环境中去使用。

源码地址

思路:利用canvas画笔线条的描边,配合鼠标摁下移动抬起或者移动端触摸开始触摸移动触摸结束,然后调用对应的方法进行绘画即可。

核心代码

分别对应PCH5环境中的操作事件,因为事件的坐标处理稍有不同,所以三个核心方法提取出来对应调用即可。

let startX = 0, startY = 0;

// 这里不用变量存储起来是为了在外部可以动态修改指定值,内部实时使用最新的,类似响应式
const getLineWidth = () => option.lineSize || 4;
const getLineColor = () => option.lineColor || "#000000";

/**
 * 绘画开始
 * @param {{ x: number, y: number }} size 坐标点
 */
function drawStart(size) {
  const x = size.x * ratio, y = size.y * ratio;
  context.beginPath();
  context.moveTo(x, y);
  context.lineTo(x, y);
  context.strokeStyle = getLineColor();
  context.lineWidth = getLineWidth() * ratio;
  context.stroke();
  context.closePath();
  startY = y;
  startX = x;
}

/**
 * 绘画拖拽
 * @param {{ x: number, y: number }} size 坐标点
 */
function drawMove(size) {
  const x = size.x * ratio, y = size.y * ratio;
  context.beginPath();
  context.moveTo(startX, startY);
  context.lineTo(x, y);
  context.strokeStyle = getLineColor();
  context.lineWidth = getLineWidth() * ratio;
  context.stroke();
  context.closePath();
  startY = y;
  startX = x;
}

/**
 * 绘画结束
 * @param {{ x: number, y: number }} size 坐标点
 */
function drawEnd(size) {
  context.beginPath();
  context.moveTo(startX, startY);
  context.lineTo(size.x * ratio, size.y * ratio);
  context.stroke();
  context.closePath();
}

完整功能代码

注意事项:因为移动端的屏幕分辨率缩放比是不一样的,如果都是按固定的1去绘制的话,会有模糊情况,所以要根据设备的比率去放大canvas的尺寸,包括对应的坐标乘以比率等,具体看ratio变量的使用代码片段。

/**
 * `canvas`签名工具
 * @param {object} option 
 * @param {HTMLElement} option.el 挂载的节点元素
 * @param {number} option.lineSize (可选)画笔线条的厚度:像素
 * @param {string} option.lineColor (可选)画笔线条的颜色
 * @param {string} option.backgroundColor (可选)背景颜色
 * @param {number} option.ratio (可选)缩放比率
 */
function canvasAutograph(option) {
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");
  const ratio = option.ratio || window.devicePixelRatio;

  /** 更新`canvas`尺寸 */
  function updateSize() {
    canvas.width = option.el.clientWidth * ratio;
    canvas.height = option.el.clientHeight * ratio;
  }

  /** 设置画布样式 */
  function setStyle() {
    canvas.style.width = "100%";
    canvas.style.height = "100%";
    context.lineCap = "round";
    context.lineJoin = "round";
    context.fillStyle = option.backgroundColor || "#ffffff";
    context.fillRect(0, 0, canvas.width, canvas.height);
  }

  let startX = 0, startY = 0;

  // 这里不用变量存储起来是为了在外部可以动态修改指定值,内部实时使用最新的,类似响应式
  const getLineWidth = () => option.lineSize || 4;
  const getLineColor = () => option.lineColor || "#000000";

  /**
   * 绘画开始
   * @param {{ x: number, y: number }} size 坐标点
   */
  function drawStart(size) {
    const x = size.x * ratio, y = size.y * ratio;
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x, y);
    context.strokeStyle = getLineColor();
    context.lineWidth = getLineWidth() * ratio;
    context.stroke();
    context.closePath();
    startY = y;
    startX = x;
  }

  /**
   * 绘画拖拽
   * @param {{ x: number, y: number }} size 坐标点
   */
  function drawMove(size) {
    const x = size.x * ratio, y = size.y * ratio;
    context.beginPath();
    context.moveTo(startX, startY);
    context.lineTo(x, y);
    context.strokeStyle = getLineColor();
    context.lineWidth = getLineWidth() * ratio;
    context.stroke();
    context.closePath();
    startY = y;
    startX = x;
  }

  /**
   * 绘画结束
   * @param {{ x: number, y: number }} size 坐标点
   */
  function drawEnd(size) {
    context.beginPath();
    context.moveTo(startX, startY);
    context.lineTo(size.x * ratio, size.y * ratio);
    context.stroke();
    context.closePath();
  }

  /** 是否有绘画过 */
  let hasDraw = false;
  /** 是否正在绘画中 */
  let isDrawing = false;
  
  /**
   * 鼠标摁下
   * @param {MouseEvent} e 
   */
  function onMouseDown(e) {
    e.preventDefault();
    isDrawing = true;
    hasDraw = true;
    drawStart({
      x: e.offsetX,
      y: e.offsetY
    })
  }

  /**
   * 鼠标移动
   * @param {MouseEvent} e 
   */
  function onMouseMove(e) {
    e.preventDefault();
    if (!isDrawing) return;
    drawMove({
      x: e.offsetX,
      y: e.offsetY
    })
  }

  /**
   * 鼠标抬起
   * @param {MouseEvent} e
   */
  function onMouseUp(e) {
    e.preventDefault();
    if (!isDrawing) return;
    drawEnd({
      x: e.offsetX,
      y: e.offsetY
    })
    isDrawing = false;
  }

  /**
   * 触摸开始
   * @param {TouchEvent} e 
   */
  function onTouchStart(e) {
    e.preventDefault();
    if (e.touches.length === 1) {
      isDrawing = true;
      hasDraw = true;
      const size = e.touches[0];
      const box = canvas.getBoundingClientRect();
      drawStart({
        x: size.clientX - box.left,
        y: size.clientY - box.top
      })
    }
  }

  /**
   * 触摸移动
   * @param {TouchEvent} e
   */
  function onTouchMove(e) {
    e.preventDefault();
    if (!isDrawing) return;
    if (e.touches.length === 1) {
      const size = e.touches[0];
      const box = canvas.getBoundingClientRect();
      drawMove({
        x: size.clientX - box.left,
        y: size.clientY - box.top
      })
    }
  }

  /**
   * 触摸结束
   * @param {TouchEvent} e
   */
  function onTouchEnd(e) {
    e.preventDefault();
    if (!isDrawing) return;
    if (e.touches.length === 1) {
      const size = e.touches[0];
      const box = canvas.getBoundingClientRect();
      drawEnd({
        x: size.clientX - box.left,
        y: size.clientY - box.top
      })
    }
  }

  /** 整个文档抬起事件 */
  function documentUp() {
    isDrawing = false;
    // 如果节点被销毁了,那就取消`document`的绑定事件
    if (!document.body.contains(canvas)) {
      document.removeEventListener("mouseup", documentUp);
      document.removeEventListener("touchend", documentUp);
    }
  }

  // 输出节点
  option.el.appendChild(canvas);
  // 先更新一次
  updateSize();
  setStyle();
  // 添加事件
  canvas.addEventListener("mousedown", onMouseDown);
  canvas.addEventListener("mousemove", onMouseMove);
  canvas.addEventListener("mouseup", onMouseUp);
  canvas.addEventListener("touchstart", onTouchStart);
  canvas.addEventListener("touchmove", onTouchMove);
  canvas.addEventListener("touchend", onTouchEnd);
  document.addEventListener("mouseup", documentUp);
  document.addEventListener("touchend", documentUp);

  return {
    canvas,
    /** 重置签名版 */
    reset() {
      hasDraw = false;
      context.clearRect(0, 0, canvas.width, canvas.height);
      setStyle();
    },
    /**
     * 生成图片
     * @param imageType 图片类型
     * @returns 
     */
    getBase64(imageType = "image/jpeg") {
      return hasDraw ? canvas.toDataURL(imageType) : "";
    }
  }
}

生成的图片上传到服务器

通常上传到服务端都是以FormData的形式,传参类型要求为File;所以在调用getBase64()之后,再使用下面方法转换一次即可。

/**
 * `base64`转`file`或者`blob`对象
 * @param {string} base64
 * @param {"blob"|"file"} type 转换的类型,默认`"blob"`
 * @param {string} filename 转换后的文件名,`type: "file"`时生效
 */
function base64ToBlobOrFile(base64, type, filename) {
  const arr = base64.split(",");
  const mime = arr[0].match(/:(.*?);/)[1];
  const suffix = mime.split("/")[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  if (type === "file") {
    return new File([u8arr], `${filename}.${suffix}`, { type: mime });
  } else {
    return new Blob([u8arr], { type: mime });
  }
}