简简单单实现一个Vue3手写板组件Tablet

231 阅读2分钟

需求描述

基于Vue3实现一个手写板组件,用于用户手写签名并上传的需求。

  1. 签名并导出图片;
  2. 支持清除。

image.png

实现思路

通过我们平时书写的经验,大致可以分成3个阶段:

  1. 落笔
  2. 运笔
  3. 提笔

然后通过touch/mouse事件判断以上3个阶段,并在canvas上进行绘制,最后再通过canvas的toDataURL方法进行导出。另外,需要注意的地方是,根据像素比devicePixelRatio对canvas进行缩放,否则导出的图片不够清晰

准备工作

开始实现之前,需要进行一些准备工作:

canvas初始化
<template>
  <canvas 
      ref="tablet"       
      @touchstart="onStart"
      @touchmove="onMove"
      @touchend="onEnd"
      @mousedown="onStart"
      @mouseup="onEnd"
      @mousemove="onMove"
  ></canvas>
</template>
// 设备像素比,对canvas进行缩放提高导出图的清晰度
const ratio = window.devicePixelRatio || 1;

let canvas!: HTMLCanvasElement;
let ctx!: CanvasRenderingContext2D;

// 支持修改手写板高/宽
const props = defineProps<{
  width: number;
  height: number;
}>();

const tablet = ref<HTMLCanvasElement>();

onMounted(() => {
  canvas = tablet.value as HTMLCanvasElement;
  canvas.width = props.width * ratio;
  canvas.height = props.width * ratio;

  ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
  // 绘制白色背景
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width * ratio, canvas.height * ratio);
});
准备状态及工具函数
// 书写状态,用于控制绘画
let writing = false;

// 将payload转换成坐标,主要用于兼容PC跟H5
const coord = (payload: Partial<Event>) => {
  const { touches, clientX = 0, clientY = 0 } = payload;
  if (touches && touches.length) {
    const { clientX, clientY } = touches[0];
    return {
      clientX,
      clientY,
    };
  }
  return { clientX, clientY };
};

落笔-运笔-提笔

落笔

状态设置为书写中,开始新路径并设置起始点。

const onStart = (payload: Partial<Event>) => {
  writing = true;
  const { clientX, clientY } = coord(payload);
  ctx.beginPath();
  ctx.moveTo(clientX * ratio, clientY * ratio);
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
};
运笔

当状态为书写中时,通过touchmove/mousemove事件进行绘制。

const onMove = (payload: Partial<Event>) => {
  if (!writing) {
    return;
  }
  const { clientX, clientY } = coord(payload);
  ctx.lineTo(clientX * ratio, clientY * ratio);
  ctx.lineWidth = 3 * ratio;
  ctx.stroke();
  payload.preventDefault?.();
};
提笔

取消书写中的状态为时,结束绘制。

const onEnd = () => {
  writing = false;
};

导出、清除功能

const confirm = () => {
  return canvas.toDataURL();
};

const clear = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width * ratio, canvas.height * ratio);
};

// 将方法暴露给父组件
defineExpose({ confirm, clear });

完整代码

<script setup lang="ts">
import { onMounted, ref } from "vue";

interface Event {
  touches: TouchList;
  clientX: number;
  clientY: number;
  preventDefault: Function;
}

const ratio = window.devicePixelRatio || 1;

let canvas!: HTMLCanvasElement;
let ctx!: CanvasRenderingContext2D;
let writing = false;

const props = defineProps<{
  width: number;
  height: number;
}>();

const tablet = ref<HTMLCanvasElement>();

/**
 * @description 将payload转换成坐标,主要用于兼容PC跟WAP
 */
const coord = (payload: Partial<Event>) => {
  const { touches, clientX = 0, clientY = 0 } = payload;
  if (touches && touches.length) {
    const { clientX, clientY } = touches[0];
    return {
      clientX,
      clientY,
    };
  }
  return { clientX, clientY };
};

/**
 * @description 落笔
 */
const onStart = (payload: Partial<Event>) => {
  writing = true;
  const { clientX, clientY } = coord(payload);
  ctx.beginPath();
  ctx.moveTo(clientX * ratio, clientY * ratio);
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
};

/**
 * @description 运笔
 */
const onMove = (payload: Partial<Event>) => {
  if (!writing) {
    return;
  }
  const { clientX, clientY } = coord(payload);
  ctx.lineTo(clientX * ratio, clientY * ratio);
  ctx.lineWidth = 3 * ratio;
  ctx.stroke();
  payload.preventDefault?.();
};

/**
 * @description 提笔
 */
const onEnd = () => {
  writing = false;
};

const confirm = () => {
  return canvas.toDataURL();
};

const clear = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width * ratio, canvas.height * ratio);
};

defineExpose({ confirm, clear });

onMounted(() => {
  canvas = tablet.value as HTMLCanvasElement;
  canvas.width = props.width * ratio;
  canvas.height = props.height * ratio;

  ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width * ratio, canvas.height * ratio);
});
</script>

<template>
  <div
    class="tablet"
    :style="{
      width: `${props.width || 300}px`,
      height: `${props.height || 300}px`,
    }"
  >
    <canvas
      ref="tablet"
      @touchstart="onStart"
      @touchmove="onMove"
      @touchend="onEnd"
      @mousedown="onStart"
      @mouseup="onEnd"
      @mousemove="onMove"
    />
  </div>
</template>

<style scoped lang="scss">
.tablet {
  canvas {
    width: 100%;
    height: 100%;
  }
}
</style>

AI解读

这段代码似乎是用JavaScript编写的,它使用了Vue.js框架。它定义了一些函数和变量,用于创建一个画布元素并在其上绘制。 onMounted函数是Vue.js中的一个生命周期钩子,它在组件挂载时运行。它在这里用于设置画布元素及其上下文。 interface Event定义了一个对象,该对象具有触摸事件和客户端坐标的属性。 const ratio变量用于获取窗口的设备像素比率,或默认为1。 **let canvas!let ctx!**变量用于存储对画布元素及其上下文的引用。 ref()函数创建对画布元素的反应性引用。 const coord函数接受事件对象并返回具有触摸事件的客户端坐标或具有鼠标事件的默认客户端坐标的对象。 const onStartconst onMoveconst onEnd函数定义了用户开始、移动或结束在画布上绘制时发生的情况。 const confirm函数返回画布元素的数据URL。 const clear函数清除画布元素。