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/...
相关推荐
PineappleCoder3 天前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
德育处主任4 天前
p5.js 用 cylinder() 绘制 3D 圆柱体
前端·数据可视化·canvas
蛋蛋_dandan6 天前
Fabric.js从0到1实现图片框选功能
canvas
wayhome在哪8 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任8 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任9 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任11 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼11 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲12 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户25191624271114 天前
Canvas之画图板
前端·javascript·canvas