vue3 smooth-signature 带笔锋手写签名

mini-smooth-signature 小程序版带笔锋手写签名,支持多平台小程序使用

参考:GitHub - linjc/smooth-signature: H5带笔锋手写签名,支持PC端和移动端,任何前端框架均可使用

一、安装

npm install smooth-signature
# 或
yarn add smooth-signature
 

或通过<script>引用,全局变量 window.SmoothSignature

<script src="https://unpkg.com/smooth-signature/dist/index.umd.min.js" />
 

也可自行下载smooth-signature.js到本地引用

二、使用

<div>
<canvas />
</div>
 
import SmoothSignature from "smooth-signature";
const canvas = document.querySelector("canvas");
const signature = new SmoothSignature(canvas);
// 生成PNG
signature.getPNG() // 或者 signature.toDataURL()
// 生成JPG
signature.getJPG() // 或者 signature.toDataURL('image/jpeg')
// 清屏
signature.clear()
// 撤销
signature.undo()
// 是否为空
signature.isEmpty()
// 生成旋转后的新画布 -90/90/-180/180
signature.getRotateCanvas(90)
 

配置[options]

所有配置项均是可选的

const signature = new SmoothSignature(canvas, {
width: 1000,
height: 600,
scale: 2,
minWidth: 4,
maxWidth: 10,
color: '#1890ff',
bgColor: '#efefef'
});
 

options.width

画布在页面实际渲染的宽度(px)

  • Type: number
  • Default:canvas.clientWidth || 320

options.height

画布在页面实际渲染的高度(px)

  • Type: number
  • Default:canvas.clientHeight || 200

options.scale

画布缩放,可用于提高清晰度

  • Type: number
  • Default:window.devicePixelRatio || 1

options.color

画笔颜色

  • Type: string
  • Default:black

options.bgColor

画布背景颜色,默认透明

  • Type: string
  • Default:

options.openSmooth

是否开启笔锋效果,默认开启

  • Type: boolean
  • Default:true

options.minWidth

画笔最小宽度(px),开启笔锋时画笔最小宽度

  • Type: number
  • Default:2

options.maxWidth

画笔最大宽度(px),开启笔锋时画笔最大宽度,或未开启笔锋时画笔正常宽度

  • Type: number
  • Default:6

options.minSpeed

画笔达到最小宽度所需最小速度(px/ms),取值范围1.0-10.0,值越小,画笔越容易变细,笔锋效果会比较明显,可以自行调整查看效果,选出自己满意的值。

  • Type: number
  • Default:1.5

options.maxWidthDiffRate

相邻两线宽度增(减)量最大百分比,取值范围1-100,为了达到笔锋效果,画笔宽度会随画笔速度而改变,如果相邻两线宽度差太大,过渡效果就会很突兀,使用maxWidthDiffRate限制宽度差,让过渡效果更自然。可以自行调整查看效果,选出自己满意的值。

  • Type: number
  • Default:20

options.maxHistoryLength

限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能

  • Type: number
  • Default:20

options.onStart

绘画开始回调函数

  • Type: function

options.onEnd

绘画结束回调函数

  • Type: function

三、实现原理

我们平时纸上写字,细看会发现笔画的粗细是不均匀的,这是写字过程中,笔的按压力度和移动速度不同而形成的。而在电脑手机浏览器上,虽然我们无法获取到触摸的压力,但可以通过画笔移动的速度来实现不均匀的笔画效果,让字体看起来和纸上写字一样有“笔锋”。下面介绍具体实现过程(以下展示代码只为方便理解,非最终实现代码)。

1、采集画笔经过的点坐标和时间

通过监听画布move事件采集移动经过的点坐标,并记录当前时间,然后保存到points数组中。

function onMove(event) {
const e = event.touches && event.touches[0] || event;
const rect = this.canvas.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
t: Date.now()
}
points.push(point);
}
 

2、计算两点之间移动速度

通过两点坐标计算出两点距离,再除以时间差,即可得到移动速度。

const distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
const speed = distance / (end.t - start.t);
 

3、计算两点之间线的宽度

得到两点间移动速度,接下来通过简单算法计算出线的宽度,其中maxWidth、minWidth、minSpeed为配置项

const addWidth = (maxWidth - minWidth) * speed / minSpeed;
const lineWidth = Math.min(Math.max(maxWidth - addWidth, minWidth), maxWidth);
 

另外,为了防止相邻两条线宽度差太大,而出现突兀的过渡效果,需要做下限制,其中maxWidthDiffRate为配置项,preLineWidth为上一条线的宽度

const rate = (lineWidth - preLineWidth) / preLineWidth;
const maxRate = maxWidthDiffRate / 100;
if (Math.abs(rate) > maxRate) {
const per = rate > 0 ? maxRate : -maxRate;
lineWidth = preLineWidth * (1 + per);
}
 

4、画线

现在已经知道每两点间线的宽度,接下来就是画线了。为了让线条看起来圆润以及线粗细过渡更自然,我把两点之间的线平均成三段,其中:

  1. 第一段(x0,y0 - x1,y1)线宽设置为当前线宽和上一条线宽的平均值lineWidth1 = (preLineWidth + lineWidth) / 2
  2. 第二段(x1,y1 - x2,y2)
  3. 第三段(x2,y2 - next_x0,next_y0)线宽设置为当前线宽和下一条线宽的平均值lineWidth3 = (nextLineWidth + lineWidth) / 2

开始画线,先来看第一段线,因为第一段线和上一条线相交,为了保证两条线过渡比较圆润,采用二次贝塞尔曲线,起点为上一条线的第三段起点(pre_x2, pre_y2)

ctx.lineWidth = lineWidth1
ctx.beginPath();
ctx.moveTo(pre_x2, pre_y2);
ctx.quadraticCurveTo(x0, y0, x1, y1);
ctx.stroke();
 

第二段线为承接第一段和第三段的过渡线,由于第一段和第三段线宽有差异,所以第二段线使用梯形填充,让过渡效果更自然。

ctx.beginPath();
ctx.moveTo(point1.x, point1.y);
ctx.lineTo(point2.x, point2.y);
ctx.lineTo(point3.x, point3.y);
ctx.lineTo(point4.x, point4.y);
ctx.fill();
 

第三段等画下一条线时重复上述操作即可。

四、例子

(1)PC版

<template>
<a-modal :default-visible="true" width="880px" :footer="false">
<template #title>签约</template>
<div class="wrapper">
<canvas id="drag-canvas" canvas-id="drag-canvas" style="width: 800px; height: 400px" />
<div class="actions">
<a-button class="btn" @click="handleClear">清空</a-button>
<a-button class="btn" @click="handleUndo">撤销</a-button>
<a-button class="btn" type="primary" @click="handlePreview">确认</a-button>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import SmoothSignature from 'smooth-signature';
import { Message } from '@arco-design/web-vue';
let canvas = {} as HTMLCanvasElement;
let signature = {} as SmoothSignature;
const init = () => {
canvas = document.querySelector('canvas') as HTMLCanvasElement;
const options = {
width: 800,
height: 400,
minWidth: 4,
maxWidth: 12,
bgColor: '#f6f6f6',
};
signature = new SmoothSignature(canvas, options);
};
const handleClear = () => {
signature.clear();
};
const handleUndo = () => {
signature.undo();
};
const emits = defineEmits(['handlePreview']);
const handlePreview = () => {
const isEmpty = signature.isEmpty();
if (isEmpty) {
Message.warning('签名不得为空');
return;
}
const pngUrl = signature.getPNG();
emits('handlePreview', pngUrl);
};
setTimeout(() => {
init();
}, 1000);
</script>
<style scoped lang="less">
.wrapper {
padding: 15px 20px;
canvas {
border: 2px dashed #ccc;
cursor: crosshair;
margin: auto;
display: block;
}
.actions {
margin: 30px 0 0px;
display: flex;
text-align: center;
.btn {
flex: 1;
margin-right: 20px;
font-size: 18px !important;
height: 56px !important;
padding: 0 20px;
&:last-child {
margin-right: 0;
}
}
}
.tip {
color: #108eff;
}
}
</style>

 

 (2)小程序版

<template>
<!-- 签名 -->
<view class="sign-box" v-if="signShow">
<view class="btn">
<button class="item" @click="cancel">
<text class="text">取消</text>
</button>
<button class="item" @click="clear">
<text class="text">清空</text>
</button>
<button class="item active" @click="save">
<text class="text">确定</text>
</button>
</view>
<canvas
class="canvas"
disable-scroll="true"
:style="{ width: width + 'px', height: height + 'px' }"
canvas-id="designature"
id="designature"
@touchstart="start"
@touchmove="move"
@touchend="end"
></canvas>
<view class="title">
<text class="text">请签字</text>
</view>
</view>
<!-- -->
<view
class="sign-small-box"
:class="(isSign == false && signImage == '') || sign_image_url ? 'border' : ''"
@click="show"
>
<view class="tip" v-if="isSign == false && signImage == ''">点击这里签署您的姓名 </view>
<image class="sign-img" :src="signImage" v-else mode="heightFix" />
</view>
<view class="bottom-tip" v-if="sign_image_url">请点击上方签名图片重新签名</view>
<!-- 旋转画布 -->
<view />
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import SmoothSignature from 'mini-smooth-signature';
import { onLoad } from '@dcloudio/uni-app';
const signShow = ref(false);
const signImage = ref('');
const width = ref(window.innerWidth - 110); //
const height = ref(window.innerHeight - 100);
const line = ref([]);
const scale = ref(1);
const isSign = ref(false);
const signature = ref(null);
const props = defineProps({
sign_image_url: {
type: String,
},
});
const show = () => {
signShow.value = true;
nextTick(() => {
initSignature();
});
};
const cancel = () => {
uni.navigateBack();
};
const distance = (a: any, b: any) => {
let x = b.x - a.x;
let y = b.y - a.y;
return Math.sqrt(x * x + y * y);
};
// 初始化
const initSignature = () => {
const ctx = uni.createCanvasContext('designature');
signature.value = new SmoothSignature(ctx, {
minWidth: 4,
maxWidth: 10,
width: width.value,
height: height.value,
scale: scale.value,
getImagePath: () => {
return new Promise((resolve) => {
uni.canvasToTempFilePath(
{
canvasId: 'designature',
fileType: 'png',
quality: 1, //图片质量
success(res) {
resolve(res.tempFilePath);
},
},
this,
);
});
},
});
};
// 绑定touchstart事件
const start = (e: any) => {
isSign.value = false;
const pos = e.touches[0];
signature.value.onDrawStart(pos.x, pos.y);
};
// 绑定touchmove事件
const move = (e: any) => {
const pos = e.touches[0];
signature.value.onDrawMove(pos.x, pos.y);
};
// 绑定touchend/touchcancel事件
const end = () => {
signature.value.onDrawEnd();
isSign.value = true;
};
const clear = () => {
signature.value.clear();
isSign.value = false;
};
const emit = defineEmits(['backImage']);
const save = () => {
if (!isSign.value) {
uni.showToast({
title: '请先签字再保存',
icon: 'none',
});
return;
}
signShow.value = false;
signature.value.getImagePath().then((imageUrl) => {
signImage.value = imageUrl;
emit('backImage', imageUrl);
});
};
onLoad(() => {
if (props.sign_image_url) {
signImage.value = props.sign_image_url;
}
uni.getSystemInfo({
success: (res) => {
width.value = res.windowWidth - 110;
height.value = res.windowHeight - 100;
},
});
});
</script>
<style lang="scss" scoped>
.sign-small-box {
overflow: hidden;
width: 90%;
height: 340rpx;
cursor: pointer;
margin: 0 auto 30rpx;
display: flex;
align-items: center;
justify-content: center;
&.border {
border: 2rpx dashed #ccc;
border-radius: 20rpx;
}
.tip {
display: flex;
align-items: center;
justify-content: center;
color: #999;
width: 100%;
height: 100%;
font-size: 28rpx;
}
.sign-img {
transform: rotate(-90deg);
}
}
.bottom-tip {
font-size: 26rpx;
display: block;
color: #999;
width: 90%;
margin: 0 auto 30rpx;
}
.sign-box {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background: #fff;
.title {
font-size: 40rpx;
font-weight: bold;
white-space: nowrap;
letter-spacing: 12rpx;
width: 110rpx;
min-width: 110rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.text {
position: absolute;
top: 0;
bottom: 0;
right: -60rpx;
transform: rotate(90deg);
width: 200rpx;
}
}
.canvas {
border: 2rpx dashed #ccc;
border-radius: 20rpx;
overflow: hidden;
cursor: crosshair;
}
.btn {
width: 120rpx;
min-width: 120rpx;
white-space: nowrap;
height: 100%;
display: grid;
.item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
background: none;
box-sizing: border-box;
padding: 0;
letter-spacing: 10rpx;
position: relative;
width: 100%;
color: #666;
cursor: pointer;
.text {
display: block;
transform: rotate(90deg);
}
&.active {
color: $uni-main-color;
}
&:after {
content: '';
position: absolute;
top: 0;
background: #eee;
width: 100%;
left: 50%;
transform: translate(-50%, 0%);
height: 1rpx;
display: block;
}
&:first-child {
&:after {
display: none;
}
}
}
}
}
</style>

 

posted @   蓦然JL  阅读(288)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
  1. 1 唯一 G.E.M.邓紫棋
  2. 2 他只是经过 白敬亭 魏大勋
  3. 3 Uptown Funk Mark Ronson / Bruno Mars
  4. 4 在你的身边 盛哲
  5. 5 Edge of My Life Manafest
  6. 6 凄美地 郭顶
  7. 7 Wonderful Tonight Boyce Avenue
  8. 8 心如止水 Ice Paper
  9. 9 Sugar Maroon 5
  10. 10 静谧时光 JIAxNING
  11. 11 Right Now (Na Na Na) Aamir
  12. 12 Dangerously Charlie Puth
  13. 13 Someone You Loved Madilyn Paige
  14. 14 Shape of My Heart Boyce Avenue
  15. 15 We Can't Stop Boyce Avenue / Bea Miller
  16. 16 Perfect Boyce Avenue
  17. 17 Love Me Like You Do Boyce Avenue
  18. 18 Thank You Boyce Avenue
  19. 19 Don’t Wanna Know Boyce Avenue / Sarah Hyland
他只是经过 - 白敬亭 魏大勋
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

Not available

访问主页
关注我
关注微博
私信我
返回顶部
点击右上角即可分享
微信分享提示

目录导航