最近因为项目需求研究了一阵子微信小程序手写签名,刚开始是从dcloud插件市场下载了一个插件来直接用(图一)。这个插件其实已经具备了很多功能,可以根据需要设置横屏和竖屏,但是有一个很大的问题是横屏签好字之后,生成的图片是竖屏的,这一点很不好,前端可以在展示的时候用 transform: rotate(-90deg);把图片逆时针旋转到正面显示,但是传给后端的格式就不对了,所以,再此基础之上,我做了一些小小的改动,让这个插件更完美。
图一:
步骤1:使用插件,签字图片显示
<template>
<view class="">
<view class="preview" v-if="imgUrl">
<image :src="imgUrl" mode=""></image>
</view>
<view class="sign">
<!--abeam-横屏,portrait ==> 纵向 默认纵向 -->
<csr-sign @finish="signatureChange" orientation="abeam"></csr-sign>
</view>
</view>
</template>
<script>
export default {
data(){
return{
imgUrl:'',
}
},
methods:{
signatureChange(e) {
this.imgUrl = e
//根据后端需要自己做处理即可
//由于我的后端需要把'data:image/png;base64,'去掉,在这里处理一下,
let str,imgURL
str = this.imgUrl.indexOf('/',this.imgUrl.indexOf('/')+1)
imgURL = this.imgUrl.substring(str+1)
},
}
}
</script>
<style>
.preview>image{
/*如果不在插件里面做旋转处理就直接用transform旋转图片就行了 */
/* transform: rotate(-90deg);
width:100rpx;
height: 300rpx; */
/* 在插件里面做了旋转处理就直接设置图片显示宽高就行 */
width:500rpx;
height:200rpx;
}
</style>
2、 插件内部调整
(1)创建 Canvas 组件:在 .wxml
文件中添加一个 Canvas 组件。
<view class="">
<canvas id="myCanvas" canvas-id="myCanvas" @touchmove="move" @touchstart="start" @error="error"
@touchend="touchend" :style="{width:canvasWidth + 'px',height:canvasHeight + 'px'}">
</canvas>
</view>
注意:新版canvas需要把canvas-id="myCanvas"换成type="2d"
(2)在onReady中获取canvas上下文: let ctx = uni.createCanvasContext('myCanvas', this);
onReady() {
this.createCanvas();
},
methods:{
createCanvas() {
const pr = this.pr; // 像素比
ctx = uni.createCanvasContext('myCanvas', this);
ctx.lineGap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = this.lineWidth; // 字体粗细
ctx.strokeStyle = this.strokeStyle; // 字体粗细
this.ctx = ctx;
},
}
注意:在微信小程序中,获取 Canvas 上下文时需要确保 DOM 已经完全加载完毕。在 onLoad
或 onShow
生命周期钩子中直接获取 Canvas 上下文可能会因为 DOM 还未加载完成而导致获取不到正确的节点。
(3) 触摸开始 :
- 当用户开始触摸屏幕时,记录触摸点的位置,并开始一条新的路径。
- 使用
beginPath
方法开始一条新的路径,并将起点设置为当前触摸点的位置。 - 将当前触摸点的位置保存到this.points 中。
start(e) {
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
});
//每次触摸开始,开启新的路径
this.ctx.beginPath();
},
(4) 触摸移动 :
- 当用户移动手指时,从上次触摸点的位置绘制到当前触摸点的位置。
- 使用
lineTo
方法将路径延伸到当前触摸点的位置,并使用stroke
方法绘制路径。 - 更新
this.points
为当前触摸点的位置。
// 开始移动
move(e) {
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
}); //存点
if (this.points.length >= 2) {
this.draw(); //绘制路径
}
},
/* ***********************************************
绘制笔迹
1.为保证笔迹实时显示,必须在移动的同时绘制笔迹
2.为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo)
3.将上一次的终点作为下一次绘制的起点(即清除第一个点)
************************************************ */
draw() {
let point1 = this.points[0];
let point2 = this.points[1];
this.points.shift();
this.ctx.moveTo(point1.X, point1.Y);
this.ctx.lineTo(point2.X, point2.Y);
this.ctx.stroke();
this.ctx.draw(true);
this.tempPoint.push(this.points)
},
(5)触摸结束:
- 当用户抬起手指时,停止绘制。
// 触摸结束,将未绘制的点清空防止对后续路径产生干扰
touchend() {
this.points = [];
},
(6)完成签名
这里用tempPoint之前存的触摸点的坐标来判断是否有签名,没有会提示
// 点击完成签名
finish() {
let that = this
if (that.tempPoint.length == 0) {
uni.showToast({
title: '您还未签名,请先签名',
icon: 'none',
duration: 2000
});
return
}
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function(res) {
if (res.tempFilePath.startsWith('data:image/png;base64')) {
that.emit(res.tempFilePath)
} else {
that.translateImg(res.tempFilePath)
}
},
fail(e) {
console.log(JSON.stringify(e))
}
}, this)//要用this指向,要不然canvasToTempFilePath会报undefined
},
//回调父组件finish方法
emit(tempFilePath) {
this.$emit("finish", tempFilePath);
},
(7)最后一步,旋转画布
a、获取图片信息wx.getImageInfo,拿到刚刚生成的签名图片信息,宽高和本地路径
wx.getImageInfo({
src: tempFilePath,
success: (res) => {
const path = res.path
const height = res.width; //宽305
const width = res.height; //高603
})
b、重新获取一个新的上下文,把之前的笔记清除掉
// 2. 创建画布上下文
const contx = uni.createCanvasContext('myCanvas', _this);
//清除掉原本的笔迹
contx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 将旋转中心设置为canvas的中心
contx.translate(150, 260);
// 逆时针旋转90度
contx.rotate(-Math.PI / 2);
//3、将图片绘制到canvas上,注意:这里的坐标是旋转后的坐标,需要调整
contx.drawImage(path, -300, -150, 500, 200);
c、将临时文件路径转换为Base64
pathToBase64(path) {
return new Promise(function(resolve, reject) {
// app
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(path, function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(evt) {
resolve(evt.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
// 微信小程序
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: function(res) {
resolve('data:image/png;base64,' + res.data)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
3、完整代码
<template>
<view class="csr-sign">
<view v-if="orientation == 'abeam'">
<view class="sighButtons">
<view class="sighButtons_button"
style="border: solid 1rpx #999; color: #999;background-color: #fff;"
@click.tap="back">
<view class="sighButtons_button_xiao">返回</view>
</view>
<view class="sighButtons_button" style="background: #999;" @click.tap="clearClick">
<view class="sighButtons_button_xiao">重签</view>
</view>
<view class="sighButtons_button" style="background-color: #0a8afd;" @click.tap="finish">
<view class="sighButtons_button_xiao">完成签名</view>
</view>
</view>
</view>
<view class="">
<canvas id="myCanvas" canvas-id="myCanvas" @touchmove="move" @touchstart="start" @error="error"
@touchend="touchend" :style="{width:canvasWidth + 'px',height:canvasHeight + 'px'}">
</canvas>
</view>
<view v-if="orientation == 'portrait'">
<view class="sighButton ">
<view class="sighButton_button" style="border: solid 1rpx #999; color: #999;background-color: #fff;"
@click.tap="back">
<view class="sighButton_button_xiao">返回</view>
</view>
<view class="sighButton_button" style="background: #999;" @click.tap="clearClick">
<view class="sighButton_button_xiao">重签</view>
</view>
<view class="sighButton_button" style="background-color: #0a8afd;" @click.tap="finish">
<view class="sighButton_button_xiao">完成签名</view>
</view>
</view>
</view>
</view>
</template>
<script>
let ctx
export default {
name: "csr-sign",
props: {
orientation: {
type: String,
default: 'portrait'
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
lineWidth: {
type: Number,
default: 3
},
strokeStyle: {
type: String,
default: "black"
}
},
data() {
return {
canvas: '',
ctx: '',
pr: 0,
canvasWidth: '',
canvasHeight: '',
points: [],
tempPoint: []
};
},
onReady() {
this.getSystemInfo();
this.createCanvas();
},
methods: {
createCanvas() {
const pr = this.pr; // 像素比
ctx = uni.createCanvasContext('myCanvas', this);
ctx.lineGap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = this.lineWidth; // 字体粗细
ctx.strokeStyle = this.strokeStyle; // 字体粗细
this.ctx = ctx;
},
// 获取系统信息
getSystemInfo() {
uni.getSystemInfo({
success: (res) => {
this.pr = res.pixelRatio;
if (this.orientation == 'portrait') {
if (this.width > res.windowWidth || this.width == 0) {
this.canvasWidth = res.windowWidth;
} else {
this.canvasWidth = this.width;
}
if (this.height > res.windowHeight - 70 || this.height == 0) {
this.canvasHeight = res.windowHeight - 70;
} else {
this.canvasHeight = this.height;
}
} else if (this.orientation == 'abeam') {
if (this.width > res.windowWidth - 70 || this.width == 0) {
this.canvasWidth = res.windowWidth - 70;
} else {
this.canvasWidth = this.width;
}
if (this.height > res.windowHeight || this.height == 0) {
this.canvasHeight = res.windowHeight;
} else {
this.canvasHeight = this.height;
}
}
}
})
},
// 触摸开始
start(e) {
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
});
//每次触摸开始,开启新的路径
this.ctx.beginPath();
},
// 开始移动
move(e) {
this.points.push({
X: e.touches[0].x,
Y: e.touches[0].y
}); //存点
if (this.points.length >= 2) {
this.draw(); //绘制路径
}
},
// 触摸结束,将未绘制的点清空防止对后续路径产生干扰
touchend() {
this.points = [];
},
/* ***********************************************
绘制笔迹
1.为保证笔迹实时显示,必须在移动的同时绘制笔迹
2.为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo)
3.将上一次的终点作为下一次绘制的起点(即清除第一个点)
************************************************ */
draw() {
let point1 = this.points[0];
let point2 = this.points[1];
this.points.shift();
this.ctx.moveTo(point1.X, point1.Y);
this.ctx.lineTo(point2.X, point2.Y);
this.ctx.stroke();
this.ctx.draw(true);
this.tempPoint.push(this.points)
},
error(e) {
console.log("画布触摸错误" + e);
},
// 点击完成签名
finish() {
let that = this
if (that.tempPoint.length == 0) {
uni.showToast({
title: '您还未签名,请先签名',
icon: 'none',
duration: 2000
});
return
}
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function(res) {
if (res.tempFilePath.startsWith('data:image/png;base64')) {
that.emit(res.tempFilePath)
} else {
that.translateImg(res.tempFilePath)
}
},
fail(e) {
console.log(JSON.stringify(e))
}
}, this)//要用this指向,要不然canvasToTempFilePath会报undefined
},
emit(tempFilePath) {
this.$emit("finish", tempFilePath);
},
//重签
clearClick() {
//清除画布
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw(true);
this.tempPoint = []
},
// 返回事件
back() {
uni.navigateBack();
},
//因为设置了横屏手写签名,输出的图片需要逆时针旋转90度才能正确显示,传给后端的图片也是正确的
// 1. 将Base64转换为临时文件路径
translateImg(tempFilePath) {
let _this = this
// 1、获取图片信息
wx.getImageInfo({
src: tempFilePath,
success: (res) => {
const path = res.path
const height = res.width; //宽305
const width = res.height; //高603
// 2. 创建画布上下文
const contx = uni.createCanvasContext('myCanvas', _this);
//清除掉原本的笔迹
contx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 将旋转中心设置为canvas的中心
contx.translate(150, 260);
// 逆时针旋转90度
contx.rotate(-Math.PI / 2);
//3、将图片绘制到canvas上,注意:这里的坐标是旋转后的坐标,需要调整
contx.drawImage(path, -300, -150, 500, 200);
contx.draw(true, () => { // 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function(res) {
//4. 将临时文件路径转换为Base64
_this.pathToBase64(res.tempFilePath).then(e => {
_this.emit(e)
}).catch(e => {
console.log(JSON.stringify(e))
})
},
fail(e) {
console.log(JSON.stringify(e))
}
}, _this)
}, _this) // 在自定义组件下,当前组件实例的this,以操作组件内 canvas 组件
},
fail: err => {
console.error('Failed to get image info:', err);
}
});
},
pathToBase64(path) {
return new Promise(function(resolve, reject) {
// app
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(path, function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(evt) {
resolve(evt.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
// 微信小程序
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: function(res) {
resolve('data:image/png;base64,' + res.data)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
}
}
</script>
<style lang="scss">
page {
width: 100%;
height: 100%;
background-color: #e9f2f1;
}
canvas {
background-color: white;
}
.csr-sign {
width: 100%;
height: 100%;
display: flex;
flex-flow: row;
align-items: flex-end;
}
.sighButton {
width: 690rpx;
margin: 0rpx auto;
height: 60px;
padding-top: 10px;
display: flex;
justify-content: space-between;
&_button {
width: 30%;
height: 100rpx;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
}
}
.sighButtons {
width: 200rpx;
// margin: 0rpx auto;
height: calc(100vh);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
// margin-left: 80rpx;
.sighButtons_button {
width: 200rpx;
height: 55px;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
transform: rotate(90deg);
}
}
</style>
4、效果图
5、我的代码里面用的都是旧版的canvas,新版的canvas api需要参考文档,内容有不足之处,请多多指正。