vue+canvas實(shí)現(xiàn)簡(jiǎn)易的九宮格手勢(shì)解鎖器
前言
此篇文章用于記錄柏成從零開發(fā)一個(gè)canvas九宮格手勢(shì)解鎖器
的歷程,最終效果如下:
設(shè)置圖案密碼時(shí),需進(jìn)行兩次繪制圖案操作,若兩次繪制圖案一致,則密碼設(shè)置成功;若不一致,則需重新設(shè)置密碼
輸入圖案密碼時(shí),密碼一致則驗(yàn)證通過(guò);密碼不一致則提示圖案密碼錯(cuò)誤,請(qǐng)重試
介紹
我們基于 canvas 實(shí)現(xiàn)了一款簡(jiǎn)單的九宮格手勢(shì)解鎖器,用戶可以通過(guò)在九宮格中繪制特定的手勢(shì)來(lái)解鎖
我們可以通過(guò) new Locker
創(chuàng)建一個(gè)圖案解鎖器,其接收一個(gè)容器作為第一個(gè)參數(shù),第二個(gè)參數(shù)為選項(xiàng),下面是個(gè)基本例子:
<template> <div class="pattren-locker"> <div id="container" ref="container" style="width: 360px; height: 600px"></div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import Locker from '@/canvas/locker' const container = ref(null) onMounted(() => { // 新建一個(gè)解鎖器 new Locker(container.value,{ radius: 30, // 圓圈半徑 columnSpacing: 50, // 圓圈列間距 rowsSpacing: 90, // 圓圈行間距 stroke: '#b5b5b5', // 圓圈描邊顏色 lineStroke: '#237fb4', // 路徑描邊顏色 selectedFill: '#237fb4', // 圖案選中填充顏色 backgroundColor: '#f7f7f7', // 畫布背景顏色 }) }) </script>
初始化
Locker 的實(shí)現(xiàn)是一個(gè)類,在 src/canvas/locker.js
中定義。
new Locker(container,{...})
時(shí)做了什么?我們?cè)跇?gòu)造函數(shù)中創(chuàng)建一個(gè) canvas 畫布追加到了 container 容器中,并定義了一系列屬性,最后執(zhí)行了 init 初始化方法。
在初始化方法中,我們繪制了9個(gè)宮格圓圈,作為解鎖單元;并注冊(cè)監(jiān)聽(tīng)了鼠標(biāo)事件,用于繪制解鎖軌跡。
// 初始化 init() { this.drawCellGrids() this.drawText('請(qǐng)繪制新的圖案密碼') this.canvas.addEventListener('contextmenu', (e) => e.preventDefault()) this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this)) } // 繪制9個(gè)宮格圓圈 drawCellGrids() { const columns = 3 const rows = 3 const width = this.canvas.width const height = this.canvas.height const paddingTop = (height - rows * 2 * this.radius - (rows - 1) * this.rowsSpacing) / 2 const paddingLeft = (width - columns * 2 * this.radius - (columns - 1) * this.columnSpacing) / 2 for (let i = 0; i < rows; i++) { for (let j = 0; j < columns; j++) { const data = { x: paddingLeft + (2 * j + 1) * this.radius + j * this.columnSpacing, y: paddingTop + (2 * i + 1) * this.radius + i * this.rowsSpacing, id: i * columns + j } this.lockerCells.push(data) this.ctx.beginPath() this.ctx.arc(data.x, data.y, this.radius, 0, 2 * Math.PI, true) this.ctx.strokeStyle = this.stroke this.ctx.lineWidth = 3 this.ctx.stroke() } } this.cellImageData = this.lastImageData = this.getImageData() }
自定義鼠標(biāo)事件
我們之前在 init 初始化方法中注冊(cè)了 onmousedown 鼠標(biāo)按下事件,需要在此處實(shí)現(xiàn)鼠標(biāo)按下拖拽可以繪制解鎖軌跡的邏輯
鼠標(biāo)按下:先執(zhí)行 selectCellAt 方法(如果在圓圈內(nèi)按下鼠標(biāo),會(huì)立即繪制選中樣式,并保存選中樣式之后的畫布快照)
鼠標(biāo)移動(dòng):先恢復(fù)快照,再繪制路徑中最后一個(gè)點(diǎn)到當(dāng)前鼠標(biāo)坐標(biāo)的軌跡,最后再執(zhí)行 selectCellAt 方法,一直重復(fù)此過(guò)程。。。直到鼠標(biāo)移動(dòng)到圓圈內(nèi)部(則先恢復(fù)快照,然后繪制點(diǎn)的選中樣式,繪制路徑中最后一個(gè)點(diǎn)到當(dāng)前點(diǎn)的路徑,最后保存繪制路徑之后的畫布快照)
鼠標(biāo)抬起:清空onmousemove、onmouseup事件,并校驗(yàn)密碼
此時(shí)我們小小的腦袋里可能有兩個(gè)大大的問(wèn)號(hào)??
selectCellAt 方法作用是什么?
如果鼠標(biāo)移動(dòng)到圓圈內(nèi)部,則會(huì)將圖案路徑連接到當(dāng)前圓圈,并繪制選中樣式
快照是什么?
快照是當(dāng)前畫布的像素點(diǎn)信息。我們永遠(yuǎn)會(huì)在激活一個(gè)解鎖單元后(即鼠標(biāo)移動(dòng)到圓圈內(nèi)部時(shí)),先恢復(fù)畫布快照,然后去繪制圓圈的選中樣式,并將圖案路徑延伸連接到當(dāng)前圓圈,然后!會(huì)保存此時(shí)此刻的畫布快照!之后,我們會(huì)在鼠標(biāo)移動(dòng)時(shí),不停的恢復(fù)快照,然后繪制最后一個(gè)圓圈到當(dāng)前鼠標(biāo)坐標(biāo)的連線軌跡,直到我們激活下一個(gè)解鎖單元(即鼠標(biāo)移動(dòng)到下一個(gè)圓圈內(nèi)部)。我們會(huì)又會(huì)重復(fù)上面的過(guò)程,這就構(gòu)成一個(gè)一個(gè)的循環(huán)
mousedownEvent(e) { const that = this // 選中宮格,并繪制點(diǎn)到點(diǎn)路徑 const selected = this.selectCellAt(e.offsetX, e.offsetY) if (!selected) return // 鼠標(biāo)移動(dòng)事件 this.canvas.onmousemove = function (e) { // 路徑的最后一個(gè)點(diǎn) const lastData = that.currentPath[that.currentPath.length - 1] // 恢復(fù)快照 that.restoreImageData(that.lastImageData) // 繪制路徑 that.drawLine(lastData, { x: e.offsetX, y: e.offsetY }) // 選中宮格,并繪制點(diǎn)到點(diǎn)路徑 that.selectCellAt(e.offsetX, e.offsetY) } // 鼠標(biāo)抬起/移出事件 this.canvas.onmouseup = this.canvas.onmouseout = function () { const canvas = this canvas.onmousemove = null canvas.onmouseup = null canvas.onmouseout = null const currentPathIds = that.currentPath.map((item) => item.id) let text = '' if (that.password.length === 0) { that.password = currentPathIds text = '請(qǐng)?jiān)俅卫L制圖案進(jìn)行確認(rèn)' } else if (that.confirmPassword.length === 0) { that.confirmPassword = currentPathIds if (that.password.join('') === that.confirmPassword.join('')) { text = '圖案密碼設(shè)置成功,請(qǐng)輸入您的密碼' } else { text = '與上次繪制不一致,請(qǐng)重試' that.password = [] that.confirmPassword = [] } } else { if (that.password.join('') === currentPathIds.join('')) { text = '圖案密碼正確 (づ ̄3 ̄)づ╭?~' } else { text = '圖案密碼錯(cuò)誤,請(qǐng)重試' } } that.ctx.clearRect(0, 0, canvas.width, canvas.height) // 清空畫布 that.restoreImageData(that.cellImageData) // 恢復(fù)背景宮格快照 that.drawText(text) // 繪制提示文字 that.currentPath = [] // 清空當(dāng)前繪制路徑 that.lastImageData = that.cellImageData // 重置上一次繪制的畫布快照 } }
繪制路徑及選中樣式
我們會(huì)在鼠標(biāo)按下(onmousedown)、鼠標(biāo)移動(dòng)(onmousemove)事件中調(diào)用 selectCellAt 方法,并傳入當(dāng)前鼠標(biāo)坐標(biāo)信息
若當(dāng)前坐標(biāo)在宮格圓圈內(nèi) 且 改圓圈未被連接過(guò),則先恢復(fù)畫布快照,然后繪制圓圈選中樣式,繪制路徑中最后一個(gè)圓圈到當(dāng)前圓圈的路徑,最后保存此時(shí)此刻的畫布快照,返回true
若當(dāng)前坐標(biāo)不在宮格圓圈內(nèi) 或者 該圓圈被連接過(guò),則返回false
selectCellAt(x, y) { // 當(dāng)前坐標(biāo)點(diǎn)是否在圓內(nèi) const data = this.lockerCells.find((item) => { return Math.pow(item.x - x, 2) + Math.pow(item.y - y, 2) <= Math.pow(this.radius, 2) }) const existing = this.currentPath.some((item) => item.id === data?.id) if (!data || existing) return false // 恢復(fù)畫布快照 this.restoreImageData(this.lastImageData) // 繪制選中樣式 this.drawCircle(data.x, data.y, this.radius / 1.5, 'rgba(0,0,0,0.2)') this.drawCircle(data.x, data.y, this.radius / 2.5, this.selectedFill) // 繪制路徑 從最后一個(gè)點(diǎn)到當(dāng)前點(diǎn) const lastData = this.currentPath[this.currentPath.length - 1] if (lastData) { this.drawLine(lastData, data) } // 保存畫布快照 this.lastImageData = this.getImageData() // 保存當(dāng)前點(diǎn) this.currentPath.push(data) return true } // 繪制選中樣式 drawCircle(x, y, radius, fill) { this.ctx.beginPath() this.ctx.arc(x, y, radius, 0, 2 * Math.PI, true) this.ctx.fillStyle = fill this.ctx.fill() } // 繪制路徑 drawLine(start, end, stroke = this.lineStroke) { this.ctx.beginPath() this.ctx.moveTo(start.x, start.y) this.ctx.lineTo(end.x, end.y) this.ctx.strokeStyle = stroke this.ctx.lineWidth = 3 this.ctx.lineCap = 'round' this.ctx.lineJoin = 'round' this.ctx.stroke() }
畫布快照
我們?nèi)绾潍@取到當(dāng)前畫布快照?又如何根據(jù)快照數(shù)據(jù)恢復(fù)畫布呢?
查閱 canvas官方API文檔 得知,獲取快照 API 為 getImageData;通過(guò)快照恢復(fù)畫布的 API 為 putImageData
// 獲取畫布快照 getImageData() { return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) } // 恢復(fù)畫布快照 restoreImageData(imageData) { if (!imageData) return this.ctx.putImageData(imageData, 0, 0) }
源碼
涂鴉面板demo代碼:vue-canvas
到此這篇關(guān)于vue+canvas實(shí)現(xiàn)簡(jiǎn)易的九宮格手勢(shì)解鎖器的文章就介紹到這了,更多相關(guān)vue canvas九宮格手勢(shì)解鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue-cli5.0?webpack?采用?copy-webpack-plugin?打包復(fù)制文件的方法
今天就好好說(shuō)說(shuō)vue-cli5.0種使用copy-webpack-plugin插件該如何配置的問(wèn)題。這里我們安裝的 copy-webpack-plugin 的版本是 ^11.0.0,感興趣的朋友一起看看吧2022-06-06基于Vue3實(shí)現(xiàn)一個(gè)小相冊(cè)詳解
這篇文章主要為大家詳細(xì)介紹了如何基于Vue3實(shí)現(xiàn)一個(gè)小相冊(cè)效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-12-12Vue封裝遠(yuǎn)程下拉框組件的實(shí)現(xiàn)示例
本文主要介紹了Vue封裝遠(yuǎn)程下拉框組件的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07vue如何實(shí)現(xiàn)列表自動(dòng)滾動(dòng)、向上滾動(dòng)的效果(vue-seamless-scroll)
這篇文章主要介紹了vue如何實(shí)現(xiàn)列表自動(dòng)滾動(dòng)、向上滾動(dòng)的效果(vue-seamless-scroll),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05Vue使用Less與Scss實(shí)現(xiàn)主題切換方法詳細(xì)講解
目前,在眾多的后臺(tái)管理系統(tǒng)中,換膚功能已是一個(gè)很常見(jiàn)的功能。用戶可以根據(jù)自己的喜好,設(shè)置頁(yè)面的主題,從而實(shí)現(xiàn)個(gè)性化定制。目前,我所了解到的換膚方式,也是我目前所掌握的兩種換膚方式,想同大家一起分享2023-02-02