Files
2026-09-24 16:25:22 +08:00

427 lines
11 KiB
Plaintext

<script lang="ts">
import { nextTick, ComponentInternalInstance } from "vue"
import { px2dp, checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type HUIZHI_TYPE = {
width : string,
height : string,
backgroundColor : string,
strokeColor : string,
strokeWidth : number,
}
type EvtCustomDetail = {
x : number,
y : number,
type : string
}
type PointVegByXsignBorad = {
x : number,
y : number,
lineWidth:number
}
/**
* @name 签名板 xSignBoard
* @description 手写签名板,适合需要签字的场景
* @page /pages/index/sign-board
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
ctx: null as null | CanvasRenderingContext2D,
canvas: null as UniCanvasElement | null,
isDrawer: false,
id: "xSignBoard" + getUid(),
ratio: 1,
_x: 0,
_y: 0,
_left: 0,
_top: 0,
tid: 0,
realWidth: 0,
realHeight: 0,
// 添加笔峰相关变量
lastTime: 0,
lastX: 0,
lastY: 0,
diffXY: 1, // 差异,比如两点之间小于1就不会存储。可减少点数提高 性能。
ponits: [] as PointVegByXsignBorad[][],
currentPath:[] as PointVegByXsignBorad[],
step:0
}
},
props: {
/**
* 宽度,可任意单位
*/
width: {
type: String,
default: '100%'
},
/**
* 高度,可任意单位
*/
height: {
type: String,
default: '150'
},
/**
* 背景颜色
*/
backgroundColor: {
type: String,
default: 'rgba(0,0,0,0)'
},
/**
* 画笔颜色
*/
strokeColor: {
type: String,
default: 'primary'
},
/**
* 画笔粗细
*/
strokeWidth: {
type: Number,
default: 8
}
},
computed: {
_props() : HUIZHI_TYPE {
return {
width: checkIsCssUnit(this.width, xConfig.unit),
height: checkIsCssUnit(this.height, xConfig.unit),
backgroundColor: getDefaultColor(this.backgroundColor),
strokeColor: getDefaultColor(this.strokeColor),
strokeWidth: this.strokeWidth,
} as HUIZHI_TYPE
}
},
mounted() {
this.init()
// #ifdef WEB||MP
this.getNodes();
// #endif
// #ifdef APP
let t = this;
this.tid = setTimeout(function () {
t.getNodes();
}, 120);
// #endif
uni.$on('onResize', this.getNodes)
},
beforeUnmount() {
clearTimeout(this.tid)
uni.$off('onResize', this.getNodes)
},
methods: {
init() {
let t = this;
let ratio = uni.getWindowInfo().pixelRatio;
uni.createCanvasContextAsync({
id: t.id as string,
component: t,
success(context) {
let ctx:CanvasRenderingContext2D|null = context.getContext('2d')!;
let canvas = ctx!.canvas!
if (ctx == null) return;
canvas.width = canvas.offsetWidth * ratio
canvas.height = canvas.offsetHeight * ratio
t.realWidth = canvas.width
t.realHeight = canvas.height
ctx.scale(ratio, ratio);
t.canvas = canvas!;
t.ctx = ctx!;
ratio = 1;
t.ratio = ratio;
t.clear()
},
fail() {
uni.showToast({ title: '错误', icon: 'none' })
}
})
},
eventDips(evt : EvtCustomDetail) {
if (evt.type == 'start') {
this.isDrawer = true;
this._x = evt.x - this._left
this._y = evt.y - this._top
// 初始化笔峰相关变量
this.lastTime = Date.now()
this.lastX = this._x
this.lastY = this._y
// 初始化新的路径组
this.currentPath = [] as PointVegByXsignBorad[]
// 存储第一个点
this.currentPath.push({x: this._x, y: this._y,lineWidth:this._props.strokeWidth } as PointVegByXsignBorad)
}
if (evt.type == 'move') {
if (this.isDrawer) {
let ctx = this.ctx!
let new_x = evt.x - this._left
let new_y = evt.y - this._top
let currentTime = Date.now()
// 根据速度调整笔画粗细,速度越快笔画越细
let width = this._props.strokeWidth
// #ifndef APP-HARMONY
// 计算速度和笔画粗细
let deltaTime = currentTime - this.lastTime
let distance = Math.sqrt(Math.pow(new_x - this.lastX, 2) + Math.pow(new_y - this.lastY, 2))
let speed = distance / deltaTime
width = Math.max(this._props.strokeWidth * (1 - speed * 0.5), this._props.strokeWidth * 0.4)
// #endif
// 判断是否需要存储当前点
let lastStoredPoint = this.currentPath[this.currentPath.length - 1]
let distanceFromLastStored = Math.sqrt(
Math.pow(new_x - lastStoredPoint.x, 2) +
Math.pow(new_y - lastStoredPoint.y, 2)
)
// 当距离大于阈值且当前路径点数小于最大限制时存储点
if (distanceFromLastStored > this.diffXY && this.currentPath.length < 100) {
this.currentPath.push({x: new_x, y: new_y,lineWidth:width} as PointVegByXsignBorad)
}
ctx.beginPath();
ctx.strokeStyle = this._props.strokeColor
// #ifndef APP-HARMONY
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// #endif
ctx.lineWidth = width * this.ratio;
ctx.moveTo(this._x * this.ratio, this._y * this.ratio);
ctx.lineTo(new_x * this.ratio, new_y * this.ratio);
ctx.stroke();
// 更新位置和时间
this._x = new_x
this._y = new_y
this.lastTime = currentTime
this.lastX = new_x
this.lastY = new_y
ctx.closePath()
}
}
if (evt.type == 'end') {
this.isDrawer = false;
this._x = evt.x - this._x
this._y = evt.y - this._y
// 将当前路径组添加到ponits数组中
if (this.currentPath.length > 1) {
this.ponits.push(this.currentPath)
}
}
},
mStart(evt : UniTouchEvent) {
evt.stopPropagation()
evt.preventDefault()
let t = this;
let event = evt.changedTouches[0];
uni.createSelectorQuery().in(t)
.select(".canvas")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
t._left = nodeinfo.left!;
t._top = nodeinfo.top!;
// #ifdef WEB
this._top += uni.getWindowInfo().windowTop;
// #endif
t.eventDips({ x: event.clientX, y: event.clientY, type: 'start' } as EvtCustomDetail)
})
},
mMove(evt : UniTouchEvent) {
if (!this.isDrawer) return
evt.stopPropagation()
evt.preventDefault()
let event = evt.changedTouches[0];
this.eventDips({ x: event.clientX, y: event.clientY, type: 'move' } as EvtCustomDetail)
},
mEnd(evt : UniTouchEvent) {
let event = evt.changedTouches[0];
this.eventDips({ x: event.clientX, y: event.clientY, type: 'end' } as EvtCustomDetail)
},
// #ifdef WEB
mmStart(evt : UniMouseEvent) {
this.eventDips({ x: evt.clientX, y: evt.clientY, type: 'start' } as EvtCustomDetail)
},
mmMove(evt : UniMouseEvent) {
if (!this.isDrawer) return
evt.stopPropagation()
evt.preventDefault()
this.eventDips({ x: evt.clientX, y: evt.clientY, type: 'move' } as EvtCustomDetail)
},
mmEnd(evt : UniMouseEvent) {
this.eventDips({ x: evt.clientX, y: evt.clientY, type: 'end' } as EvtCustomDetail)
},
// #endif
/**
* 获取当前的笔画数量
* 这是个大概值,并非完全准确,但至少可以验证
* 出用户写了多少笔,是否真的签名了。
* @public
*/
getLineCount():number{
return this.ponits.length;
},
/**
* 清空画布
* @public
*/
clear() {
let ctx = this.ctx!;
let canvas = this.canvas!;
this.ponits = [];
this.step = 0
ctx.fillStyle = 'rgba(0,0,0,0)'
ctx.fillRect(0, 0, this.realWidth*this.ratio, this.realHeight*this.ratio)
ctx.reset();
let ratio = uni.getWindowInfo().pixelRatio;
ctx.scale(ratio,ratio)
},
drawOffC(pos : PointVegByXsignBorad[][]) {
let ctx = this.ctx!;
let canvas = this.canvas!;
ctx.fillStyle = 'rgba(0,0,0,0)'
ctx.fillRect(0, 0, this.realWidth, this.realHeight)
// #ifdef MP||WEB
ctx!.clearRect(0, 0, this.realWidth, this.realHeight)
// #endif
// #ifdef APP
ctx.reset();
// #endif
ctx!.strokeStyle = this._props.strokeColor
pos.forEach((el : PointVegByXsignBorad[]) => {
let p = el[0]
let x0 = p.x
let y0 = p.y
ctx!.lineWidth = p.lineWidth * this.ratio;
ctx!.beginPath();
// ctx.moveTo(px2dp(x0), px2dp(y0));
el.forEach((d : PointVegByXsignBorad) => {
let x1 = d.x
let y1 = d.y
ctx!.moveTo(x0 * this.ratio, y0 * this.ratio);
ctx!.lineTo(x1 * this.ratio, y1 * this.ratio);
x0 = x1;
y0 = y1;
// ctx.moveTo(px2dp(x0), px2dp(y0));
ctx!.lineCap = 'round';
ctx!.stroke();
})
ctx!.closePath()
})
},
/**
* 回退的步数,暂不开放
* @param {number} n - 回退的步数
*/
back(n : number) {
this.step += n
if (this.ponits.length - this.step < 0 || this.step < 0) {
uni.showToast({ title: "超出步骤数", icon: 'none' })
} else {
let newdata = this.ponits.slice(0, this.ponits.length - (this.step+1))
this.drawOffC(newdata)
}
},
/**
* 前进的步数,暂不开放
* @param {number} n - 前进的步数
*/
go(n : number) {
this.step -= n
if (this.step < 0) {
uni.showToast({ title: "超出步骤数", icon: 'none' })
} else {
let newdata = this.ponits.slice(0, this.ponits.length - this.step)
this.drawOffC(newdata)
}
},
getNodes() {
uni.createSelectorQuery().in(this)
.select(".canvas")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
this._left = nodeinfo.left!;
this._top = nodeinfo.top!;
// #ifdef WEB
this._top += uni.getWindowInfo().windowTop;
// #endif
})
},
/**
* 获取图片数据
* @public
* @returns {Promise<UTSJSONObject>} - {src:图片路径,width:图片宽,height:图片高,error:错误信息}
*/
getImage() : Promise<UTSJSONObject> {
let t = this;
let canvas = this.canvas!;
let width = this.canvas?.offsetWidth;
let height = this.canvas?.offsetHeight
return new Promise((res, rej) => {
var imageData = t.canvas!.toDataURL('image/png');
res({
src: imageData,
width: width,
height: height,
error: "成功"
} as UTSJSONObject)
})
},
},
}
</script>
<template>
<canvas :id="id" ref="canvas" type="2d" class="canvas"
@touchstart="mStart"
@touchend="mEnd"
@touchmove.stop="mMove"
<!-- #ifdef WEB -->
@mousedown="mmStart"
@mouseup="mmEnd"
@mousemove.stop="mmMove"
@mouseleave="mmEnd"
<!-- #endif -->
:style="{width:_props.width,height:_props.height}"></canvas>
</template>
<style scoped lang="scss">
</style>