canvas实现签名

发布时间 2023-08-22 13:57:45作者: 月下云生

在开源项目中发现canvas实现签名功能以此记录:http://www.youlai.tech/pages/52d5c3/

HTML:

<div class="canvas-dom">
    <el-button plain type="text" style="margin-left:20px;margin-top:20px;font-size:18px;" @click="back">返回</el-button>
    <canvas ref="canvas" height="200" width="500" @mousedown="onEventStart" @mousemove.stop.prevent="onEventMove"
      @mouseup="onEventEnd" @touchstart="onEventStart" @touchmove.stop.prevent="onEventMove" @touchend="onEventEnd">
    </canvas>
    <header>
      <el-button type="primary" @click="handleSaveImg">保存为图片</el-button>
      <el-button @click="handleToFile"> 保存到后端 </el-button>
      <el-button @click="handleClearSign"> 清空签名 </el-button>
    </header>
    <img v-if="imgUrl" :src="imgUrl" alt="签名" />
  </div>

 script:

 

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// import { uploadFileApi } from "@/api/file"

const imgUrl = ref("")
const canvas = ref()
let ctx: CanvasRenderingContext2D

// 正在绘制中,用来控制 move 和 end 事件
let painting = false

// 获取触发点相对被触发dom的左、上角距离
const getOffset = (event: MouseEvent | TouchEvent) => {
  let offset: [number, number]
  if ((event as MouseEvent).offsetX) {
    // pc端
    const { offsetX, offsetY } = event as MouseEvent;
    offset = [offsetX, offsetY]
  } else {
    // 移动端
    const { top, left } = canvas.value.getBoundingClientRect()
    const offsetX = (event as TouchEvent).touches[0].clientX - left
    const offsetY = (event as TouchEvent).touches[0].clientY - top
    offset = [offsetX, offsetY]
  }

  return offset;
};

// 绘制起点
let startX = 0, startY = 0

// 鼠标/触摸 按下时,保存 触发点相对被触发dom的左、上 距离
const onEventStart = (event: MouseEvent | TouchEvent) => {
  [startX, startY] = getOffset(event)
  painting = true
}

const onEventMove = (event: MouseEvent | TouchEvent) => {
  if (painting) {
    // 鼠标/触摸 移动时,保存 移动点相对 被触发dom的左、上 距离
    const [endX, endY] = getOffset(event)
    paint(startX, startY, endX, endY, ctx)

    // 每次绘制 或 清除结束后,起点要重置为上次的终点
    startX = endX
    startY = endY
  }
};

const onEventEnd = () => {
  if (painting) {
    painting = false; // 停止绘制
  }
};

onMounted(() => {
  ctx = canvas.value.getContext("2d") as CanvasRenderingContext2D
});
const handleToFile = async () => {
  if (isCanvasBlank(canvas.value)) {
    ElMessage({
      type: "warning",
      message: "当前签名文件为空",
    })
    return
  }
  const file = dataURLtoFile(canvas.value.toDataURL(), "签名.png")

  if (!file) return
  // 文件上传后端,返回地址,显示
  // const { data } = await uploadFileApi(file)
  // handleClearSign();
  // imgUrl.value = data.url
};
const handleClearSign = () => {
  ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
};
const isCanvasBlank = (canvas: HTMLCanvasElement) => {
  const blank = document.createElement("canvas"); //系统获取一个空canvas对象
  blank.width = canvas.width;
  blank.height = canvas.height;
  return canvas.toDataURL() == blank.toDataURL(); //比较值相等则为空
};

// 保存为图片
const handleSaveImg = () => {
  if (isCanvasBlank(canvas.value)) {
    ElMessage({
      type: "warning",
      message: "当前签名文件为空",
    })
    return
  }
  const el = document.createElement("a")
  // 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
  el.href = canvas.value.toDataURL()
  el.download = "签名";
  // 创建一个点击事件并对 a 标签进行触发
  const event = new MouseEvent("click")
  el.dispatchEvent(event);
}

// 转为file格式,可传递给后端
const dataURLtoFile = (dataurl: string, filename: string) => {
  const arr: string[] = dataurl.split(",")
  if (!arr.length) return

  const mime = arr[0].match(/:(.*?);/)
  if (mime) {
    const bstr = atob(arr[1])
    let n = bstr.length
    const u8arr = new Uint8Array(n)
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n)
    }
    return new File([u8arr], filename, { type: mime[1] })
  }
};
// canvas 画图
function paint(
  startX: number,
  startY: number,
  endX: number,
  endY: number,
  ctx: CanvasRenderingContext2D
) {
  ctx.beginPath()
  ctx.globalAlpha = 1
  ctx.lineWidth = 2
  ctx.strokeStyle = "#000"
  ctx.moveTo(startX, startY)
  ctx.lineTo(endX, endY)
  ctx.closePath()
  ctx.stroke()
}

// 橡皮
function eraser(
  startX: number,
  startY: number,
  endX: number,
  endY: number,
  ctx: CanvasRenderingContext2D,
  size: number,
  shape: "rect" | "circle"
) {
  ctx.beginPath()
  ctx.globalAlpha = 1
  switch (shape) {
    case "rect":
      ctx.lineWidth = size
      ctx.strokeStyle = "#fff"
      ctx.moveTo(startX, startY)
      ctx.lineTo(endX, endY)
      ctx.closePath()
      ctx.stroke()
      break
    case "circle":
      ctx.fillStyle = "#fff";
      ctx.arc(startX, startY, size, 0, 2 * Math.PI)
      ctx.fill()
      break
  }
}
const back = () => {
  window.history.back()
}
</script>

css:

<style scoped lang="scss">
.canvas-dom {
  width: 100%;
  height: 100%;
  padding: 0 20px;
  background-color: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  canvas {
    border: 1px solid #e6e6e6;
  }

  header {
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
    margin: 8px;

    .eraser-option {
      display: flex;

      label {
        white-space: nowrap;
      }
    }
  }
}
</style>

代码地址:https://gitee.com/yuexiayunsheng/vue3learn/blob/master/src/views/Signature.vue