123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- <template>
- <view v-if="show" class="tn-sign-board-class tn-sign-board" :style="{top: `${customBarHeight}px`, height: `calc(100% - ${customBarHeight}px)`}">
- <!-- 签名canvas -->
- <view class="tn-sign-board__content">
- <view class="tn-sign-board__content__wrapper">
- <canvas class="tn-sign-board__content__canvas" :canvas-id="canvasName" :disableScroll="true" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"></canvas>
- </view>
- </view>
-
- <!-- 底部工具栏 -->
- <view class="tn-sign-board__tools">
- <!-- 可选颜色 -->
- <view class="tn-sign-board__tools__color">
- <view
- v-for="(item, index) in signSelectColor"
- :key="index"
- class="tn-sign-board__tools__color__item"
- :class="[{'tn-sign-board__tools__color__item--active': currentSelectColor === item}]"
- :style="{backgroundColor: item}"
- @tap="colorSwitch(item)"
- ></view>
- </view>
-
- <!-- 按钮 -->
- <view class="tn-sign-board__tools__button">
- <view class="tn-sign-board__tools__button__item tn-bg-red" @tap="reDraw">清除</view>
- <view class="tn-sign-board__tools__button__item tn-bg-blue" @tap="save">保存</view>
- <view class="tn-sign-board__tools__button__item tn-bg-indigo" @tap="previewImage">预览</view>
- <view class="tn-sign-board__tools__button__item tn-bg-orange" @tap="closeBoard">关闭</view>
- </view>
- </view>
-
- <!-- 伪全屏生成旋转图片canvas容器,不在页面上展示 -->
- <view style="position: fixed; left: -2000px;width: 0;height: 0;overflow: hidden;">
- <canvas canvas-id="temp-tn-sign-canvas" :style="{width: `${canvasHeight}px`, height: `${canvasHeight}px`}"></canvas>
- </view>
- </view>
- </template>
- <script>
- export default {
- name: 'tn-sign-board',
- props: {
- // 是否显示
- show: {
- type: Boolean,
- default: false
- },
- // 可选签名颜色
- signSelectColor: {
- type: Array,
- default() {
- return ['#080808', '#E83A30']
- }
- },
- // 是否旋转输出图片
- rotate: {
- type: Boolean,
- default: true
- },
- // 自定义顶栏的高度
- customBarHeight: {
- type: [String, Number],
- default: 0
- }
- },
- data() {
- return {
- canvasName: 'tn-sign-canvas',
- ctx: null,
- canvasWidth: 0,
- canvasHeight: 0,
- currentSelectColor: this.signSelectColor[0],
- // 第一次触摸
- firstTouch: false,
- // 透明度
- transparent: 1,
- // 笔迹倍数
- lineSize: 1.5,
- // 最小画笔半径
- minLine: 0.5,
- // 最大画笔半径
- maxLine: 4,
- // 画笔压力
- pressure: 1,
- // 顺滑度,用60的距离来计算速度
- smoothness: 60,
- // 当前触摸的点
- currentPoint: {},
- // 当前线条
- currentLine: [],
- // 画笔圆半径
- radius: 1,
- // 裁剪区域
- cutArea: {
- top: 0,
- right: 0,
- bottom: 0,
- left: 0
- },
- // 所有线条, 生成贝塞尔点
- // bethelPoint: [],
- // 上一个点
- lastPoint: 0,
- // 笔迹
- chirography: [],
- // 当前笔迹
- // currentChirography: {},
- // 画线轨迹,生成线条的实际点
- linePrack: []
- }
- },
- watch: {
- show(value) {
- if (value && this.canvasWidth === 0 && this.canvasHeight === 0) {
- this.$nextTick(() => {
- this.getCanvasInfo()
- })
- }
- },
- signSelectColor(value) {
- if (value.length > 0) {
- this.currentSelectColor = value[0]
- }
- }
- },
- created() {
- // 创建canvas
- this.ctx = uni.createCanvasContext(this.canvasName, this)
- },
- mounted() {
- // 获取画板的相关信息
- // this.$nextTick(() => {
- // this.getCanvasInfo()
- // })
- },
- methods: {
- // 获取画板的相关信息
- getCanvasInfo() {
- this._tGetRect('.tn-sign-board__content__canvas').then(res => {
- this.canvasWidth = res.width
- this.canvasHeight = res.height
-
- // 初始化Canvas
- this.$nextTick(() => {
- this.initCanvas('#FFFFFF')
- })
- })
- },
- // 初始化Canvas
- initCanvas(color) {
- /* 将canvas背景设置为 白底,不设置 导出的canvas的背景为透明 */
- // rect() 参数说明 矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
- // 矩形的宽高需要减去边框的宽度
- this.ctx.rect(0, 0, this.canvasWidth - uni.upx2px(4), this.canvasHeight - uni.upx2px(4))
- this.ctx.setFillStyle(color)
- this.ctx.fill()
- this.ctx.draw()
- },
- // 开始画
- onTouchStart(e) {
- if (e.type != 'touchstart') return false
-
- // 设置线条颜色
- this.ctx.setFillStyle(this.currentSelectColor)
- // 设置透明度
- this.ctx.setGlobalAlpha(this.transparent)
- let currentPoint = {
- x: e.touches[0].x,
- y: e.touches[0].y
- }
- let currentLine = this.currentLine
- currentLine.unshift({
- time: new Date().getTime(),
- dis: 0,
- x: currentPoint.x,
- y: currentPoint.y
- })
- this.currentPoint = currentPoint
-
- if (this.firstTouch) {
- this.cutArea = {
- top: currentPoint.y,
- right: currentPoint.x,
- bottom: currentPoint.y,
- left: currentPoint.x
- }
- this.firstTouch = false
- }
-
- this.pointToLine(currentLine)
- },
- // 正在画
- onTouchMove(e) {
- if (e.type != 'touchmove') return false
- if (e.cancelable) {
- // 判断默认行为是否已经被禁用
- if (!e.defaultPrevented) {
- e.preventDefault()
- }
- }
- let point = {
- x: e.touches[0].x,
- y: e.touches[0].y
- }
-
- 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.canvasWidth - point.x <= 0) {
- this.cutArea.right = this.canvasWidth
- }
- if (point.y > this.cutArea.bottom) {
- this.cutArea.bottom = this.canvasHeight
- }
- if (this.canvasHeight - point.y <= 0) {
- this.cutArea.bottom = this.canvasHeight
- }
- if (point.x < this.cutArea.left) {
- this.cutArea.left = point.x
- }
- if (point.x < 0) this.cutArea.left = 0
-
- this.lastPoint = this.currentPoint
- this.currentPoint = point
-
- let currentLine = this.currentLine
- currentLine.unshift({
- time: new Date().getTime(),
- dis: this.distance(this.currentPoint, this.lastPoint),
- x: point.x,
- y: point.y
- })
-
- this.pointToLine(currentLine)
- },
- // 移动结束
- onTouchEnd(e) {
- if (e.type != 'touchend') return false
- let point = {
- x: e.changedTouches[0].x,
- y: e.changedTouches[0].y
- }
- this.lastPoint = this.currentPoint
- this.currentPoint = point
-
- let currentLine = this.currentLine
- currentLine.unshift({
- time: new Date().getTime(),
- dis: this.distance(this.currentPoint, this.lastPoint),
- x: point.x,
- y: point.y
- })
-
- //一笔结束,保存笔迹的坐标点,清空,当前笔迹
- //增加判断是否在手写区域
- this.pointToLine(currentLine)
- let currentChirography = {
- lineSize: this.lineSize,
- lineColor: this.currentSelectColor
- }
-
- let chirography = this.chirography
- chirography.unshift(currentChirography)
- this.chirography = chirography
-
- let linePrack = this.linePrack
- linePrack.unshift(this.currentLine)
- this.linePrack = linePrack
- this.currentLine = []
- },
- // 重置绘画板
- reDraw() {
- this.initCanvas('#FFFFFF')
- },
- // 保存
- save() {
- // 在组件内使用需要第二个参数this
- uni.canvasToTempFilePath({
- canvasId: this.canvasName,
- fileType: 'png',
- quality: 1,
- success: (res) => {
- if (this.rotate) {
- this.getRotateImage(res.tempFilePath).then((res) => {
- this.$emit('save', res)
- }).catch(err => {
- this.$t.message.toast('旋转图片失败')
- })
- } else {
- this.$emit('save', res.tempFilePath)
- }
- },
- fail: () => {
- this.$t.message.toast('保存失败')
- }
- }, this)
- },
- // 预览图片
- previewImage() {
- // 在组件内使用需要第二个参数this
- uni.canvasToTempFilePath({
- canvasId: this.canvasName,
- fileType: 'png',
- quality: 1,
- success: (res) => {
- if (this.rotate) {
- this.getRotateImage(res.tempFilePath).then((res) => {
- uni.previewImage({
- urls: [res]
- })
- }).catch(err => {
- this.$t.message.toast('旋转图片失败')
- })
- } else {
- uni.previewImage({
- urls: [res.tempFilePath]
- })
- }
- },
- fail: (e) => {
- this.$t.message.toast('预览失败')
- }
- }, this)
- },
- // 关闭签名板
- closeBoard() {
- this.$t.message.modal('提示信息','关闭后内容将被清除,是否确认关闭',() => {
- this.$emit('closed')
- }, true)
- },
- // 切换画笔颜色
- colorSwitch(color) {
- this.currentSelectColor = color
- },
- // 绘制两点之间的线条
- pointToLine(line) {
- this.calcBethelLine(line)
- },
- // 计算插值,让线条更加圆滑
- calcBethelLine(line) {
- if (line.length <= 1) {
- line[0].r = this.radius
- return
- }
- 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
- 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 i = 0; i < line.length - 1; i++) {
- dis += line[i].dis
- time += line[i].time - line[i + 1].time
- if (dis > this.smoothness) break
- }
-
- this.radius = Math.min((time / len) * this.pressure + this.minLine, this.maxLine) * this.lineSize
- line[0].r = this.radius
- // 计算笔迹半径
- if (line.length <= 2) {
- r0 = (lastRadius + this.radius) / 2
- r1 = r0
- r2 = r1
- } 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,
- y,
- 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)
- a[0].color = this.currentSelectColor
-
- this.drawBethel(a, true)
- point = [{
- x,
- y,
- r
- }]
- }
- }
- this.currentLine = line
- },
- // 求两点之间的距离
- distance(a, b) {
- let x = b.x - a.x
- let y = b.y - a.y
- return Math.sqrt(x * x + y * y)
- },
- // 计算点信息
- 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: '#080808'
- })
- 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
- },
- // 绘制贝塞尔曲线
- drawBethel(point, is_fill, color) {
- this.ctx.beginPath()
- this.ctx.moveTo(point[0].mx, point[0].my)
- if (color != undefined) {
- this.ctx.setFillStyle(color)
- this.ctx.setStrokeStyle(color)
- } else {
- this.ctx.setFillStyle(point[0].color)
- this.ctx.setStrokeStyle(point[0].color)
- }
- for (let i = 1; i < point.length; i++) {
- this.ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey)
- }
- this.ctx.stroke()
- if (is_fill != undefined) {
- //填充图形 ( 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 )
- this.ctx.fill()
- }
- this.ctx.draw(true)
- },
- // 旋转图片
- async getRotateImage(dataUrl) {
- // const url = await this.base64ToPath(dataUrl)
- const url = dataUrl
-
- // 创建新画布
- const tempCtx = uni.createCanvasContext('temp-tn-sign-canvas', this)
- const width = this.canvasWidth
- const height = this.canvasHeight
- tempCtx.restore()
- tempCtx.save()
- tempCtx.translate(0, height)
- tempCtx.rotate(270 * Math.PI / 180)
- tempCtx.drawImage(url, 0, 0, width, height)
- tempCtx.draw()
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- uni.canvasToTempFilePath({
- canvasId: 'temp-tn-sign-canvas',
- fileType: 'png',
- x: 0,
- y: height - width,
- width: height,
- height: width,
- success: res => resolve(res.tempFilePath),
- fail: reject
- }, this)
- }, 50)
- })
- },
- // 将base64转换为本地
- base64ToPath(dataUrl) {
- return new Promise((resolve, reject) => {
- // 判断地址是否包含bas64字样,不包含直接返回
- if (dataUrl.indexOf('base64') !== -1) {
- const data = uni.base64ToArrayBuffer(dataUrl.replace(/^data:image\/\w+;base64,/, ''))
- // #ifdef MP-WEIXIN
- const filePath = `${wx.env.USER_DATA_PATH}/${new Date().getTime()}-${Math.random().toString(32).slice(2)}.png`
- // #endif
- // #ifndef MP-WEIXIN
- const filePath = `${new Date().getTime()}-${Math.random().toString(32).slice(2)}.png`
- // #endif
- uni.getFileSystemManager().writeFile({
- filePath,
- data,
- encoding: 'base64',
- success: () => resolve(filePath),
- fail: reject
- })
- } else {
- resolve(dataUrl)
- }
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
-
- .tn-sign-board {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- width: 100%;
- height: 100%;
- background-color: #E6E6E6;
- z-index: 997;
- display: flex;
- flex-direction: row-reverse;
-
- &__content {
- width: 84%;
- height: 100%;
-
- &__wrapper {
- width: calc(100% - 60rpx);
- height: calc(100% - 60rpx);
- margin: 30rpx;
- border-radius: 20rpx;
- border: 2rpx dotted #AAAAAA;
- overflow: hidden;
- }
-
- &__canvas {
- width: 100%;
- height: 100%;
- background-color: #FFFFFF;
- }
- }
-
- &__tools {
- width: 16%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
-
- &__color {
- margin-top: 30rpx;
-
- &__item {
- width: 70rpx;
- height: 70rpx;
- border-radius: 100rpx;
- margin: 20rpx auto;
-
- &--active {
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 40%;
- height: 40%;
- border-radius: 100rpx;
- background-color: #FFFFFF;
- transform: translate(-50%, -50%);
- }
- }
- }
- }
-
- &__button {
- margin-bottom: 30rpx;
- display: flex;
- flex-direction: column;
-
- &__item {
- width: 130rpx;
- height: 60rpx;
- line-height: 60rpx;
- text-align: center;
- margin: 60rpx auto;
- border-radius: 10rpx;
- color: #FFFFFF;
- transform-origin: center center;
- transform: rotateZ(90deg);
- }
- }
- }
- }
- </style>
|