经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
vue + canvas 实现九宮格手势解锁器
来源:cnblogs  作者:柏成  时间:2023/9/9 11:30:40  对本文有异议

前言

专栏分享:vue2源码专栏vue router源码专栏玩具项目专栏,硬核??推荐??
欢迎各位 ITer 关注点赞收藏??????

此篇文章用于记录柏成从零开发一个canvas九宮格手势解锁器的历程,最终效果如下:

  1. 设置图案密码时,需进行两次绘制图案操作,若两次绘制图案一致,则密码设置成功;若不一致,则需重新设置密码

  2. 输入图案密码时,密码一致则验证通过;密码不一致则提示图案密码错误,请重试

介绍

我们基于 canvas 实现了一款简单的九宫格手势解锁器,用户可以通过在九宫格中绘制特定的手势来解锁

我们可以通过 new Locker 创建一个图案解锁器,其接收一个容器作为第一个参数,第二个参数为选项,下面是个基本例子:

  1. <template>
  2. <div class="pattren-locker">
  3. <div id="container" ref="container" style="width: 360px; height: 600px"></div>
  4. </div>
  5. </template>
  6. <script setup>
  7. import { ref, onMounted } from 'vue'
  8. import Locker from '@/canvas/locker'
  9. const container = ref(null)
  10. onMounted(() => {
  11. // 新建一个解锁器
  12. new Locker(container.value,{
  13. radius: 30, // 圆圈半径
  14. columnSpacing: 50, // 圆圈列间距
  15. rowsSpacing: 90, // 圆圈行间距
  16. stroke: '#b5b5b5', // 圆圈描边颜色
  17. lineStroke: '#237fb4', // 路径描边颜色
  18. selectedFill: '#237fb4', // 图案选中填充颜色
  19. backgroundColor: '#f7f7f7', // 画布背景颜色
  20. })
  21. })
  22. </script>

初始化

Locker 的实现是一个类,在 src/canvas/locker.js中定义。

new Locker(container,{...})时做了什么?我们在构造函数中创建一个 canvas 画布追加到了 container 容器中,并定义了一系列属性,最后执行了 init 初始化方法。

在初始化方法中,我们绘制了9个宫格圆圈,作为解锁单元;并注册监听了鼠标事件,用于绘制解锁轨迹。

  1. // 初始化
  2. init() {
  3. this.drawCellGrids()
  4. this.drawText('请绘制新的图案密码')
  5. this.canvas.addEventListener('contextmenu', (e) => e.preventDefault())
  6. this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))
  7. }
  8. // 绘制9个宫格圆圈
  9. drawCellGrids() {
  10. const columns = 3
  11. const rows = 3
  12. const width = this.canvas.width
  13. const height = this.canvas.height
  14. const paddingTop = (height - rows * 2 * this.radius - (rows - 1) * this.rowsSpacing) / 2
  15. const paddingLeft = (width - columns * 2 * this.radius - (columns - 1) * this.columnSpacing) / 2
  16. for (let i = 0; i < rows; i++) {
  17. for (let j = 0; j < columns; j++) {
  18. const data = {
  19. x: paddingLeft + (2 * j + 1) * this.radius + j * this.columnSpacing,
  20. y: paddingTop + (2 * i + 1) * this.radius + i * this.rowsSpacing,
  21. id: i * columns + j
  22. }
  23. this.lockerCells.push(data)
  24. this.ctx.beginPath()
  25. this.ctx.arc(data.x, data.y, this.radius, 0, 2 * Math.PI, true)
  26. this.ctx.strokeStyle = this.stroke
  27. this.ctx.lineWidth = 3
  28. this.ctx.stroke()
  29. }
  30. }
  31. this.cellImageData = this.lastImageData = this.getImageData()
  32. }

自定义鼠标事件

我们之前在 init 初始化方法中注册了 onmousedown 鼠标按下事件,需要在此处实现鼠标按下拖拽可以绘制解锁轨迹的逻辑

鼠标按下:先执行 selectCellAt 方法(如果在圆圈内按下鼠标,会立即绘制选中样式,并保存选中样式之后的画布快照)

鼠标移动:先恢复快照,再绘制路径中最后一个点到当前鼠标坐标的轨迹,最后再执行 selectCellAt 方法,一直重复此过程。。。直到鼠标移动到圆圈内部(则先恢复快照,然后绘制点的选中样式,绘制路径中最后一个点到当前点的路径,最后保存绘制路径之后的画布快照)

鼠标抬起:清空onmousemove、onmouseup事件,并校验密码

此时我们小小的脑袋里可能有两个大大的问号??

  1. selectCellAt 方法作用是什么?
    如果鼠标移动到圆圈内部,则会将图案路径连接到当前圆圈,并绘制选中样式

  2. 快照是什么?
    快照是当前画布的像素点信息。我们永远会在激活一个解锁单元后(即鼠标移动到圆圈内部时),先恢复画布快照,然后去绘制圆圈的选中样式,并将图案路径延伸连接到当前圆圈,然后!会保存此时此刻的画布快照!
    之后,我们会在鼠标移动时,不停的恢复快照,然后绘制最后一个圆圈到当前鼠标坐标的连线轨迹,直到我们激活下一个解锁单元(即鼠标移动到下一个圆圈内部)。我们会又会重复上面的过程,这就构成一个一个的循环

  1. mousedownEvent(e) {
  2. const that = this
  3. // 选中宫格,并绘制点到点路径
  4. const selected = this.selectCellAt(e.offsetX, e.offsetY)
  5. if (!selected) return
  6. // 鼠标移动事件
  7. this.canvas.onmousemove = function (e) {
  8. // 路径的最后一个点
  9. const lastData = that.currentPath[that.currentPath.length - 1]
  10. // 恢复快照
  11. that.restoreImageData(that.lastImageData)
  12. // 绘制路径
  13. that.drawLine(lastData, { x: e.offsetX, y: e.offsetY })
  14. // 选中宫格,并绘制点到点路径
  15. that.selectCellAt(e.offsetX, e.offsetY)
  16. }
  17. // 鼠标抬起/移出事件
  18. this.canvas.onmouseup = this.canvas.onmouseout = function () {
  19. const canvas = this
  20. canvas.onmousemove = null
  21. canvas.onmouseup = null
  22. canvas.onmouseout = null
  23. const currentPathIds = that.currentPath.map((item) => item.id)
  24. let text = ''
  25. if (that.password.length === 0) {
  26. that.password = currentPathIds
  27. text = '请再次绘制图案进行确认'
  28. } else if (that.confirmPassword.length === 0) {
  29. that.confirmPassword = currentPathIds
  30. if (that.password.join('') === that.confirmPassword.join('')) {
  31. text = '图案密码设置成功,请输入您的密码'
  32. } else {
  33. text = '与上次绘制不一致,请重试'
  34. that.password = []
  35. that.confirmPassword = []
  36. }
  37. } else {
  38. if (that.password.join('') === currentPathIds.join('')) {
  39. text = '图案密码正确 (づ ̄3 ̄)づ╭?~'
  40. } else {
  41. text = '图案密码错误,请重试'
  42. }
  43. }
  44. that.ctx.clearRect(0, 0, canvas.width, canvas.height) // 清空画布
  45. that.restoreImageData(that.cellImageData) // 恢复背景宫格快照
  46. that.drawText(text) // 绘制提示文字
  47. that.currentPath = [] // 清空当前绘制路径
  48. that.lastImageData = that.cellImageData // 重置上一次绘制的画布快照
  49. }
  50. }

绘制路径及选中样式

我们会在鼠标按下(onmousedown)、鼠标移动(onmousemove)事件中调用 selectCellAt 方法,并传入当前鼠标坐标信息

  1. 若当前坐标在宫格圆圈内 且 改圆圈未被连接过,则先恢复画布快照,然后绘制圆圈选中样式,绘制路径中最后一个圆圈到当前圆圈的路径,最后保存此时此刻的画布快照,返回true

  2. 若当前坐标不在宫格圆圈内 或者 该圆圈被连接过,则返回false

  1. selectCellAt(x, y) {
  2. // 当前坐标点是否在圆内
  3. const data = this.lockerCells.find((item) => {
  4. return Math.pow(item.x - x, 2) + Math.pow(item.y - y, 2) <= Math.pow(this.radius, 2)
  5. })
  6. const existing = this.currentPath.some((item) => item.id === data?.id)
  7. if (!data || existing) return false
  8. // 恢复画布快照
  9. this.restoreImageData(this.lastImageData)
  10. // 绘制选中样式
  11. this.drawCircle(data.x, data.y, this.radius / 1.5, 'rgba(0,0,0,0.2)')
  12. this.drawCircle(data.x, data.y, this.radius / 2.5, this.selectedFill)
  13. // 绘制路径 从最后一个点到当前点
  14. const lastData = this.currentPath[this.currentPath.length - 1]
  15. if (lastData) {
  16. this.drawLine(lastData, data)
  17. }
  18. // 保存画布快照
  19. this.lastImageData = this.getImageData()
  20. // 保存当前点
  21. this.currentPath.push(data)
  22. return true
  23. }
  24. // 绘制选中样式
  25. drawCircle(x, y, radius, fill) {
  26. this.ctx.beginPath()
  27. this.ctx.arc(x, y, radius, 0, 2 * Math.PI, true)
  28. this.ctx.fillStyle = fill
  29. this.ctx.fill()
  30. }
  31. // 绘制路径
  32. drawLine(start, end, stroke = this.lineStroke) {
  33. this.ctx.beginPath()
  34. this.ctx.moveTo(start.x, start.y)
  35. this.ctx.lineTo(end.x, end.y)
  36. this.ctx.strokeStyle = stroke
  37. this.ctx.lineWidth = 3
  38. this.ctx.lineCap = 'round'
  39. this.ctx.lineJoin = 'round'
  40. this.ctx.stroke()
  41. }

画布快照

我们如何获取到当前画布快照?又如何根据快照数据恢复画布呢?

查阅 canvas官方API文档 得知,获取快照 API 为 getImageData;通过快照恢复画布的 API 为 putImageData

  1. // 获取画布快照
  2. getImageData() {
  3. return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
  4. }
  5. // 恢复画布快照
  6. restoreImageData(imageData) {
  7. if (!imageData) return
  8. this.ctx.putImageData(imageData, 0, 0)
  9. }

源码

涂鸦面板demo代码vue-canvas

原文链接:https://www.cnblogs.com/burc/p/17673996.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号