Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-sign/cl-sign.uvue
2025-08-03 16:19:19 +08:00

336 lines
7.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="cl-sign" :class="[pt.className]">
<canvas
class="cl-sign__canvas"
ref="canvasRef"
:id="canvasId"
:style="{
height: `${height}px`,
width: `${width}px`
}"
@touchstart="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend="onTouchEnd"
></canvas>
</view>
</template>
<script lang="ts" setup>
import { canvasToPng, getDevicePixelRatio, parsePt, uuid } from "@/cool";
import { computed, getCurrentInstance, nextTick, onMounted, ref, shallowRef, watch } from "vue";
defineOptions({
name: "cl-sign"
});
// 定义组件属性
const props = defineProps({
pt: {
type: Object,
default: () => ({})
},
// 画布宽度
width: {
type: Number,
default: 300
},
// 画布高度
height: {
type: Number,
default: 200
},
// 线条颜色
strokeColor: {
type: String,
default: "#000000"
},
// 线条宽度
strokeWidth: {
type: Number,
default: 3
},
// 背景颜色
backgroundColor: {
type: String,
default: "#ffffff"
},
// 是否启用毛笔效果
enableBrush: {
type: Boolean,
default: true
},
// 最小线条宽度
minStrokeWidth: {
type: Number,
default: 1
},
// 最大线条宽度
maxStrokeWidth: {
type: Number,
default: 6
},
// 速度敏感度
velocitySensitivity: {
type: Number,
default: 0.7
}
});
// 定义事件发射器
const emit = defineEmits(["change", "clear"]);
// 获取当前实例
const { proxy } = getCurrentInstance()!;
// 获取设备像素比
const dpr = getDevicePixelRatio();
// 触摸点类型
type Point = { x: number; y: number; time: number };
// 矩形类型
type Rect = { left: number; top: number };
// 透传样式类型定义
type PassThrough = {
className?: string;
};
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 签名组件画布
const canvasRef = shallowRef<UniElement | null>(null);
// 绘图上下文
let drawCtx: CanvasRenderingContext2D | null = null;
// 生成唯一的canvas ID
const canvasId = `cl-sign__${uuid()}`;
// 触摸状态
const isDrawing = ref(false);
// 上一个触摸点
let lastPoint: Point | null = null;
// 当前线条宽度
let currentStrokeWidth = ref(3);
// 速度缓冲数组(用于平滑速度变化)
const velocityBuffer: number[] = [];
// canvas位置信息缓存
let canvasRect: Rect | null = null;
// 获取canvas位置信息
function getCanvasRect(): Promise<Rect> {
return new Promise((resolve) => {
// #ifdef MP
uni.createSelectorQuery()
.in(proxy)
.select(`#${canvasId}`)
.boundingClientRect((rect: any) => {
if (rect) {
canvasRect = { left: rect.left, top: rect.top };
resolve(canvasRect!);
} else {
resolve({ left: 0, top: 0 });
}
})
.exec();
// #endif
// #ifndef MP
// 非小程序平台在需要时通过DOM获取位置信息
canvasRect = { left: 0, top: 0 };
resolve(canvasRect!);
// #endif
});
}
// 获取触摸点在canvas中的坐标
function getTouchPos(e: TouchEvent): Point {
const touch = e.touches[0];
// #ifdef H5
const rect = (e.target as any).getBoundingClientRect();
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
time: Date.now()
};
// #endif
// #ifndef H5
// 小程序中使用缓存的位置信息或直接使用触摸坐标
const left = canvasRect?.left ?? 0;
const top = canvasRect?.top ?? 0;
return {
x: touch.clientX - left,
y: touch.clientY - top,
time: Date.now()
};
// #endif
}
// 计算速度并返回动态线条宽度
function calculateStrokeWidth(currentPoint: Point): number {
if (lastPoint == null || !props.enableBrush) {
return props.strokeWidth;
}
// 计算距离和时间差
const distance = Math.sqrt(
Math.pow(currentPoint.x - lastPoint!.x, 2) + Math.pow(currentPoint.y - lastPoint!.y, 2)
);
const timeDelta = currentPoint.time - lastPoint!.time;
if (timeDelta <= 0) return currentStrokeWidth.value;
// 计算速度 (像素/毫秒)
const velocity = distance / timeDelta;
// 添加到速度缓冲区(用于平滑)
velocityBuffer.push(velocity);
if (velocityBuffer.length > 5) {
velocityBuffer.shift();
}
// 计算平均速度
const avgVelocity = velocityBuffer.reduce((sum, v) => sum + v, 0) / velocityBuffer.length;
// 根据速度计算线条宽度(速度越快越细)
const normalizedVelocity = Math.min(avgVelocity * props.velocitySensitivity, 1);
const widthRange = props.maxStrokeWidth - props.minStrokeWidth;
const targetWidth = props.maxStrokeWidth - normalizedVelocity * widthRange;
// 平滑过渡到目标宽度
const smoothFactor = 0.3;
return currentStrokeWidth.value + (targetWidth - currentStrokeWidth.value) * smoothFactor;
}
// 触摸开始
async function onTouchStart(e: TouchEvent) {
e.preventDefault();
isDrawing.value = true;
// #ifdef MP
// 小程序中,如果没有缓存位置信息,先获取
if (canvasRect == null) {
await getCanvasRect();
}
// #endif
lastPoint = getTouchPos(e);
// 初始化线条宽度和清空速度缓冲
currentStrokeWidth.value = props.enableBrush ? props.maxStrokeWidth : props.strokeWidth;
velocityBuffer.length = 0;
}
// 触摸移动
function onTouchMove(e: TouchEvent) {
e.preventDefault();
if (!isDrawing.value || lastPoint == null || drawCtx == null) return;
const currentPoint = getTouchPos(e);
// 计算动态线条宽度
const strokeWidth = calculateStrokeWidth(currentPoint);
currentStrokeWidth.value = strokeWidth;
// 绘制线条
drawCtx!.beginPath();
drawCtx!.moveTo(lastPoint!.x * dpr, lastPoint!.y * dpr);
drawCtx!.lineTo(currentPoint.x * dpr, currentPoint.y * dpr);
drawCtx!.strokeStyle = props.strokeColor;
drawCtx!.lineWidth = strokeWidth * dpr;
drawCtx!.lineCap = "round";
drawCtx!.lineJoin = "round";
drawCtx!.stroke();
lastPoint = currentPoint;
emit("change");
}
// 触摸结束
function onTouchEnd(e: TouchEvent) {
e.preventDefault();
isDrawing.value = false;
lastPoint = null;
}
// 清除画布
function clear() {
if (drawCtx == null) return;
// #ifdef APP
drawCtx!.reset();
// #endif
// #ifndef APP
drawCtx!.clearRect(0, 0, props.width * dpr, props.height * dpr);
// #endif
// 填充背景色
drawCtx!.fillStyle = props.backgroundColor;
drawCtx!.fillRect(0, 0, props.width * dpr, props.height * dpr);
emit("clear");
}
// 获取签名图片
function toPng(): Promise<string> {
return canvasToPng(canvasRef.value!);
}
// 初始化画布
function initCanvas() {
uni.createCanvasContextAsync({
id: canvasId,
component: proxy,
success: (context: CanvasContext) => {
// 获取绘图上下文
drawCtx = context.getContext("2d")!;
// 设置宽高
drawCtx!.canvas.width = props.width * dpr;
drawCtx!.canvas.height = props.height * dpr;
// 优化渲染质量
drawCtx!.textBaseline = "middle";
drawCtx!.textAlign = "center";
drawCtx!.miterLimit = 10;
// 初始化背景
clear();
// #ifdef MP
// 小程序中初始化时获取canvas位置信息
getCanvasRect();
// #endif
}
});
}
onMounted(() => {
initCanvas();
watch(
computed(() => [props.width, props.height]),
() => {
nextTick(() => {
initCanvas();
});
}
);
});
defineExpose({
clear,
toPng
});
</script>