本文的目的在于实践SCSS
(Grid/Flex
布局,属性选择器),尝试学习CANVAS
,以及运用算法(随机数算法,动态规划寻路算法)。其主要内容在于将力扣上经典的寻找路径题目-iii,寻找路径题目-ii综合起来用可视化的方法呈现。该项目使用React+Vite
创建。
题目背景:
在二维网格
grid
上,有 4 种类型的方格:
1
表示起始方格。且只有一个起始方格。2
表示结束方格,且只有一个结束方格。0
表示我们可以走过的空方格。-1
表示我们无法跨越的障碍。返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。 每一个无障碍方格不需要都通过一次,但是一条路径中不能重复通过同一个方格 。 请注意,起始和结束方格可以位于网格中的任意位置。
具体实现主要分为以下几点:
- 随机生成地图:随机数生成算法,CSS属性选择器
- 寻路算法:回溯
CANVAS
绘制路径
偷懒
我将地图的大小固定为 6×6
,障碍物个数固定为 12
。
js
const size = 6
const rate = 12 / (size**2)
STEP1. 随机生成地图
要求:
- 控制0,-1的比例,随机生成
- 随机生成起始点和结束点:即需要生成两个不同值的随机数
- 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-gap
和column-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/...
- 监听 scroll 函数 页面滚动事件(scroll)的监听函数
- 大量数据渲染(十万条数据进行渲染的解决方案)
- 监控卡顿方法 :每秒中计算一次网页的 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)
}
}
最终实现效果:
地址:待编辑。
未来工作:
- 美化界面
- 优化算法
- 提高地图复杂度: 自定义地图大小,自定义障碍物比例,随机障碍物比例等