SCSS/TS/CANVAS实践笔记:寻找路径 | 青训营

本文的目的在于实践SCSSGrid/Flex布局,属性选择器),尝试学习CANVAS,以及运用算法(随机数算法,动态规划寻路算法)。其主要内容在于将力扣上经典的寻找路径题目-iii寻找路径题目-ii综合起来用可视化的方法呈现。该项目使用React+Vite创建。

题目背景:

在二维网格 grid 上,有 4 种类型的方格:

  • 1 表示起始方格。且只有一个起始方格。
  • 2 表示结束方格,且只有一个结束方格。
  • 0 表示我们可以走过的空方格。
  • -1 表示我们无法跨越的障碍。

返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。 每一个无障碍方格不需要都通过一次,但是一条路径中不能重复通过同一个方格 。 请注意,起始和结束方格可以位于网格中的任意位置

具体实现主要分为以下几点:

  1. 随机生成地图:随机数生成算法,CSS属性选择器
  2. 寻路算法:回溯
  3. CANVAS绘制路径

偷懒

我将地图的大小固定为 6×6,障碍物个数固定为 12

js 复制代码
const size = 6
const rate = 12 / (size**2)

STEP1. 随机生成地图

要求:

  1. 控制0,-1的比例,随机生成
  2. 随机生成起始点和结束点:即需要生成两个不同值的随机数
  3. CSS属性选择器动态渲染

按比例生成随机数

按比例随机生成障碍物,障碍物的比例不宜过高。算法思想:按比例生成由 0,-1组成的数组,然后随机化索引:

js 复制代码
const list = [...new Array(Math.floor(size ** 2 * rate)).fill(-1), 
              ...new Array(size ** 2 - Math.floor(size ** 2 * rate)).fill(0)]

for (let i = list.length; i > 0; i--) {
  const pIdx = Math.floor(Math.random() * i);
  [list[pIdx], list[i - 1]] = [list[i - 1], list[pIdx]]; // 索引打乱
}

不重复的随机数

随机生成起始点和结束点。算法思想:利用生成器函数。该算法思想来自青训营老师给出的分红包案例

js 复制代码
function * draw (cards: number[]) {
    const c = [...cards];
  
    for(let i = c.length; i > 0; i--) {
      const pIdx = Math.floor(Math.random() * i); // 随机下标
      [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]]; // 交换值
      yield c[i - 1]; // 执行暂停
    }
 }

// 随机生成若干不同的数
const generateDot = (len: number) => {
    const cards = Array(size ** 2).fill(0).map((_, i) => i)
    const pick = draw(cards)
    const result = []
    for (let i = 0; i < len; i++) {
      result.push(pick.next().value)
    }
    return result
 }

Grid布局画棋盘

通过grid布局实现

scss 复制代码
.map {
  display: grid;
  place-content: center;
  grid-template-columns: repeat($size, 40px);
  grid-template-rows: repeat($size, 40px);
  flex-wrap: wrap;
  gap: 20px;
  padding: 50px;
  width: 300px;
}
  • place-content:第一个值为 align-content 属性,第二个值为 justify-content。二合一。center表示所有的子元素堆叠在父元素的中间对齐
  • grid-template-columns/rows:该属性是基于网格列/行 的维度,去定义网格线的名称和网格轨道的尺寸大小。
    • repeat( [ <positive-integer> | auto-fill | auto-fit ] , <track-list> ) 表示网格轨道的重复部分,以一种更简洁的方式去表示大量而且重复列的表达式。
  • gap:置网格行与列之间的间隙。是 row-gapcolumn-gap 的简写形式。

属性选择器填充

功能: 所有同一值的格子显示同一颜色

scss 复制代码
.item[data-tag='1'] {
  background: #b3dbe4;
}

此时页面的绘制效果如下:

STEP2. 寻路算法

js 复制代码
export function findPath(arr: Array<number>) {
  const grid = arrReform(JSON.parse(JSON.stringify(arr))) // arrReform 在这里是把一维地图处理成二维数组
  const n = grid.length
  const m = grid[0].length
  const directions = [
      [-1, 0],
      [1, 0],
      [0, -1],
      [0, 1]
    ]
  if (n === 0 || m === 0) return 0

  const allPath: Array<string> = [] // 存储所有合法路径
  const visited = new Array(n) // 存储访问记录
  let entry // 起始点
  for (let i = 0; i < n; i++) {
    visited[i] = new Array(m)
    for (let j = 0; j < m; j++) {
      if (grid[i][j] === 1) entry = [i, j] // 找到起点
    }
  }
  
  // 路径点是否合法:在二维数组内部+未访问过+非障碍物(-1)
  const isValid = (x: number, y: number) => {
    return x >= 0 && x < n && y >= 0 && y < m && !visited[x][y] && grid[x][y] !== -1
  }

  const dfs = (startX: number, startY: number, path: string) => {
    const cur = grid[startX][startY] // 当前点位
    if (cur === 2) {
      // 是终点则结束
      path = path + `->${[startX, startY]}`
      allPath.push(path) 
      return
    } else if (cur === 0) {
      // 是路则接着走
      path = path + `->${[startX, startY]}`
    }
    
    // 向四个方向延伸
    for (const direction of directions) {
      const [dirX, dirY] = direction
      const nextX = startX + dirX
      const nextY = startY + dirY
      // 如果下一个点位合法,则继续走
      if (isValid(nextX, nextY)) {
        visited[nextX][nextY] = true
        dfs(nextX, nextY, path)

        // 状态重置
        visited[nextX][nextY] = false
      }
    }
  }
  const [entryX, entryY] = entry as [number, number]
  visited[entryX][entryY] = true
  dfs(entryX, entryY, `${[entryX, entryY]}`) // 从起点开始
  
  return allPath
}

时间复杂度:O(3^mn)

空间复杂度:O(mn)

STEP3. CANVAS绘制路径

利用requestAnimationFrame绘制帧

window.requestAnimationFrame(callback) 告诉浏览器------你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

callback 当你的动画需要更新时,为下一次重绘所调用的函数。

警告: 请确保总是使用第一个参数(或其他一些获取当前时间的方法)来计算动画在一帧中的进度,否则动画在高刷新率的屏幕中会运行得更快

requestAnimationFrame是宏任务吗?

是宏任务requestAnimationFrame的回调是根据显示器的sync信号触发的,在下次sync之前回调会进入渲染进程的消息队列。它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率【减少DOM操作 】。当页面处于未激活的状态下,页面的屏幕刷新任务会被系统暂停,此时requestAnimationFrame会停止渲染;当页面被激活时,动画就从上次停留的地方继续执行【CPU节能 】,有效节省了CPU开销。在高频率 事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次【函数节流 】,这样既能保证流畅性,也能更好的节省函数执行的开销。requestAnimationFrame能够精准跟随浏览器刷新频率,而setTimeout会受到事件循环机制的影响。

复习用小TIPS:

微任务:Promise 回调函数、process.nextTick、MutationObserver。

宏任务:setTimeout、setInterval、setImmediate(Node.js 独有)、requestAnimationFrame、I/O 操作、UI 渲染。

requestAnimationFrame其他应用

阅读链接blog.csdn.net/animatecat/...

  1. 监听 scroll 函数 页面滚动事件(scroll)的监听函数
  2. 大量数据渲染(十万条数据进行渲染的解决方案)
  3. 监控卡顿方法 :每秒中计算一次网页的 FPS(画面每秒传输帧数)

最终路径的绘制方法:

ts 复制代码
const generatePath = (path: Array<Array<number>>) => {
    const duration = 1000 // 每走一条路的动画持续的时间
    const canvas = document.getElementById('myPath') as HTMLCanvasElement
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
    
    // 首先需要清空画布
    ctx.clearRect(0, 0, 500, 500);

    // 线条样式
    ctx.strokeStyle = 'blue'
    ctx.lineWidth = 3
    ctx.lineJoin = 'round'

    // 单条路径绘制
    const createPath = (i: number) => {
      let startTime: number;
      // 当前的点位
      const curX = path[i][0] * 60 + 20
      const curY = path[i][1] * 60 + 20
      // 下一个点位
      const nextX = path[i+1][0] * 60 + 20
      const nextY = path[i+1][1] * 60 + 20

      // 帧绘制当前点位
      let prevX = curX
      let prevY = curY

      // 帧绘制下一个点位
      let stepX
      let stepY

      ctx.beginPath() // 创建新的路径

      // requestAnimationFrame的回调函数
      const step = (currentTime: number) => {
        !startTime && (startTime = currentTime)
        const timePased = (currentTime - startTime) > i*duration ? (currentTime - startTime) : i*duration
        const progress = Math.min((timePased - i*duration) / duration, 1) // 需要按顺序加载Path

        const draw = () => {
          ctx.moveTo(prevY,prevX) // 起点
          prevX = stepX = curX + (nextX - curX) * progress;
          prevY = stepY = curY + (nextY - curY) * progress;
          ctx.lineTo(stepY, stepX) // 终点
          ctx.stroke() // 把这一帧的路径绘制出来
        }
        
        draw() 
        
        if (progress < 1) {
          window.requestAnimationFrame(step)
        } else {
          console.log('----> 路径', i)
          if (i === path.length - 2) {
            console.log('----> 结束')
            setDisable(false) // 此时取消按钮禁用
          }
        }
      }

      window.requestAnimationFrame(step)
    }

    // 生成CANVAS路径
    for (let i = 0; i < path.length-1; i ++) {
      createPath(i)
    }
  }

最终实现效果:

地址:待编辑。

未来工作:

  • 美化界面
  • 优化算法
  • 提高地图复杂度: 自定义地图大小,自定义障碍物比例,随机障碍物比例等

参考资料

  1. 青训营课程 《如何写好 JavaScript》
  2. MDN 生成器函数
  3. 力扣-寻找路径题目-iii 寻找路径题目-ii
  4. 掘金《详解Canvas路径动画》
  5. MDN: grid
  6. blog.csdn.net/animatecat/...
相关推荐
漠河愁22 天前
pdf文件渲染到canvas
canvas·pdf.js·fabirc.js
xachary1 个月前
前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
canvas·konva
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
甄齐才2 个月前
canvas绘制文本时,该如何处理首行缩进、自动换行、多内容以省略号结束、竖排的呢?
canvas·html2canvas·海报·html转图片·文章分享·dom-to-image·html转image
万水千山走遍TML2 个月前
canvas绘制表格
前端·javascript·vue.js·canvas·canvas绘图·在vue中使用canvas·canvas绘制表格
xachary2 个月前
前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
javascript·vue·canvas·konva
梦想身高1米82 个月前
canvas.toDataURL后图片背景变成黑色
前端·canvas
x007xyz2 个月前
Fabric.js实时播放视频并扣除绿幕
前端·javascript·canvas
xachary2 个月前
前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
算法·canvas·konva
LeaferJS2 个月前
LeaferJS 1.0 重磅发布:强悍的前端 Canvas 渲染引擎
前端·canvas