2 files added
2 files modified
New file |
| | |
| | | /** |
| | | * 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> |
| | |
| | | /> |
| | | </view> |
| | | </view> |
| | | <CSignatureLayer |
| | | v-if="drawSelf" |
| | | ref="drawLayer" |
| | | /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | 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'; |
| | | |
| | |
| | | components: { |
| | | AtInput, |
| | | AtIcon, |
| | | CSignatureLayer, |
| | | }, |
| | | props: { |
| | | // 表单数据资源(表单组件内部机制专用) |
| | |
| | | }, |
| | | data() { |
| | | return { |
| | | // id: 'CUserSignatureCanvas' + Date.now() + parseInt(Math.random() * 10000), |
| | | // cavWidth: 1000, |
| | | // cavHeight: 1600, |
| | | drawSelf: process.env.TARO_ENV === 'weapp', |
| | | }; |
| | | }, |
| | | computed: {}, |
| | |
| | | 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(','); |
| | |
| | | } else if (process.env.TARO_ENV === 'weapp') { |
| | | $(this.$refs.input.$el) |
| | | .find('.at-input__container') |
| | | .append(this.$refs.drawing.$el); |
| | | .append(this.$refs.drawing); |
| | | } |
| | | }, |
| | | }; |
New file |
| | |
| | | /** |
| | | * 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%; |
| | | } |
| | | } |
| | | } |
| | |
| | | .weui-input { |
| | | display: none; |
| | | } |
| | | .at-input__input { |
| | | display: none; |
| | | } |
| | | } |
| | | .c-user-signature-drawing { |
| | | padding: 18px 18px 18px 0; |