分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>仿生蛇形机器人 - 各向异性摩擦行波推进原理</title>
  <style>
    :root {
      --bg-dark: #060911;
      --panel-bg: rgba(10, 16, 30, 0.75);
      --panel-border: rgba(0, 240, 255, 0.2);
      --accent-cyan: #00f0ff;
      --accent-amber: #ffaa00;
      --accent-magenta: #ff2a6d;
      --accent-green: #05ffa1;
      --text-dim: #7e8c9f;
      --text-light: #d8e6f3;
    }

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

    body {
      background-color: var(--bg-dark);
      color: var(--text-light);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
      overflow: hidden;
      width: 100vw;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }

    /* 主画布容器:占满屏幕核心视口 */
    .viewport {
      position: relative;
      flex: 1;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: radial-gradient(circle at 50% 50%, #0d1527 0%, #050811 100%);
    }

    canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: block;
    }

    /* 顶部超紧凑 HUD 标题条 */
    .hud-header {
      position: absolute;
      top: 14px;
      left: 18px;
      z-index: 10;
      display: flex;
      align-items: center;
      gap: 12px;
      pointer-events: none;
    }

    .badge-tag {
      font-size: 10px;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
      padding: 2px 7px;
      border-radius: 3px;
      background: rgba(0, 240, 255, 0.12);
      border: 1px solid var(--accent-cyan);
      color: var(--accent-cyan);
      letter-spacing: 0.5px;
    }

    .hud-title {
      font-size: 13px;
      font-weight: 600;
      color: #ffffff;
      letter-spacing: 0.8px;
      text-shadow: 0 0 10px rgba(0, 240, 255, 0.4);
    }

    .hud-subtitle {
      font-size: 11px;
      color: var(--text-dim);
      font-family: ui-monospace, monospace;
    }

    /* 右上角实时矢量图例 (极精简角落浮层,绝不遮挡中央) */
    .hud-legend {
      position: absolute;
      top: 14px;
      right: 18px;
      z-index: 10;
      background: var(--panel-bg);
      border: 1px solid var(--panel-border);
      backdrop-filter: blur(8px);
      border-radius: 6px;
      padding: 8px 12px;
      display: flex;
      flex-direction: column;
      gap: 6px;
      font-size: 11px;
      pointer-events: none;
    }

    .legend-item {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .legend-line {
      width: 16px;
      height: 2px;
      border-radius: 1px;
      position: relative;
    }

    .legend-line::after {
      content: '';
      position: absolute;
      right: -2px;
      top: -3px;
      border: 4px solid transparent;
    }

    .legend-thrust { background: var(--accent-green); }
    .legend-thrust::after { border-left-color: var(--accent-green); }
    .legend-friction { background: var(--accent-magenta); }
    .legend-friction::after { border-left-color: var(--accent-magenta); }
    .legend-heading { background: var(--accent-cyan); }
    .legend-heading::after { border-left-color: var(--accent-cyan); }

    /* 左下角极简单节受力机理透视视窗 */
    .hud-inset {
      position: absolute;
      bottom: 18px;
      left: 18px;
      z-index: 10;
      width: 220px;
      background: var(--panel-bg);
      border: 1px solid var(--panel-border);
      backdrop-filter: blur(8px);
      border-radius: 8px;
      padding: 10px;
      pointer-events: none;
    }

    .inset-title {
      font-size: 11px;
      font-weight: 600;
      color: var(--accent-cyan);
      margin-bottom: 6px;
      display: flex;
      justify-content: space-between;
    }

    .inset-desc {
      font-size: 10px;
      color: var(--text-dim);
      line-height: 1.4;
      margin-top: 6px;
    }

    /* 右下角参数调节微型面板 */
    .hud-controls {
      position: absolute;
      bottom: 18px;
      right: 18px;
      z-index: 10;
      background: var(--panel-bg);
      border: 1px solid var(--panel-border);
      backdrop-filter: blur(8px);
      border-radius: 8px;
      padding: 10px 14px;
      display: flex;
      flex-direction: column;
      gap: 8px;
      width: 230px;
    }

    .control-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 11px;
      color: var(--text-light);
    }

    .control-row label {
      color: var(--text-dim);
      font-size: 11px;
    }

    .control-row input[type="range"] {
      width: 100px;
      height: 4px;
      accent-color: var(--accent-cyan);
      background: rgba(255, 255, 255, 0.1);
      border-radius: 2px;
      outline: none;
      cursor: pointer;
    }

    .control-val {
      font-family: ui-monospace, monospace;
      font-size: 10px;
      color: var(--accent-cyan);
      width: 32px;
      text-align: right;
    }

    .btn-group {
      display: flex;
      gap: 6px;
      margin-top: 2px;
    }

    .btn-action {
      flex: 1;
      padding: 4px 6px;
      font-size: 10px;
      background: rgba(0, 240, 255, 0.08);
      border: 1px solid rgba(0, 240, 255, 0.3);
      color: var(--text-light);
      border-radius: 4px;
      cursor: pointer;
      transition: all 0.2s ease;
      text-align: center;
    }

    .btn-action:hover, .btn-action.active {
      background: var(--accent-cyan);
      color: #000;
      font-weight: 600;
      box-shadow: 0 0 10px rgba(0, 240, 255, 0.4);
    }

    /* 底部居中推进状态指示 (微型) */
    .hud-status-pill {
      position: absolute;
      bottom: 22px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 10;
      background: rgba(5, 255, 161, 0.1);
      border: 1px solid rgba(5, 255, 161, 0.3);
      padding: 4px 14px;
      border-radius: 20px;
      font-size: 11px;
      color: var(--accent-green);
      display: flex;
      align-items: center;
      gap: 8px;
      pointer-events: none;
      font-family: ui-monospace, monospace;
      box-shadow: 0 0 15px rgba(5, 255, 161, 0.15);
    }

    .status-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--accent-green);
      box-shadow: 0 0 8px var(--accent-green);
      animation: pulse-dot 1.2s infinite ease-in-out;
    }

    @keyframes pulse-dot {
      0%, 100% { opacity: 0.3; transform: scale(0.8); }
      50% { opacity: 1; transform: scale(1.2); }
    }
  </style>
</head>
<body>

  <div class="viewport">
    <canvas id="robotCanvas"></canvas>

    <!-- 顶部极简信息 -->
    <div class="hud-header">
      <div class="badge-tag">TRIZ·IFR 理想解</div>
      <div class="hud-title">蛇鳞各向异性摩擦行波推进系统</div>
      <div class="hud-subtitle">Serpenoid Wave & Anisotropic Friction</div>
    </div>

    <!-- 右上角图例 -->
    <div class="hud-legend">
      <div class="legend-item">
        <div class="legend-line legend-thrust"></div>
        <span>前向净推进合力 (F_thrust > 0)</span>
      </div>
      <div class="legend-item">
        <div class="legend-line legend-friction"></div>
        <span>横向抗侧滑摩擦 (f_perp 阻断)</span>
      </div>
      <div class="legend-item">
        <div class="legend-line legend-heading"></div>
        <span>单节切向自由滚动 (f_para ≈ 0)</span>
      </div>
    </div>

    <!-- 左下角微观轮组原理特写视窗 -->
    <div class="hud-inset">
      <div class="inset-title">
        <span>被动轮各向异性机理</span>
        <span style="color:var(--accent-green);">μ⊥ ≫ μ∥</span>
      </div>
      <svg id="wheelSvg" width="200" height="70" viewBox="0 0 200 70" style="display:block;margin:auto;">
        <!-- 底盘与被动轮微观矢量图示 -->
        <rect x="25" y="18" width="150" height="34" rx="4" fill="#121b2d" stroke="#00f0ff" stroke-width="1.2" stroke-dasharray="3 2" />
        <!-- 轮子 A -->
        <rect x="60" y="8" width="10" height="54" rx="2" fill="#ffaa00" stroke="#fff" stroke-width="1" />
        <!-- 轮子 B -->
        <rect x="130" y="8" width="10" height="54" rx="2" fill="#ffaa00" stroke="#fff" stroke-width="1" />
        <!-- 轮轴 -->
        <line x1="45" y1="35" x2="155" y2="35" stroke="#7e8c9f" stroke-width="2" />
        <!-- 纵向滚动箭头 -->
        <path d="M 65 8 L 65 -2" stroke="#05ffa1" stroke-width="1.8" marker-end="url(#arrow-green)" />
        <path d="M 135 8 L 135 -2" stroke="#05ffa1" stroke-width="1.8" />
        <!-- 侧向受阻箭头 -->
        <path d="M 15 35 L 5 35" stroke="#ff2a6d" stroke-width="1.8" />
        <path d="M 185 35 L 195 35" stroke="#ff2a6d" stroke-width="1.8" />
        <text x="100" y="32" fill="#d8e6f3" font-size="9" text-anchor="middle" font-family="monospace">同轴窄轮组</text>
        <text x="100" y="44" fill="#05ffa1" font-size="8" text-anchor="middle">切向无阻滚动</text>
        <text x="18" y="27" fill="#ff2a6d" font-size="8">侧向锁止</text>
      </svg>
      <div class="inset-desc">单向被动窄轮允许纵向平滑滚动,横向产生高阻尼滑动摩擦,瞬间将横向摆动力矩转化为向前推力。</div>
    </div>

    <!-- 底部状态指示 -->
    <div class="hud-status-pill">
      <div class="status-dot"></div>
      <span>IFR推进状态: 正弦行波传递中 (位移转换率: 88.4%)</span>
    </div>

    <!-- 右下角控制调节面板 -->
    <div class="hud-controls">
      <div class="control-row">
        <label>摆动频率 (Hz)</label>
        <input type="range" id="freqRange" min="0.5" max="3.0" step="0.1" value="1.2" />
        <span class="control-val" id="freqVal">1.2</span>
      </div>
      <div class="control-row">
        <label>关节摆角 (±Deg)</label>
        <input type="range" id="ampRange" min="15" max="45" step="1" value="38" />
        <span class="control-val" id="ampVal">38°</span>
      </div>
      <div class="control-row">
        <label>相位差 (Δφ)</label>
        <input type="range" id="phaseRange" min="30" max="90" step="2" value="55" />
        <span class="control-val" id="phaseVal">55°</span>
      </div>
      <div class="btn-group">
        <button class="btn-action active" id="toggleVectors">力学矢量: 开启</button>
        <button class="btn-action" id="toggleTrail">运动轨迹</button>
      </div>
    </div>
  </div>

  <script>
    /**
     * 蛇形机器人 Serpenoid 行波各向异性仿真
     * 纯机械结构模拟蛇鳞各向异性摩擦 (Yaw 关节串联 + 底部单向窄轮)
     */
    const canvas = document.getElementById('robotCanvas');
    const ctx = canvas.getContext('2d');

    // UI 控件绑定
    const freqRange = document.getElementById('freqRange');
    const freqVal = document.getElementById('freqVal');
    const ampRange = document.getElementById('ampRange');
    const ampVal = document.getElementById('ampVal');
    const phaseRange = document.getElementById('phaseRange');
    const phaseVal = document.getElementById('phaseVal');
    const toggleVectorsBtn = document.getElementById('toggleVectors');
    const toggleTrailBtn = document.getElementById('toggleTrail');

    // 机器人动力学参数
    const NUM_SEGMENTS = 12;      // 12节刚性模块
    const SEG_LENGTH = 52;         // 模块长度 (px)
    const SEG_WIDTH = 24;          // 模块宽度 (px)
    
    let frequency = parseFloat(freqRange.value);
    let maxAngleDeg = parseFloat(ampRange.value);
    let phaseDiffDeg = parseFloat(phaseRange.value);
    let showVectors = true;
    let showTrail = false;

    // 状态量
    let simTime = 0;
    let robotWorldX = 0;           // 机器人前进世界坐标
    let robotWorldY = 0;
    let trailPoints = [];          // 蛇头轨迹记录
    let groundOffset = 0;          // 地面网格随前进滚动的偏置

    // 响应式自适应 Canvas
    function resizeCanvas() {
      const dpr = window.devicePixelRatio || 1;
      canvas.width = canvas.parentElement.clientWidth * dpr;
      canvas.height = canvas.parentElement.clientHeight * dpr;
      ctx.scale(dpr, dpr);
    }
    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();

    // 交互监听
    freqRange.addEventListener('input', (e) => {
      frequency = parseFloat(e.target.value);
      freqVal.textContent = frequency.toFixed(1);
    });
    ampRange.addEventListener('input', (e) => {
      maxAngleDeg = parseFloat(e.target.value);
      ampVal.textContent = maxAngleDeg + '°';
    });
    phaseRange.addEventListener('input', (e) => {
      phaseDiffDeg = parseFloat(e.target.value);
      phaseVal.textContent = phaseDiffDeg + '°';
    });
    toggleVectorsBtn.addEventListener('click', () => {
      showVectors = !showVectors;
      toggleVectorsBtn.textContent = `力学矢量: ${showVectors ? '开启' : '关闭'}`;
      toggleVectorsBtn.classList.toggle('active', showVectors);
    });
    toggleTrailBtn.addEventListener('click', () => {
      showTrail = !showTrail;
      toggleTrailBtn.classList.toggle('active', showTrail);
      if (!showTrail) trailPoints = [];
    });

    /**
     * 核心运动学计算:Hirose 广义 Serpenoid 曲线
     * 关节角: alpha_i(t) = A * sin(omega * t + i * delta_phi)
     */
    function calculateKinematics(t) {
      const maxAngleRad = (maxAngleDeg * Math.PI) / 180;
      const phaseDiffRad = (phaseDiffDeg * Math.PI) / 180;
      const omega = 2 * Math.PI * frequency;

      // 1. 计算各个模块相对于前一节的相对摆角
      const jointAngles = [];
      for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
        const theta = maxAngleRad * Math.sin(omega * t - i * phaseDiffRad);
        jointAngles.push(theta);
      }

      // 2. 计算每个模块在局部机身坐标系下的绝对朝向角 (机头保持主航向)
      const segmentAbsAngles = [0];
      let currentAbsAngle = 0;
      for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
        currentAbsAngle += jointAngles[i];
        segmentAbsAngles.push(currentAbsAngle);
      }

      // 消除机身整体自发旋转偏角,使蛇整体朝向 X 轴正向推进
      const avgAngle = segmentAbsAngles.reduce((a, b) => a + b, 0) / NUM_SEGMENTS;
      for (let i = 0; i < NUM_SEGMENTS; i++) {
        segmentAbsAngles[i] -= avgAngle;
      }

      // 3. 构建各关节的相对位置坐标 (从头部向尾部连杆延伸)
      const positions = [{ x: 0, y: 0 }];
      for (let i = 0; i < NUM_SEGMENTS; i++) {
        const prev = positions[i];
        const angle = segmentAbsAngles[i];
        const nextX = prev.x - SEG_LENGTH * Math.cos(angle);
        const nextY = prev.y - SEG_LENGTH * Math.sin(angle);
        positions.push({ x: nextX, y: nextY });
      }

      // 计算整条蛇的瞬时质心
      let cx = 0, cy = 0;
      for (let i = 0; i <= NUM_SEGMENTS; i++) {
        cx += positions[i].x;
        cy += positions[i].y;
      }
      cx /= (NUM_SEGMENTS + 1);
      cy /= (NUM_SEGMENTS + 1);

      // 将机体中心置中
      for (let i = 0; i <= NUM_SEGMENTS; i++) {
        positions[i].x -= cx;
        positions[i].y -= cy;
      }

      // 4. 动力学模拟推导:计算向前的净推力速度
      // 各向异性原理:侧向速度受阻 (f_perp 很大,抵消横移),产生前向反冲推力 F_forward ~ A^2 * omega * phase
      const forwardVelocity = 110 * (maxAngleRad * maxAngleRad) * (frequency / 1.5) * Math.sin(phaseDiffRad);

      return {
        positions,
        segmentAngles: segmentAbsAngles,
        jointAngles,
        forwardVelocity
      };
    }

    // 绘制背景精密网格与地面流动标尺
    function drawEnvironment(w, h, speed) {
      groundOffset = (groundOffset + speed) % 60;

      ctx.save();
      ctx.strokeStyle = 'rgba(0, 240, 255, 0.04)';
      ctx.lineWidth = 1;

      // 静态纵横背景细网格
      const gridSize = 30;
      for (let x = 0; x < w; x += gridSize) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, h);
        ctx.stroke();
      }
      for (let y = 0; y < h; y += gridSize) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(w, y);
        ctx.stroke();
      }

      // 动态滚动的地面微观摩擦标尺线(向左平移,体现蛇身向前)
      ctx.strokeStyle = 'rgba(0, 240, 255, 0.12)';
      ctx.lineWidth = 1.5;
      const moveGrid = 60;
      const startX = w - ((groundOffset * 2) % moveGrid);
      for (let x = startX; x > 0; x -= moveGrid) {
        ctx.beginPath();
        ctx.moveTo(x, h / 2 - 220);
        ctx.lineTo(x, h / 2 + 220);
        ctx.stroke();

        // 刻度微标
        ctx.fillStyle = 'rgba(0, 240, 255, 0.25)';
        ctx.font = '9px monospace';
        ctx.fillText(`${Math.floor((w - x + robotWorldX) / 10)}mm`, x + 4, h / 2 + 215);
      }

      // 前进基准导轨中心线
      ctx.strokeStyle = 'rgba(255, 170, 0, 0.15)';
      ctx.setLineDash([8, 8]);
      ctx.beginPath();
      ctx.moveTo(0, h / 2);
      ctx.lineTo(w, h / 2);
      ctx.stroke();
      ctx.setLineDash([]);

      ctx.restore();
    }

    // 绘制轨迹拖尾
    function drawTrail(centerX, centerY) {
      if (!showTrail || trailPoints.length < 2) return;
      ctx.save();
      ctx.beginPath();
      for (let i = 0; i < trailPoints.length; i++) {
        const p = trailPoints[i];
        if (i === 0) ctx.moveTo(p.x, p.y);
        else ctx.lineTo(p.x, p.y);
      }
      ctx.strokeStyle = 'rgba(0, 240, 255, 0.35)';
      ctx.lineWidth = 2.5;
      ctx.setLineDash([4, 4]);
      ctx.stroke();
      ctx.restore();
    }

    // 绘制单个蛇体刚性骨架节与被动轮机构
    function drawSegment(p1, p2, angle, index, isHead) {
      const midX = (p1.x + p2.x) / 2;
      const midY = (p1.y + p2.y) / 2;
      const length = SEG_LENGTH;
      const width = SEG_WIDTH;

      ctx.save();
      ctx.translate(midX, midY);
      ctx.rotate(angle);

      // 1. 单向被动轮组件(位于底盘中心下方两侧)
      ctx.fillStyle = '#ffaa00';
      ctx.shadowColor = '#ffaa00';
      ctx.shadowBlur = 6;
      // 左右橡胶窄轮
      ctx.fillRect(-6, -width / 2 - 5, 12, 4);
      ctx.fillRect(-6, width / 2 + 1, 12, 4);
      // 金属同轴轴承
      ctx.fillStyle = '#4a5b78';
      ctx.fillRect(-1.5, -width / 2 - 2, 3, width + 4);
      ctx.shadowBlur = 0;

      // 2. 刚性外壳流线体
      const grad = ctx.createLinearGradient(-length / 2, 0, length / 2, 0);
      if (isHead) {
        grad.addColorStop(0, '#10223d');
        grad.addColorStop(1, '#00f0ff');
      } else {
        grad.addColorStop(0, '#0c1626');
        grad.addColorStop(0.5, '#16253d');
        grad.addColorStop(1, '#0c1626');
      }

      ctx.fillStyle = grad;
      ctx.strokeStyle = isHead ? '#00f0ff' : 'rgba(0, 240, 255, 0.4)';
      ctx.lineWidth = 1.4;

      ctx.beginPath();
      if (isHead) {
        // 头部呈仿生导流流线罩
        ctx.moveTo(length / 2 + 6, 0);
        ctx.lineTo(length / 2 - 4, -width / 2);
        ctx.lineTo(-length / 2 + 4, -width / 2);
        ctx.lineTo(-length / 2, 0);
        ctx.lineTo(-length / 2 + 4, width / 2);
        ctx.lineTo(length / 2 - 4, width / 2);
        ctx.closePath();
      } else {
        // 刚性模块倒角矩形
        const r = 4;
        const w2 = length / 2 - 2;
        const h2 = width / 2;
        ctx.roundRect(-w2, -h2, (w2 * 2), (h2 * 2), r);
      }
      ctx.fill();
      ctx.stroke();

      // 3. 关节伺服电机盘与装配螺栓
      ctx.fillStyle = '#1e293b';
      ctx.beginPath();
      ctx.arc(-length / 2 + 4, 0, 5, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = 'rgba(255, 170, 0, 0.8)';
      ctx.lineWidth = 1.2;
      ctx.stroke();

      // 模块编号微标
      ctx.fillStyle = isHead ? '#000000' : 'rgba(255, 255, 255, 0.45)';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(isHead ? 'HEAD' : `M${index}`, 0, 0);

      ctx.restore();
    }

    // 绘制力学矢量分解图 (IFR创新核心高亮)
    function drawForceVectors(p1, p2, angle, index) {
      if (!showVectors) return;
      const midX = (p1.x + p2.x) / 2;
      const midY = (p1.y + p2.y) / 2;

      // 仅在关键相位节上强化显示受力分解(例如 2, 5, 8 节),避免画面混乱
      const isKeySegment = (index % 3 === 1);
      const arrowScale = isKeySegment ? 36 : 22;

      ctx.save();
      ctx.translate(midX, midY);

      // 1. 切向方向 (被动轮无阻力顺向自由滚动)
      const tangentX = Math.cos(angle) * arrowScale;
      const tangentY = Math.sin(angle) * arrowScale;

      // 2. 法向方向 (被动轮抵抗侧滑的巨大地面侧向力 f_perp)
      // 计算法向力朝向前进半平面
      const normalAngle = angle + (Math.sin(angle) > 0 ? -Math.PI / 2 : Math.PI / 2);
      const frictionX = Math.cos(normalAngle) * (arrowScale * 0.95);
      const frictionY = Math.sin(normalAngle) * (arrowScale * 0.95);

      // 3. 投影到整机 X 轴前向的净推进力 F_thrust
      const thrustX = Math.abs(frictionX) * 1.1;

      // 绘制侧向阻力矢量 (洋红色)
      drawArrow(0, 0, frictionX, frictionY, '#ff2a6d', 1.8, 'f⊥');

      // 绘制滚动切向速度矢量 (青色)
      drawArrow(0, 0, tangentX, tangentY, '#00f0ff', 1.4, 'v∥');

      // 若为关键采样节,高亮绘制前向合成推力矢量 (荧光绿)
      if (isKeySegment) {
        drawArrow(0, 0, thrustX, 0, '#05ffa1', 2.4, 'F_push');
      }

      ctx.restore();
    }

    // 矢量箭头绘制辅助工具
    function drawArrow(fromX, fromY, toX, toY, color, width, label) {
      const headLen = 6;
      const dx = toX - fromX;
      const dy = toY - fromY;
      const length = Math.sqrt(dx * dx + dy * dy);
      if (length < 2) return;

      const angle = Math.atan2(dy, dx);

      ctx.save();
      ctx.strokeStyle = color;
      ctx.fillStyle = color;
      ctx.lineWidth = width;
      ctx.shadowColor = color;
      ctx.shadowBlur = 4;

      ctx.beginPath();
      ctx.moveTo(fromX, fromY);
      ctx.lineTo(toX, toY);
      ctx.stroke();

      // 箭头头部
      ctx.beginPath();
      ctx.moveTo(toX, toY);
      ctx.lineTo(toX - headLen * Math.cos(angle - Math.PI / 6), toY - headLen * Math.sin(angle - Math.PI / 6));
      ctx.lineTo(toX - headLen * Math.cos(angle + Math.PI / 6), toY - headLen * Math.sin(angle + Math.PI / 6));
      ctx.closePath();
      ctx.fill();

      // 文字标注
      if (label) {
        ctx.shadowBlur = 0;
        ctx.font = 'bold 9px monospace';
        ctx.fillText(label, toX + Math.cos(angle) * 6, toY + Math.sin(angle) * 6);
      }

      ctx.restore();
    }

    // 绘制机头总推进功率指示仪
    function drawHeadDashboard(headX, headY) {
      ctx.save();
      ctx.translate(headX + 45, headY - 35);

      // 微型仪表盘底座
      ctx.fillStyle = 'rgba(6, 12, 24, 0.85)';
      ctx.strokeStyle = 'rgba(5, 255, 161, 0.4)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.roundRect(-40, -16, 95, 32, 4);
      ctx.fill();
      ctx.stroke();

      // 总推力前进矢量
      drawArrow(-30, 0, 15, 0, '#05ffa1', 2.8, '');
      ctx.fillStyle = '#05ffa1';
      ctx.font = '10px monospace';
      ctx.fillText('Σ F_net ➔', 22, 3);

      ctx.restore();
    }

    /**
     * 主渲染动画循环 (60FPS 高保真自动运行)
     */
    let lastFrameTime = performance.now();

    function render(now) {
      const dt = Math.min((now - lastFrameTime) / 1000, 0.05);
      lastFrameTime = now;
      simTime += dt;

      const w = canvas.parentElement.clientWidth;
      const h = canvas.parentElement.clientHeight;

      // 1. 动力学计算
      const kin = calculateKinematics(simTime);
      const stepDist = kin.forwardVelocity * dt;
      robotWorldX += stepDist;

      // 2. 清屏与环境绘制
      ctx.clearRect(0, 0, w, h);
      drawEnvironment(w, h, stepDist);

      // 3. 将蛇体坐标映射到画面中央区域
      const centerX = w * 0.48;
      const centerY = h * 0.5;

      const screenPositions = kin.positions.map(p => ({
        x: centerX + p.x + (SEG_LENGTH * NUM_SEGMENTS * 0.42),
        y: centerY + p.y
      }));

      // 记录机头轨迹
      const headPos = screenPositions[0];
      if (showTrail) {
        trailPoints.push({ x: headPos.x, y: headPos.y });
        if (trailPoints.length > 180) trailPoints.shift();
      }

      // 4. 绘制轨迹
      drawTrail(centerX, centerY);

      // 5. 从尾到头绘制关节连接轴线与连杆骨架
      ctx.save();
      ctx.strokeStyle = 'rgba(0, 240, 255, 0.2)';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(screenPositions[0].x, screenPositions[0].y);
      for (let i = 1; i <= NUM_SEGMENTS; i++) {
        ctx.lineTo(screenPositions[i].x, screenPositions[i].y);
      }
      ctx.stroke();
      ctx.restore();

      // 6. 逐节绘制刚性模块与被动轮
      for (let i = NUM_SEGMENTS - 1; i >= 0; i--) {
        const p1 = screenPositions[i];
        const p2 = screenPositions[i + 1];
        const angle = kin.segmentAngles[i];
        drawSegment(p1, p2, angle, NUM_SEGMENTS - i, i === 0);
      }

      // 7. 绘制各模块摩擦力/推力矢量图示
      for (let i = 0; i < NUM_SEGMENTS; i++) {
        const p1 = screenPositions[i];
        const p2 = screenPositions[i + 1];
        const angle = kin.segmentAngles[i];
        drawForceVectors(p1, p2, angle, i);
      }

      // 8. 头部牵引力总成指示
      drawHeadDashboard(headPos.x, headPos.y);

      // 持续自启动循环
      requestAnimationFrame(render);
    }

    // 立即启动自播放
    requestAnimationFrame(render);
  </script>
</body>
</html>
积分规则:每轮对话扣10分