You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
15 KiB

2 years ago
<template>
<view class="lime-signature" :style="[canvasStyle, styles]" ref="limeSignature">
<!-- #ifndef APP-VUE || APP-NVUE -->
<canvas v-if="useCanvas2d" class="lime-signature__canvas" :id="canvasId" type="2d"
:disableScroll="disableScroll" @touchstart="touchStart" @touchmove="touchMove"
@touchend="touchEnd"></canvas>
<canvas v-else :disableScroll="disableScroll" class="lime-signature__canvas" :canvas-id="canvasId"
:id="canvasId" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @mousedown="touchStart"
@mousemove="touchMove" @mouseup="touchEnd"></canvas>
<!-- #endif -->
<!-- #ifdef APP-VUE -->
<view :id="canvasId" :disableScroll="disableScroll" :rparam="param" :change:rparam="sign.update"
:rclear="rclear" :change:rclear="sign.clear" :rundo="rundo" :change:rundo="sign.undo" :rsave="rsave"
:change:rsave="sign.save" :rempty="rempty" :change:rempty="sign.isEmpty"></view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view src="/uni_modules/lime-signature/static/index.html" class="lime-signature__canvas" ref="webview"
@pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage"></web-view>
<!-- #endif -->
</view>
</template>
<!-- #ifdef APP-VUE -->
<script module="sign" lang="renderjs">
// #ifdef APP-VUE
// import { Signature } from '@signature'
import {
Signature
} from './signature'
// import {base64ToPath} from './utils'
export default {
data() {
return {
canvasid: null,
signature: null,
observer: null,
options: {},
saveCount: 0,
}
},
mounted() {
this.$nextTick(this.init)
},
methods: {
init() {
const el = this.$refs.limeSignature;
const canvas = document.createElement('canvas')
canvas.style = 'width:100%; height: 100%;'
el.appendChild(canvas)
this.signature = new Signature({
el: canvas
})
this.signature.pen.setOption(this.options)
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
this.emit({
changeSize: {
width,
height
}
})
},
undo(v) {
if (v && this.signature) {
this.signature.undo()
}
},
clear(v) {
if (v && this.signature) {
this.signature.clear()
}
},
save(v) {
if (v !== this.saveCount) {
this.saveCount = v;
const image = this.signature.canvas.get('el').toDataURL()
const {
backgroundColor
} = this.options
if (backgroundColor) {
const canvas = document.createElement('canvas')
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
const pixelRatio = this.signature.canvas.get('pixelRatio')
canvas.width = width * pixelRatio
canvas.height = height * pixelRatio
const context = canvas.getContext('2d')
context.scale(pixelRatio, pixelRatio)
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(this.signature.canvas.get('el'), 0, 0, width, height)
this.emit({
save: canvas.toDataURL()
})
canvas.remove()
} else {
this.emit({
save: image
})
}
// base64ToPath(image).then((res) => {
// this.emit({save: res})
// })
}
},
isEmpty(v) {
if (v && this.signature) {
const isEmpty = this.signature.isEmpty()
this.emit({
isEmpty
})
}
},
emit(event) {
this.$ownerInstance.callMethod('onMessage', {
detail: {
data: [{
event
}]
}
})
},
update(v) {
if (v) {
if (this.signature) {
this.options = v
this.signature.pen.setOption(v)
} else {
this.options = v
}
}
}
}
}
// #endif
</script>
<!-- #endif -->
<script>
// #ifndef APP-NVUE
import {
getCanvas2d,
wrapEvent,
requestAnimationFrame,
sleep
} from './utils'
import {
Signature
} from './signature'
// import {Signature} from '@signature';
import {
uniContext,
createImage,
toDataURL
} from './context'
// #endif
import {
base64ToPath
} from './utils'
export default {
props: {
styles: String,
disableScroll: Boolean,
type: {
type: String,
default: '2d'
},
// 画笔颜色
penColor: {
type: String,
default: 'black'
},
penSize: {
type: Number,
default: 2
},
// 画板背景颜色
backgroundColor: String,
// 笔锋
openSmooth: Boolean,
// 画笔最小值
minLineWidth: {
type: Number,
default: 2
},
// 画笔最大值
maxLineWidth: {
type: Number,
default: 6
},
// 画笔达到最小宽度所需最小速度(px/ms)取值范围1.0-10.0,值越小,画笔越容易变细,笔锋效果会比较明显,可以自行调整查看效果,选出自己满意的值。
minSpeed: {
type: Number,
default: 1.5
},
// 相邻两线宽度增(减)量最大百分比取值范围1-100为了达到笔锋效果画笔宽度会随画笔速度而改变如果相邻两线宽度差太大过渡效果就会很突兀使用maxWidthDiffRate限制宽度差让过渡效果更自然。可以自行调整查看效果选出自己满意的值。
maxWidthDiffRate: {
type: Number,
default: 20
},
// 限制历史记录数即最大可撤销数传入0则关闭历史记录功能
maxHistoryLength: {
type: Number,
default: 20
},
beforeDelay: {
type: Number,
default: 0
}
},
data() {
return {
canvasWidth: null,
canvasHeight: null,
useCanvas2d: true,
// #ifdef APP-PLUS
rclear: 0,
rundo: 0,
rsave: 0,
rempty: 0,
risEmpty: true,
toDataURL: null,
tempFilePath: [],
// #endif
}
},
computed: {
canvasId() {
return `lime-signature${this._uid||this._.uid}`
},
canvasStyle() {
const {
canvasWidth,
canvasHeight,
backgroundColor
} = this
return {
width: canvasWidth && (canvasWidth + 'px'),
height: canvasHeight && (canvasHeight + 'px'),
background: backgroundColor
}
},
param() {
const {
penColor,
penSize,
backgroundColor,
openSmooth,
minLineWidth,
maxLineWidth,
minSpeed,
maxWidthDiffRate,
maxHistoryLength,
disableScroll
} = this
return JSON.parse(JSON.stringify({
penColor,
penSize,
backgroundColor,
openSmooth,
minLineWidth,
maxLineWidth,
minSpeed,
maxWidthDiffRate,
maxHistoryLength,
disableScroll
}))
}
},
// #ifdef APP-NVUE
watch: {
param(v) {
this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
}
},
// #endif
// #ifndef APP-PLUS
created() {
this.useCanvas2d = this.type === '2d' && getCanvas2d()
},
// #endif
// #ifndef APP-PLUS
async mounted() {
if (this.beforeDelay) {
await sleep(this.beforeDelay)
}
const config = await this.getContext()
this.signature = new Signature(config)
this.canvasEl = this.signature.canvas.get('el')
this.canvasWidth = this.signature.canvas.get('width')
this.canvasHeight = this.signature.canvas.get('height')
this.stopWatch = this.$watch('param', (v) => {
this.signature.pen.setOption(v)
}, {
immediate: true
})
},
// #endif
// #ifndef APP-PLUS
// #ifdef VUE3
beforeUnmount() {
this.stopWatch()
this.signature.destroy()
},
// #endif
// #ifdef VUE2
beforeDestroy() {
this.stopWatch()
this.signature.destroy()
},
// #endif
// #endif
methods: {
// #ifdef APP-PLUS
onPageFinish() {
this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
},
onMessage(e = {}) {
const {
detail: {
data: [res]
}
} = e
if (res.event?.save) {
this.toDataURL = res.event.save
}
if (res.event?.changeSize) {
const {
width,
height
} = res.event.changeSize
}
if (res.event.hasOwnProperty('isEmpty')) {
this.risEmpty = res.event.isEmpty
}
if (res.event?.file) {
this.tempFilePath.push(res.event.file)
if (this.tempFilePath.length > 7) {
this.tempFilePath.shift()
}
return
}
if (res.event?.success) {
if (res.event.success) {
this.tempFilePath.push(res.event.success)
if (this.tempFilePath.length > 8) {
this.tempFilePath.shift()
}
this.toDataURL = this.tempFilePath.join('')
this.tempFilePath = []
// base64ToPath(this.tempFilePath.join('')).then(res => {
// })
} else {
this.$emit('fail', 'canvas no data')
}
return
}
},
// #endif
undo() {
// #ifdef APP-VUE || APP-NVUE
this.rundo += 1
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`undo()`)
// #endif
// #ifndef APP-VUE
if (this.signature)
this.signature.undo()
// #endif
},
clear() {
// #ifdef APP-VUE || APP-NVUE
this.rclear += 1
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`clear()`)
// #endif
// #ifndef APP-VUE
if (this.signature)
this.signature.clear()
// #endif
},
isEmpty() {
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`isEmpty()`)
// #endif
// #ifdef APP-VUE || APP-NVUE
this.rempty += 1
// #endif
// #ifndef APP-VUE || APP-NVUE
return this.signature.isEmpty()
// #endif
},
canvasToTempFilePath(param) {
const isEmpty = this.isEmpty()
// #ifdef APP-NVUE
this.$refs.webview.evalJS(`save()`)
// #endif
// #ifdef APP-VUE || APP-NVUE
const stopURLWatch = this.$watch('toDataURL', (v, n) => {
if (v && v !== n) {
if (param.pathType == 'url') {
base64ToPath(v).then(res => {
param.success({
tempFilePath: res,
isEmpty: this.risEmpty
})
})
} else {
param.success({
tempFilePath: v,
isEmpty: this.risEmpty
})
}
this.toDataURL = ''
}
stopURLWatch && stopURLWatch()
})
this.rsave += 1
// #endif
// #ifndef APP-VUE || APP-NVUE
const success = (success) => param.success && param.success(success)
const fail = (fail) => param.fail && param.fail(err)
const {
canvas
} = this.signature.canvas.get('el')
const context = this.signature.canvas.get('context')
const {
backgroundColor
} = this
const width = this.signature.canvas.get('width')
const height = this.signature.canvas.get('height')
if (this.useCanvas2d) {
try {
// #ifndef MP-ALIPAY
const tempFilePath = canvas.toDataURL()
if (backgroundColor) {
const image = canvas.createImage()
image.src = tempFilePath
image.onload = () => {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
const tempFilePath = canvas.toDataURL()
success({
tempFilePath,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
}
} else {
success({
tempFilePath,
isEmpty
})
}
// #endif
// #ifdef MP-ALIPAY
canvas.toTempFilePath({
canvasid: this.canvasid,
success(res) {
if (backgroundColor) {
const image = canvas.createImage()
image.src = tempFilePath
image.onload = () => {
canvas.toTempFilePath({
canvasid: this.canvasid,
success(res) {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
success({
tempFilePath,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
}
})
}
} else {
success({
tempFilePath: res,
isEmpty
})
}
},
fail
})
// #endif
} catch (err) {
console.warn(err)
fail(err)
}
} else {
toDataURL(this.canvasId, this).then(res => {
if (backgroundColor) {
const image = createImage()
image.src = res
image.onload = () => {
context.fillStyle = backgroundColor
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
context.draw && context.draw(true, () => {
toDataURL(this.canvasId, this).then(res => {
success({
tempFilePath: res,
isEmpty
})
context.clearRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height);
context.draw && context.draw(true)
})
});
}
} else {
success({
tempFilePath: res,
isEmpty
})
}
}).catch(err => {
console.warn(err)
fail(err)
})
}
// #endif
},
// #ifndef APP-PLUS
getContext() {
const {
pixelRatio
} = uni.getSystemInfoSync()
return new Promise(resolve => {
if (this.useCanvas2d) {
uni.createSelectorQuery().in(this)
.select(`#${this.canvasId}`)
.fields({
node: true,
size: true,
rect: true,
})
.exec(res => {
if (res) {
const {
width,
height,
node,
left,
top,
right
} = res[0]
const context = node.getContext('2d')
node.width = width * pixelRatio;
node.height = height * pixelRatio;
resolve({
left,
top,
right,
width,
height,
context,
canvas: node,
pixelRatio
})
}
})
} else {
uni.createSelectorQuery().in(this)
.select(`#${this.canvasId}`)
.boundingClientRect()
.exec(res => {
if (res) {
const {
width,
height,
left,
top,
right
} = res[0]
const context = uniContext(uni.createCanvasContext(this.canvasId, this))
const canvas = {
createImage,
toDataURL: () => toDataURL(this.canvasId, this),
requestAnimationFrame
}
resolve({
left,
top,
right,
width,
height,
context,
pixelRatio: 1,
canvas
})
}
})
}
})
},
touchStart(e) {
if (!this.canvasEl) return
this.isStart = true
this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
},
touchMove(e) {
if (!this.canvasEl || !this.isStart && this.canvasEl) return
this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
},
touchEnd(e) {
if (!this.canvasEl) return
this.isStart = false
this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
},
// #endif
}
}
</script>
<style lang="stylus">
.lime-signature,
.lime-signature__canvas {
// #ifndef APP-NVUE
width: 100%;
height: 100% // #endif
// #ifdef APP-NVUE
flex: 1;
// #endif}
</style>