移动端V1.0

This commit is contained in:
2025-07-16 15:34:34 +08:00
commit 194b0750fd
1083 changed files with 178295 additions and 0 deletions

View 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>

View File

@@ -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支持bas64url 等图片显示)
* 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>

View 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)
}
})
}

View File

@@ -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>

View File

@@ -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>

View 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
}

View 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

View 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() { }
}

View 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;
}