<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
之前在公司開發活動專案的時候,遇到一個專案需求要讓使用者使用手勢畫星點陣圖來解鎖星座運勢,一看設計稿,這不就是我們平時的手機螢幕解鎖嗎?於是上網搜了一些關於手勢解鎖的文章,沒找到可以直接複用的,於是只能自己開啟canvas教學,邊學習邊設計實現了這個功能,同時相容了行動端和PC端,在這裡把程式碼分享出來,感興趣的可以看看。
<div class="starMap" ref="starMap"> <canvas id="starMap" ref="canvas" class="canvasBox" :width="width" :height="height" :style="{ width, height }" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @mousedown="touchStart" @mousemove="touchMove" @mouseup="touchEnd" @mouseout="touchEnd" @mouseenter="touchStart" ></canvas> <div class="starsList"> <div v-for="n in starXNum" :key="n" class="starColBox" :style="{ marginBottom: `${spaceY}px` }"> <div v-for="j in starYNum" :key="j" class="starRow" :style="{ marginRight: `${spaceX}px` }"> <div :class="['starIcon', showStar(n, j) && 'show']" :style="{ width: `${starX}px`, height: `${starX}px` }"> <div :class="['starCenter', isSelectedStar(n, j) && `animate-${getRandom(0, 2, 0)}`]"></div> </div> </div> </div> </div> <canvas id="judgeCanvas" :width="width" :height="height" class="judgeCanvas" :style="{ width, height }"></canvas> </div>
/* * this.width=畫布寬 * this.height=畫布高 * this.starX=星星的大小,寬高相等不做區分 */ spaceX () { // 星星橫向間距 return (this.width - this.starX * this.starXNum) / 4 } spaceY () { // 星星縱向間距 return (this.height - this.starX * this.starYNum) / 4 }
初始化canvas畫布和基礎資料
function setData () { // 初始化canvas資料 this.initStarPos() this.lineWidth = 2 // 連線線寬度 this.lineBlurWidth = 6 // 連線線shadow寬 this.canvas = document.getElementById('starMap') if (!this.canvas) return console.error('starMap: this.canvas is null') this.ctx = this.canvas.getContext('2d') this.ctx.strokeStyle = '#c9b8ff' this.ctx.lineCap = 'round' this.ctx.lineJoin = 'bevel' const judgeCanvas = document.getElementById('judgeCanvas') this.judgeCtx = judgeCanvas.getContext('2d') } function initStarPos () { // 初始化星星位置 const arr = this.pointIndexArr = this.initPointShowArr() const pointPos = [] /** * spaceX=橫向間距;spaceY:縱向間距 * 星星中點x位置: 星星/2 + (星星的尺寸 + 橫向間距)* 前面的星星數量 * 星星中點y位置: 星星/2 + (星星的尺寸 + 豎向間距)* 前面的星星數量 * pointPos=所有頁面渲染的星星(x, y)座標 */ arr.forEach(item => { let x = 0 let y = 0 x = this.starX / 2 + (this.starX + this.spaceX) * (item % this.starXNum) y = this.starX / 2 + (this.starX + this.spaceY) * Math.floor(item / this.starXNum) pointPos.push({ x, y, index: item }) }) this.pointPos = [...pointPos] } function initPointShowArr () { const result = [] const originArr = [] const arrLen = getRandom(25, this.starXNum * this.starYNum, 0) // 可選擇隨機選擇需要顯示星星的數量 getRandom(21, 25, 0) const starOriginLen = this.starXNum * this.starYNum for (let i = 0; i < starOriginLen; i++) { originArr.push(i) } // 獲取星星展示亂陣列後進行排序重組 for (let i = 0; i < arrLen; i++) { const random = Math.floor(Math.random() * originArr.length) if (result.includes(originArr[random])) { continue } result.push(originArr[random]) originArr.splice(random, 1) } result.sort((a, b) => a - b) return result }
監聽手指開始觸控事件:
function touchStart (e) { if (this.checkLimit()) return this.lockScroll() const rect = this.$refs.canvas.getBoundingClientRect() // 此處獲取canvas位置,防止頁面捲動時位置發生變化 this.canvasRect = { x: rect.left, y: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom, top: rect.top } const [x, y] = this.getEventPos(e) const index = this.indexOfPoint(x, y) if (this.pointsLen) { this.reconnectStart = true } else { this.pushToPoints(index) } } function getEventPos (event) { // 當前觸控座標點相對canvas畫布的位置 const x = event.clientX || event.touches[0].clientX const y = event.clientY || event.touches[0].clientY return [x - this.canvasRect.x, y - this.canvasRect.y] } function indexOfPoint (x, y) { if (this.pointPos.length === 0) throw new Error('未找到星星座標') // 為了減少計算量,將星星當初正方形計算 for (let i = 0; i < this.pointPos.length; i++) { if ((Math.abs(x - this.pointPos[i].x) < this.starX / 1.5) && (Math.abs(y - this.pointPos[i].y) < this.starX / 1.5)) { return i } } return -1 } function pushToPoints (index) { if (index === -1 || this.points.includes(index)) return false this.points.push(index) return true } function checkBeyondCanvas (e) { // 校驗手指是否超出canvas區域 const x = e.clientX || e.touches[0].clientX const y = e.clientY || e.touches[0].clientY const { left, top, right, bottom } = this.canvasRect const outDistance = 40 // 放寬邊界的判斷 if (x < left - outDistance || x > right + outDistance || y < top - outDistance || y > bottom + outDistance) { this.connectEnd() return true } return false }
監聽手指滑動事件:
drawLine中涉及到一些canvas的基本方法和屬性:
canvas.beginPath() // 表示開始畫線或重置當前路徑 canvas.moveTo(x, y) // 指定目標路徑的開始位置,不建立線條 canvas.lineTo(x, y) // 新增一個新點,建立從該點到畫布中最後指定點的線條,不建立線條 canvas.closePath() // 結束路徑,應與開始路徑呼應 canvas.stroke() // 實際地繪製出通過 moveTo() 和 lineTo() 方法定義的路徑,預設為黑色 const grd = canvas.createLinearGradient(x1, y1, x2, y2) // 建立線性漸變的起止座標 grd.addColorStop(0, '#c9b8ff') // 定義從 0 到 1 的顏色漸變 grd.addColorStop(1, '#aa4fff') canvas.strokeStyle = grd
function touchMove (e) { console.log('touchMove', e) if (this.checkBeyondCanvas(e)) return // 防止touchmove移出canvas區域後不鬆手,捲動後頁面位置改變在canvas外其他位置觸發連線 if (this.checkLimit()) return this.lockScroll() // 手指活動過程中禁止頁面捲動 const [x, y] = this.getEventPos(e) const idx = this.indexOfPoint(x, y) if (this.reconnectStart && (idx === this.points[this.pointsLen - 1] || idx !== this.points[0])) { this.reconnectStart = false idx === this.points[0] && this.points.reverse() } this.pushToPoints(idx) this.draw(x, y) } function draw (x, y) { if (!this.canvas) return this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) if (this.pointsLen === 0) return this.rearrangePoints(x, y) this.drawLine(x, y) } function drawLine (x, y) { this.ctx.lineWidth = this.lineWidth const startPos = this.getPointPos(0) const endPos = this.getPointPos(this.pointsLen - 1) for (let i = 1; i < this.pointsLen; i++) { const movePos = i === 1 ? startPos : this.getPointPos(i - 1) this.drawradientLine(movePos.x, movePos.y, this.getPointPos(i).x, this.getPointPos(i).y, true) } if (x !== undefined && y !== undefined) { this.drawradientLine(endPos.x, endPos.y, x, y, false) } else { this.ctx.stroke() } } drawradientLine (x1, y1, x2, y2, closePath) { // 漸變線條 if (!this.ctx) return this.ctx.beginPath() this.ctx.moveTo(x1, y1) // 開始位置 this.ctx.lineTo(x2, y2) // 畫到此處 const grd = this.ctx.createLinearGradient(x1, y1, x2, y2) // 線性漸變的起止座標 grd.addColorStop(0, '#c9b8ff') grd.addColorStop(1, '#aa4fff') this.ctx.strokeStyle = grd this.ctx.shadowBlur = this.lineBlurWidth this.ctx.shadowColor = '#5a00ff' closePath && this.ctx.closePath() this.ctx.stroke() }
手指離開螢幕時, 當前連線星星如果少於兩顆(至少連線兩個點),則清空陣列,否則按照當前已連線的點重新繪製線條,當已連線的點小於最小限制時,給使用者toast提示。
至此,連線星星的基本功能就完成了,還需要進行一些細節的處理。
function touchEnd (e) { this.connectEnd(true) } connectEnd () { this.unlockScroll() if (this.pointsLen === 1) { this.points = [] } this.draw() if (this.pointsLen > 1 && this.pointsLen < this.minLength && !this.reconnectStart) { this.showToast(`至少連線${this.minLength}顆星星哦~`) } }
當頁面有卷軸是,連線過程中容易連帶著頁面捲動,導致觸控點錯位,並且使用者體驗不好。解決方案是:每當手指觸控畫布區域開始連線時,先禁止頁面的捲動,當手指放開後或離開畫布後再恢復頁面捲動。
具體程式碼如下:
function lockScroll () { if (this.unlock) return this.unlock = lockScrollFunc() } function unlockScroll () { if (this.unlock) { this.unlock() this.unlock = null } } function unLockScrollFunc () { const str = document.body.getAttribute(INTERNAL_LOCK_KEY) if (!str) return try { const { height, pos, top, left, right, scrollY } = JSON.parse(str) document.documentElement.style.height = height const bodyStyle = document.body.style bodyStyle.position = pos bodyStyle.top = top bodyStyle.left = left bodyStyle.right = right window.scrollTo(0, scrollY) setTimeout(() => { document.body.removeAttribute(LOCK_BODY_KEY) document.body.removeAttribute(INTERNAL_LOCK_KEY) }, 30) } catch (e) {} } function lockScrollFunc () { if (isLocked) return unLockScrollFunc const htmlStyle = document.documentElement.style const bodyStyle = document.body.style const scrollY = window.scrollY const height = htmlStyle.height const pos = bodyStyle.position const top = bodyStyle.top const left = bodyStyle.left const right = bodyStyle.right bodyStyle.position = 'fixed' bodyStyle.top = -scrollY + 'px' bodyStyle.left = '0' bodyStyle.right = '0' htmlStyle.height = '100%' document.body.setAttribute(LOCK_BODY_KEY, scrollY + '') document.body.setAttribute(INTERNAL_LOCK_KEY, JSON.stringify({ height, pos, top, left, right, scrollY })) return unLockScrollFunc }
如上所示,當連線的兩顆星星路徑上有其他的星星時,視覺上四連線了4顆星星,實際上中間兩顆手指未觸控過的星星並未加入到當前繪製星星的陣列中,這時候如果想要做最大最小星星數量的限制就會失誤,因此這裡通過判斷方向,將中間兩顆星星也接入到已連線星星陣列中,每次 draw() 時判斷一下。
如下列出了連線所有可能的8種情況和處理步驟:
判斷是否有多餘的點
判斷方向 a.豎線: x1 = x2
給點陣列重新排序
與points合併
長度超出最大限制個則從末尾丟擲
開始畫線
canvas.isPointInPath(x, y) // 判斷點 (x, y)是否在canvas路徑的區域內
function rearrangePoints () { // 根據最後兩個點之間連線,如果有多出的點進行重排,否則不處理 if (this.pointsLen === 1) return const endPrevPos = this.getPointPos(this.pointsLen - 2) const endPos = this.getPointPos(this.pointsLen - 1) const x1 = endPrevPos.x const y1 = endPrevPos.y const x2 = endPos.x const y2 = endPos.y this.judgeCtx.beginPath() this.judgeCtx.moveTo(x1, y1) // 開始位置 this.judgeCtx.lineTo(x2, y2) // 畫到此處 const extraArr = [] const realArr = [] this.pointPos.forEach((item, i) => { if (this.judgeCtx.isPointInStroke(item.x, item.y)) realArr.push(i) if (this.judgeCtx.isPointInStroke(item.x, item.y) && !this.points.includes(i)) { extraArr.push(i) } }) if (!extraArr.length) return const extraPosArr = extraArr.map(item => { return { ...this.pointPos[item], i: item } }) const getExtraSortMap = new Map([ [[0, -1], (a, b) => a.y - b.y], [[0, 1], (a, b) => b.y - a.y], [[-1, 0], (a, b) => a.x - b.x], [[1, 0], (a, b) => b.x - a.x], [[-1, -1], (a, b) => (a.x - b.x) && (a.y - b.y)], [[1, 1], (a, b) => (b.x - a.x) && (b.y - a.y)], [[1, -1], (a, b) => (b.x - a.x) && (a.y - b.y)], [[-1, 1], (a, b) => (a.x - b.x) && (b.y - a.y)] ]) const extraSortArr = extraPosArr.sort(getExtraSortMap.get([this.getEqualVal(x1, x2), this.getEqualVal(y1, y2)])) this.points.splice(this.pointsLen - 1, 0, ...(extraSortArr.map(item => item.i))) this.pointsLen > this.maxLength && this.points.splice(this.maxLength, this.pointsLen - this.maxLength) } function getEqualVal (a, b) { return a - b === 0 ? 0 : a - b > 0 ? 1 : -1 }
最後找了個星空背景的demo貼到程式碼中,功能就完成了,關於星空背景的實現感興趣的可以自己研究一下。
以上就是JS前端使用Canvas快速實現手勢解鎖特效的詳細內容,更多關於JS前端Canvas手勢解鎖的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45