移动端V1.0
This commit is contained in:
103
uni_modules/jp-signature/components/jp-merge/jp-merge.vue
Normal file
103
uni_modules/jp-signature/components/jp-merge/jp-merge.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view class="share">
|
||||
<canvas
|
||||
canvas-id="shareCanvas"
|
||||
class="canvas"
|
||||
bindlongpress="saveImg"
|
||||
catchtouchmove="true"
|
||||
style="position:fixed;left:500%"
|
||||
:style="{height: canvasHeight+'px',width:canvasWidth+'px'}"
|
||||
>
|
||||
</canvas>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
canvasHeight: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
canvasWidth: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 320,
|
||||
},
|
||||
bgImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
ctx:null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//初始化画布
|
||||
this.ctx = wx.createCanvasContext('shareCanvas',this)
|
||||
},
|
||||
methods:{
|
||||
//获取图片的基本信息,即将网络图片转成本地图片,
|
||||
getImageInfo(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: (res) => resolve(res),
|
||||
fail: (res) => reject(res)
|
||||
})
|
||||
});
|
||||
},
|
||||
exportPost(image2){
|
||||
let that = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
let image = that.bgImage
|
||||
//获取系统的基本信息,为后期的画布和底图适配宽高
|
||||
uni.getSystemInfo({
|
||||
success: function (res) {
|
||||
Promise.all([that.getImageInfo(image),that.getImageInfo(image2)]).then(res=>{
|
||||
//获取底图和二维码图片的基本信息,通常前端导出的二维码是base64格式的,所以要转成图片格式的才可以获取图片的基本信息
|
||||
that.ctx.drawImage(res[0].path,0 , 0,that.canvasWidth,that.canvasHeight);
|
||||
that.ctx.drawImage(res[1].path,that.left,that.top,that.width, that.height);
|
||||
that.ctx.draw(false,function(){
|
||||
wx.canvasToTempFilePath({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width:that.canvasWidth,
|
||||
height:that.canvasHeight,
|
||||
destWidth:that.canvasWidth*2,//这里乘以2是为了保证合成图片的清晰度
|
||||
destHeight:that.canvasHeight*2,
|
||||
canvasId: 'shareCanvas',
|
||||
fileType:'jpg',//设置导出图片的后缀名
|
||||
success: function (res) {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: function (res) {
|
||||
reject(res)
|
||||
},
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="signature">
|
||||
<div class="inputs" v-if="!popup">
|
||||
<div class="label" :class="required?'labelqr':''">{{label}}</div>
|
||||
<div>
|
||||
<div v-if="value" class="images">
|
||||
<image @tap="toImg" class="images" mode="aspectFit" :src="value"></image>
|
||||
<view v-if="!readonly" @click="toDeleteImg" class="icons">
|
||||
<view class="Deletes">×</view>
|
||||
</view>
|
||||
</div>
|
||||
<div v-if="!value && !readonly" class="explain" @click="toPop">
|
||||
{{placeholder?placeholder:'点击签名'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="bottomPopup" v-if="showPopup" @touchmove.stop.prevent="moveHandle">
|
||||
<transition name="slide-up" appear>
|
||||
<view class="popup-content">
|
||||
<view class="popup">
|
||||
<div class="hader" v-if="!isHeight">
|
||||
<div @click="toclear">取消</div>
|
||||
<div class="text">{{label}}</div>
|
||||
<div @click="isEmpty">确定</div>
|
||||
</div>
|
||||
<div :class="isHeight?'wgSignatureq':'wgSignature'">
|
||||
<div v-if="isHeight" key="999" style="width: 750rpx ;height: 100vh;">
|
||||
<jp-signature :beforeDelay="200" :landscape="true" disableScroll ref="signatureRef" :openSmooth="openSmooth" :penSize="6" :bounding-box="boundingBox"></jp-signature>
|
||||
</div>
|
||||
<div v-else key="888" style="width: 750rpx ;height: 35vh;">
|
||||
<jp-signature :beforeDelay="200" disableScroll ref="signatureRef" :openSmooth="openSmooth" :bounding-box="boundingBox" :penSize="3"></jp-signature>
|
||||
</div>
|
||||
<div v-if="!isHeight" class="appBut" >
|
||||
<div class="buts" @click="undo" >撤销</div>
|
||||
<div class="buts" @click="deleteImg" >清除</div>
|
||||
<div class="buts" style="background-color: #55aaff;color: #fff;" @click="Tomagnify" >全屏</div>
|
||||
</div>
|
||||
<div v-else class="appBut" style="height: 80px;">
|
||||
<div class="butx" @click="undo" >撤销</div>
|
||||
<div class="butx" @click="deleteImg">清除</div>
|
||||
<div class="butx" style="background-color: #55aaff;color: #fff;" @click="Tomagnify" >小屏</div>
|
||||
<div class="butx" @click="toclear">取消</div>
|
||||
<div class="butx" style="background-color: #E59C36;color: #fff;" @click="isEmpty">完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</transition>
|
||||
</view>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<script>
|
||||
/**
|
||||
* 手写签名组件
|
||||
* 用于手写签名(弹框签名支持小屏和全屏)
|
||||
*
|
||||
*********参数********
|
||||
* label 选项名称
|
||||
* value 初始值String(支持bas64,url 等图片显示)
|
||||
* required 是否显示必填
|
||||
* placeholder 默认值
|
||||
* readonly 是否只读
|
||||
*
|
||||
* *********回调********
|
||||
* @input(e) 点击确认 e生成的图片数据(bas64)
|
||||
*
|
||||
*********方法********
|
||||
* isEmpty() 生成图片
|
||||
* deleteImg() 删除图片
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
popup: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '手写签名',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
required: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '点击签名',
|
||||
},
|
||||
readonly: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
openSmooth: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
boundingBox: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isHeight: false,
|
||||
height1: uni.getSystemInfoSync().windowWidth / 2,
|
||||
width: uni.getSystemInfoSync().windowWidth, //实时屏幕宽度
|
||||
height: uni.getSystemInfoSync().windowHeight, //实时屏幕高度
|
||||
showPicker: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
moveHandle(){
|
||||
|
||||
},
|
||||
toImg(){
|
||||
this.$emit('toImg',this.value)
|
||||
},
|
||||
undo() {
|
||||
this.$refs.signatureRef.undo()
|
||||
},
|
||||
toPop() {
|
||||
this.showPopup = true
|
||||
},
|
||||
toDeleteImg() {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input','')
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value','')
|
||||
// #endif
|
||||
},
|
||||
toclear() {
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
},
|
||||
close() {
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
deleteImg() {
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
toDataURL(url) {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input',url)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',url)
|
||||
// #endif
|
||||
this.showPicker = false
|
||||
},
|
||||
Tomagnify() {
|
||||
this.isHeight = !this.isHeight
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
isEmpty() {
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.canvasToTempFilePath({
|
||||
quality: 0.8,
|
||||
success: (res) => {
|
||||
if (this.required) {
|
||||
if (!res.isEmpty) {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input', res.tempFilePath)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',res.tempFilePath)
|
||||
// #endif
|
||||
this.$emit('change', res.tempFilePath)
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先签名',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input', res.tempFilePath)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',res.tempFilePath)
|
||||
// #endif
|
||||
this.$emit('change', res.tempFilePath)
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
beforeCreate() {},
|
||||
created() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wgSignatureq{
|
||||
|
||||
}
|
||||
.appBut{
|
||||
display: flex;justify-content: flex-start;align-items: center;text-align: center;height: 50px;line-height: 35px;
|
||||
.buts{
|
||||
color: #333;flex: 1;margin: 0 15px;background-color: #ccc;border-radius: 5px;height: 35px;
|
||||
}
|
||||
.butx{
|
||||
color: #333;flex: 1;margin: 0 5px;background-color: #ccc;border-radius: 5px;height: 35px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bottomPopup {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.popup-content {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// top: 0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.signature {
|
||||
.inputs {
|
||||
background-color: #fff;
|
||||
padding: 10px 16px;
|
||||
|
||||
.label {
|
||||
line-height: 35px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.labelqr:before {
|
||||
content: "*";
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
.explain {
|
||||
width: 100%;
|
||||
background-color: #f1f1f1;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border: 1px dotted #ccc;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.Deletes {
|
||||
border: 1px solid #f00;
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 50%;
|
||||
color: #f00;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
width: 300rpx;
|
||||
height: 150rpx;
|
||||
position: relative;
|
||||
|
||||
.icons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.hader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 45px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
color: #E59C36;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
199
uni_modules/jp-signature/components/jp-signature/context.js
Normal file
199
uni_modules/jp-signature/components/jp-signature/context.js
Normal file
@@ -0,0 +1,199 @@
|
||||
const uniPlatform = uni.getSystemInfoSync().uniPlatform
|
||||
|
||||
export const uniContext = (canvasId, context) => {
|
||||
let ctx = uni.createCanvasContext(canvasId, context)
|
||||
if (!ctx.uniDrawImage) {
|
||||
ctx.uniDrawImage = ctx.drawImage
|
||||
ctx.drawImage = (image, ...agrs) => {
|
||||
ctx.uniDrawImage(image.src, ...agrs)
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.getImageData) {
|
||||
ctx.getImageData = (x, y, width, height) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP || VUE2
|
||||
if (context.proxy) context = context.proxy
|
||||
// #endif
|
||||
uni.canvasGetImageData({
|
||||
canvasId,
|
||||
x,
|
||||
y,
|
||||
width:parseInt(width),
|
||||
height:parseInt(height),
|
||||
success(res) {
|
||||
resolve(res)
|
||||
},
|
||||
fail(error) {
|
||||
reject(error)
|
||||
}
|
||||
}, context)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ctx._getImageData = ctx.getImageData
|
||||
ctx.getImageData = (x, y, width, height) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ctx._getImageData({
|
||||
x,
|
||||
y,
|
||||
width: parseInt(width) ,
|
||||
height:parseInt(height),
|
||||
success(res) {
|
||||
resolve(res)
|
||||
},
|
||||
fail(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
class Image {
|
||||
constructor() {
|
||||
this.currentSrc = null
|
||||
this.naturalHeight = 0
|
||||
this.naturalWidth = 0
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.tagName = 'IMG'
|
||||
}
|
||||
onerror() {}
|
||||
onload() {}
|
||||
set src(src) {
|
||||
this.currentSrc = src
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
this.naturalWidth = this.width = res.width
|
||||
this.naturalHeight = this.height = res.height
|
||||
this.onload()
|
||||
},
|
||||
fail: () => {
|
||||
this.onerror()
|
||||
}
|
||||
})
|
||||
}
|
||||
get src() {
|
||||
return this.currentSrc
|
||||
}
|
||||
}
|
||||
|
||||
export const createImage = () => {
|
||||
return new Image()
|
||||
}
|
||||
export function useCurrentPage() {
|
||||
const pages = getCurrentPages();
|
||||
return pages[pages.length - 1];
|
||||
}
|
||||
export const toDataURL = (canvasId, context, options = {}) => {
|
||||
// #ifdef MP-QQ
|
||||
// context = context.$scope
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
context = ''
|
||||
// #endif
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let {
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
destWidth = 0,
|
||||
destHeight = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
preferToDataURL
|
||||
} = options
|
||||
const {
|
||||
pixelRatio
|
||||
} = uni.getSystemInfoSync()
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
const isDD = typeof dd != 'undefined'
|
||||
if (!isDD && (!destWidth || !destHeight)) {
|
||||
destWidth = width * pixelRatio;
|
||||
destHeight = height * pixelRatio;
|
||||
width = destWidth;
|
||||
height = destHeight;
|
||||
x = x * pixelRatio
|
||||
y = y * pixelRatio
|
||||
}
|
||||
// #endif
|
||||
const params = {
|
||||
...options,
|
||||
canvasId,
|
||||
id: canvasId,
|
||||
// #ifdef MP-ALIPAY
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
destWidth,
|
||||
destHeight,
|
||||
// #endif
|
||||
canvas,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
// 抖音小程序canvas 2d不支持canvasToTempFilePath
|
||||
if (canvas && canvas.toDataURL && preferToDataURL) {
|
||||
let next = true
|
||||
const devtools = uni.getSystemInfoSync().platform == 'devtools'
|
||||
// #ifdef MP-TOUTIAO
|
||||
next = uni.getSystemInfoSync().platform != 'devtools'
|
||||
if (!next) {
|
||||
console.warn('[lime-signature] 抖音开发工具不支持bbox')
|
||||
}
|
||||
// #endif
|
||||
if ((x || y) && next) {
|
||||
const offCanvas = uni.createOffscreenCanvas({
|
||||
type: '2d'
|
||||
});
|
||||
const ctx = offCanvas.getContext("2d");
|
||||
const destWidth = Math.floor(width * pixelRatio)
|
||||
const destHeight = Math.floor(height * pixelRatio)
|
||||
offCanvas.width = destWidth // canvas.width;
|
||||
offCanvas.height = destHeight // canvas.height;
|
||||
// ctx.scale(pixelRatio, pixelRatio)
|
||||
// ctx.drawImage(canvas, Math.floor(x*pixelRatio), Math.floor(y*pixelRatio), destWidth, destHeight, 0,0, destWidth, destHeight);
|
||||
// 抖音不能在drawImage使用canvas
|
||||
const image = canvas.createImage()
|
||||
image.onload = () => {
|
||||
ctx.drawImage(image, Math.floor(x * pixelRatio), Math.floor(y * pixelRatio),
|
||||
destWidth, destHeight, 0, 0, destWidth, destHeight)
|
||||
const tempFilePath = offCanvas.toDataURL();
|
||||
resolve(tempFilePath)
|
||||
if (params.success) {
|
||||
params.success({
|
||||
tempFilePath
|
||||
})
|
||||
}
|
||||
}
|
||||
image.src = canvas.toDataURL()
|
||||
|
||||
} else {
|
||||
const tempFilePath = canvas.toDataURL()
|
||||
resolve(tempFilePath)
|
||||
if (params.success) {
|
||||
params.success({
|
||||
tempFilePath
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (canvas && canvas.toTempFilePath) {
|
||||
canvas.toTempFilePath(params)
|
||||
} else {
|
||||
uni.canvasToTempFilePath(params, context)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<view class="l-signature" ref="signatureRef" :style="drawableStyle">
|
||||
<!-- #ifdef APP -->
|
||||
<view class="l-signature-landscape" ref="signatureLandscapeRef" v-if="landscape && url !=''"
|
||||
:style="landscapeStyle">
|
||||
<image class="l-signature-image" :style="landscapeImageStyle" :src="url"></image>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef WEB -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
// @ts-nocheck
|
||||
// #ifdef APP
|
||||
import { Signature } from './signature.uts'
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
import { Signature } from './signature.js'
|
||||
// #endif
|
||||
import { nextTick } from 'vue'
|
||||
import { LSignatureToTempFilePathOptions, LSignatureToFileSuccess, LSignatureOptions } from '../../index.uts'
|
||||
// type SignatureToFileSuccessCallback = (res : UTSJSONObject) => void
|
||||
// type SignatureToFileFailCallback = (res : TakeSnapshotFail) => void
|
||||
// type SignatureToFileCompleteCallback = (res : any) => void
|
||||
|
||||
/**
|
||||
* LimeSignature 手写板签名
|
||||
* @description 手写板签名插件,uvue专用版。
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=4354
|
||||
* @property {Number} penSize 画笔大小
|
||||
* @property {String} penColor 画笔颜色
|
||||
* @property {String} backgroundColor 背景颜色,不填则为透明
|
||||
* @property {Boolean} disableScroll 当在写字时,禁止屏幕滚动以及下拉刷新,nvue无效
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
styles: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
penColor: {
|
||||
type: String,
|
||||
default: 'black'
|
||||
},
|
||||
penSize: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
openSmooth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
minLineWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxLineWidth: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
minSpeed: {
|
||||
type: Number,
|
||||
default: 1.5
|
||||
},
|
||||
maxWidthDiffRate: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
maxHistoryLength: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
disableScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
landscape: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
|
||||
const drawableStyle = computed<string>(() : string => {
|
||||
let style : string = ''
|
||||
|
||||
if (props.backgroundColor != '') {
|
||||
style += `background-color: ${props.backgroundColor};`
|
||||
}
|
||||
if (props.styles != '') {
|
||||
style += props.styles
|
||||
}
|
||||
return style
|
||||
})
|
||||
const signatureRef = ref<UniElement | null>(null)
|
||||
let signatureLandscapeRef = ref<UniElement | null>(null)
|
||||
let landscapeStyle = ref<Map<string, string>>(new Map())
|
||||
let landscapeImageStyle = ref<Map<string, string>>(new Map())
|
||||
|
||||
let signature : Signature | null = null
|
||||
let url = ref('')
|
||||
// #ifdef WEB
|
||||
let canvas : HTMLCanvasElement | null = null
|
||||
let touchstart,touchmove,touchend
|
||||
// #endif
|
||||
|
||||
const clear = () => {
|
||||
signature?.clear()
|
||||
}
|
||||
const redo = () => {
|
||||
signature?.redo()
|
||||
}
|
||||
const undo = () => {
|
||||
signature?.undo()
|
||||
}
|
||||
const canvasToTempFilePath = (options : LSignatureToTempFilePathOptions) => {
|
||||
const success = options.success // as SignatureToFileSuccessCallback | null
|
||||
const fail = options.fail // as SignatureToFileFailCallback | null
|
||||
const complete = options.complete// as SignatureToFileCompleteCallback | null
|
||||
const format = options.format ?? 'png'
|
||||
// #ifdef APP
|
||||
signatureRef.value?.takeSnapshot({
|
||||
format,
|
||||
success: (res) => {
|
||||
if (props.landscape) {
|
||||
url.value = res.tempFilePath;
|
||||
setTimeout(() => {
|
||||
signatureLandscapeRef.value?.takeSnapshot({
|
||||
format,
|
||||
success: (res2) => {
|
||||
success?.({
|
||||
tempFilePath: res2.tempFilePath,
|
||||
isEmpty: signature?.isEmpty ?? false
|
||||
} as LSignatureToFileSuccess)
|
||||
}
|
||||
})
|
||||
}, 300)
|
||||
|
||||
} else {
|
||||
success?.({
|
||||
tempFilePath: res.tempFilePath,
|
||||
isEmpty: signature?.isEmpty ?? false
|
||||
} as LSignatureToFileSuccess)
|
||||
}
|
||||
},
|
||||
fail: (res) => {
|
||||
fail?.(res)
|
||||
},
|
||||
complete: (res) => {
|
||||
complete?.(res)
|
||||
}
|
||||
} as TakeSnapshotOptions)
|
||||
// #endif
|
||||
|
||||
// #ifdef WEB
|
||||
// @ts-ignore
|
||||
const { backgroundColor, backgroundImage, landscape, boundingBox } = props
|
||||
const { quality = 1 } = options
|
||||
const flag = landscape || backgroundColor || boundingBox
|
||||
const type = `image/${format}`.replace(/jpg/, 'jpeg');
|
||||
const image = canvas?.toDataURL(!flag && type, !flag && quality)
|
||||
|
||||
if (flag) {
|
||||
// @ts-ignore
|
||||
const canvas = document.createElement('canvas')
|
||||
// @ts-ignore
|
||||
const pixelRatio = signature?.canvas.get('pixelRatio')
|
||||
// @ts-ignore
|
||||
let width = signature?.canvas.get('width')
|
||||
// @ts-ignore
|
||||
let height = signature?.canvas.get('height')
|
||||
let x = 0
|
||||
let y = 0
|
||||
// @ts-ignore
|
||||
const next = () => {
|
||||
const size = [width, height]
|
||||
if (landscape) {
|
||||
size.reverse()
|
||||
}
|
||||
canvas.width = size[0] * pixelRatio
|
||||
canvas.height = size[1] * pixelRatio
|
||||
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
||||
const context = canvas.getContext('2d')
|
||||
if (landscape) {
|
||||
context.translate(0, width * pixelRatio)
|
||||
context.rotate(-Math.PI / 2)
|
||||
}
|
||||
if (backgroundColor) {
|
||||
context.fillStyle = backgroundColor
|
||||
context.fillRect(0, 0, width * pixelRatio, height * pixelRatio)
|
||||
}
|
||||
const drawImage = () => {
|
||||
// @ts-ignore
|
||||
context.drawImage(signature?.canvas!.get('el'), ...param)
|
||||
success?.({
|
||||
tempFilePath: canvas.toDataURL(type, quality),
|
||||
// @ts-ignore
|
||||
isEmpty: signature?.isEmpty() ?? false
|
||||
} as LSignatureToFileSuccess)
|
||||
canvas.remove()
|
||||
}
|
||||
if (backgroundImage) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
context.drawImage(img, ...param)
|
||||
drawImage()
|
||||
}
|
||||
img.src = backgroundImage
|
||||
}
|
||||
if (!backgroundImage) {
|
||||
drawImage()
|
||||
}
|
||||
}
|
||||
if (boundingBox) {
|
||||
// @ts-ignore
|
||||
const res = signature?.getContentBoundingBox()
|
||||
width = res.width
|
||||
height = res.height
|
||||
x = res.startX
|
||||
y = res.startY
|
||||
next()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
success?.({
|
||||
tempFilePath: image,
|
||||
// @ts-ignore
|
||||
isEmpty: signature?.isEmpty() ?? false
|
||||
} as LSignatureToFileSuccess)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
defineExpose({
|
||||
clear,
|
||||
redo,
|
||||
undo,
|
||||
canvasToTempFilePath,
|
||||
})
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const width = signatureRef.value?.offsetWidth
|
||||
const height = signatureRef.value?.offsetHeight
|
||||
// #ifdef APP
|
||||
landscapeStyle.value.set('width', `${height}px`)
|
||||
landscapeStyle.value.set('height', `${width}px`)
|
||||
landscapeImageStyle.value.set('width', `${width}px`)
|
||||
landscapeImageStyle.value.set('height', `${height}px`)
|
||||
landscapeImageStyle.value.set('transform', `rotate(-90deg) translateY(${width}px)`)
|
||||
|
||||
signature = new Signature(signatureRef.value!)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
canvas = document.createElement('canvas')
|
||||
canvas!.style = 'width: 100%; height: 100%;'
|
||||
signatureRef.value?.appendChild(canvas as UniElement)
|
||||
// @ts-ignore
|
||||
signature = new Signature({ el: canvas })
|
||||
let isTouch = false
|
||||
touchstart = (event: UniMouseEvent) => {
|
||||
isTouch = true
|
||||
const rect = canvas?.getBoundingClientRect()
|
||||
// @ts-ignore
|
||||
signature!.canvas.emit('touchstart', {
|
||||
points: [
|
||||
{
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
touchmove = (event: UniMouseEvent) => {
|
||||
if(!isTouch) return
|
||||
const rect = canvas?.getBoundingClientRect()
|
||||
// @ts-ignore
|
||||
signature!.canvas.emit('touchmove', {
|
||||
points: [
|
||||
{
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
touchend = (event: UniMouseEvent) => {
|
||||
isTouch = false
|
||||
const rect = canvas?.getBoundingClientRect();
|
||||
// @ts-ignore
|
||||
signature!.canvas.emit('touchend', {
|
||||
points: [
|
||||
{
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
canvas?.addEventListener('mousedown', touchstart)
|
||||
canvas?.addEventListener('mousemove', touchmove)
|
||||
canvas?.addEventListener('mouseup', touchend)
|
||||
canvas?.addEventListener('mouseleave', touchend)
|
||||
|
||||
|
||||
// #endif
|
||||
|
||||
watchEffect(() => {
|
||||
const options : LSignatureOptions = {
|
||||
penColor: props.penColor,
|
||||
openSmooth: props.openSmooth,
|
||||
disableScroll: props.disableScroll,
|
||||
disabled: props.disabled,
|
||||
penSize: props.penSize,
|
||||
minLineWidth: props.minLineWidth,
|
||||
maxLineWidth: props.maxLineWidth,
|
||||
minSpeed: props.minSpeed,
|
||||
maxWidthDiffRate: props.maxWidthDiffRate,
|
||||
maxHistoryLength: props.maxHistoryLength
|
||||
}
|
||||
// #ifdef APP
|
||||
signature?.setOption(options)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
// @ts-ignore
|
||||
signature?.pen.setOption(options)
|
||||
// #endif
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
// #ifdef WEB
|
||||
canvas?.removeEventListener('mousedown', touchstart)
|
||||
canvas?.removeEventListener('mousemove', touchmove)
|
||||
canvas?.removeEventListener('mouseup', touchend)
|
||||
canvas?.removeEventListener('mouseleave', touchend)
|
||||
canvas?.remove()
|
||||
// #endif
|
||||
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.l-signature {
|
||||
flex: 1;
|
||||
|
||||
&-landscape {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 1000rpx;
|
||||
}
|
||||
|
||||
&-image {
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,728 @@
|
||||
<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 v-if="showOffscreen" 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'
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
/**
|
||||
* LimeSignature 手写板签名
|
||||
* @description 手写板签名插件:一款能跑在uniapp各端中的签名插件,支持横屏、背景色、笔画颜色、笔画大小等功能,可生成有内容的区域,减小图片尺寸,节省空间。
|
||||
* @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,
|
||||
showOffscreen: 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
|
||||
},
|
||||
async 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
|
||||
this.showOffscreen = true
|
||||
|
||||
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
|
||||
} else {
|
||||
this.offscreenWidth = width
|
||||
this.offscreenHeight = height
|
||||
}
|
||||
await sleep(100)
|
||||
const context = uni.createCanvasContext('offscreen', this)
|
||||
const size = Math.max(this.offscreenWidth, this.offscreenHeight)
|
||||
const success = (success) => param.success && param.success(success)
|
||||
const fail = (fail) => param.fail && param.fail(fail)
|
||||
|
||||
this.signature.pen.getMaskedImageData((imageData) => {
|
||||
let canvasPutImageData = (options, comp) => {
|
||||
if (uni.canvasPutImageData) {
|
||||
uni.canvasPutImageData(options, comp)
|
||||
} else if (context.putImageData) {
|
||||
context.putImageData(options)
|
||||
}
|
||||
}
|
||||
canvasPutImageData({
|
||||
canvasId: 'offscreen',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height:height,
|
||||
data: imageData,
|
||||
fail(err) {
|
||||
fail(err)
|
||||
},
|
||||
success: (re) => {
|
||||
toDataURL('offscreen', this, param).then((res) => {
|
||||
context.restore()
|
||||
context.clearRect(0, 0, size, size)
|
||||
this.offscreenWidth = width
|
||||
this.offscreenHeight = height
|
||||
this.showOffscreen = false
|
||||
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 = async (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]
|
||||
}
|
||||
}
|
||||
|
||||
if (boundingBox && !this.isPC || landscape || backgroundColor && !isTransparent(backgroundColor)) {
|
||||
|
||||
this.showOffscreen = true
|
||||
await sleep(100)
|
||||
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()
|
||||
this.showOffscreen = false
|
||||
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)
|
||||
this.showOffscreen = false
|
||||
success({
|
||||
tempFilePath: res,
|
||||
isEmpty
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
success({
|
||||
tempFilePath: image,
|
||||
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: 0;
|
||||
pointer-events:none;
|
||||
// background: rgba(0,255,0,0.5);
|
||||
left: 9999px;
|
||||
}
|
||||
</style>
|
64
uni_modules/jp-signature/components/jp-signature/props.js
Normal file
64
uni_modules/jp-signature/components/jp-signature/props.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export default {
|
||||
styles: String,
|
||||
disableScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '2d'
|
||||
},
|
||||
// 画笔颜色
|
||||
penColor: {
|
||||
type: String,
|
||||
default: 'black'
|
||||
},
|
||||
penSize: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
// 画板背景颜色
|
||||
backgroundColor: String,
|
||||
backgroundImage: 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
|
||||
},
|
||||
landscape: {
|
||||
type: Boolean
|
||||
},
|
||||
boundingBox: {
|
||||
type: Boolean
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean
|
||||
},
|
||||
preferToDataURL: Boolean
|
||||
}
|
228
uni_modules/jp-signature/components/jp-signature/render.js
Normal file
228
uni_modules/jp-signature/components/jp-signature/render.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// #ifdef APP-VUE
|
||||
// import { Signature } from '@signature'
|
||||
import {
|
||||
Signature
|
||||
} from './signature.js'
|
||||
import {
|
||||
isTransparent
|
||||
} 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 || this.$ownerInstance.$el;
|
||||
this.canvas = document.createElement('canvas')
|
||||
this.canvas.style = 'width: 100%; height: 100%;'
|
||||
el.appendChild(this.canvas)
|
||||
this.signature = new Signature({
|
||||
el: this.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
|
||||
}
|
||||
})
|
||||
},
|
||||
redo(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.redo()
|
||||
}
|
||||
},
|
||||
undo(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.undo()
|
||||
}
|
||||
},
|
||||
clear(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.clear()
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
if (this.canvas) {
|
||||
this.canvas.remove()
|
||||
}
|
||||
},
|
||||
mask(param={}) {
|
||||
if (this.signature) {
|
||||
let {destWidth=0, destHeight=0} = JSON.parse(param)
|
||||
let canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d');
|
||||
const pixelRatio = this.signature.canvas.get('pixelRatio')
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let context = this.signature.canvas.get('context')
|
||||
canvas.width = width * pixelRatio
|
||||
canvas.height = height * pixelRatio
|
||||
|
||||
const imageData = context.getImageData(0, 0, width * pixelRatio, height * pixelRatio);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
// 判断当前像素是否透明
|
||||
const isTransparent = imageData.data[i + 3] === 0;
|
||||
|
||||
if (isTransparent) {
|
||||
// 将透明像素设置为黑色背景
|
||||
imageData.data[i] = 0;
|
||||
imageData.data[i + 1] = 0;
|
||||
imageData.data[i + 2] = 0;
|
||||
} else {
|
||||
// 将非透明像素设置为白色内容
|
||||
imageData.data[i] = 255;
|
||||
imageData.data[i + 1] = 255;
|
||||
imageData.data[i + 2] = 255;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
if(destWidth&&destHeight){
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
this.emit({
|
||||
save: canvas.toDataURL()
|
||||
})
|
||||
canvas.remove()
|
||||
}
|
||||
|
||||
},
|
||||
save(param) {
|
||||
let {
|
||||
fileType = 'png',
|
||||
quality = 1,
|
||||
n,
|
||||
destWidth = 0,
|
||||
destHeight = 0,
|
||||
} = JSON.parse(param)
|
||||
const type = `image/${fileType}`.replace(/jpg/, 'jpeg');
|
||||
if (n !== this.saveCount) {
|
||||
this.saveCount = n;
|
||||
const {
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
landscape,
|
||||
boundingBox
|
||||
} = this.options
|
||||
const flag = landscape || backgroundColor || boundingBox||destWidth&&destHeight
|
||||
const image = this.signature.canvas.get('el').toDataURL(!flag && type, !flag && quality)
|
||||
if (flag) {
|
||||
let canvas = document.createElement('canvas')
|
||||
const pixelRatio = this.signature.canvas.get('pixelRatio')
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
const next = () => {
|
||||
const size = [width, height]
|
||||
if (landscape) {
|
||||
size.reverse()
|
||||
}
|
||||
canvas.width = size[0] * pixelRatio
|
||||
canvas.height = size[1] * pixelRatio
|
||||
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
||||
const context = canvas.getContext('2d')
|
||||
if (landscape) {
|
||||
context.translate(0, width * pixelRatio)
|
||||
context.rotate(-Math.PI / 2)
|
||||
}
|
||||
if (backgroundColor && !isTransparent(backgroundColor)) {
|
||||
context.fillStyle = backgroundColor
|
||||
context.fillRect(0, 0, width * pixelRatio, height * pixelRatio)
|
||||
}
|
||||
const drawImage = () => {
|
||||
// param
|
||||
context.drawImage(this.signature.canvas.get('el'), ...param)
|
||||
if(destWidth&&destHeight){
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
this.emit({
|
||||
save: canvas.toDataURL(type, quality)
|
||||
})
|
||||
canvas.remove()
|
||||
}
|
||||
if (backgroundImage) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
context.drawImage(img, ...param)
|
||||
drawImage()
|
||||
}
|
||||
img.src = backgroundImage
|
||||
}
|
||||
if (!backgroundImage) {
|
||||
drawImage()
|
||||
}
|
||||
}
|
||||
if (boundingBox) {
|
||||
const res = this.signature.getContentBoundingBox()
|
||||
width = res.width
|
||||
height = res.height
|
||||
x = res.startX
|
||||
y = res.startY
|
||||
next()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
} else {
|
||||
this.emit({
|
||||
save: image
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
File diff suppressed because one or more lines are too long
165
uni_modules/jp-signature/components/jp-signature/signature.uts
Normal file
165
uni_modules/jp-signature/components/jp-signature/signature.uts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { LSignatureOptions, Point, Line } from '../../index.uts'
|
||||
|
||||
let points : Line = []
|
||||
let undoStack : Line[] = [];
|
||||
let redoStack : Line[] = [];
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
|
||||
export class Signature {
|
||||
el : UniElement
|
||||
options : LSignatureOptions = {
|
||||
penColor: 'black',
|
||||
openSmooth: true,
|
||||
disableScroll: true,
|
||||
disabled: false,
|
||||
penSize: 2,
|
||||
minLineWidth: 2,
|
||||
maxLineWidth: 6,
|
||||
minSpeed: 1.5,
|
||||
maxWidthDiffRate: 20,
|
||||
maxHistoryLength: 20
|
||||
} as LSignatureOptions
|
||||
ctx : DrawableContext
|
||||
isEmpty : boolean = true
|
||||
isDrawing : boolean = false
|
||||
// historyList : Point[][] = []
|
||||
// id : string
|
||||
// instance : ComponentPublicInstance
|
||||
touchstartCallbackWrapper: UniCallbackWrapper|null = null
|
||||
touchmoveCallbackWrapper: UniCallbackWrapper|null= null
|
||||
touchendCallbackWrapper: UniCallbackWrapper|null= null
|
||||
constructor(el : UniElement) {
|
||||
this.el = el
|
||||
this.ctx = el.getDrawableContext() as DrawableContext
|
||||
this.init()
|
||||
}
|
||||
init() {
|
||||
this.touchstartCallbackWrapper = this.el.addEventListener('touchstart', this.onTouchStart)
|
||||
this.touchmoveCallbackWrapper = this.el.addEventListener('touchmove', this.onTouchMove)
|
||||
this.touchendCallbackWrapper = this.el.addEventListener('touchend', this.onTouchEnd)
|
||||
}
|
||||
remove() {
|
||||
if(this.touchstartCallbackWrapper == null) return
|
||||
this.el.removeEventListener('touchstart', this.touchstartCallbackWrapper!)
|
||||
this.el.removeEventListener('touchmove', this.touchmoveCallbackWrapper!)
|
||||
this.el.removeEventListener('touchend', this.touchendCallbackWrapper!)
|
||||
}
|
||||
setOption(options : LSignatureOptions) {
|
||||
this.options = options
|
||||
}
|
||||
disableScroll(event : UniTouchEvent) {
|
||||
event.stopPropagation()
|
||||
if (this.options.disableScroll) {
|
||||
{
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
getTouchPoint(event : UniTouchEvent) : Point {
|
||||
const rect = this.el.getBoundingClientRect()
|
||||
const touche = event.touches[0];
|
||||
const x = touche.clientX
|
||||
const y = touche.clientY
|
||||
// const force = touche.force
|
||||
return {
|
||||
x: x - rect.left,
|
||||
y: y - rect.top
|
||||
} as Point
|
||||
}
|
||||
onTouchStart: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
if (this.options.disabled) {
|
||||
return
|
||||
}
|
||||
this.disableScroll(event)
|
||||
const { x, y } = this.getTouchPoint(event)
|
||||
this.isDrawing = true;
|
||||
this.isEmpty = false
|
||||
lastX = x
|
||||
lastY = y
|
||||
points.push({ x, y } as Point);
|
||||
}
|
||||
onTouchMove: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
if (this.options.disabled || !this.isDrawing) {
|
||||
return
|
||||
}
|
||||
this.disableScroll(event)
|
||||
const { x, y } = this.getTouchPoint(event)
|
||||
const lineWidth = this.options.penSize
|
||||
const strokeStyle = this.options.penColor
|
||||
const point = { x, y } as Point
|
||||
const last = { x: lastX, y: lastY } as Point
|
||||
this.drawLine(point, last, lineWidth, strokeStyle)
|
||||
|
||||
lastX = x
|
||||
lastY = y
|
||||
points.push({ x, y, c: strokeStyle, w: lineWidth } as Point);
|
||||
}
|
||||
onTouchEnd: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
this.disableScroll(event)
|
||||
this.isDrawing = false;
|
||||
undoStack.push(points);
|
||||
redoStack = [] as Line[];
|
||||
points = [] as Point[];
|
||||
}
|
||||
drawLine(point : Point, last : Point, lineWidth : number, strokeStyle : string) {
|
||||
const ctx = this.ctx
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(last.x, last.y)
|
||||
ctx.lineTo(point.x, point.y)
|
||||
ctx.stroke()
|
||||
ctx.update()
|
||||
}
|
||||
// addHistory() { }
|
||||
clear() {
|
||||
this.ctx.reset()
|
||||
this.ctx.update()
|
||||
this.isEmpty = true
|
||||
undoStack = [] as Line[];
|
||||
redoStack = [] as Line[];
|
||||
points = [] as Point[];
|
||||
}
|
||||
undo() {
|
||||
if(redoStack.length == this.options.maxHistoryLength && this.options.maxHistoryLength != 0){
|
||||
return
|
||||
}
|
||||
this.ctx.reset()
|
||||
if(undoStack.length > 0){
|
||||
const lastPath : Line = undoStack.pop()!;
|
||||
redoStack.push(lastPath);
|
||||
if(undoStack.length == 0){
|
||||
this.isEmpty = true
|
||||
this.ctx.update()
|
||||
return
|
||||
}
|
||||
for (let l = 0; l < undoStack.length; l++) {
|
||||
for (let i = 1; i < undoStack[l].length; i++) {
|
||||
const last = undoStack[l][i - 1]
|
||||
const point = undoStack[l][i]
|
||||
this.drawLine(point, last, point.w!, point.c!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.ctx.update()
|
||||
}
|
||||
}
|
||||
redo() {
|
||||
if(redoStack.length < 1) return
|
||||
const lastPath : Line = redoStack.pop()!;
|
||||
undoStack.push(lastPath);
|
||||
this.isEmpty = false
|
||||
for (let l = 0; l < undoStack.length; l++) {
|
||||
for (let i = 1; i < undoStack[l].length; i++) {
|
||||
const last = undoStack[l][i - 1]
|
||||
const point = undoStack[l][i]
|
||||
this.drawLine(point, last, point.w!, point.c!)
|
||||
}
|
||||
}
|
||||
}
|
||||
// restore() { }
|
||||
}
|
181
uni_modules/jp-signature/components/jp-signature/utils.js
Normal file
181
uni_modules/jp-signature/components/jp-signature/utils.js
Normal file
@@ -0,0 +1,181 @@
|
||||
export function compareVersion(v1, v2) {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
const len = Math.max(v1.length, v2.length)
|
||||
while (v1.length < len) {
|
||||
v1.push('0')
|
||||
}
|
||||
while (v2.length < len) {
|
||||
v2.push('0')
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(v1[i], 10)
|
||||
const num2 = parseInt(v2[i], 10)
|
||||
|
||||
if (num1 > num2) {
|
||||
return 1
|
||||
} else if (num1 < num2) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function gte(version) {
|
||||
let { SDKVersion } = uni.getSystemInfoSync()
|
||||
// #ifdef MP-ALIPAY
|
||||
SDKVersion = my.SDKVersion
|
||||
// #endif
|
||||
return compareVersion(SDKVersion, version) >= 0;
|
||||
}
|
||||
|
||||
export function canIUseCanvas2d() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return gte('2.9.0');
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
return gte('2.7.0');
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
return gte('1.78.0');
|
||||
// #endif
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
export const wrapEvent = (e) => {
|
||||
if (!e) return;
|
||||
if (!e.preventDefault) {
|
||||
e.preventDefault = function() {};
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
export const requestAnimationFrame = (cb) => {
|
||||
setTimeout(cb, 30)
|
||||
}
|
||||
|
||||
// #ifdef MP
|
||||
export const prefix = () => {
|
||||
// #ifdef MP-TOUTIAO
|
||||
return tt
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
return wx
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU
|
||||
return swan
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
return my
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
return qq
|
||||
// #endif
|
||||
// #ifdef MP-360
|
||||
return qh
|
||||
// #endif
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* base64转路径
|
||||
* @param {Object} base64
|
||||
*/
|
||||
export function base64ToPath(base64) {
|
||||
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP
|
||||
const p = prefix()
|
||||
const fs = p.getFileSystemManager()
|
||||
//自定义文件名
|
||||
if (!format) {
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
const filePath = `${p.env.USER_DATA_PATH}/${time}.${format}`;
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64.split(',')[1],
|
||||
encoding: 'base64',
|
||||
success() {
|
||||
resolve(filePath)
|
||||
},
|
||||
fail(err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
|
||||
bitmap.loadBase64Data(base64, () => {
|
||||
if (!format) {
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
const filePath = `_doc/uniapp_temp/${time}.${format}`
|
||||
bitmap.save(filePath, {},
|
||||
() => {
|
||||
bitmap.clear()
|
||||
resolve(filePath)
|
||||
},
|
||||
(error) => {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
}, (error) => {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function sleep(delay) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
|
||||
export function getRect(selector, options = {}) {
|
||||
const typeDefault = 'boundingClientRect'
|
||||
const { context, type = typeDefault} = options
|
||||
return new Promise((resolve, reject) => {
|
||||
const dom = uni.createSelectorQuery().in(context).select(selector);
|
||||
const result = (rect) => {
|
||||
if(rect) {
|
||||
resolve(rect)
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
if(type == typeDefault) {
|
||||
dom[type](result).exec()
|
||||
} else {
|
||||
dom[type]({
|
||||
node: true,
|
||||
size: true,
|
||||
rect: true
|
||||
}, result).exec()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function isTransparent(color) {
|
||||
// 判断颜色是否为 transparent
|
||||
if (color === 'transparent') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 判断颜色是否为 rgba 的 a 为 0
|
||||
if (color.startsWith('rgba')) {
|
||||
const regex = /\d+(\.\d+)?/g;
|
||||
const matches = color.match(regex);
|
||||
if (matches !== null) {
|
||||
const alpha = parseFloat(matches[3]);
|
||||
if (alpha === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
Reference in New Issue
Block a user