微信小程序?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 實(shí)例,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的上下文實(shí)例
// 初始話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 實(shí)例,size:是否返回節(jié)點(diǎn)尺寸 .exec((res) => { // 執(zhí)行所有的請求。請求結(jié)果按請求次序構(gòu)成數(shù)組 // Canvas 對象實(shí)例 canvas = res[0].node // Canvas 對象上下文實(shí)例(動畫動作繪圖等都是在他的身上完成) ctx = canvas.getContext('2d') // Canvas 畫布的實(shí)際繪制寬高 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()來實(shí)現(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(); },
但這只是一條直線段,并未實(shí)現(xiàn)簽名所需的曲線(曲線實(shí)質(zhì)上也是由無數(shù)個(gè)非常短小的直線段構(gòu)成)
bindtouchmove事件會在手指觸摸后移動時(shí),實(shí)時(shí)返回當(dāng)前狀態(tài)
那么可否通過bindtouchmove 結(jié)合 moveTo ==> lineTo ==> stroke ==> moveTo ==> … 以上一次的結(jié)束為下一次的開始這樣的方式來實(shí)時(shí)渲染直線段合并為一個(gè)近似的曲線
// 手指觸摸后移動 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)用實(shí)例 // 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 對象實(shí)例 ctx: null, // Canvas 對象上下文實(shí)例 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 實(shí)例,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 實(shí)例,size:是否返回節(jié)點(diǎn)尺寸 .exec((res) => { // 執(zhí)行所有的請求。請求結(jié)果按請求次序構(gòu)成數(shù)組 // Canvas 對象實(shí)例 canvas = res[0].node // Canvas 對象上下文實(shí)例(動畫動作繪圖等都是在他的身上完成) ctx = canvas.getContext('2d') // Canvas 畫布的實(shí)際繪制寬高 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時(shí)直接加載一張初始的空白圖片 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 => { // 生成的圖片臨時(shí)文件路徑 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 => { // 生成的圖片臨時(shí)文件路徑 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 => { // 生成的圖片臨時(shí)文件路徑 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: '', // 僅為示例,非真實(shí)的接口地址 // 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的方法實(shí)現(xiàn)
由于主流瀏覽器對select元素渲染不同,所以在每種瀏覽器下顯示也不一樣,最主要的是默認(rèn)情況下UI太粗糙,即使通過css加以美化也不能達(dá)到很美觀的效果2012-11-11使用javascript實(shí)現(xiàn)有效時(shí)間的控制,并顯示將要過期的時(shí)間
本篇文章主要介紹了使用javascript實(shí)現(xiàn)有效時(shí)間的控制,并顯示將要過期的時(shí)間示例代碼。需要的朋友可以過來參考下,希望對大家有所幫助2014-01-01JS?Angular?服務(wù)器端渲染應(yīng)用設(shè)置渲染超時(shí)時(shí)間???????
這篇文章主要介紹了JS?Angular服務(wù)器端渲染應(yīng)用設(shè)置渲染超時(shí)時(shí)間,???????通過setTimeout模擬一個(gè)需要5秒鐘才能完成調(diào)用的API展開詳情,需要的小伙伴可以參考一下2022-06-06JS SetInterval 代碼實(shí)現(xiàn)頁面輪詢
setInterval 是一個(gè)實(shí)現(xiàn)定時(shí)調(diào)用的函數(shù),可按照指定的周期(以毫秒計(jì))來調(diào)用函數(shù)或計(jì)算表達(dá)式。下面通過本文給大家分享JS SetInterval 代碼實(shí)現(xiàn)頁面輪詢,感興趣的朋友一起看看吧2017-08-08高效的獲取當(dāng)前元素是父元素的第幾個(gè)子元素
例如處理事件的時(shí)候,有時(shí)候需要知道當(dāng)前點(diǎn)擊的是第幾個(gè)子節(jié)點(diǎn),而HTML DOM本身并沒有直接提供相應(yīng)的屬性,需要自己來計(jì)算。感興趣的朋友可以了解下本文2013-10-10js實(shí)現(xiàn)數(shù)字跳動到指定數(shù)字
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)數(shù)字跳動到指定數(shù)字,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-08-08javascript取消文本選定的實(shí)現(xiàn)代碼
最近在做拖動布局. 發(fā)現(xiàn)有文本選定的時(shí)候, 進(jìn)行拖動很不好看.2010-11-11