微信小程序?qū)崿F(xiàn)canvas電子簽名功能
一、先看效果

小程序canvas電子簽名
二、文檔
三、分析
1、初始話Canvas容器
2、Canvas觸摸事件,bindtouchstart(手指觸摸動作開始)、bindtouchmove(手指觸摸后移動)、bindtouchend(手指觸摸動作結(jié)束)、bindtouchcancel(手指觸摸動作被打斷,如來電提醒,彈窗)
3、記錄每次從開始到結(jié)束的路徑段
4、清除、撤銷
四、代碼分析
1、頁面的布局、Canvas容器的初始化
先將屏幕橫過來,index.json配置文件,“pageOrientation”: “landscape”
wx.getSystemInfoSync() 獲取可使用窗口的寬高,賦值給Canvas畫布(注意若存在按鈕區(qū)域、屏幕安全區(qū)之類的,需要減去)
// 獲取可使用窗口的寬高,賦值給Canvas(寬高要減去上下左右padding的20,以及高度要減去footer區(qū)域)
wx.createSelectorQuery()
.select('.footer') // canvas獲取節(jié)點(diǎn)
.fields({node: true, size: true}) // 獲取節(jié)點(diǎn)的相關(guān)信息,node:是否返回節(jié)點(diǎn)對應(yīng)的 Node 實例,size:是否返回節(jié)點(diǎn)尺寸
.exec((res) => {
// 獲取手機(jī)左側(cè)安全區(qū)域(劉海)
const deviceInFo = wx.getSystemInfoSync()
const canvasWidth = deviceInFo.windowWidth - 20 - deviceInFo?.safeArea?.left || 0
const canvasHeight = deviceInFo.windowHeight - res[0].height - 20
console.log('canvasWidth', canvasWidth);
console.log('canvasHeight', canvasHeight);
this.setData({
deviceInFo,
canvasWidth,
canvasHeight
})
this.initCanvas('init')
})
通過wx.createSelectorQuery()獲取到canvas節(jié)點(diǎn),隨即可獲取到canvas的上下文實例
// 初始話Canvas畫布
initCanvas() {
let ctx = null
let canvas = null
// 獲取Canvas畫布以及渲染上下文
wx.createSelectorQuery()
.select('#myCanvas') // canvas獲取節(jié)點(diǎn)
.fields({node: true, size: true}) // 獲取節(jié)點(diǎn)的相關(guān)信息,node:是否返回節(jié)點(diǎn)對應(yīng)的 Node 實例,size:是否返回節(jié)點(diǎn)尺寸
.exec((res) => { // 執(zhí)行所有的請求。請求結(jié)果按請求次序構(gòu)成數(shù)組
// Canvas 對象實例
canvas = res[0].node
// Canvas 對象上下文實例(動畫動作繪圖等都是在他的身上完成)
ctx = canvas.getContext('2d')
// Canvas 畫布的實際繪制寬高
const width = res[0].width;
const height = res[0].height;
// 獲取設(shè)備像素比
const dpr = wx.getWindowInfo().pixelRatio;
// 初始化畫布大小
canvas.width = width * dpr;
canvas.height = height * dpr;
// 畫筆的顏色
ctx.fillStyle = 'rgb(200, 0, 0)';
// 指定了畫筆(繪制線條)操作的線條寬度
ctx.lineWidth = 5
// 縮小/放大圖像
ctx.scale(dpr, dpr)
this.setData({canvas, ctx});
})
},
2、線條的繪制
通過canva組件的觸摸事件bindtouchstart、bindtouchmove、bindtouchend、bindtouchcancel結(jié)合canvas的路徑繪制的方法moveTo(x,y)、lineTo(x,y)、stroke()來實現(xiàn)一段線條的繪制
bindtouchstart手指觸摸動作開始,結(jié)合moveTo(x,y) 用來設(shè)置繪圖起始坐標(biāo)的方法確定線段的開始坐標(biāo)
// 手指觸摸動作開始
bindtouchstart(event) {
let {type, changedTouches} = event;
let {x, y} = changedTouches[0];
ctx.moveTo(x, y); // 設(shè)置繪圖起始坐標(biāo)。
},
bindtouchend手指觸摸動作結(jié)束,結(jié)合lineTo(x,y) 來繪制一條直線,最后stroke()渲染路徑
// 手指觸摸動作結(jié)束
bindtouchend(event) {
let {type, changedTouches} = event;
let {x, y} = changedTouches[0];
ctx.lineTo(x, y);
// 繪制
ctx.stroke();
},
但這只是一條直線段,并未實現(xiàn)簽名所需的曲線(曲線實質(zhì)上也是由無數(shù)個非常短小的直線段構(gòu)成)
bindtouchmove事件會在手指觸摸后移動時,實時返回當(dāng)前狀態(tài)
那么可否通過bindtouchmove 結(jié)合 moveTo ==> lineTo ==> stroke ==> moveTo ==> … 以上一次的結(jié)束為下一次的開始這樣的方式來實時渲染直線段合并為一個近似的曲線
// 手指觸摸后移動
bindtouchmove(event) {
let {type, changedTouches} = event;
let {x, y} = changedTouches[0];
// 上一段終點(diǎn)
ctx.lineTo(x, y) // 從最后一點(diǎn)到點(diǎn)(x,y)繪制一條直線。
// 繪制
ctx.stroke();
// 下一段起點(diǎn)
ctx.moveTo(x, y) // 設(shè)置繪圖起始坐標(biāo)。
},
歸納封裝
// 手指觸摸動作開始
bindtouchstart(event) {
this.addPathDrop(event)
},
// 手指觸摸后移動
bindtouchmove(event) {
this.addPathDrop(event)
},
// 手指觸摸動作結(jié)束
bindtouchend(event) {
this.addPathDrop(event)
},
// 手指觸摸動作被打斷,如來電提醒,彈窗
bindtouchcancel(event) {
this.addPathDrop(event)
},
// 添加路徑點(diǎn)
addPathDrop(event) {
let {ctx, historyImag, canvas} = this.data
let {type, changedTouches} = event
let {x, y} = changedTouches[0]
if(type === 'touchstart') { // 每次開始都是一次新動作
// 最開始點(diǎn)
ctx.moveTo(x, y) // 設(shè)置繪圖起始坐標(biāo)。
} else {
// 上一段終點(diǎn)
ctx.lineTo(x, y) // 從最后一點(diǎn)到點(diǎn)(x,y)繪制一條直線。
// 繪制
ctx.stroke();
// 下一段起點(diǎn)
ctx.moveTo(x, y) // 設(shè)置繪圖起始坐標(biāo)。
}
},
3、上一步、重繪、提交
主體思路為每一次繪制完成后都通過wx.canvasToTempFilePath生成圖片,并記錄下來,通過canvas的drawImage方法將圖片繪制到 canvas 上
五、完整代碼
1、inde.json
{
"navigationBarTitleText": "電子簽名",
"backgroundTextStyle": "dark",
"pageOrientation": "landscape",
"disableScroll": true,
"usingComponents": {
"van-button": "@vant/weapp/button/index",
"van-toast": "@vant/weapp/toast/index"
}
}
2、index.wxml
<!-- index.wxml -->
<view>
<view class="content" style="padding-left: {{deviceInFo.safeArea.left || 10}}px">
<view class="canvas_box">
<!-- 定位到canvas畫布的下方作為背景 -->
<view class="canvas_tips">
簽字區(qū)
</view>
<!-- canvas畫布 -->
<canvas class="canvas_content" type="2d" style='width:{{canvasWidth}}px; height:{{canvasHeight}}px' id="myCanvas" bindtouchstart="bindtouchstart" bindtouchmove="bindtouchmove" bindtouchend="bindtouchend" bindtouchcancel="bindtouchcancel"></canvas>
</view>
</view>
<!-- footer -->
<view class="footer" style="padding-left: {{deviceInFo.safeArea.left}}px">
<van-button plain class="item" block icon="replay" bind:click="overwrite" type="warning">清除重寫</van-button>
<van-button plain class="item" block icon="revoke" bind:click="prev" type="danger">撤銷</van-button>
<van-button class="item" block icon="passed" bind:click="confirm" type="info">提交</van-button>
</view>
</view>
<!-- 提示框組件 -->
<van-toast id="van-toast" />
3、index.less
.content {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 10px;
.canvas_box {
width: 100%;
height: 100%;
background-color: #E8E9EC;
position: relative;
// 定位到canvas畫布的下方作為背景
.canvas_tips {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 80px;
color: #E2E2E2;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
// .canvas_content {
// width: 100%;
// height: 100%;
// }
}
}
// 底部按鈕
.footer {
box-sizing: border-box;
padding: 20rpx 0;
z-index: 2;
background-color: #ffffff;
text-align: center;
position: fixed;
width: 100%;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
left: 0;
bottom: 0;
display: flex;
.item {
flex: 1;
margin: 0 10rpx;
}
.scan {
width: 80rpx;
margin: 0 10rpx;
}
.moreBtn {
width: 150rpx
}
}
4、index.js
// index.js
// 獲取應(yīng)用實例
// import request from "../../request/index";
import Toast from '@vant/weapp/toast/toast';
const app = getApp()
Page({
data: {
// expertId: '', // 專家id
deviceInFo: {}, // 設(shè)備信息
canvasWidth: '', // 畫布寬
canvasHeight: '', // 畫布高
canvas: null, // Canvas 對象實例
ctx: null, // Canvas 對象上下文實例
historyImag: [], // 歷史記錄,每一筆動作完成后的圖片數(shù)據(jù),用于每一次回退上一步是當(dāng)作圖片繪制到畫布上
fileList: [], // 簽名后生成的附件
initialCanvasImg: '', // 初始畫布圖,解決非ios設(shè)備重設(shè)置寬高不能清空畫布的問題
},
onReady() {
// 獲取可使用窗口的寬高,賦值給Canvas(寬高要減去上下左右padding的20,以及高度要減去footer區(qū)域)
wx.createSelectorQuery()
.select('.footer') // canvas獲取節(jié)點(diǎn)
.fields({ node: true, size: true }) // 獲取節(jié)點(diǎn)的相關(guān)信息,node:是否返回節(jié)點(diǎn)對應(yīng)的 Node 實例,size:是否返回節(jié)點(diǎn)尺寸
.exec((res) => {
console.log('res', res);
// 獲取手機(jī)左側(cè)安全區(qū)域(劉海)
const deviceInFo = wx.getSystemInfoSync()
const canvasWidth = deviceInFo.windowWidth - 20 - deviceInFo?.safeArea?.left || 0
const canvasHeight = deviceInFo.windowHeight - res[0].height - 20
this.setData({
deviceInFo,
canvasWidth,
canvasHeight
})
this.initCanvas('init')
})
},
onLoad(option) {
wx.setNavigationBarTitle({
title: '電子簽名'
})
// const {expertId} = option
// this.setData({expertId})
},
// 初始話Canvas畫布
initCanvas(type) {
let ctx = null
let canvas = null
let {historyImag, canvasWidth, canvasHeight, deviceInFo, initialCanvasImg} = this.data
// 獲取Canvas畫布以及渲染上下文
wx.createSelectorQuery()
.select('#myCanvas') // canvas獲取節(jié)點(diǎn)
.fields({ node: true, size: true }) // 獲取節(jié)點(diǎn)的相關(guān)信息,node:是否返回節(jié)點(diǎn)對應(yīng)的 Node 實例,size:是否返回節(jié)點(diǎn)尺寸
.exec((res) => { // 執(zhí)行所有的請求。請求結(jié)果按請求次序構(gòu)成數(shù)組
// Canvas 對象實例
canvas = res[0].node
// Canvas 對象上下文實例(動畫動作繪圖等都是在他的身上完成)
ctx = canvas.getContext('2d')
// Canvas 畫布的實際繪制寬高
const width = res[0].width
const height = res[0].height
// 獲取設(shè)備像素比
const dpr = wx.getWindowInfo().pixelRatio
// 初始化畫布大小
canvas.width = width * dpr
canvas.height = height * dpr
// 畫筆的顏色
ctx.fillStyle = 'rgb(200, 0, 0)';
// 指定了畫筆(繪制線條)操作的線條寬度
ctx.lineWidth = 5
// 如果存在歷史記錄,則將歷史記錄最新的一張圖片拿出來進(jìn)行繪制。非ios時直接加載一張初始的空白圖片
if(historyImag.length !== 0 || (deviceInFo.platform !== 'ios' && type !== 'init')) {
// 圖片對象
const image = canvas.createImage()
// 圖片加載完成回調(diào)
image.onload = () => {
// 將圖片繪制到 canvas 上
ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight)
}
// 設(shè)置圖片src
image.src = historyImag[historyImag.length - 1] || initialCanvasImg;
}
// 縮小/放大圖像
ctx.scale(dpr, dpr)
this.setData({canvas, ctx})
// 保存一張初始空白圖片
if(type === 'init') {
wx.canvasToTempFilePath({
canvas,
png: 'png',
success: res => {
// 生成的圖片臨時文件路徑
const tempFilePath = res.tempFilePath
this.setData({initialCanvasImg: tempFilePath})
},
})
}
})
},
// 手指觸摸動作開始
bindtouchstart(event) {
this.addPathDrop(event)
},
// 手指觸摸后移動
bindtouchmove(event) {
this.addPathDrop(event)
},
// 手指觸摸動作結(jié)束
bindtouchend(event) {
this.addPathDrop(event)
},
// 手指觸摸動作被打斷,如來電提醒,彈窗
bindtouchcancel(event) {
this.addPathDrop(event)
},
// 添加路徑點(diǎn)
addPathDrop(event) {
let {ctx, historyImag, canvas} = this.data
let {type, changedTouches} = event
let {x, y} = changedTouches[0]
if(type === 'touchstart') { // 每次開始都是一次新動作
// 最開始點(diǎn)
ctx.moveTo(x, y) // 設(shè)置繪圖起始坐標(biāo)。
} else {
// 上一段終點(diǎn)
ctx.lineTo(x, y) // 從最后一點(diǎn)到點(diǎn)(x,y)繪制一條直線。
// 繪制
ctx.stroke();
// 下一段起點(diǎn)
ctx.moveTo(x, y) // 設(shè)置繪圖起始坐標(biāo)。
}
// 每一次結(jié)束或者意外中斷,保存一份圖片到歷史記錄中
if(type === 'touchend' || type === 'touchcancel') {
// 生成圖片
// historyImag.push(canvas.toDataURL('image/png'))
wx.canvasToTempFilePath({
canvas,
png: 'png',
success: res => {
// 生成的圖片臨時文件路徑
const tempFilePath = res.tempFilePath
historyImag.push(tempFilePath)
this.setData(historyImag)
},
})
}
},
// 上一步
prev() {
this.setData({
historyImag: this.data.historyImag.slice(0, this.data.historyImag.length - 1)
})
this.initCanvas()
},
// 重寫
overwrite() {
this.setData({
historyImag: []
})
this.initCanvas()
},
// 提交
confirm() {
const {canvas, historyImag} = this.data
if(historyImag.length === 0) {
Toast.fail('請先簽名后保存!');
return
}
// 生成圖片
wx.canvasToTempFilePath({
canvas,
png: 'png',
success: res => {
// 生成的圖片臨時文件路徑
const tempFilePath = res.tempFilePath
// 保存圖片到系統(tǒng)
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
})
// this.beforeRead(res.tempFilePath)
},
})
},
// // 圖片上傳
// async beforeRead(tempFilePath) {
// const that = this;
// wx.getImageInfo({
// src: tempFilePath,
// success(imageRes) {
// wx.uploadFile({
// url: '', // 僅為示例,非真實的接口地址
// filePath: imageRes.path,
// name: 'file',
// header: {token: wx.getStorageSync('token')},
// formData: {
// ext: imageRes.type
// },
// success(fileRes) {
// const response = JSON.parse(fileRes.data);
// if (response.code === 200) {
// that.setData({
// fileList: [response.data]
// })
// that.submit();
// } else {
// wx.hideLoading();
// Toast.fail('附件上傳失敗');
// return false;
// }
// },
// fail(err) {
// wx.hideLoading();
// Toast.fail('附件上傳失敗');
// }
// });
// },
// fail(err) {
// wx.hideLoading();
// Toast.fail('附件上傳失敗');
// }
// })
// },
// 提交
// submit() {
// const {fileList} = this.data
// wx.showLoading({title: '提交中...',})
// request('post', '', {
// fileIds: fileList.map(item => item.id),
// }).then(res => {
// if (res.code === 200) {
// wx.hideLoading();
// Toast.success('提交成功!');
// setTimeout(() => {
// wx.navigateBack({delta: 1});
// }, 1000)
// }
// })
// },
})以上就是微信小程序?qū)崿F(xiàn)canvas電子簽名功能的詳細(xì)內(nèi)容,更多關(guān)于微信小程序電子簽名的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript模擬select,jselect的方法實現(xiàn)
由于主流瀏覽器對select元素渲染不同,所以在每種瀏覽器下顯示也不一樣,最主要的是默認(rèn)情況下UI太粗糙,即使通過css加以美化也不能達(dá)到很美觀的效果2012-11-11
使用javascript實現(xiàn)有效時間的控制,并顯示將要過期的時間
本篇文章主要介紹了使用javascript實現(xiàn)有效時間的控制,并顯示將要過期的時間示例代碼。需要的朋友可以過來參考下,希望對大家有所幫助2014-01-01
JS?Angular?服務(wù)器端渲染應(yīng)用設(shè)置渲染超時時間???????
這篇文章主要介紹了JS?Angular服務(wù)器端渲染應(yīng)用設(shè)置渲染超時時間,???????通過setTimeout模擬一個需要5秒鐘才能完成調(diào)用的API展開詳情,需要的小伙伴可以參考一下2022-06-06

