Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-sign/cl-sign.uvue

338 lines
7.1 KiB
Plaintext
Raw Normal View History

2025-07-29 10:44:18 +08:00
<template>
<view class="cl-sign" :class="[pt.className]">
<canvas
class="cl-sign__canvas"
2025-07-29 13:26:31 +08:00
ref="canvasRef"
2025-07-29 10:44:18 +08:00
:id="canvasId"
:style="{
2025-07-29 13:26:31 +08:00
height: `${height}px`,
width: `${width}px`
2025-07-29 10:44:18 +08:00
}"
@touchstart="onTouchStart"
2025-07-29 13:26:31 +08:00
@touchmove.stop.prevent="onTouchMove"
2025-07-29 10:44:18 +08:00
@touchend="onTouchEnd"
></canvas>
</view>
</template>
<script lang="ts" setup>
2025-08-03 16:19:19 +08:00
import { canvasToPng, getDevicePixelRatio, parsePt, uuid } from "@/cool";
2025-08-02 17:17:13 +08:00
import { computed, getCurrentInstance, nextTick, onMounted, ref, shallowRef, watch } from "vue";
2025-07-29 10:44:18 +08:00
defineOptions({
name: "cl-sign"
});
2025-08-01 20:35:28 +08:00
// 定义组件属性
2025-07-29 10:44:18 +08:00
const props = defineProps({
pt: {
type: Object,
default: () => ({})
},
// 画布宽度
width: {
type: Number,
2025-08-03 13:40:19 +08:00
default: 300
2025-07-29 10:44:18 +08:00
},
// 画布高度
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
}
});
2025-08-01 20:35:28 +08:00
// 定义事件发射器
2025-08-03 16:19:19 +08:00
const emit = defineEmits(["change", "clear"]);
2025-07-29 10:44:18 +08:00
2025-08-01 20:35:28 +08:00
// 获取当前实例
2025-07-29 10:44:18 +08:00
const { proxy } = getCurrentInstance()!;
2025-08-03 16:19:19 +08:00
// 获取设备像素比
const dpr = getDevicePixelRatio();
2025-07-29 10:44:18 +08:00
// 触摸点类型
type Point = { x: number; y: number; time: number };
2025-07-29 13:26:31 +08:00
// 矩形类型
type Rect = { left: number; top: number };
2025-07-29 10:44:18 +08:00
// 透传样式类型定义
type PassThrough = {
className?: string;
};
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
2025-07-29 13:26:31 +08:00
// 签名组件画布
const canvasRef = shallowRef<UniElement | null>(null);
2025-07-29 10:44:18 +08:00
// 绘图上下文
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[] = [];
2025-07-29 13:26:31 +08:00
// 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
});
}
2025-07-29 10:44:18 +08:00
// 获取触摸点在canvas中的坐标
function getTouchPos(e: TouchEvent): Point {
const touch = e.touches[0];
2025-07-29 13:26:31 +08:00
// #ifdef H5
2025-07-29 10:44:18 +08:00
const rect = (e.target as any).getBoundingClientRect();
2025-07-29 13:26:31 +08:00
2025-07-29 10:44:18 +08:00
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
time: Date.now()
};
2025-07-29 13:26:31 +08:00
// #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
2025-07-29 10:44:18 +08:00
}
// 计算速度并返回动态线条宽度
function calculateStrokeWidth(currentPoint: Point): number {
2025-07-29 13:26:31 +08:00
if (lastPoint == null || !props.enableBrush) {
2025-07-29 10:44:18 +08:00
return props.strokeWidth;
}
// 计算距离和时间差
const distance = Math.sqrt(
2025-07-29 13:26:31 +08:00
Math.pow(currentPoint.x - lastPoint!.x, 2) + Math.pow(currentPoint.y - lastPoint!.y, 2)
2025-07-29 10:44:18 +08:00
);
2025-07-29 13:26:31 +08:00
const timeDelta = currentPoint.time - lastPoint!.time;
2025-07-29 10:44:18 +08:00
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;
}
// 触摸开始
2025-07-29 13:26:31 +08:00
async function onTouchStart(e: TouchEvent) {
2025-07-29 10:44:18 +08:00
e.preventDefault();
isDrawing.value = true;
2025-07-29 13:26:31 +08:00
// #ifdef MP
// 小程序中,如果没有缓存位置信息,先获取
if (canvasRect == null) {
await getCanvasRect();
}
// #endif
2025-07-29 10:44:18 +08:00
lastPoint = getTouchPos(e);
// 初始化线条宽度和清空速度缓冲
currentStrokeWidth.value = props.enableBrush ? props.maxStrokeWidth : props.strokeWidth;
velocityBuffer.length = 0;
}
// 触摸移动
function onTouchMove(e: TouchEvent) {
e.preventDefault();
2025-07-29 13:26:31 +08:00
if (!isDrawing.value || lastPoint == null || drawCtx == null) return;
2025-07-29 10:44:18 +08:00
const currentPoint = getTouchPos(e);
// 计算动态线条宽度
const strokeWidth = calculateStrokeWidth(currentPoint);
currentStrokeWidth.value = strokeWidth;
// 绘制线条
drawCtx!.beginPath();
2025-08-03 16:19:19 +08:00
drawCtx!.moveTo(lastPoint!.x * dpr, lastPoint!.y * dpr);
drawCtx!.lineTo(currentPoint.x * dpr, currentPoint.y * dpr);
2025-07-29 10:44:18 +08:00
drawCtx!.strokeStyle = props.strokeColor;
2025-08-03 16:19:19 +08:00
drawCtx!.lineWidth = strokeWidth * dpr;
2025-07-29 10:44:18 +08:00
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() {
2025-07-29 13:26:31 +08:00
if (drawCtx == null) return;
2025-07-29 10:44:18 +08:00
// #ifdef APP
drawCtx!.reset();
// #endif
2025-07-29 13:26:31 +08:00
2025-07-29 10:44:18 +08:00
// #ifndef APP
2025-08-03 16:19:19 +08:00
drawCtx!.clearRect(0, 0, props.width * dpr, props.height * dpr);
2025-07-29 10:44:18 +08:00
// #endif
// 填充背景色
drawCtx!.fillStyle = props.backgroundColor;
2025-08-03 16:19:19 +08:00
drawCtx!.fillRect(0, 0, props.width * dpr, props.height * dpr);
2025-07-29 10:44:18 +08:00
2025-08-03 16:19:19 +08:00
emit("clear");
2025-07-29 10:44:18 +08:00
}
// 获取签名图片
function toPng(): Promise<string> {
2025-08-02 17:17:13 +08:00
return canvasToPng(canvasRef.value!);
2025-07-29 10:44:18 +08:00
}
// 初始化画布
function initCanvas() {
uni.createCanvasContextAsync({
id: canvasId,
component: proxy,
success: (context: CanvasContext) => {
// 获取绘图上下文
drawCtx = context.getContext("2d")!;
2025-08-19 18:11:00 +08:00
2025-07-29 10:44:18 +08:00
// 设置宽高
2025-08-03 16:19:19 +08:00
drawCtx!.canvas.width = props.width * dpr;
drawCtx!.canvas.height = props.height * dpr;
2025-07-29 10:44:18 +08:00
// 优化渲染质量
drawCtx!.textBaseline = "middle";
drawCtx!.textAlign = "center";
drawCtx!.miterLimit = 10;
// 初始化背景
clear();
2025-07-29 13:26:31 +08:00
// #ifdef MP
// 小程序中初始化时获取canvas位置信息
getCanvasRect();
// #endif
2025-07-29 10:44:18 +08:00
}
});
}
onMounted(() => {
watch(
2025-07-29 13:26:31 +08:00
computed(() => [props.width, props.height]),
2025-07-29 10:44:18 +08:00
() => {
2025-08-02 17:17:13 +08:00
nextTick(() => {
initCanvas();
});
2025-09-03 19:03:39 +08:00
},
{
immediate: true
2025-07-29 10:44:18 +08:00
}
);
});
defineExpose({
clear,
toPng
});
</script>