From 456b21846e549dcfed9524d1c8325c44ec6afafe Mon Sep 17 00:00:00 2001 From: YFeng <499756901@qq.com> Date: Tue, 27 Feb 2024 18:34:38 +0800 Subject: [PATCH] 实现小程序中签名 --- forms/userSignature/cUserSignature.scss | 3 forms/userSignature/CUserSignature.vue | 25 ++ forms/userSignature/CSignatureLayer.vue | 432 +++++++++++++++++++++++++++++++++++++++++++ forms/userSignature/cSignatureLayer.scss | 87 ++++++++ 4 files changed, 542 insertions(+), 5 deletions(-) diff --git a/forms/userSignature/CSignatureLayer.vue b/forms/userSignature/CSignatureLayer.vue new file mode 100644 index 0000000..23f6766 --- /dev/null +++ b/forms/userSignature/CSignatureLayer.vue @@ -0,0 +1,432 @@ +/** + * CSignatureLayer - 签名层 + * @author Tevin + */ + +<template> + <AtFloatLayout + class="c-signature-layer" + title="手写板 / 请书写签名" + :isOpened="layerOpened" + :onClose="evt => null" + > + <view class="c-signature-layer-draw"> + <view class="size-box-top"></view> + <view class="size-box-bottom"></view> + <canvas + class="drawing" + ref="drawing" + :canvasId="cavId" + :width="cavWidth" + :height="cavHeight" + :disableScroll="true" + @touchstart="evt => handleWriteStart(evt)" + @touchmove="evt => handleWriteMove(evt)" + @touchend="evt => handleWriteEnd(evt)" + ></canvas> + </view> + <view class="c-signature-layer-btns"> + <AtButton + class="btn-warning" + type="primary" + size="large" + :onClick="evt => handleRestDraw()" + >重写</AtButton> + <AtButton + type="primary" + size="large" + :onClick="evt => handleSaveDraw()" + >完成</AtButton> + </view> + </AtFloatLayout> +</template> + +<script> +import Taro from '@tarojs/taro'; +import { $ } from '@tarojs/extend'; +import { AtFloatLayout, AtButton } from 'taro-ui-vue'; +import './cSignatureLayer.scss'; +import { Tools } from '@components/common/Tools'; + +export default { + name: 'CSignatureLayer', + components: { + AtFloatLayout, + AtButton, + }, + props: {}, + data() { + return { + cavId: 'signCanvas-' + Date.now() + '-' + parseInt(Math.random() * 10000), + layerOpened: false, + cavWidth: 0, + cavHeight: 0, + curPoint: {}, + lastPoint: {}, + curLine: [], + // 第一次触发 + firstTouch: true, + // 所有笔迹 + chirography: [], + // 初始画圆的半径 + radius: 1, + }; + }, + methods: { + $onDraw(callback) { + this.layerOpened = true; + this._callback = callback; + this.handleRestDraw(); + }, + handleRestDraw() { + this.firstTouch = true; + this.curLine = []; + this.chirography = []; + this.canvasContext.clearRect(0, 0, this.cavWidth, this.cavHeight); + this.canvasContext.draw(); + }, + handleWriteStart(evt) { + if (evt.type != 'touchstart') { + return false; + } + this.canvasContext.setFillStyle('#1A1A1A'); + const point = { + x: evt.touches[0].x, + y: evt.touches[0].y, + }; + this.curLine.unshift({ + time: Date.now(), + dis: 0, + x: point.x, + y: point.y, + }); + if (this.firstTouch) { + this.cutArea = { + top: point.y, + right: point.x, + bottom: point.y, + left: point.x, + }; + this.firstTouch = false; + } + this._pointToLine(this.curLine); + }, + handleWriteMove(evt) { + if (evt.type != 'touchmove') { + return false; + } + if (evt.cancelable) { + // 判断默认行为是否已经被禁用 + if (!evt.defaultPrevented) { + evt.preventDefault(); + } + } + const point = { + x: evt.touches[0].x, + y: evt.touches[0].y, + }; + this._updateCutArea(point); + this.lastPoint = this.curPoint; + this.curPoint = point; + this.curLine.unshift({ + time: Date.now(), + dis: this._distance(point, this.lastPoint), + x: point.x, + y: point.y, + }); + this._pointToLine(this.curLine); + }, + handleWriteEnd(evt) { + if (evt.type != 'touchend') { + return 0; + } + const point = { + x: evt.changedTouches[0].x, + y: evt.changedTouches[0].y, + }; + this.lastPoint = this.curPoint; + this.curPoint = point; + this.curLine.unshift({ + time: Date.now(), + dis: this._distance(point, this.lastPoint), + x: point.x, + y: point.y, + }); + this._pointToLine(this.curLine); + this.chirography.unshift(this.curLine); + this.curLine = []; + }, + // 更新裁剪区域 + _updateCutArea(point) { + if (point.y < this.cutArea.top) { + this.cutArea.top = point.y; + } + if (point.y < 0) { + this.cutArea.top = 0; + } + if (point.x > this.cutArea.right) { + this.cutArea.right = point.x; + } + if (this.cavWidth - point.x <= 0) { + this.cutArea.right = this.cavWidth; + } + if (point.y > this.cutArea.bottom) { + this.cutArea.bottom = point.y; + } + if (this.cavHeight - point.y <= 0) { + this.cutArea.bottom = this.cavHeight; + } + if (point.x < this.cutArea.left) { + this.cutArea.left = point.x; + } + if (point.x < 0) { + this.cutArea.left = 0; + } + }, + // 求两点之间距离 + _distance(a, b) { + let x = b.x - a.x; + let y = b.y - a.y; + return Math.sqrt(x * x + y * y); + }, + // 绘制笔迹 + _pointToLine(line) { + this._calcBethelLine(line); + }, + // 计算插值的方式 + _calcBethelLine(line) { + if (line.length <= 1) { + line[0].r = this.radius; + return; + } + // 笔迹倍数 + const lineSize = 1.5; + // 最小笔画半径 + const lineMin = 0.5; + // 最大笔画半径 + const lineMax = 4; + // 默认压力 + const pressure = 1; + // 顺滑度,用60的距离来计算速度 + const smoothness = 60; + // 开始计算 + let x0, + x1, + x2, + y0, + y1, + y2, + r0, + r1, + r2, + len, + lastRadius, + dis = 0, + time = 0, + curveValue = 0.5; + if (line.length <= 2) { + x0 = line[1].x; + y0 = line[1].y; + x2 = line[1].x + (line[0].x - line[1].x) * curveValue; + y2 = line[1].y + (line[0].y - line[1].y) * curveValue; + //x2 = line[1].x; + //y2 = line[1].y; + x1 = x0 + (x2 - x0) * curveValue; + y1 = y0 + (y2 - y0) * curveValue; + } else { + x0 = line[2].x + (line[1].x - line[2].x) * curveValue; + y0 = line[2].y + (line[1].y - line[2].y) * curveValue; + x1 = line[1].x; + y1 = line[1].y; + x2 = x1 + (line[0].x - x1) * curveValue; + y2 = y1 + (line[0].y - y1) * curveValue; + } + // 从计算公式看,三个点分别是(x0,y0),(x1,y1),(x2,y2) ;(x1,y1)这个是控制点,控制点不会落在曲线上;实际上,这个点还会手写获取的实际点,却落在曲线上 + len = this._distance({ x: x2, y: y2 }, { x: x0, y: y0 }); + lastRadius = this.radius; + for (let n = 0; n < line.length - 1; n++) { + dis += line[n].dis; + time += line[n].time - line[n + 1].time; + if (dis > smoothness) { + break; + } + } + this.radius = Math.min((time / len) * pressure + lineMin, lineMax) * lineSize; + line[0].r = this.radius; + //计算笔迹半径; + if (line.length <= 2) { + r0 = (lastRadius + this.radius) / 2; + r1 = r0; + r2 = r1; + //return; + } else { + r0 = (line[2].r + line[1].r) / 2; + r1 = line[1].r; + r2 = (line[1].r + line[0].r) / 2; + } + let n = 5; + let point = []; + for (let i = 0; i < n; i++) { + let t = i / (n - 1); + let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2; + let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2; + let r = lastRadius + ((this.radius - lastRadius) / n) * i; + point.push({ x: x, y: y, r: r }); + if (point.length == 3) { + let a = this._ctaCalc( + point[0].x, + point[0].y, + point[0].r, + point[1].x, + point[1].y, + point[1].r, + point[2].x, + point[2].y, + point[2].r + ); + this._bethelDraw(a, true); + point = [{ x: x, y: y, r: r }]; + } + } + this.curLine = line; + }, + _ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) { + let a = [], + vx01, + vy01, + norm, + n_x0, + n_y0, + vx21, + vy21, + n_x2, + n_y2; + vx01 = x1 - x0; + vy01 = y1 - y0; + norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2; + vx01 = (vx01 / norm) * r0; + vy01 = (vy01 / norm) * r0; + n_x0 = vy01; + n_y0 = -vx01; + vx21 = x1 - x2; + vy21 = y1 - y2; + norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2; + vx21 = (vx21 / norm) * r2; + vy21 = (vy21 / norm) * r2; + n_x2 = -vy21; + n_y2 = vx21; + a.push({ mx: x0 + n_x0, my: y0 + n_y0, color: '#1A1A1A' }); + a.push({ + c1x: x1 + n_x0, + c1y: y1 + n_y0, + c2x: x1 + n_x2, + c2y: y1 + n_y2, + ex: x2 + n_x2, + ey: y2 + n_y2, + }); + a.push({ + c1x: x2 + n_x2 - vx21, + c1y: y2 + n_y2 - vy21, + c2x: x2 - n_x2 - vx21, + c2y: y2 - n_y2 - vy21, + ex: x2 - n_x2, + ey: y2 - n_y2, + }); + a.push({ + c1x: x1 - n_x2, + c1y: y1 - n_y2, + c2x: x1 - n_x0, + c2y: y1 - n_y0, + ex: x0 - n_x0, + ey: y0 - n_y0, + }); + a.push({ + c1x: x0 - n_x0 - vx01, + c1y: y0 - n_y0 - vy01, + c2x: x0 + n_x0 - vx01, + c2y: y0 + n_y0 - vy01, + ex: x0 + n_x0, + ey: y0 + n_y0, + }); + a[0].mx = a[0].mx.toFixed(1); + a[0].mx = parseFloat(a[0].mx); + a[0].my = a[0].my.toFixed(1); + a[0].my = parseFloat(a[0].my); + for (let i = 1; i < a.length; i++) { + a[i].c1x = a[i].c1x.toFixed(1); + a[i].c1x = parseFloat(a[i].c1x); + a[i].c1y = a[i].c1y.toFixed(1); + a[i].c1y = parseFloat(a[i].c1y); + a[i].c2x = a[i].c2x.toFixed(1); + a[i].c2x = parseFloat(a[i].c2x); + a[i].c2y = a[i].c2y.toFixed(1); + a[i].c2y = parseFloat(a[i].c2y); + a[i].ex = a[i].ex.toFixed(1); + a[i].ex = parseFloat(a[i].ex); + a[i].ey = a[i].ey.toFixed(1); + a[i].ey = parseFloat(a[i].ey); + } + return a; + }, + _bethelDraw(point, isFill) { + const ctx = this.canvasContext; + ctx.beginPath(); + ctx.moveTo(point[0].mx, point[0].my); + for (let i = 1; i < point.length; i++) { + ctx.bezierCurveTo( + point[i].c1x, + point[i].c1y, + point[i].c2x, + point[i].c2y, + point[i].ex, + point[i].ey + ); + } + ctx.stroke(); + if (isFill !== undefined) { + // 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 + ctx.fill(); + } + ctx.draw(true); + }, + handleSaveDraw() { + if (this.firstTouch) { + Tools.toast('请书写签名!'); + } + const delta = 20; + const clipArea = { x: 0, y: 0, w: 0, h: 0 }; + clipArea.x = Math.max(this.cutArea.left - delta, 0); + clipArea.y = Math.max(this.cutArea.top - delta, 0); + const realRight = Math.min(this.cutArea.right + delta, this.cavWidth); + const realBottom = Math.min(this.cutArea.bottom + delta, this.cavHeight); + clipArea.w = realRight - clipArea.x; + clipArea.h = realBottom - clipArea.y; + Taro.canvasToTempFilePath({ + canvasId: this.cavId, + x: clipArea.x, + y: clipArea.y, + width: clipArea.w, + height: clipArea.h, + destWidth: clipArea.w, + destHeight: clipArea.h, + quality: 0.6, + fileType: 'jpeg', + success: res => { + this._callback(res.tempFilePath); + this.layerOpened = false; + }, + }); + }, + }, + mounted() { + this.$nextTick(() => { + this.canvasContext = Taro.createCanvasContext(this.cavId, this); + const $container = $(this.$refs.drawing).parent(); + setTimeout(() => { + $container.width().then(w => (this.cavWidth = w)); + $container.height().then(h => (this.cavHeight = h)); + }, 0); + }); + }, +}; +</script> \ No newline at end of file diff --git a/forms/userSignature/CUserSignature.vue b/forms/userSignature/CUserSignature.vue index f04bf42..8182423 100644 --- a/forms/userSignature/CUserSignature.vue +++ b/forms/userSignature/CUserSignature.vue @@ -32,6 +32,10 @@ /> </view> </view> + <CSignatureLayer + v-if="drawSelf" + ref="drawLayer" + /> </view> </template> @@ -43,6 +47,7 @@ import { $fetchCommon } from '@fetchers/FCommon'; import { $bridge } from '@components/common/Bridge'; import { $hostBoot } from '@components/bases/HostBoot'; +import CSignatureLayer from '@components/forms/userSignature/CSignatureLayer'; import project from '@project'; import './cUserSignature.scss'; @@ -51,6 +56,7 @@ components: { AtInput, AtIcon, + CSignatureLayer, }, props: { // 表单数据资源(表单组件内部机制专用) @@ -58,9 +64,7 @@ }, data() { return { - // id: 'CUserSignatureCanvas' + Date.now() + parseInt(Math.random() * 10000), - // cavWidth: 1000, - // cavHeight: 1600, + drawSelf: process.env.TARO_ENV === 'weapp', }; }, computed: {}, @@ -76,7 +80,18 @@ this.itemRes.onChange(url); }); } - // TODO: 普通h5、小程序中,使用 canvas 签名 + // 小程序中 + else if (process.env.TARO_ENV === 'weapp') { + this.$refs.drawLayer.$onDraw(sign => { + if (sign.indexOf('http') >= 0) { + this.itemRes.onChange(sign); + } else { + const url = this._transBase64ToBlob(sign); + this.itemRes.onChange(url); + } + }); + } + // TODO: 普通h5,使用 canvas 签名 }, _transBase64ToBlob(base64) { const arr = base64.split(','); @@ -160,7 +175,7 @@ } else if (process.env.TARO_ENV === 'weapp') { $(this.$refs.input.$el) .find('.at-input__container') - .append(this.$refs.drawing.$el); + .append(this.$refs.drawing); } }, }; diff --git a/forms/userSignature/cSignatureLayer.scss b/forms/userSignature/cSignatureLayer.scss new file mode 100644 index 0000000..3a1d751 --- /dev/null +++ b/forms/userSignature/cSignatureLayer.scss @@ -0,0 +1,87 @@ +/** + * cUserSignature + * @author Tevin + */ + +@import "../../common/sassMixin"; + +.c-signature-layer { + &.at-float-layout { + .at-float-layout__container { + height: 94%; + max-height: 100%; + } + .layout-body { + padding: 0; + } + .layout-body__content { + height: 100%; + } + } + .c-signature-layer-btns { + @include position(absolute, n 0 0 n); + width: 100%; + height: 92px; + font-size: 0; + .at-button { + width: 50%; + border-radius: 0; + } + } + .c-signature-layer-draw { + position: relative; + height: calc(100% - 92px); + .size-box-top { + @include position(absolute, 3% 3% n n); + width: 94%; + height: 36px; + pointer-events: none; + &::before { + @include position(absolute, 0 0 n n); + width: 36px; + height: 36px; + border-top: #ccc 1PX solid; + border-left: #ccc 1PX solid; + border-top-left-radius: 8px; + content: " "; + } + &::after { + @include position(absolute, 0 n n 0); + width: 36px; + height: 36px; + border-top: #ccc 1PX solid; + border-right: #ccc 1PX solid; + border-top-right-radius: 8px; + content: " "; + } + } + .size-box-bottom { + @include position(absolute, n 3% 3% n); + width: 94%; + height: 36px; + pointer-events: none; + &::before { + @include position(absolute, n 0 0 n); + width: 36px; + height: 36px; + border-bottom: #ccc 1PX solid; + border-left: #ccc 1PX solid; + border-bottom-left-radius: 8px; + content: " "; + } + &::after { + @include position(absolute, n n 0 0); + width: 36px; + height: 36px; + border-bottom: #ccc 1PX solid; + border-right: #ccc 1PX solid; + border-bottom-right-radius: 8px; + content: " "; + } + } + .drawing { + width: 100%; + height: 100%; + } + } +} \ No newline at end of file diff --git a/forms/userSignature/cUserSignature.scss b/forms/userSignature/cUserSignature.scss index 987bc61..4f1ac34 100644 --- a/forms/userSignature/cUserSignature.scss +++ b/forms/userSignature/cUserSignature.scss @@ -13,6 +13,9 @@ .weui-input { display: none; } + .at-input__input { + display: none; + } } .c-user-signature-drawing { padding: 18px 18px 18px 0; -- Gitblit v1.9.1