HTML代码

<svg 
  viewBox="0 0 1000 1000" 
  width="1000" 
  height="1000" 
  role="img"
>
  <title>
    A square maze.
  </title>
  
  <style> 
    g {  
      --cell-size: 50px;
      --stroke-width: 10px;
    }
    
    .grid-line {
      fill: none;
      stroke: var(--maze-stroke-color);
      stroke-width: var(--stroke-width);
      stroke-linecap: square;
    }

    .maze-path {
      fill: none;
      stroke: red;
      stroke-width: 10;
      stroke: white;
      stroke-width: calc(var(--cell-size) - var(--stroke-width));
      stroke-linecap: square;
    }
  </style>

  <g class="grid"></g>
  <g class="pattern">
    <!-- 
      Our graphics code goes here 
    -->
  </g>
</svg>

<div class="buttons">
  <button class="randomize">绘制路径</button>
</div>

javaScript代码

// 引入随机数帮助函数,用于生成随机整数、从数组中随机选项,以及随机概率判断
import { randomInt, randomItemInArray, randomChance } from 'https://unpkg.com/randomness-helpers@0.0.1/dist/index.js';

// 获取 SVG 元素、迷宫图案元素和网格元素
const svgEl = document.querySelector('svg');
const patternEl =  document.querySelector('.pattern');
const gridEl = document.querySelector('.grid');

// 网格宽度和高度(单位:格子数)
const gridWidth = 20;
const gridHeight = 20;
// 每个格子的缩放比例
const scale = 50;
// 随机分支路径的生成概率
const splittingChance = 0.1;
// 动画播放的速度,单位:毫秒
const animationSpeed = 20;
// 失败重试的最大次数
const retryLimit = 30;

let interval; // 存储动画间隔
let failedCount = 0; // 记录失败的次数
let mainPathPoints = []; // 存储主路径的点
let otherPaths = []; // 存储其他分支路径

// 初始化网格数据
let gridData = buildFreshGrid();

// 绘制网格函数
function drawGrid() {
  let gridMarkup = '';
  // 绘制水平网格线
  for (let y = 0; y <= gridHeight; y++) {
    gridMarkup += `
      <line 
        class="grid-line"
        x1="0" 
        x2="${gridWidth * scale}"
        y1="${y * scale}"
        y2="${y * scale}"
      />
    `;
  }
  // 绘制垂直网格线
  for (let x = 0; x <= gridWidth; x++) {
    gridMarkup += `
      <line 
        class="grid-line"
        y1="0" 
        y2="${gridHeight * scale}"
        x1="${x * scale}"
        x2="${x * scale}"
      />
    `;
  }
  return gridMarkup;
}

// 构建一个全新的空网格,初始值为 0
function buildFreshGrid() {
  return new Array(gridHeight).fill().map(
    () => new Array(gridWidth).fill(0)
  );
}

// 调整迷宫点的位置,使其适应缩放比例
function adjustMazePoint(point) {
  return {
    x: scale / 2 + point.x * scale,
    y: scale / 2 + point.y * scale
  }
}

// 将路径点转换为路径数据(SVG 的路径命令)
function buildPathData(points) {
  points = points.map(adjustMazePoint);
  
  const firstPoint = points.shift(); // 获取第一个点
  
  let commands = [`M ${firstPoint.x}, ${firstPoint.y}`]; // 起始点命令
  
  // 遍历后续的点,生成路径命令
  points.forEach(point => {
    commands.push(`L ${point.x}, ${point.y}`); // 连接命令
  });
  
  return commands.join(' '); // 返回完整的路径命令
}

// 绘制路径(接受点和可选的类名)
function drawLine(points, className = '') {
  return `
    <path
      class="maze-path ${className}" 
      d="${buildPathData(points)}"
    />
  `;
}

// 生成主路径的起始点
function mainPathStartPoints() {
  const yStart = randomInt(0, gridHeight - 1); // 随机选择起始行
  
  return [
    { x: -1, y: yStart }, // 起始点在左侧外面
    { x: 0, y: yStart }   // 入口点位于网格内
  ]
}

// 标记网格中的一个点为已占用
function markPointAsTaken(point, value = 1) {
  gridData[point.y][point.x] = value;
}

// 寻找当前点的下一个可行点(四个方向)
function findNextPoint(point) {
  const potentialPoints = [];
  
  // 检查上方
  if (gridData[point.y - 1]?.[point.x] === 0) {
    potentialPoints.push({ y: point.y - 1, x: point.x });
  }
  
  // 检查下方
  if (gridData[point.y + 1]?.[point.x] === 0) {
    potentialPoints.push({ y: point.y + 1, x: point.x });
  }
  
  // 检查左方
  if (gridData[point.y]?.[point.x - 1] === 0) {
    potentialPoints.push({ y: point.y, x: point.x - 1 });
  }
  
  // 检查右方
  if (gridData[point.y]?.[point.x + 1] === 0) {
    potentialPoints.push({ y: point.y, x: point.x + 1 });
  }
  
  // 如果没有可行点,返回 undefined
  if (potentialPoints.length === 0) {
    return undefined;
  }
  
  // 随机选择一个方向
  return randomItemInArray(potentialPoints);
}

// 重置迷宫状态
function refreshState() {
  mainPathPoints = mainPathStartPoints(); // 重新设置主路径起点
  gridData = buildFreshGrid(); // 重建空网格
  markPointAsTaken(mainPathPoints.at(-1)); // 标记起点为占用
  otherPaths = []; // 清空其他路径
  failedCount = 0; // 重置失败计数
}

// 绘制路径
function drawLines() {
  let markup = '';
  
  // 绘制分支路径
  markup += otherPaths.map(drawLine).join('');
  
  // 绘制主路径
  markup += drawLine(mainPathPoints, 'main');
  
  patternEl.innerHTML = markup; // 将路径渲染到页面
}

// 构建主路径
function buildMainPath() {
  refreshState(); // 重置迷宫状态
  drawLines(); // 绘制初始路径
  
  // 启动一个定时器来逐步绘制路径
  interval = setInterval(() => {
    const nextPoint = findNextPoint(mainPathPoints.at(-1)); // 找到下一个路径点
  
    // 如果找不到路径点,判断是否失败次数超过限制
    if (!nextPoint) {
      if (failedCount > retryLimit) {
        refreshState(); // 超过限制,重置迷宫
      } else {
        failedCount++; // 增加失败次数
        for (let i = 0; i < failedCount; i++) {
          markPointAsTaken(mainPathPoints.pop(), 0); // 回溯并标记点为未占用
        }
      }
    } else {
      mainPathPoints.push(nextPoint); // 添加新的路径点
      markPointAsTaken(nextPoint); // 标记新点为已占用
      
      // 如果到达右边缘,结束主路径绘制
      if (nextPoint.x === gridWidth - 1) {
        mainPathPoints.push({ x: nextPoint.x + 1, y: nextPoint.y });
        clearInterval(interval); // 停止定时器
        
        buildOtherPaths(); // 开始生成其他分支路径
      }
    }
    
    drawLines(); // 绘制当前的路径
  }, animationSpeed);
}

// 为迷宫添加更多分支路径
function addMorePaths() {
  gridData.forEach((row, y) => {
    row.forEach((cell, x) => {
      // 如果该格子已占用且随机生成分支路径
      if (cell && randomChance(splittingChance)) {
        otherPaths.push([{ y, x }]);
      }
    })
  });
}

// 判断迷宫是否完成(所有格子都被占用)
function mazeComplete() { 
  return gridData.flat().every(cell => cell === 1);
}

// 生成其他分支路径
function buildOtherPaths() {
  interval = setInterval(() => {
    addMorePaths(); // 添加分支路径
  
    otherPaths.forEach((path) => {
      const nextPoint = findNextPoint(path.at(-1)); // 找到下一个路径点
      if (nextPoint) {
        path.push(nextPoint); // 添加路径点
        markPointAsTaken(nextPoint); // 标记为已占用
      }
    });
  
    drawLines(); // 绘制路径
    
    // 如果迷宫已完成,停止定时器
    if (mazeComplete()) {
      clearInterval(interval);
      console.log('maze done');
    }
  }, animationSpeed);
}

// 绘制迷宫并初始化视图
function draw() { 
  gridEl.innerHTML = drawGrid(); // 绘制网格
  
  const mazeWidth = gridWidth * scale;
  const mazeHeight = gridHeight * scale;
  svgEl.setAttribute('viewBox', `0 0 ${mazeWidth} ${mazeHeight}`);
  svgEl.setAttribute('width', mazeWidth);
  svgEl.setAttribute('height', mazeHeight);
  
  patternEl.innerHTML = ''; // 清空已有路径
  
  clearInterval(interval); // 清除现有的定时器
  buildMainPath(); // 开始生成主路径
}

// 初始化迷宫
draw();

// 绑定“随机化”按钮的点击事件,重新生成迷宫
document.querySelector('.randomize').addEventListener('click', draw);

CSS代码

html, body {
  --hue: 205;
  
  display: grid;
  place-items: center;
  min-height: 100%;
  --maze-bg-color: hsl(270deg, 15%, 44%);
  --maze-stroke-color: hsl(270deg, 15%, 40%);
}

svg {
  background: #fff;
  max-width: 90vw;
  max-height: 85vh;
  width: auto;
  height: auto;
  overflow: visible;
  background: var(--maze-bg-color);
}


button {
  
  /* Text Colors */
  --text-saturation: 90%;
  --text-lightness: 40%;
  
  --text-saturation-hover: calc(var(--text-saturation) + 10%);
  --text-lightness-hover: calc(var(--text-lightness) - 5%); ;
  
  --text-saturation-active: var(--text-saturation-hover);
  --text-lightness-active: calc(var(--text-lightness) - 10%); ;
  
  --text-saturation-disabled: calc(var(--text-saturation) - 60%);
  --text-lightness-disabled: calc(var(--text-lightness) + 10%);
  
  /* Background Colors */
  --background-saturation: 0%;
  --background-lightness: 100%;
  
  --background-saturation-hover: calc(var(--background-saturation) + 80%);
  --background-lightness-hover: calc(var(--background-lightness) - 5%);
  
  --background-saturation-active: var(--background-saturation-hover);
  --background-lightness-active: calc(var(--background-lightness) - 10%);
  
  --background-saturation-disabled: calc(var(--background-saturation) + 30%);
  --background-lightness-disabled: calc(var(--background-lightness) - 10%);
  
  /* Border Colors */
  --border-saturation: 90%;
  --border-lightness: 60%;
  
  --border-saturation-hover: calc(var(--border-saturation) + 10%);
  --border-lightness-hover: calc(var(--border-lightness) - 10%);
  
  --border-saturation-active: var(--border-saturation-hover);
  --border-lightness-active: calc(var(--border-lightness) - 20%);
  
  --border-saturation-disabled: calc(var(--border-saturation) - 60%);
  --border-lightness-disabled: calc(var(--border-lightness) + 20%);
  
  /* Focus shadow styles */
  --shadow-saturation-focus: 100%;
  --shadow-lightness-focus: 85%;
  
  /* Color Styles */
  color: hsl(var(--hue), var(--text-saturation), var(--text-lightness));
  background-color: hsl(var(--hue), var(--background-saturation), var(--background-lightness)); 
  border:0.1em solid hsl(var(--hue), var(--border-saturation), var(--border-lightness)); 
  
  /* Misc. Styles */
  border-radius: 0.25em;
  cursor: pointer;
  display: inline-block;
  font-size: 1em;
  padding: 0.5em 1em;
  transition-property: box-shadow, background-color, border-color, color;
  transition-timing-function: ease-out;
  transition-duration: 0.2s;
}

button:hover {
  color: hsl(
    var(--hue), 
    var(--text-saturation-hover), 
    var(--text-lightness-hover)
  );
  
  background-color: hsl(
    var(--hue), 
    var(--background-saturation-hover), 
    var(--background-lightness-hover)
  );
  
  border-color: hsl(
    var(--hue), 
    var(--border-saturation-hover), 
    var(--border-lightness-hover)
  );
}

button:active {
  color: hsl(
    var(--hue), 
    var(--text-saturation-active), 
    var(--text-lightness-active)
  );
  
  background-color: hsl(
    var(--hue), 
    var(--background-saturation-active), 
    var(--background-lightness-active)
  );
  
  border-color: hsl(
    var(--hue), 
    var(--border-saturation-active), 
    var(--border-lightness-active)
  );
}

button:focus {
  outline: none;
  box-shadow: 0 0 0 0.25em hsl(
    var(--hue), 
    var(--shadow-saturation-focus),
    var(--shadow-lightness-focus)
  );
}

.buttons {
  display: flex;
  gap: 0.5em;
}