分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>正交铰接仿生蛇形机器人 - Serpenoid 行波控制与越障原理</title>
  <style>
    :root {
      --bg: #090d16;
      --panel-bg: rgba(13, 20, 36, 0.78);
      --border: rgba(0, 242, 254, 0.22);
      --cyan: #00f2fe;
      --amber: #ff9f1c;
      --blue: #2575fc;
      --text: #e2e8f0;
      --muted: #64748b;
      --accent-green: #10b981;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
    }

    body {
      background: var(--bg);
      color: var(--text);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Noto Sans CJK SC", monospace;
      overflow: hidden;
      width: 100vw;
      height: 100vh;
      display: flex;
      flex-direction: column;
      position: relative;
    }

    /* 背景工程网格与极光微晕 */
    .bg-grid {
      position: absolute;
      inset: 0;
      background-image: 
        radial-gradient(circle at 50% 40%, rgba(37, 117, 252, 0.12) 0%, transparent 65%),
        linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
      background-size: 100% 100%, 40px 40px, 40px 40px;
      pointer-events: none;
    }

    /* 主画布区域:让动画占据绝对视觉中心 */
    #stage-container {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1;
    }

    svg#robot-stage {
      width: 100%;
      height: 100%;
      max-width: 1400px;
      max-height: 900px;
      filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.8));
    }

    /* 悬浮微型仪表与说明卡片:精简、小字号、边缘靠齐,绝不遮挡中央 */
    .hud-header {
      position: absolute;
      top: 18px;
      left: 24px;
      z-index: 10;
      background: var(--panel-bg);
      backdrop-filter: blur(12px);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 10px 16px;
      pointer-events: auto;
    }

    .hud-title {
      font-size: 13px;
      font-weight: 700;
      letter-spacing: 0.8px;
      color: #fff;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .pulse-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--cyan);
      box-shadow: 0 0 10px var(--cyan);
      animation: pulse 1.6s infinite ease-in-out;
    }

    .hud-subtitle {
      font-size: 11px;
      color: var(--muted);
      margin-top: 3px;
      font-family: monospace;
    }

    /* 右上角:实时控制律算式与解耦通道指示 */
    .hud-math {
      position: absolute;
      top: 18px;
      right: 24px;
      z-index: 10;
      background: var(--panel-bg);
      backdrop-filter: blur(12px);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 10px 14px;
      font-size: 11px;
      display: flex;
      flex-direction: column;
      gap: 5px;
      max-width: 320px;
    }

    .formula-tag {
      color: var(--cyan);
      font-family: "Courier New", Courier, monospace;
      font-weight: bold;
      font-size: 11.5px;
    }

    .status-badge-group {
      display: flex;
      gap: 8px;
      margin-top: 3px;
    }

    .badge {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-size: 10.5px;
      padding: 2px 7px;
      border-radius: 4px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: rgba(0, 0, 0, 0.3);
    }

    .badge.yaw { border-color: var(--cyan); color: var(--cyan); }
    .badge.pitch { border-color: var(--amber); color: var(--amber); }

    /* 底部角落微型交互条 */
    .hud-controls {
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 10;
      background: var(--panel-bg);
      backdrop-filter: blur(14px);
      border: 1px solid var(--border);
      border-radius: 30px;
      padding: 6px 14px;
      display: flex;
      align-items: center;
      gap: 12px;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
    }

    .btn-chip {
      background: rgba(255, 255, 255, 0.06);
      border: 1px solid rgba(255, 255, 255, 0.12);
      color: var(--text);
      font-size: 11px;
      padding: 5px 12px;
      border-radius: 16px;
      cursor: pointer;
      transition: all 0.25s ease;
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .btn-chip:hover {
      background: rgba(0, 242, 254, 0.15);
      border-color: var(--cyan);
      color: #fff;
    }

    .btn-chip.active {
      background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(37, 117, 252, 0.3));
      border-color: var(--cyan);
      color: #fff;
      box-shadow: 0 0 12px rgba(0, 242, 254, 0.3);
    }

    .timeline-progress {
      position: absolute;
      bottom: 0;
      left: 0;
      height: 2px;
      background: linear-gradient(90deg, var(--cyan), var(--amber));
      width: 0%;
      transition: width 0.1s linear;
      z-index: 20;
    }

    /* 左下角状态提示 */
    .hud-state-label {
      position: absolute;
      bottom: 20px;
      left: 24px;
      z-index: 10;
      font-size: 11px;
      color: var(--muted);
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .hud-state-label b {
      color: #e2e8f0;
      font-weight: 600;
    }

    @keyframes pulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50% { opacity: 0.4; transform: scale(0.85); }
    }
  </style>
</head>
<body>
  <div class="bg-grid"></div>

  <!-- 顶部精简抬头与状态 -->
  <header class="hud-header">
    <div class="hud-title">
      <div class="pulse-dot"></div>
      <span>正交铰接仿生蛇形机器人 · 行波越障系统</span>
    </div>
    <div class="hud-subtitle">14 节正交铰接构型 / 20kg·cm 数字舵机 / Serpenoid 连续控制律</div>
  </header>

  <!-- 右上角控制律与正交自由度解耦解析 -->
  <aside class="hud-math">
    <div class="formula-tag">θᵢ(t) = A·sin(2πft − 2πki/N) + bᵢ</div>
    <div class="status-badge-group">
      <span class="badge yaw">奇数关节 · 偏航 (Yaw) 水平行波</span>
      <span class="badge pitch">偶数关节 · 俯仰 (Pitch) 升降偏置</span>
    </div>
  </aside>

  <!-- 左下角工况指示 -->
  <div class="hud-state-label">
    <div>实时步态阶段:<b id="phase-desc">自动巡航与自适应越障中</b></div>
    <div style="font-family: monospace;">波幅 A=±40° | 频率 f=1.5Hz | 波数 k=1.2</div>
  </div>

  <!-- 底部微型步态切换交互 -->
  <div class="hud-controls">
    <button class="btn-chip active" id="btn-auto" onclick="setMode('auto')">🔄 全自动协同演练</button>
    <button class="btn-chip" id="btn-serpentine" onclick="setMode('serpentine')">≈ 纯平面侧向蜿蜒</button>
    <button class="btn-chip" id="btn-obstacle" onclick="setMode('obstacle')">⤴ 抬升偏置搭接越障</button>
  </div>

  <div class="timeline-progress" id="time-bar"></div>

  <!-- 核心视口:三维正投影透视 SVG 画布 -->
  <div id="stage-container">
    <svg id="robot-stage" viewBox="0 0 1200 700" preserveAspectRatio="xMidYMid meet">
      <defs>
        <!-- 线性与径向渐变 -->
        <linearGradient id="cyan-glow" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#00f2fe" stop-opacity="0.9"/>
          <stop offset="100%" stop-color="#4facfe" stop-opacity="0.4"/>
        </linearGradient>

        <linearGradient id="amber-glow" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#ff9f1c" stop-opacity="0.95"/>
          <stop offset="100%" stop-color="#ff3838" stop-opacity="0.5"/>
        </linearGradient>

        <linearGradient id="link-grad" x1="0%" y1="0%" x2="0%" y2="100%">
          <stop offset="0%" stop-color="#2a3348"/>
          <stop offset="50%" stop-color="#182030"/>
          <stop offset="100%" stop-color="#0f1522"/>
        </linearGradient>

        <linearGradient id="obs-grad" x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%" stop-color="rgba(255, 159, 28, 0.15)"/>
          <stop offset="100%" stop-color="rgba(255, 159, 28, 0.05)"/>
        </linearGradient>

        <filter id="laser-glow" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
          <feMerge>
            <feMergeNode in="coloredBlur"/>
            <feMergeNode in="SourceGraphic"/>
          </feMerge>
        </filter>
      </defs>

      <!-- 动态环境底板 (台阶地形 + 辅助坐标网) -->
      <g id="terrain-layer"></g>

      <!-- 推进波形参考引导线(投影轨迹与正弦波包络) -->
      <g id="wave-envelope-layer"></g>

      <!-- 蛇形机器人主体层 (14个关节链式装配) -->
      <g id="snake-body-layer"></g>

      <!-- 头部雷达测距与超声波扫描特效 -->
      <g id="sensor-layer"></g>

      <!-- 物理特征指示标 (关节转角弧线、推进力矢量动态箭头) -->
      <g id="hud-indicators-layer"></g>
    </svg>
  </div>

  <script>
    /**
     * 仿生蛇形机器人 Serpenoid 行波驱动仿真引擎
     * 架构:14 节正交铰接串联机构
     * 奇数节 (1, 3, 5, 7, 9, 11, 13): Yaw 水平轴舵机,输出正弦行波推进力
     * 偶数节 (2, 4, 6, 8, 10, 12): Pitch 垂直轴舵机,负责起伏俯仰与越障抬升
     */
    const N = 14;              // 节段数量
    const LINK_LEN = 48;       // 单节视觉等效中心距 (px)
    const RADIUS = 17;         // 节段外壳半径

    // 控制律默认参数
    const params = {
      A: 36 * Math.PI / 180,   // 波幅 (rad)
      freq: 1.3,               // 频率 f (Hz)
      k: 1.25,                 // 波数
      pitchBias: 0             // 抬头越障俯仰偏置角
    };

    let mode = 'auto'; // 'auto' | 'serpentine' | 'obstacle'
    let globalTime = 0;
    let autoCycleTime = 0;

    // 障碍物台阶物理参数 (空间坐标)
    const obstacle = {
      x: 580,
      width: 320,
      height: 70
    };

    // 2.5D Isometric 投影变换系统
    // 世界坐标 (X: 纵向前进, Y: 横向摆动, Z: 垂直抬高) -> 屏幕平面坐标 (u, v)
    function project(x, y, z) {
      const originX = 140;
      const originY = 460;
      const isoX = originX + x * 0.94 - y * 0.35;
      const isoY = originY + x * 0.22 + y * 0.62 - z * 1.1;
      return { x: isoX, y: isoY };
    }

    // 初始化环境台阶与辅助线
    function drawEnvironment() {
      const terrain = document.getElementById('terrain-layer');
      let html = '';

      // 地面基准基线
      const pStart = project(0, -60, 0);
      const pEnd = project(1050, -60, 0);
      const pStart2 = project(0, 60, 0);
      const pEnd2 = project(1050, 60, 0);

      // 地面导轨网格
      for (let x = 0; x <= 1000; x += 80) {
        let pA = project(x, -70, 0);
        let pB = project(x, 70, 0);
        html += `<line x1="${pA.x}" y1="${pA.y}" x2="${pB.x}" y2="${pB.y}" stroke="rgba(255,255,255,0.04)" stroke-dasharray="2,3" />`;
      }

      // 障碍物立体台阶渲染
      const oP1 = project(obstacle.x, -90, 0);
      const oP2 = project(obstacle.x + obstacle.width, -90, 0);
      const oP3 = project(obstacle.x + obstacle.width, 90, 0);
      const oP4 = project(obstacle.x, 90, 0);

      const oP1_top = project(obstacle.x, -90, obstacle.height);
      const oP2_top = project(obstacle.x + obstacle.width, -90, obstacle.height);
      const oP3_top = project(obstacle.x + obstacle.width, 90, obstacle.height);
      const oP4_top = project(obstacle.x, 90, obstacle.height);

      // 障碍物顶面
      html += `<polygon points="${oP1_top.x},${oP1_top.y} ${oP2_top.x},${oP2_top.y} ${oP3_top.x},${oP3_top.y} ${oP4_top.x},${oP4_top.y}" 
                fill="url(#obs-grad)" stroke="rgba(255, 159, 28, 0.45)" stroke-width="1.2" />`;

      // 障碍物迎撞立面 (前唇)
      html += `<polygon points="${oP1.x},${oP1.y} ${oP4.x},${oP4.y} ${oP4_top.x},${oP4_top.y} ${oP1_top.x},${oP1_top.y}" 
                fill="rgba(255, 159, 28, 0.12)" stroke="rgba(255, 159, 28, 0.6)" stroke-width="1.5" />`;

      // 障碍物侧立面
      html += `<polygon points="${oP4.x},${oP4.y} ${oP3.x},${oP3.y} ${oP3_top.x},${oP3_top.y} ${oP4_top.x},${oP4_top.y}" 
                fill="rgba(255, 159, 28, 0.06)" stroke="rgba(255, 159, 28, 0.25)" stroke-width="1" />`;

      // 障碍物标签(微型技术注解)
      const tagPos = project(obstacle.x + 30, 95, obstacle.height);
      html += `<text x="${tagPos.x}" y="${tagPos.y}" fill="#ff9f1c" font-size="10.5" font-family="monospace" letter-spacing="1">OBSTACLE_ELEVATION +70mm</text>`;

      terrain.innerHTML = html;
    }

    // 核心前向运动学计算
    function computeSnakeKinematics(t) {
      // 整体虚拟前进位移
      let speed = 72; // px / sec
      let progress = (t * speed) % 950;
      let headX = 220 + progress;

      // 自动循环时序状态机 (0 ~ 12秒一个大周期)
      let autoStage = 'serpentine';
      let obstacleTriggerDist = obstacle.x - headX;

      if (mode === 'serpentine') {
        params.pitchBias = 0;
        document.getElementById('phase-desc').innerText = '纯侧向蜿蜒步态 (仅启用奇数水平偏航舵机)';
      } else if (mode === 'obstacle') {
        params.pitchBias = 32 * Math.PI / 180;
        document.getElementById('phase-desc').innerText = '持续抬头越障步态 (偶数俯仰关节叠加正偏置)';
      } else {
        // 全自动演练模式
        if (obstacleTriggerDist < 120 && obstacleTriggerDist > -obstacle.width - 80) {
          // 接近并翻越障碍阶段
          let blend = Math.sin(Math.min(Math.max((120 - obstacleTriggerDist) / 120, 0), 1) * Math.PI * 0.5);
          params.pitchBias = (30 * Math.PI / 180) * blend;
          document.getElementById('phase-desc').innerHTML = '<span style="color:#ff9f1c">感知台阶边缘 · 偶数关节叠加俯仰偏置 bᵢ 昂首跨越</span>';
        } else {
          params.pitchBias = 0;
          document.getElementById('phase-desc').innerHTML = '<span style="color:#00f2fe">平地巡航 · 仅奇数节执行 Serpenoid 侧向行波</span>';
        }
      }

      // 计算14个骨骼节点的3D位置与姿态
      let joints = [];
      let currentX = headX;

      for (let i = 0; i < N; i++) {
        // Serpenoid 控制律公式计算各节关节角
        let phaseLag = (2 * Math.PI * params.k * i) / N;
        let wave = Math.sin(2 * Math.PI * params.freq * t - phaseLag);

        // 偏航角与俯仰角按奇偶轴彻底解耦
        let yaw = (i % 2 === 1) ? params.A * wave : 0;
        
        // 俯仰角:前5节在越障时执行向上翘起
        let pitch = 0;
        if (i % 2 === 0) {
          let headDecay = Math.max(0, 1 - i / 6.5); // 越靠前偏置越大
          pitch = params.pitchBias * headDecay;
        }

        // 横向摆幅计算
        let bodyY = Math.sin(2 * Math.PI * params.freq * t - phaseLag) * 44;
        
        // 高度计算 (台阶适应 + 抬头抬升)
        let bodyZ = 0;
        if (currentX > obstacle.x + 20) {
          bodyZ = obstacle.height; // 已登上台阶
        } else if (currentX > obstacle.x - 60) {
          // 爬坡过渡
          let ratio = (currentX - (obstacle.x - 60)) / 80;
          bodyZ = obstacle.height * Math.min(Math.max(ratio, 0), 1);
        }

        // 叠加上抬头俯仰带来的抬升
        bodyZ += Math.sin(pitch) * (LINK_LEN * 0.9);

        joints.push({
          index: i,
          worldX: currentX,
          worldY: bodyY,
          worldZ: Math.max(0, bodyZ),
          yaw: yaw,
          pitch: pitch,
          isYawJoint: i % 2 === 1,
          isPitchJoint: i % 2 === 0
        });

        currentX -= LINK_LEN; // 往尾部后延
      }

      return joints;
    }

    // 渲染蛇形机器人高精度机电构件与动态指示
    function renderSnake(joints, t) {
      const snakeLayer = document.getElementById('snake-body-layer');
      const waveLayer = document.getElementById('wave-envelope-layer');
      const sensorLayer = document.getElementById('sensor-layer');
      const hudLayer = document.getElementById('hud-indicators-layer');

      let bodyHtml = '';
      let waveHtml = '';
      let hudHtml = '';

      // 1. 绘制地面行波投影波形虚线(展示机械行波传播方向)
      let wavePath = '';
      for (let i = 0; i < joints.length; i++) {
        let p = project(joints[i].worldX, joints[i].worldY, 0);
        wavePath += (i === 0 ? 'M' : 'L') + `${p.x.toFixed(1)},${p.y.toFixed(1)} `;
      }
      waveHtml += `<path d="${wavePath}" fill="none" stroke="rgba(0, 242, 254, 0.28)" stroke-width="1.8" stroke-dasharray="4,4" />`;
      waveLayer.innerHTML = waveHtml;

      // 2. 绘制从尾到头的连杆节段(按深度从后向前渲染,保证正交遮挡关系真实)
      for (let i = joints.length - 1; i >= 0; i--) {
        const j = joints[i];
        const screenPos = project(j.worldX, j.worldY, j.worldZ);

        // 如果不是最后一节,绘制连接到前一节的正交U形金属铰接支架
        if (i < joints.length - 1) {
          const next = joints[i + 1];
          const nextPos = project(next.worldX, next.worldY, next.worldZ);

          // 骨干刚性联轴器
          bodyHtml += `
            <line x1="${screenPos.x}" y1="${screenPos.y}" x2="${nextPos.x}" y2="${nextPos.y}" 
                  stroke="rgba(255,255,255,0.18)" stroke-width="9" stroke-linecap="round"/>
            <line x1="${screenPos.x}" y1="${screenPos.y}" x2="${nextPos.x}" y2="${nextPos.y}" 
                  stroke="#111827" stroke-width="6" stroke-linecap="round"/>
          `;
        }

        // 3D 节段外壳 (钛金圆柱段壳体)
        const isOdd = j.isYawJoint;
        const mainColor = isOdd ? 'var(--cyan)' : 'var(--amber)';
        const shellRadius = (i === 0) ? RADIUS + 2.5 : RADIUS; // 头部略大

        // 节段外壳轮廓
        bodyHtml += `
          <g transform="translate(${screenPos.x}, ${screenPos.y})">
            <!-- 底部被动滚轮构件 -->
            <ellipse cx="-4" cy="11" rx="4.5" ry="2" fill="#0f172a" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>
            <ellipse cx="4" cy="11" rx="4.5" ry="2" fill="#0f172a" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>

            <!-- 节段主体外壳 -->
            <circle cx="0" cy="0" r="${shellRadius}" fill="url(#link-grad)" stroke="${isOdd ? 'rgba(0, 242, 254, 0.4)' : 'rgba(255, 159, 28, 0.4)'}" stroke-width="1.8"/>

            <!-- 正交铰链状态提示环 (奇数节为水平轴承,偶数节为垂直耳座) -->
            ${isOdd 
              ? `<line x1="-8" y1="0" x2="8" y2="0" stroke="var(--cyan)" stroke-width="2.5" stroke-linecap="round" filter="url(#laser-glow)"/>`
              : `<line x1="0" y1="-8" x2="0" y2="8" stroke="var(--amber)" stroke-width="2.5" stroke-linecap="round" filter="url(#laser-glow)"/>`
            }

            <!-- 关节序号微标 -->
            <text x="0" y="3.5" font-size="8" font-family="monospace" fill="${isOdd ? '#00f2fe' : '#ff9f1c'}" text-anchor="middle" font-weight="bold">${i+1}</text>
          </g>
        `;

        // 挑选代表性关节,显示其实时角度仪表与动态应力弧
        if (i === 2 || i === 3) {
          const angleVal = (isOdd ? j.yaw : j.pitch) * (180 / Math.PI);
          const arcColor = isOdd ? 'var(--cyan)' : 'var(--amber)';
          const labelText = isOdd ? `水平偏航 J${i+1} : ${angleVal.toFixed(1)}°` : `垂直俯仰 J${i+1} : ${angleVal.toFixed(1)}°`;

          hudHtml += `
            <g transform="translate(${screenPos.x}, ${screenPos.y - 28})">
              <rect x="-42" y="-14" width="84" height="15" rx="3" fill="rgba(10, 16, 28, 0.85)" stroke="${arcColor}" stroke-width="0.8"/>
              <text x="0" y="-3.5" font-size="8.5" font-family="monospace" fill="${arcColor}" text-anchor="middle">${labelText}</text>
              <line x1="0" y1="1" x2="0" y2="10" stroke="${arcColor}" stroke-width="0.7" stroke-dasharray="2,2"/>
            </g>
          `;
        }
      }

      snakeLayer.innerHTML = bodyHtml;
      hudLayer.innerHTML = hudHtml;

      // 3. 头部超声与 IMU 传感器扫描光束特效
      const head = joints[0];
      const headPos = project(head.worldX, head.worldY, head.worldZ);
      const scanEndA = project(head.worldX + 90, head.worldY - 45, head.worldZ);
      const scanEndB = project(head.worldX + 90, head.worldY + 45, head.worldZ);

      sensorLayer.innerHTML = `
        <polygon points="${headPos.x},${headPos.y} ${scanEndA.x},${scanEndA.y} ${scanEndB.x},${scanEndB.y}" 
                 fill="rgba(0, 242, 254, 0.08)" stroke="rgba(0, 242, 254, 0.3)" stroke-width="1" />
        <line x1="${headPos.x}" y1="${headPos.y}" x2="${(scanEndA.x + scanEndB.x) / 2}" y2="${(scanEndA.y + scanEndB.y) / 2}" 
              stroke="#00f2fe" stroke-width="1.2" stroke-dasharray="3,3" opacity="0.8"/>
        <circle cx="${headPos.x}" cy="${headPos.y}" r="3.5" fill="#00f2fe" filter="url(#laser-glow)"/>
      `;
    }

    // 模式切换控制器
    function setMode(newMode) {
      mode = newMode;
      document.querySelectorAll('.btn-chip').forEach(btn => btn.classList.remove('active'));
      if (newMode === 'auto') document.getElementById('btn-auto').classList.add('active');
      if (newMode === 'serpentine') document.getElementById('btn-serpentine').classList.add('active');
      if (newMode === 'obstacle') document.getElementById('btn-obstacle').classList.add('active');
    }

    // 主动画循环 (保证 60 FPS 丝滑自启动)
    let lastTimestamp = 0;
    function animLoop(timestamp) {
      if (!lastTimestamp) lastTimestamp = timestamp;
      const dt = (timestamp - lastTimestamp) / 1000;
      lastTimestamp = timestamp;

      globalTime += dt;
      autoCycleTime = (globalTime % 12);

      // 循环时间进度条更新
      const progPercent = (autoCycleTime / 12) * 100;
      document.getElementById('time-bar').style.width = progPercent + '%';

      // 实时动力学求解与图形装配
      const joints = computeSnakeKinematics(globalTime);
      renderSnake(joints, globalTime);

      requestAnimationFrame(animLoop);
    }

    // 页面加载完成即刻无缝自启
    window.addEventListener('DOMContentLoaded', () => {
      drawEnvironment();
      requestAnimationFrame(animLoop);
    });

    // 窗口尺寸自适应重绘底板
    window.addEventListener('resize', () => {
      drawEnvironment();
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>正交铰接仿生蛇形机器人 - Serpenoid 行波驱动与越障原理</title>
  <style>
    :root {
      --bg-color: #080c14;
      --card-bg: rgba(13, 20, 35, 0.82);
      --border-color: rgba(0, 242, 254, 0.2);
      --cyan: #00f2fe;
      --cyan-glow: rgba(0, 242, 254, 0.35);
      --amber: #ff9f1c;
      --amber-glow: rgba(255, 159, 28, 0.35);
      --text-main: #f1f5f9;
      --text-sub: #94a3b8;
      --accent-green: #10b981;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
    }

    body {
      background-color: var(--bg-color);
      color: var(--text-main);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", monospace;
      width: 100vw;
      height: 100vh;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      position: relative;
    }

    /* 工程背景网格 */
    .bg-canvas {
      position: absolute;
      inset: 0;
      background-image: 
        radial-gradient(circle at 50% 45%, rgba(14, 42, 71, 0.3) 0%, transparent 70%),
        linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
      background-size: 100% 100%, 48px 48px, 48px 48px;
      pointer-events: none;
      z-index: 0;
    }

    /* 居中主视口容器 */
    #viewport-container {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1;
    }

    svg#serpentine-svg {
      width: 100%;
      height: 100%;
      max-width: 1400px;
      max-height: 850px;
      filter: drop-shadow(0 15px 35px rgba(0, 0, 0, 0.7));
    }

    /* 顶部精简抬头卡片 (贴边、不遮挡主中央) */
    .header-panel {
      position: absolute;
      top: 16px;
      left: 20px;
      z-index: 10;
      background: var(--card-bg);
      backdrop-filter: blur(14px);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      padding: 10px 16px;
      pointer-events: auto;
    }

    .header-title {
      font-size: 13.5px;
      font-weight: 700;
      letter-spacing: 0.5px;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .indicator-led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--cyan);
      box-shadow: 0 0 10px var(--cyan);
      animation: ledPulse 2s infinite ease-in-out;
    }

    .header-desc {
      font-size: 11px;
      color: var(--text-sub);
      margin-top: 3px;
      font-family: monospace;
    }

    /* 右上角控制律与解耦仪表 */
    .telemetry-panel {
      position: absolute;
      top: 16px;
      right: 20px;
      z-index: 10;
      background: var(--card-bg);
      backdrop-filter: blur(14px);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      padding: 10px 14px;
      display: flex;
      flex-direction: column;
      gap: 6px;
      min-width: 290px;
    }

    .formula-box {
      font-family: "Courier New", Courier, monospace;
      color: var(--cyan);
      font-size: 11.5px;
      font-weight: bold;
      background: rgba(0, 242, 254, 0.08);
      padding: 4px 8px;
      border-radius: 4px;
      border-left: 2px solid var(--cyan);
    }

    .dof-tags {
      display: flex;
      gap: 6px;
    }

    .dof-chip {
      font-size: 10px;
      padding: 2px 7px;
      border-radius: 3px;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      background: rgba(0, 0, 0, 0.4);
    }
    .dof-chip.yaw { border: 1px solid var(--cyan); color: var(--cyan); }
    .dof-chip.pitch { border: 1px solid var(--amber); color: var(--amber); }

    /* 左下角时序动作指示 */
    .sequence-monitor {
      position: absolute;
      bottom: 24px;
      left: 20px;
      z-index: 10;
      font-size: 11px;
      display: flex;
      flex-direction: column;
      gap: 4px;
      background: var(--card-bg);
      backdrop-filter: blur(12px);
      padding: 8px 14px;
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.08);
    }
    .sequence-label {
      color: var(--text-sub);
    }
    .sequence-text {
      color: #fff;
      font-weight: 600;
    }

    /* 底部交互控制条 (微型圆角胶囊) */
    .control-deck {
      position: absolute;
      bottom: 22px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 10;
      background: var(--card-bg);
      backdrop-filter: blur(16px);
      border: 1px solid var(--border-color);
      border-radius: 32px;
      padding: 6px 16px;
      display: flex;
      align-items: center;
      gap: 12px;
      box-shadow: 0 10px 24px rgba(0, 0, 0, 0.6);
    }

    .tab-btn {
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid rgba(255, 255, 255, 0.12);
      color: var(--text-sub);
      font-size: 11px;
      padding: 5px 12px;
      border-radius: 16px;
      cursor: pointer;
      transition: all 0.2s ease;
      display: flex;
      align-items: center;
      gap: 5px;
    }

    .tab-btn:hover {
      color: #fff;
      background: rgba(0, 242, 254, 0.12);
      border-color: var(--cyan);
    }

    .tab-btn.active {
      color: #fff;
      background: linear-gradient(135deg, rgba(0, 242, 254, 0.25), rgba(37, 117, 252, 0.3));
      border-color: var(--cyan);
      box-shadow: 0 0 10px rgba(0, 242, 254, 0.3);
    }

    /* 交互滑块微调 */
    .slider-wrap {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 10.5px;
      color: var(--text-sub);
      border-left: 1px solid rgba(255, 255, 255, 0.1);
      padding-left: 10px;
    }

    .slider-wrap input[type=range] {
      width: 75px;
      accent-color: var(--cyan);
      cursor: pointer;
    }

    /* 周期时序时间线进度条 */
    .timeline-bar {
      position: absolute;
      bottom: 0;
      left: 0;
      height: 2px;
      width: 0%;
      background: linear-gradient(90deg, var(--cyan), var(--amber));
      z-index: 20;
    }

    @keyframes ledPulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50% { opacity: 0.35; transform: scale(0.85); }
    }
  </style>
</head>
<body>
  <div class="bg-canvas"></div>

  <!-- 顶部系统抬头 -->
  <header class="header-panel">
    <div class="header-title">
      <div class="indicator-led"></div>
      <span>正交铰接仿生蛇形机器人 · 行波控制与越障仿真</span>
    </div>
    <div class="header-desc">14节正交U形铰接 / 20kg·cm 数字舵机 / 底部双被动滚轮推进 / 头部超声感知</div>
  </header>

  <!-- 右上角控制律数学表达式与状态仪表 -->
  <aside class="telemetry-panel">
    <div class="formula-box">θᵢ(t) = A·sin(2πft − 2πki/N) + bᵢ</div>
    <div class="dof-tags">
      <div class="dof-chip yaw">奇数关节 · 偏航 (Yaw) 水平行波</div>
      <div class="dof-chip pitch">偶数关节 · 俯仰 (Pitch) 升降偏置</div>
    </div>
  </aside>

  <!-- 左下角时序阶段监视器 -->
  <div class="sequence-monitor">
    <span class="sequence-label">协同控制动作阶段:</span>
    <span class="sequence-text" id="status-phase">自适应巡航与越障演练</span>
  </div>

  <!-- 底部微型交互控制坞 -->
  <nav class="control-deck">
    <button class="tab-btn active" id="btn-auto" onclick="setControlMode('auto')">⚡ 完整动作时序演练</button>
    <button class="tab-btn" id="btn-serp" onclick="setControlMode('serpentine')">≈ 侧向蜿蜒行波</button>
    <button class="tab-btn" id="btn-cross" onclick="setControlMode('obstacle')">⤴ 抬升偏置翻越</button>
    
    <div class="slider-wrap">
      <span>波幅 A:</span>
      <input type="range" id="param-amp" min="20" max="50" value="40" oninput="updateSliders()">
      <span id="amp-val" style="width:24px; font-family:monospace; color:#fff;">40°</span>
    </div>
    <div class="slider-wrap">
      <span>频率 f:</span>
      <input type="range" id="param-freq" min="8" max="25" value="15" oninput="updateSliders()">
      <span id="freq-val" style="width:28px; font-family:monospace; color:#fff;">1.5Hz</span>
    </div>
  </nav>

  <div class="timeline-bar" id="timeline-progress"></div>

  <!-- 主画布视口 (SVG 2.5D Isometric 投影) -->
  <main id="viewport-container">
    <svg id="serpentine-svg" viewBox="0 0 1300 760" preserveAspectRatio="xMidYMid meet">
      <defs>
        <!-- 材质光晕与发光滤镜 -->
        <filter id="glow-effect" x="-40%" y="-40%" width="180%" height="180%">
          <feGaussianBlur stdDeviation="3.2" result="blur" />
          <feMerge>
            <feMergeNode in="blur" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>

        <!-- 节段钛灰外壳渐变 -->
        <linearGradient id="link-grad" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#334155" />
          <stop offset="45%" stop-color="#1e293b" />
          <stop offset="100%" stop-color="#0f172a" />
        </linearGradient>

        <!-- 台阶渐变填充 -->
        <linearGradient id="step-grad" x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%" stop-color="rgba(255, 159, 28, 0.16)" />
          <stop offset="100%" stop-color="rgba(255, 159, 28, 0.04)" />
        </linearGradient>
      </defs>

      <!-- 环境层 (地面导轨与三维台阶障碍物) -->
      <g id="env-layer"></g>

      <!-- 行波物理包络投影虚线 -->
      <g id="envelope-layer"></g>

      <!-- 蛇形机器人 14 节机械骨骼装配链 -->
      <g id="snake-layer"></g>

      <!-- 头部感知雷达与超声波锥形波束 -->
      <g id="sensor-layer"></g>

      <!-- 动态数据微标与关节弧度动态标注 -->
      <g id="overlay-layer"></g>
    </svg>
  </main>

  <script>
    /**
     * 仿生蛇形机器人 Serpenoid 行波驱动仿真物理引擎
     * 遵循正交铰接构型:
     * 奇数节 (1, 3, 5, 7, 9, 11, 13) 为 Yaw 关节 (偏航/水平行波)
     * 偶数节 (2, 4, 6, 8, 10, 12, 14) 为 Pitch 关节 (俯仰/垂直抬升)
     */
    const N = 14;              // 节段数量:14 节
    const LINK_LENGTH = 46;    // 单节等效中心距 (px)
    const BODY_RADIUS = 16.5;  // 节段半径 (外径 60mm 比例缩小)
    
    // 控制参数
    let config = {
      amp: 40 * (Math.PI / 180),  // 波幅 A (rad)
      freq: 1.5,                  // 频率 f (Hz)
      k: 1.2,                     // 波数 k = 1.2
      pitchBias: 0                // 垂直关节偏置 bi
    };

    let controlMode = 'auto'; // 'auto' | 'serpentine' | 'obstacle'
    let animTime = 0;
    const CYCLE_DURATION = 13.0; // 全时序自动循环周期:13秒

    // 障碍物几何定义 (世界坐标: x, y, z)
    const obstacleGeom = {
      startX: 620,
      width: 320,
      height: 72
    };

    // 2.5D Isometric 等轴测投影变换公式
    function project3Dto2D(x, y, z) {
      const originX = 170;
      const originY = 480;
      const u = originX + x * 0.92 - y * 0.36;
      const v = originY + x * 0.22 + y * 0.58 - z * 1.08;
      return { x: u, y: v };
    }

    // 绘制环境台阶与地面基准网格
    function renderEnvironment() {
      const env = document.getElementById('env-layer');
      let markup = '';

      // 地面导向虚线
      for (let x = 0; x <= 1100; x += 85) {
        let pA = project3Dto2D(x, -65, 0);
        let pB = project3Dto2D(x, 65, 0);
        markup += `<line x1="${pA.x}" y1="${pA.y}" x2="${pB.x}" y2="${pB.y}" stroke="rgba(255,255,255,0.035)" stroke-dasharray="2,3"/>`;
      }

      // 障碍物立体多面体绘制
      const p1 = project3Dto2D(obstacleGeom.startX, -90, 0);
      const p2 = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, -90, 0);
      const p3 = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, 90, 0);
      const p4 = project3Dto2D(obstacleGeom.startX, 90, 0);

      const p1Top = project3Dto2D(obstacleGeom.startX, -90, obstacleGeom.height);
      const p2Top = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, -90, obstacleGeom.height);
      const p3Top = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, 90, obstacleGeom.height);
      const p4Top = project3Dto2D(obstacleGeom.startX, 90, obstacleGeom.height);

      // 顶面
      markup += `<polygon points="${p1Top.x},${p1Top.y} ${p2Top.x},${p2Top.y} ${p3Top.x},${p3Top.y} ${p4Top.x},${p4Top.y}" 
                  fill="url(#step-grad)" stroke="rgba(255, 159, 28, 0.45)" stroke-width="1.2" />`;
      // 前唇台阶立面 (迎面冲撞受阻面)
      markup += `<polygon points="${p1.x},${p1.y} ${p4.x},${p4.y} ${p4Top.x},${p4Top.y} ${p1Top.x},${p1Top.y}" 
                  fill="rgba(255, 159, 28, 0.12)" stroke="rgba(255, 159, 28, 0.65)" stroke-width="1.5" />`;
      // 侧立面
      markup += `<polygon points="${p4.x},${p4.y} ${p3.x},${p3.y} ${p3Top.x},${p3Top.y} ${p4Top.x},${p4Top.y}" 
                  fill="rgba(255, 159, 28, 0.05)" stroke="rgba(255, 159, 28, 0.25)" stroke-width="1" />`;

      // 障碍物高程标识文本
      const labelPos = project3Dto2D(obstacleGeom.startX + 20, 96, obstacleGeom.height);
      markup += `<text x="${labelPos.x}" y="${labelPos.y}" fill="#ff9f1c" font-size="10" font-family="monospace" letter-spacing="0.5">台阶障碍 +70mm</text>`;

      env.innerHTML = markup;
    }

    // 求解 14 节机构正向动力学与时序姿态
    function computeKinematics(t) {
      let speed = 76; // 前进位移速率 px/s
      let progress = (t * speed) % 1020;
      let headX = 220 + progress;

      let cycleTime = t % CYCLE_DURATION;
      let statusText = '';
      let effectiveAmp = config.amp;

      // 动作时序与协同过程状态机
      if (controlMode === 'auto') {
        if (cycleTime < 1.4) {
          // 阶段1:上电自检归零
          statusText = '<span style="color:#94a3b8">【时序 1/5】系统上电自检 · 各舵机复位归零中...</span>';
          effectiveAmp = config.amp * (cycleTime / 1.4);
          config.pitchBias = 0;
        } else if (headX < obstacleGeom.startX - 90) {
          // 阶段2:常规侧向蜿蜒巡航步态
          statusText = '<span style="color:#00f2fe">【时序 2/5】侧向蜿蜒巡航 · 仅奇数节执行正弦行波推进</span>';
          config.pitchBias = 0;
        } else if (headX >= obstacleGeom.startX - 90 && headX < obstacleGeom.startX + 30) {
          // 阶段3:头部超声测距发现障碍 -> 偶数节叠加偏置抬高前 4 节
          let liftProgress = (headX - (obstacleGeom.startX - 90)) / 120;
          liftProgress = Math.min(Math.max(liftProgress, 0), 1);
          config.pitchBias = (32 * Math.PI / 180) * Math.sin(liftProgress * Math.PI * 0.5);
          statusText = '<span style="color:#ff9f1c">【时序 3/5】超声测距发现障碍 · 垂直轴叠加抬升偏置 bᵢ 昂首探路</span>';
        } else if (headX >= obstacleGeom.startX + 30 && headX < obstacleGeom.startX + 180) {
          // 阶段4:前端搭上障碍边缘,波幅临时加大 20% 拉拽翻越
          effectiveAmp = config.amp * 1.2;
          config.pitchBias = 16 * Math.PI / 180;
          statusText = '<span style="color:#10b981">【时序 4/5】前身搭上台阶 · 波幅临时加大 20% 爆发拉拽越障</span>';
        } else {
          // 阶段5:全躯干登上高台,偏置恢复,转入高台正常巡航
          statusText = '<span style="color:#00f2fe">【时序 5/5】翻越成功 · 偏置归零恢复标准平地 Serpenoid 步态</span>';
          config.pitchBias = 0;
        }
      } else if (controlMode === 'serpentine') {
        config.pitchBias = 0;
        statusText = '<span style="color:#00f2fe">模式锁定:纯平面侧向蜿蜒 (仅奇数水平关节摆动)</span>';
      } else if (controlMode === 'obstacle') {
        config.pitchBias = 32 * Math.PI / 180;
        effectiveAmp = config.amp * 1.15;
        statusText = '<span style="color:#ff9f1c">模式锁定:持续昂首越障步态 (偶数俯仰关节维持大偏置)</span>';
      }

      document.getElementById('status-phase').innerHTML = statusText;

      // 求解 14 节骨骼的世界坐标及局部铰接角
      let joints = [];
      let curX = headX;

      for (let i = 0; i < N; i++) {
        let phaseLag = (2 * Math.PI * config.k * i) / N;
        let wave = Math.sin(2 * Math.PI * config.freq * t - phaseLag);

        // 正交解耦核心逻辑
        let isYawJoint = (i % 2 === 1);    // 奇数关节掌控航向 (Yaw)
        let isPitchJoint = (i % 2 === 0);  // 偶数关节掌控俯仰 (Pitch)

        let yawAngle = isYawJoint ? effectiveAmp * wave : 0;
        
        // 抬头偏置主要由前 4~5 节承担
        let pitchDecay = Math.max(0, 1 - (i / 4.5));
        let pitchAngle = isPitchJoint ? (config.pitchBias * pitchDecay) : 0;

        // 计算横向 Y 坐标 (蜿蜒起伏)
        let worldY = Math.sin(2 * Math.PI * config.freq * t - phaseLag) * 45;

        // 计算垂直 Z 坐标 (台阶高度差 + 自身俯仰翘起)
        let worldZ = 0;
        if (curX > obstacleGeom.startX + 20) {
          worldZ = obstacleGeom.height;
        } else if (curX > obstacleGeom.startX - 50) {
          let ratio = (curX - (obstacleGeom.startX - 50)) / 70;
          worldZ = obstacleGeom.height * Math.min(Math.max(ratio, 0), 1);
        }

        // 加上主动抬头俯仰高度
        worldZ += Math.sin(pitchAngle) * (LINK_LENGTH * 1.05);

        joints.push({
          index: i,
          worldX: curX,
          worldY: worldY,
          worldZ: Math.max(0, worldZ),
          yaw: yawAngle,
          pitch: pitchAngle,
          isYaw: isYawJoint,
          isPitch: isPitchJoint
        });

        curX -= LINK_LENGTH;
      }

      return joints;
    }

    // 渲染蛇身装配构件与机械特征
    function renderScene(joints, t) {
      const snakeLayer = document.getElementById('snake-layer');
      const envLayer = document.getElementById('envelope-layer');
      const sensorLayer = document.getElementById('sensor-layer');
      const overlayLayer = document.getElementById('overlay-layer');

      let snakeSvg = '';
      let envSvg = '';
      let overlaySvg = '';

      // 1. 地面正弦行波包络轨迹绘制
      let pathD = '';
      for (let i = 0; i < joints.length; i++) {
        let pt = project3Dto2D(joints[i].worldX, joints[i].worldY, 0);
        pathD += (i === 0 ? 'M' : 'L') + `${pt.x.toFixed(1)},${pt.y.toFixed(1)} `;
      }
      envSvg += `<path d="${pathD}" fill="none" stroke="rgba(0, 242, 254, 0.22)" stroke-width="1.8" stroke-dasharray="3,4" />`;
      envLayer.innerHTML = envSvg;

      // 2. 机械节段自尾向头绘制 (保持空间深度遮挡真实)
      for (let i = joints.length - 1; i >= 0; i--) {
        const curr = joints[i];
        const screenCurr = project3Dto2D(curr.worldX, curr.worldY, curr.worldZ);

        // 绘制正交U形铰接连接臂
        if (i < joints.length - 1) {
          const next = joints[i + 1];
          const screenNext = project3Dto2D(next.worldX, next.worldY, next.worldZ);

          // 铝合金刚性连杆骨架
          snakeSvg += `
            <line x1="${screenCurr.x}" y1="${screenCurr.y}" x2="${screenNext.x}" y2="${screenNext.y}" 
                  stroke="rgba(255,255,255,0.15)" stroke-width="9" stroke-linecap="round"/>
            <line x1="${screenCurr.x}" y1="${screenCurr.y}" x2="${screenNext.x}" y2="${screenNext.y}" 
                  stroke="#0f172a" stroke-width="5" stroke-linecap="round"/>
          `;
        }

        const isHead = (i === 0);
        const radius = isHead ? BODY_RADIUS + 3 : BODY_RADIUS;
        const mainColor = curr.isYaw ? 'var(--cyan)' : 'var(--amber)';

        snakeSvg += `
          <g transform="translate(${screenCurr.x}, ${screenCurr.y})">
            <!-- 节段底部被动减阻小滚轮 (双轮) -->
            <ellipse cx="-4" cy="10.5" rx="4" ry="1.8" fill="#090d16" stroke="rgba(255,255,255,0.25)" stroke-width="0.8"/>
            <ellipse cx="4" cy="10.5" rx="4" ry="1.8" fill="#090d16" stroke="rgba(255,255,255,0.25)" stroke-width="0.8"/>

            <!-- 节段 3D 打印主体圆柱外壳 -->
            <circle cx="0" cy="0" r="${radius}" fill="url(#link-grad)" 
                    stroke="${curr.isYaw ? 'rgba(0, 242, 254, 0.5)' : 'rgba(255, 159, 28, 0.5)'}" stroke-width="1.8"/>

            <!-- 舵机输出轴与正交方向键槽 (奇数水平轴,偶数垂直轴) -->
            ${curr.isYaw 
              ? `<line x1="-7.5" y1="0" x2="7.5" y2="0" stroke="var(--cyan)" stroke-width="2.6" stroke-linecap="round" filter="url(#glow-effect)"/>`
              : `<line x1="0" y1="-7.5" x2="0" y2="7.5" stroke="var(--amber)" stroke-width="2.6" stroke-linecap="round" filter="url(#glow-effect)"/>`
            }

            <!-- 节段序号标注 -->
            <text x="0" y="3" font-size="8.5" font-family="monospace" fill="${mainColor}" text-anchor="middle" font-weight="bold">${i+1}</text>
          </g>
        `;

        // 挑选第 2 节 (Pitch 俯仰) 与第 3 节 (Yaw 偏航) 显示微型动态浮动数据标
        if (i === 1 || i === 2) {
          const angleDeg = (curr.isYaw ? curr.yaw : curr.pitch) * (180 / Math.PI);
          const axisName = curr.isYaw ? `J${i+1} 水平偏航` : `J${i+1} 垂直俯仰`;
          const badgeColor = curr.isYaw ? 'var(--cyan)' : 'var(--amber)';

          overlaySvg += `
            <g transform="translate(${screenCurr.x}, ${screenCurr.y - 28})">
              <rect x="-42" y="-12" width="84" height="15" rx="3" fill="rgba(8, 12, 20, 0.88)" stroke="${badgeColor}" stroke-width="0.8"/>
              <text x="0" y="-1" font-size="8" font-family="monospace" fill="${badgeColor}" text-anchor="middle">${axisName} ${angleDeg.toFixed(1)}°</text>
              <line x1="0" y1="3" x2="0" y2="10" stroke="${badgeColor}" stroke-width="0.7" stroke-dasharray="2,2"/>
            </g>
          `;
        }
      }

      snakeLayer.innerHTML = snakeSvg;
      overlayLayer.innerHTML = overlaySvg;

      // 3. 头部超声测距雷达波纹与 IMU 视向
      const head = joints[0];
      const headPt = project3Dto2D(head.worldX, head.worldY, head.worldZ);
      const coneA = project3Dto2D(head.worldX + 95, head.worldY - 42, head.worldZ);
      const coneB = project3Dto2D(head.worldX + 95, head.worldY + 42, head.worldZ);

      sensorLayer.innerHTML = `
        <polygon points="${headPt.x},${headPt.y} ${coneA.x},${coneA.y} ${coneB.x},${coneB.y}" 
                 fill="rgba(0, 242, 254, 0.08)" stroke="rgba(0, 242, 254, 0.28)" stroke-width="1"/>
        <line x1="${headPt.x}" y1="${headPt.y}" x2="${(coneA.x + coneB.x) / 2}" y2="${(coneA.y + coneB.y) / 2}" 
              stroke="var(--cyan)" stroke-width="1.2" stroke-dasharray="3,3" opacity="0.85"/>
        <circle cx="${headPt.x}" cy="${headPt.y}" r="3.5" fill="var(--cyan)" filter="url(#glow-effect)"/>
      `;
    }

    // 控制模式切换
    function setControlMode(m) {
      controlMode = m;
      document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
      if (m === 'auto') document.getElementById('btn-auto').classList.add('active');
      if (m === 'serpentine') document.getElementById('btn-serp').classList.add('active');
      if (m === 'obstacle') document.getElementById('btn-cross').classList.add('active');
    }

    // 交互滑块更新
    function updateSliders() {
      const ampSlider = document.getElementById('param-amp');
      const freqSlider = document.getElementById('param-freq');

      config.amp = parseFloat(ampSlider.value) * (Math.PI / 180);
      config.freq = parseFloat(freqSlider.value) / 10;

      document.getElementById('amp-val').innerText = ampSlider.value + '°';
      document.getElementById('freq-val').innerText = (parseFloat(freqSlider.value) / 10).toFixed(1) + 'Hz';
    }

    // 主渲染物理引擎循环 (帧率 60FPS 自动启动且连续播放)
    let prevStamp = 0;
    function animationLoop(timestamp) {
      if (!prevStamp) prevStamp = timestamp;
      const deltaTime = (timestamp - prevStamp) / 1000;
      prevStamp = timestamp;

      animTime += deltaTime;

      // 周期时间进度条
      const cyclePercent = ((animTime % CYCLE_DURATION) / CYCLE_DURATION) * 100;
      document.getElementById('timeline-progress').style.width = cyclePercent + '%';

      // 运动学求解与画面更新
      const joints = computeKinematics(animTime);
      renderScene(joints, animTime);

      requestAnimationFrame(animationLoop);
    }

    // 网页加载后自启动(不依赖点击)
    window.addEventListener('DOMContentLoaded', () => {
      renderEnvironment();
      requestAnimationFrame(animationLoop);
    });

    // 窗口尺寸自适应
    window.addEventListener('resize', () => {
      renderEnvironment();
    });
  </script>
</body>
</html>
积分规则:每轮对话扣10分