123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- <template>
- <view class="lime-signature" v-if="show" :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" :width="canvasWidth" :height="canvasHeight" @touchstart="touchStart" @touchmove="touchMove"
- @touchend="touchEnd" @mousedown="touchStart" @mousemove="touchMove" @mouseup="touchEnd"></canvas>
- <canvas class="offscreen" canvas-id="offscreen" id="offscreen"
- :style="'width:' + offscreenSize[0] + 'px;height:' + offscreenSize[1] + 'px'" :width="offscreenSize[0]"
- :height="offscreenSize[1]">
- </canvas>
- <view v-if="showMask" class="mask" @touchstart="touchStart" @touchmove.stop.prevent="touchMove" @touchend="touchEnd"></view>
- <!-- #endif -->
- <!-- #ifdef APP-VUE -->
- <view :id="canvasId" :disableScroll="disableScroll" :rparam="param" :change:rparam="sign.update"
- :rclear="rclear"
- :change:rclear="sign.clear"
- :rundo="rundo"
- :rredo="rredo"
- :change:rredo="sign.redo"
- :change:rundo="sign.undo"
- :rsave="rsave"
- :rmask="rmask"
- :change:rsave="sign.save"
- :change:rmask="sign.mask"
- :rdestroy="rdestroy"
- :change:rdestroy="sign.destroy"
- :rempty="rempty"
- :change:rempty="sign.isEmpty">
- </view>
- <!-- #endif -->
- <!-- #ifdef APP-NVUE -->
- <web-view src="/uni_modules/lime-signature/hybrid/html/index.html" class="lime-signature__canvas" ref="webview"
- @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage"></web-view>
- <!-- #endif -->
- </view>
- </template>
- <script>
- // #ifndef APP-NVUE
- import { canIUseCanvas2d, wrapEvent, requestAnimationFrame, sleep, isTransparent} from './utils'
- import {Signature} from './signature.js'
- // import {Signature} from '@signature';
- import { uniContext, createImage, toDataURL } from './context'
- // #endif
- import props from './props';
- import { base64ToPath, getRect } from './utils'
- /**
- * LimeSignature 手写板签名
- * @description 手写板签名插件:一款能跑在uniapp各端中的签名插件,支持横屏、背景色、笔画颜色、笔画大小等功能,可生成有内容的区域,减小图片尺寸,节省空间。
- * @tutorial https://ext.dcloud.net.cn/plugin?id=4354
- * @property {Number} penSize 画笔大小
- * @property {Number} minLineWidth 线条最小宽
- * @property {Number} maxLineWidth 线条最大宽
- * @property {String} penColor 画笔颜色
- * @property {String} backgroundColor 背景颜色,不填则为透明
- * @property {type} 指定 canvas 类型
- * @value 2d canvas 2d
- * @value '' 非 canvas 2d 旧接口,微信不再维护
- * @property {Boolean} openSmooth 模拟笔锋
- * @property {Number} beforeDelay 延时初始化,在放在弹窗里可以使用 (毫秒)
- * @property {Number} maxHistoryLength 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能
- * @property {Boolean} landscape 横屏,使用后在最后生成图片时会图片旋转90度
- * @property {Boolean} disableScroll 当在写字时,禁止屏幕滚动以及下拉刷新,nvue无效
- * @property {Boolean} boundingBox 只生成内容区域,即未画部分不生成,有性能的损耗
- */
- export default {
- props,
- data() {
- return {
- canvasWidth: null,
- canvasHeight: null,
- offscreenWidth: null,
- offscreenHeight: null,
- useCanvas2d: true,
- show: true,
- offscreenStyles: '',
- showMask: false,
- isPC: false,
- // #ifdef APP-PLUS
- rclear: 0,
- rdestroy: 0,
- rundo: 0,
- rredo: 0,
- rsave: JSON.stringify({
- n: 0,
- fileType: 'png',
- quality: 1,
- destWidth: 0,
- destHeight: 0,
- }),
- rmask: JSON.stringify({
- n: 0,
- destWidth: 0,
- destHeight: 0,
- }),
- rempty: 0,
- risEmpty: true,
- toDataURL: null,
- tempFilePath: [],
- // #endif
- }
- },
- computed: {
- canvasId() {
- // #ifdef VUE2
- return `lime-signature${this._uid}`
- // #endif
- // #ifdef VUE3
- return `lime-signature${this._.uid}`
- // #endif
- },
- offscreenId() {
- return this.canvasId + 'offscreen'
- },
- offscreenSize() {
- const {offscreenWidth,offscreenHeight} = this
- return this.landscape ? [offscreenHeight, offscreenWidth] : [offscreenWidth, offscreenHeight]
- },
- canvasStyle() {
- const { canvasWidth, canvasHeight, backgroundColor } = this
- return {
- width: canvasWidth && (canvasWidth + 'px'),
- height: canvasHeight && (canvasHeight + 'px'),
- background: backgroundColor
- }
- },
- param() {
- const {
- penColor,
- penSize,
- backgroundColor,
- backgroundImage,
- landscape,
- boundingBox,
- openSmooth,
- minLineWidth,
- maxLineWidth,
- minSpeed,
- maxWidthDiffRate,
- maxHistoryLength,
- disableScroll,
- disabled
- } = this
- return JSON.parse(JSON.stringify({
- penColor,
- penSize,
- backgroundColor,
- backgroundImage,
- landscape,
- boundingBox,
- openSmooth,
- minLineWidth,
- maxLineWidth,
- minSpeed,
- maxWidthDiffRate,
- maxHistoryLength,
- disableScroll,
- disabled
- }))
- }
- },
- // #ifdef APP-NVUE
- watch: {
- param(v) {
- this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
- }
- },
- // #endif
- // #ifndef APP-PLUS
- created() {
- const {platform} = uni.getSystemInfoSync()
- this.isPC = /windows|mac/.test(platform)
- this.useCanvas2d = this.type == '2d' && canIUseCanvas2d() && !this.isPC
- // #ifndef H5
- this.showMask = this.isPC
- // #endif
-
- },
- // #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.offscreenWidth = this.canvasWidth = this.signature.canvas.get('width')
- this.offscreenHeight = 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.stopWatch()
- this.signature.destroy()
- this.signature = null
- this.show = false;
- // #ifdef APP-VUE || APP-NVUE
- this.rdestroy++
- // #endif
- },
- // #endif
- // #ifdef VUE2
- beforeDestroy() {
- this.stopWatch && this.stopWatch()
- this.signature.destroy()
- this.show = false;
- this.signature = null
- // #ifdef APP-VUE || APP-NVUE
- this.rdestroy++
- // #endif
- },
- // #endif
- // #endif
- methods: {
- // #ifdef MP-QQ
- // toJSON() { return this },
- // #endif
- // #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 = []
- } else {
- this.$emit('fail', 'canvas no data')
- }
- return
- }
- },
- // #endif
- redo(){
- // #ifdef APP-VUE || APP-NVUE
- this.rredo += 1
- // #endif
- // #ifdef APP-NVUE
- this.$refs.webview.evalJS(`redo()`)
- // #endif
- // #ifndef APP-VUE
- if (this.signature)
- this.signature.redo()
- // #endif
- },
- restore() {
- this.redo()
- },
- 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
- },
- canvasToMaskPath(param = {}){
- const isEmpty = this.isEmpty()
- // #ifdef APP-NVUE
- this.$refs.webview.evalJS(`mask(${JSON.stringify(param)})`)
- // #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()
- })
- const {
- fileType,
- quality
- } = param
- const rmask = JSON.parse(this.rmask)
- rmask.n++
- rmask.destWidth = param.destWidth??0
- rmask.destHeight = param.destHeight??0
- // rmask.fileType = fileType
- // rmask.quality = quality
- this.rmask = JSON.stringify(rmask)
- // #endif
- // #ifndef APP-VUE || APP-NVUE
- let width = this.signature.canvas.get('width')
- let height = this.signature.canvas.get('height')
-
- let {pixelRatio} = uni.getSystemInfoSync()
- if(this.useCanvas2d){
- this.offscreenWidth = width * pixelRatio
- this.offscreenHeight = height * pixelRatio
- }
- const context = uni.createCanvasContext('offscreen', this)
- const success = (success) => param.success && param.success(success)
- const fail = (fail) => param.fail && param.fail(fail)
-
- this.signature.pen.getMaskedImageData((imageData)=>{
- uni.canvasPutImageData({
- canvasId: 'offscreen',
- x: 0,
- y: 0,
- width:Math.floor(this.offscreenWidth),
- height:Math.floor(this.offscreenHeight),
- data: imageData,
- fail(err){
- fail(err)
- },
- success: (res) => {
- toDataURL('offscreen', this, param).then((res) => {
- const size = Math.max(this.offscreenWidth, this.offscreenHeight)
- context.restore()
- context.clearRect(0, 0, size, size)
- this.offscreenWidth = width
- this.offscreenHeight = height
- success({
- tempFilePath: res,
- isEmpty
- })
- })
- }
- }, this)
- })
- // #endif
- },
- canvasToTempFilePath(param = {}) {
- const isEmpty = this.isEmpty()
- // #ifdef APP-NVUE
- this.$refs.webview.evalJS(`save(${JSON.stringify(param)})`)
- // #endif
- // #ifdef APP-VUE || APP-NVUE
- const stopURLWatch = this.$watch('toDataURL', (v, n) => {
- if (v && v !== n) {
- if(this.preferToDataURL){
- param.success({tempFilePath: v,isEmpty: this.risEmpty })
- } else {
- base64ToPath(v).then(res => {
- param.success({
- tempFilePath: res,
- isEmpty: this.risEmpty
- })
- })
- }
- this.toDataURL = ''
- }
- stopURLWatch && stopURLWatch()
- })
- const {
- fileType,
- quality
- } = param
- const rsave = JSON.parse(this.rsave)
- rsave.n++
- rsave.fileType = fileType
- rsave.quality = quality
- rsave.destWidth = param.destWidth??0
- rsave.destHeight = param.destHeight??0
- this.rsave = JSON.stringify(rsave)
- // #endif
- // #ifndef APP-VUE || APP-NVUE
- const useCanvas2d = this.useCanvas2d
- const success = (success) => param.success && param.success(success)
- const fail = (err) => param.fail && param.fail(err)
- const { canvas } = this.signature.canvas.get('el')
- const {
- backgroundColor,
- landscape,
- boundingBox
- } = this
- let width = this.signature.canvas.get('width')
- let height = this.signature.canvas.get('height')
- let x = 0
- let y = 0
- const devtools = uni.getSystemInfoSync().platform == 'devtools'
- let preferToDataURL = this.preferToDataURL
- let scale = 1
- // #ifdef MP-TOUTIAO
- scale = devtools ? uni.getSystemInfoSync().pixelRatio : scale
- // 由于抖音不支持canvasToTempFilePath故优先使用createOffscreenCanvas
- preferToDataURL = true
- // #endif
- const canvasToTempFilePath = (image) => {
- const createCanvasContext = ()=>{
- const useOffscreen = (useCanvas2d && !!uni.createOffscreenCanvas && preferToDataURL)
- if(useOffscreen && !devtools){
- const offCanvas = uni.createOffscreenCanvas({type: '2d'});
- offCanvas.width = this.offscreenSize[0]*scale
- offCanvas.height = this.offscreenSize[1]*scale
- const context = offCanvas.getContext("2d");
- return [context, offCanvas]
- } else {
- const context = uni.createCanvasContext('offscreen', this)
- return [context]
- }
- }
-
- const [context, offCanvas] = createCanvasContext()
- context.save()
- context.setTransform(1, 0, 0, 1, 0, 0)
- if (landscape) {
- context.translate(0, width*scale)
- context.rotate(-Math.PI / 2)
- }
- if (backgroundColor && !isTransparent(backgroundColor)) {
- context.fillStyle = backgroundColor
- context.fillRect(0, 0, width, height)
- }
- if(offCanvas){
- const img = canvas.createImage();
- img.src = image
- img.onload = () => {
- context.drawImage(img, 0, 0, width*scale, height*scale);
- const tempFilePath = offCanvas.toDataURL()
- success({
- tempFilePath,
- isEmpty
- })
- }
-
- } else {
- context.drawImage(image, 0, 0, width*scale, height*scale);
- context.draw(false, () => {
- toDataURL('offscreen', this, param).then((res) => {
- const size = Math.max(width, height)
- context.restore()
- context.clearRect(0, 0, size, size)
- success({
- tempFilePath: res,
- isEmpty
- })
- })
- })
- }
-
- }
- const next = async () => {
- if(this.offscreenWidth != width || this.offscreenHeight != height) {
- this.offscreenWidth = width
- this.offscreenHeight = height
- await sleep(100)
- }
-
- // #ifndef MP-WEIXIN
- const param = { x, y, width, height, canvas, preferToDataURL }
- // #endif
-
- // #ifdef MP-WEIXIN
- const param = { x, y, width, height, canvas: useCanvas2d ? canvas : null, preferToDataURL }
- // #endif
- toDataURL(this.canvasId, this, param).then(canvasToTempFilePath).catch(fail)
- }
- // PC端小程序获取不到 ImageData 数据,长度为0
- if (boundingBox && !this.isPC) {
- this.signature.getContentBoundingBox(async res => {
- this.offscreenWidth = width = res.width
- this.offscreenHeight = height = res.height
-
- x = res.startX
- y = res.startY
- next()
- })
- } else {
- next()
- }
- // #endif
- },
- // #ifndef APP-PLUS
- getContext() {
- return getRect(`#${this.canvasId}`, {
- context: this,
- type: this.useCanvas2d ? 'fields' : 'boundingClientRect'
- }).then(res => {
- if (res) {
- let { width, height, node: canvas, left, top, right} = res
- let {pixelRatio} = uni.getSystemInfoSync()
- let context;
- if (canvas) {
- context = canvas.getContext('2d')
- canvas.width = width * pixelRatio;
- canvas.height = height * pixelRatio;
- } else {
- pixelRatio = 1
- context = uniContext(this.canvasId, this)
- canvas = {
- getContext: (type)=> type=='2d' ? context : null,
- createImage,
- toDataURL: () => toDataURL(this.canvasId, this),
- requestAnimationFrame
- }
- }
- // 支付宝小程序 使用stroke有个默认背景色
- context.clearRect(0, 0, width, height)
- return {
- left,
- top,
- right,
- width,
- height,
- context,
- canvas,
- pixelRatio
- };
- }
- })
- },
- getTouch(e) {
- if(this.isPC && this.canvasRect) {
- e.touches = e.touches.map(item => {
- return {
- ...item,
- x: item.clientX - this.canvasRect.left,
- y: item.clientY - this.canvasRect.top,
- }
- })
- }
- return e
- },
- touchStart(e) {
- if (!this.canvasEl ) return
- this.isStart = true
- // 微信小程序PC端不支持事件,使用这方法模拟一下
- if(this.isPC) {
- getRect(`#${this.canvasId}`, {context: this}).then(res => {
- this.canvasRect = res
- this.canvasEl.dispatchEvent('touchstart', wrapEvent(this.getTouch(e)))
- })
- return
- }
- this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
- },
- touchMove(e) {
- if (!this.canvasEl || !this.isStart && this.canvasEl ) return
- this.canvasEl.dispatchEvent('touchmove', wrapEvent(this.getTouch(e)))
- },
- touchEnd(e) {
- if (!this.canvasEl ) return
- this.isStart = false
- this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
- },
- // #endif
- }
- }
- </script>
- <!-- #ifdef APP-VUE -->
- <script module="sign" lang="renderjs">
- import sign from './render'
- export default sign
- </script>
- <!-- #endif -->
- <style lang="scss">
- .lime-signature,
- .lime-signature__canvas {
- /* #ifndef APP-NVUE */
- position: relative;
- width: 100%;
- height: 100%;
- /* #endif */
- /* #ifdef APP-NVUE */
- flex: 1;
- /* #endif */
- }
- .mask {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- top: 0;
- }
- .offscreen {
- position: fixed;
- top: 0;
- left: 9999px;
- }
- </style>
|