微信小程序可以通过canvas实现手写签名的效果,本文中使用的是微信小程序Canvas 2D接口
本示例中绘制的是横屏签名的效果,效果图如下:
这里我们需要调整canvas的物理宽高,默认物理宽高为300*150px,物理宽高调整通过css样式即可,本文中需要根据屏幕高度进行动态调整,使用的是行内样式
页面布局:
<template>
<view class="sign-page" :style="{paddingTop: top + 'px'}">
<view class="canvas-box">
<view class="left-pane">
<view class="f28 text-gray6 left-text">请签字确认</view>
<view class="right-box">
<view class="left-button" @click="clearContext">
<text class="ic ic-delete text-gray6"></text>
<text class="f30 text-gray6 ml5">清空</text>
</view>
<button class="right-button" @click="confirm">完成</button>
</view>
</view>
<!-- canvas的物理宽高可通过样式调整 -->
<canvas
class="canvas"
disable-scroll
type="2d"
id="myCanvas"
@touchstart="handleTouchstart"
@touchmove="handleTouchmove"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px'}">
</canvas>
<view class="right-pane">
<view class="dis-flex back-button" @click="back">
<text class="ic ic-left text-gray6"></text>
<text class="text-gray6 ml15">取消</text>
</view>
<view class="title">
<text class="text text-gray6">{{title || '检测人员'}}</text>
</view>
</view>
</view>
<canvas
class="canvas2"
disable-scroll
type="2d"
id="myCanvas2"
:style="{
width: canvasHeight + 'px',
height: canvasWidth + 'px'}">
</canvas>
</view>
</template>
js代码:canvas的物理宽高调整后,canvas的逻辑宽高也需要进行调整,默认逻辑宽高是300*150px,(小程序Canvas 2D接口支持修改逻辑宽高),具体参考本文中的initCanvas方法
<script>
export default {
data() {
return {
canvasWidth: 300,
canvasHeight: 150,
top: 0,
canvas: null,
title: ''
}
},
onLoad() {
const menuData = uni.getMenuButtonBoundingClientRect()
uni.getSystemInfo({
success: (res) => {
let navPadding = menuData.top - res.statusBarHeight
// 顶部高度 = 状态栏高度 + 胶囊按钮行高度 + 胶囊按钮上下的padding
let navHeight = res.statusBarHeight + navPadding * 2 + menuData.height
// 设置canvas的物理宽高
this.canvasWidth = res.windowWidth - 100
this.canvasHeight = res.windowHeight - navHeight - 20
this.top = navHeight
}
})
},
onReady() {
this.initCanvas()
},
methods: {
initCanvas() {
uni.createSelectorQuery()
.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
// 修改canvas的逻辑宽高
// 如果不修改canvas的逻辑宽高,仅通过样式修改canvas的宽高,会导致绘图时比例不对,
// 如将物理宽度改为600,但逻辑宽度还是300,假设画图时的起点x是100,那么实际看到的绘图起点是200
const canvas = res[0].node
this.canvas = canvas
this.ctx = canvas.getContext('2d')
// canvas.width = this.canvasWidth
// canvas.height = this.canvasHeight
// 注意:按照上面方式调整,虽然逻辑宽高和物理宽高保持一致了,但是会发现画出来的线会有锯齿不够清晰
// 因为不同设备上物理像素与逻辑像素是不一致的
// 因此canvas的逻辑宽高等于物理宽高分别*dpr
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = this.canvasWidth * dpr
canvas.height = this.canvasHeight * dpr
// 假设dpr等于2,,那么canvas的物理宽度是600,逻辑宽度就是1200,
// 假设画图时的起点x是100,那么实际看到的绘图起点是50,此时只需要将绘图内容进行等比例放大即可
this.ctx.scale(dpr, dpr)
})
},
handleTouchstart(e) {
this.lineBegin(e.touches[0].x, e.touches[0].y)
},
handleTouchmove(e) {
this.lineTo(e.touches[0].x, e.touches[0].y)
},
lineBegin(x, y) {
this.ctx.beginPath()
// 新版Canvas 2D接口,直接修改属性即可
this.ctx.lineCap = 'round'
this.ctx.lineWidth = 5
this.startX = x
this.startY = y
this.ctx.moveTo(this.startX, this.startY)
},
lineTo(x, y) {
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.ctx.moveTo(x, y)
},
clearContext() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
},
confirm() {
uni.canvasToTempFilePath({
canvas: this.canvas,
success: (res) => {
this.rotateImage(res.tempFilePath)
}
})
},
// 横屏签名,但是canvas的方向是垂直的,导出的图片也是竖屏,需要将图片进行旋转
rotateImage(filePath) {
uni.createSelectorQuery()
.select('#myCanvas2')
.fields({ node: true, size: true })
.exec((res) => {
// 首先绘制一个宽高与上面canvas相反的canvas
const canvas = res[0].node
this.canvas2 = canvas
this.ctx2 = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = this.canvasHeight * dpr
canvas.height = this.canvasWidth * dpr
this.ctx2.scale(dpr, dpr)
// 绘制上述导出的签名图片到新的canvas上
const img = this.canvas2.createImage()
img.src = filePath
img.onload = () => {
// 签名图片旋转绘画解析在下方
this.ctx2.translate(0, this.canvasWidth);
this.ctx2.rotate(270 * Math.PI / 180)
this.ctx2.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight)
uni.canvasToTempFilePath({
canvas: this.canvas2,
success: (res) => {
this.handleUploadFile(res.tempFilePath)
}
})
}
})
},
handleUploadFile(filePath) {
uni.uploadFile({
url: config.requestUrl + '/biz/file/upload/annex',
filePath,
name: 'file',
header: {
'Authorization': getToken()
},
success: (res) => {
// 调用接口成功
if(res.statusCode == 200) {
// 解析服务器返回数据
const data = JSON.parse(res.data)
if(data.code == 200) {
const responseUrl = config.requestUrl + data.filePath
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('getSignImage', {filePath: responseUrl, fileId: data.fileId});
this.back()
}
} else {
uni.hideLoading()
}
},
fail: (res) => {
uni.hideLoading()
}
})
},
back() {
uni.navigateBack()
}
}
}
</script>
由于签名的方向是横向的,但是canvas本身是竖向,导出的签名图片也为竖向,那么我们需要将导出的图片旋转为横向后重新绘制到canvas上然后导出。签名图旋转绘制的效果图如下:
图中黑色部分为canvas区域,竖向的签名图片为刚刚我们导出的图片,绘制到canvas上的效果如下图,如果我们要将其横向绘制到面板上,此时需要将绘画内容沿左上角顺时针旋转270deg,此时可以发现旋转后图片的覆盖区域在canvas向上移动canvasWdth的位置,那么旋转前将canvas的绘画起点重置到(0,canvasWidth)即可保证绘画内容旋转后刚好覆盖在canvas上
需要注意的点是,后面绘画旋转后的canvas在实际页面中我们是不需要看到的,需要通过样式将其隐藏,如果需要使用canvasToTempFilePath方法导出图片的话,不能使用display:none的隐藏canvas,否则会报错no image found,
可以通过定位和visiblity(opacity)属性隐藏。
页面样式:
<style lang="scss" scoped>
.sign-page {
min-height: 100vh;
background-color: #f5f5f5;
.canvas-box {
position: relative;
width: 100%;
}
.left-pane {
width: 100rpx;
.left-text {
position: absolute;
top: 0;
line-height: 100rpx;
transform: translateX(100rpx) rotate(90deg) ;
transform-origin: 0 0;
}
.right-box {
position: absolute;
display: flex;
align-items: center;
bottom: 0;
transform: translateX(100rpx) rotate(90deg) translateX(-100%) translateX(100rpx);
transform-origin: 0 0;
.left-button {
line-height: 100rpx;
margin-right: 30rpx;
}
.right-button {
font-size: 30rpx;
color: #fff;
width: 140rpx;
height: 60rpx;
line-height: 60rpx;
background-color: green;
}
}
}
.canvas {
margin: 0 auto;
background-color: #fff;
border: 2rpx dashed #d9d9d9;
transform-origin: center center;
}
.canvas2 {
/*设置display:none会导致wx.canvasToTempFilePath报错no image found*/
/*display: none;*/
position: absolute;
opacity: 0;
}
.right-pane {
position: absolute;
width: 100rpx;
height: 100%;
right: 0;
top: 0;
.back-button {
position: relative;
z-index: 5;
white-space: nowrap;
line-height: 100rpx;
align-items: center;
transform: translateX(100rpx) rotate(90deg);
transform-origin: 0 0;
}
.title {
position: absolute;
top: 0;
z-index: 4;
width: 100rpx;
height: 100%;
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
line-height: 100rpx;
text-align: center;
.text {
display: inline-block;
transform: rotate(90deg);
}
}
}
}
</style>