分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仿生鳞片单向推进原理 — IFR 动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #08080E;
    --surface: #0E0E1A;
    --grid-line: #1A1A30;
    --text: #C8C4BE;
    --text-dim: #5E5A54;
    --grip: #FF5722;
    --grip-glow: rgba(255,87,34,0.45);
    --glide: #00E5A0;
    --glide-glow: rgba(0,229,160,0.30);
    --gold: #FFD54F;
    --body-fill: #E8E4DC;
    --body-stroke: #B0ACA4;
    --panel: #0C0C18;
    --panel-border: #1E1E36;
  }
  *, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
  html, body { height:100%; overflow:hidden; background:var(--bg); color:var(--text); font-family:'Fira Code',monospace; }
  #app { display:flex; flex-direction:column; height:100vh; }

  /* ---- 顶栏 ---- */
  header {
    padding:14px 28px 6px;
    display:flex; align-items:baseline; gap:18px;
    border-bottom:1px solid var(--panel-border);
    background:linear-gradient(180deg,rgba(14,14,26,.9),transparent);
    flex-shrink:0;
  }
  header h1 {
    font-family:'Cormorant Garamond',serif;
    font-weight:700; font-size:22px; letter-spacing:1px;
    color:#F0ECE4;
  }
  header p { font-size:11px; color:var(--text-dim); letter-spacing:.5px; }

  /* ---- 主舞台 ---- */
  .stage { flex:1; display:flex; justify-content:center; align-items:center; padding:8px 16px; min-height:0; }
  .stage svg { width:100%; max-width:1100px; height:100%; }

  /* ---- 底栏 ---- */
  .bottom {
    flex-shrink:0; display:flex; gap:0;
    border-top:1px solid var(--panel-border);
    background:var(--panel);
    height:200px;
  }
  .panel { padding:14px 20px; border-right:1px solid var(--panel-border); overflow:hidden; }
  .panel:last-child { border-right:none; }
  .panel h3 { font-family:'Cormorant Garamond',serif; font-size:14px; font-weight:600; color:#D0CCC4; margin-bottom:8px; letter-spacing:.5px; }
  .panel-inset { flex:0 0 380px; }
  .panel-inset svg { width:100%; height:130px; }
  .panel-metrics { flex:0 0 240px; display:flex; flex-direction:column; gap:6px; }
  .metric-row { display:flex; justify-content:space-between; align-items:center; font-size:11px; }
  .metric-label { color:var(--text-dim); }
  .metric-value { font-weight:500; }
  .metric-value.grip { color:var(--grip); }
  .metric-value.glide { color:var(--glide); }
  .metric-value.gold { color:var(--gold); }
  .panel-controls { flex:1; display:flex; flex-direction:column; gap:8px; }
  .ctrl-row { display:flex; align-items:center; gap:10px; font-size:11px; }
  .ctrl-row label { width:80px; color:var(--text-dim); flex-shrink:0; text-align:right; }
  .ctrl-row input[type=range] { flex:1; accent-color:var(--gold); height:4px; }
  .ctrl-row .val { width:44px; text-align:right; color:var(--text); font-variant-numeric:tabular-nums; }
  .btn-row { display:flex; gap:8px; margin-top:4px; }
  .btn {
    padding:5px 14px; border:1px solid var(--panel-border); background:transparent;
    color:var(--text); font-family:inherit; font-size:11px; cursor:pointer;
    border-radius:3px; transition:all .2s;
  }
  .btn:hover { border-color:var(--gold); color:var(--gold); }
  .btn.active { background:var(--gold); color:#08080E; border-color:var(--gold); }
  .ifr-badge {
    display:inline-block; font-size:9px; padding:2px 7px; border-radius:2px;
    background:rgba(255,213,79,.12); color:var(--gold); letter-spacing:.5px; margin-top:6px;
  }

  /* 警告 */
  .warn-overlay {
    position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
    background:rgba(255,87,34,.12); border:1px solid var(--grip);
    padding:14px 28px; border-radius:6px; pointer-events:none;
    font-size:13px; color:var(--grip); opacity:0; transition:opacity .4s;
    z-index:10; text-align:center;
  }
  .warn-overlay.show { opacity:1; }
</style>
</head>
<body>
<div id="app">
  <header>
    <h1>仿生鳞片单向推进原理</h1>
    <p>Passive Asymmetric Friction &rarr; Lateral-to-Forward Thrust</p>
  </header>

  <div class="stage">
    <svg id="main-svg" viewBox="0 0 900 360" preserveAspectRatio="xMidYMid meet">
      <defs>
        <!-- 发光滤镜 -->
        <filter id="f-grip" x="-40%" y="-40%" width="180%" height="180%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="f-glide" x="-40%" y="-40%" width="180%" height="180%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="f-gold" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <!-- 地面渐变 -->
        <linearGradient id="gnd-grad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="#0E0E1A"/>
          <stop offset="1" stop-color="#08080E"/>
        </linearGradient>
      </defs>

      <!-- 地面 -->
      <g id="ground-group">
        <rect x="0" y="290" width="900" height="70" fill="url(#gnd-grad)"/>
        <g id="ground-lines" stroke="#1A1A30" stroke-width="1"></g>
      </g>

      <!-- 轨迹 -->
      <polyline id="trail" fill="none" stroke="#FFD54F" stroke-width="1.5" opacity="0.35" stroke-dasharray="6,4"/>

      <!-- 蛇身段 -->
      <g id="snake-group"></g>

      <!-- 力箭头 -->
      <g id="force-group"></g>

      <!-- 前进大箭头 -->
      <g id="thrust-arrow" filter="url(#f-gold)">
        <line x1="0" y1="0" x2="0" y2="0" stroke="#FFD54F" stroke-width="3" stroke-linecap="round"/>
        <polygon points="0,0 0,0 0,0" fill="#FFD54F"/>
      </g>

      <!-- IFR 标注 -->
      <g id="ifr-anno" transform="translate(730,32)">
        <rect x="0" y="0" width="155" height="62" rx="4" fill="rgba(255,213,79,.06)" stroke="rgba(255,213,79,.25)" stroke-width="0.8"/>
        <text x="12" y="18" font-size="9" fill="#FFD54F" font-family="'Fira Code',monospace" letter-spacing="1">IFR · 最终理想解</text>
        <text x="12" y="33" font-size="8.5" fill="#A8A49E" font-family="'Fira Code',monospace">被动鳞片自调节</text>
        <text x="12" y="46" font-size="8.5" fill="#A8A49E" font-family="'Fira Code',monospace">零额外动力推进</text>
        <text x="12" y="57" font-size="7.5" fill="#6B6760" font-family="'Fira Code',monospace">摩擦不对称 → 前向推力</text>
      </g>

      <!-- 刻度尺 -->
      <g id="ruler" transform="translate(60,308)">
        <line x1="0" y1="0" x2="780" y2="0" stroke="#2A2A44" stroke-width="0.5"/>
      </g>
    </svg>
  </div>

  <div class="bottom">
    <!-- 鳞片微观 -->
    <div class="panel panel-inset">
      <h3>鳞片微观机理</h3>
      <svg id="inset-svg" viewBox="0 0 380 130">
        <defs>
          <filter id="f-inset-glow" x="-30%" y="-30%" width="160%" height="160%">
            <feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="b"/>
            <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
          </filter>
        </defs>
        <!-- 地面 -->
        <rect x="0" y="92" width="380" height="38" fill="#0E0E1A" rx="0"/>
        <line x1="0" y1="92" x2="380" y2="92" stroke="#2A2A44" stroke-width="1"/>
        <!-- 标签 -->
        <text id="inset-mode-label" x="190" y="16" text-anchor="middle" font-size="10" fill="#8A8680" font-family="'Fira Code',monospace"></text>
        <!-- 身体底面 -->
        <rect id="inset-body" x="60" y="60" width="260" height="18" rx="3" fill="#D8D4CC" stroke="#AAA69E" stroke-width="0.8"/>
        <!-- 鳞片组 -->
        <g id="inset-scales"></g>
        <!-- 运动箭头 -->
        <g id="inset-arrows"></g>
        <!-- 摩擦系数 -->
        <text id="inset-mu-label" x="190" y="128" text-anchor="middle" font-size="10" font-family="'Fira Code',monospace"></text>
      </svg>
    </div>

    <!-- 指标 -->
    <div class="panel panel-metrics">
      <h3>实时参数</h3>
      <div class="metric-row"><span class="metric-label">前进距离</span><span class="metric-value gold" id="m-dist">0.0 mm</span></div>
      <div class="metric-row"><span class="metric-label">前进速度</span><span class="metric-value gold" id="m-speed">0.0 mm/s</span></div>
      <div class="metric-row"><span class="metric-label">μ<sub>高</sub> (抓地)</span><span class="metric-value grip" id="m-mu-h">0.80</span></div>
      <div class="metric-row"><span class="metric-label">μ<sub>低</sub> (滑行)</span><span class="metric-value glide" id="m-mu-l">0.20</span></div>
      <div class="metric-row"><span class="metric-label">摩擦比</span><span class="metric-value" id="m-ratio" style="color:#FFD54F">4.0</span></div>
      <div class="metric-row"><span class="metric-label">表面状态</span><span class="metric-value" id="m-surface" style="color:#00E5A0">正常</span></div>
      <div class="ifr-badge">IFR:被动结构 · 自调节摩擦不对称</div>
    </div>

    <!-- 控制 -->
    <div class="panel panel-controls">
      <h3>交互控制</h3>
      <div class="ctrl-row">
        <label>波速</label>
        <input type="range" id="c-speed" min="0.3" max="3" step="0.1" value="1.5">
        <span class="val" id="v-speed">1.5</span>
      </div>
      <div class="ctrl-row">
        <label>鳞片角度</label>
        <input type="range" id="c-angle" min="15" max="75" step="1" value="45">
        <span class="val" id="v-angle">45°</span>
      </div>
      <div class="ctrl-row">
        <label>表面粗糙</label>
        <input type="range" id="c-rough" min="0" max="1" step="0.05" value="0.85">
        <span class="val" id="v-rough">0.85</span>
      </div>
      <div class="btn-row">
        <button class="btn" id="btn-pause">暂停</button>
        <button class="btn active" id="btn-force">力向量</button>
        <button class="btn" id="btn-reset">重置</button>
      </div>
    </div>
  </div>

  <div class="warn-overlay" id="warn-glass">⚠ 极度光滑表面:棘齿无法抓地,摩擦差异消失,方案失效</div>
</div>

<script>
const NS = 'http://www.w3.org/2000/svg';

/* ===== 配置 ===== */
const CFG = {
  numSeg: 10,
  segLen: 34,      // 段长(前进方向)
  segW: 24,        // 段宽(横向)
  segSpacing: 30,  // 段中心距
  headX: 700,      // 头段 x
  centerY: 195,    // 中心 y
  amplitude: 58,   // 波幅
  waveK: 0.72,     // 波数 rad/段
  edgeW: 3.5,      // 摩擦边宽度
  rulerSpacing: 40 // 刻度间距
};

/* ===== 状态 ===== */
const S = {
  time: 0,
  waveSpeed: 1.5,
  scaleAngle: 45,
  roughness: 0.85,
  showForce: true,
  paused: false,
  dist: 0,
  trail: [],
  particles: []
};

/* ===== 工具函数 ===== */
function el(tag, attrs) {
  const e = document.createElementNS(NS, tag);
  for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
  return e;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function deg(r) { return r * 180 / Math.PI; }

/* ===== 摩擦系数 ===== */
function muHigh() { return 0.1 + 0.7 * S.roughness; }
function muLow()  { return 0.1 + 0.1 * S.roughness; }
function forwardFactor() {
  const a = S.scaleAngle * Math.PI / 180;
  return Math.sin(2 * a) * (muHigh() - muLow()) / 0.6; // 归一化,45°粗糙=1
}
function forwardSpeed() {
  return S.waveSpeed * forwardFactor() * 50; // 像素/秒
}

/* ===== 创建蛇段 SVG ===== */
const segData = [];
const snakeG = document.getElementById('snake-group');
const forceG = document.getElementById('force-group');

for (let i = 0; i < CFG.numSeg; i++) {
  const g = el('g');
  // 段体
  const body = el('rect', {
    x: -CFG.segLen / 2, y: -CFG.segW / 2,
    width: CFG.segLen, height: CFG.segW,
    rx: 5, ry: 5,
    fill: '#E8E4DC', stroke: '#B0ACA4', 'stroke-width': 0.8
  });
  g.appendChild(body);

  // 鳞片纹理(底部微倾斜线)
  for (let j = -2; j <= 2; j++) {
    const ln = el('line', {
      x1: j * 7 - 3, y1: CFG.segW / 2 - 1,
      x2: j * 7 + 3, y2: CFG.segW / 2 - 5,
      stroke: '#C0BCB4', 'stroke-width': 0.6, opacity: 0.5
    });
    g.appendChild(ln);
  }

  // 左摩擦边
  const leftEdge = el('rect', {
    x: -CFG.segLen / 2, y: -CFG.segW / 2,
    width: CFG.segLen, height: CFG.edgeW,
    rx: 2, fill: '#555', opacity: 0.5
  });
  g.appendChild(leftEdge);

  // 右摩擦边
  const rightEdge = el('rect', {
    x: -CFG.segLen / 2, y: CFG.segW / 2 - CFG.edgeW,
    width: CFG.segLen, height: CFG.edgeW,
    rx: 2, fill: '#555', opacity: 0.5
  });
  g.appendChild(rightEdge);

  snakeG.appendChild(g);

  // 力箭头
  const arrowG = el('g', { opacity: 0 });
  const arrowLine = el('line', { x1: 0, y1: 0, x2: 20, y2: 0, stroke: '#FFD54F', 'stroke-width': 2, 'stroke-linecap': 'round' });
  const arrowHead = el('polygon', { points: '20,-4 28,0 20,4', fill: '#FFD54F' });
  arrowG.appendChild(arrowLine);
  arrowG.appendChild(arrowHead);
  forceG.appendChild(arrowG);

  segData.push({ g, leftEdge, rightEdge, arrowG, arrowLine, arrowHead, cx: 0, cy: 0, angle: 0, vy: 0, leftHigh: false });
}

/* ===== 地面线 ===== */
const groundLinesG = document.getElementById('ground-lines');
const groundLines = [];
for (let i = 0; i < 30; i++) {
  const ln = el('line', { x1: i * 40, y1: 290, x2: i * 40, y2: 355, 'stroke-width': 0.6, 'stroke-dasharray': '3,5' });
  groundLinesG.appendChild(ln);
  groundLines.push(ln);
}

/* ===== 刻度尺标记 ===== */
const rulerG = document.getElementById('ruler');
const rulerMarks = [];
for (let i = 0; i < 25; i++) {
  const mk = el('line', { x1: i * 40, y1: -3, x2: i * 40, y2: 3, stroke: '#2A2A44', 'stroke-width': 0.8 });
  rulerG.appendChild(mk);
  rulerMarks.push(mk);
  if (i % 3 === 0) {
    const tx = el('text', { x: i * 40, y: 12, 'text-anchor': 'middle', 'font-size': 7, fill: '#3A3A54', 'font-family': "'Fira Code',monospace" });
    tx.textContent = i * 40;
    rulerG.appendChild(tx);
  }
}

/* ===== 轨迹 ===== */
const trailEl = document.getElementById('trail');

/* ===== 鳞片微观 inset ===== */
const insetScalesG = document.getElementById('inset-scales');
const insetArrowsG = document.getElementById('inset-arrows');
const insetScaleEls = [];
for (let i = 0; i < 7; i++) {
  const sc = el('polygon', { fill: '#AAA69E', stroke: '#888478', 'stroke-width': 0.6 });
  insetScalesG.appendChild(sc);
  insetScaleEls.push(sc);
}

/* ===== 粒子池 ===== */
const particleG = el('g');
snakeG.parentNode.insertBefore(particleG, forceG);
const PMAX = 60;
const particles = [];
for (let i = 0; i < PMAX; i++) {
  const c = el('circle', { r: 1.8, fill: '#FF5722', opacity: 0 });
  particleG.appendChild(c);
  particles.push({ el: c, life: 0, x: 0, y: 0, vx: 0, vy: 0 });
}
let pIdx = 0;
function emitParticle(x, y) {
  const p = particles[pIdx % PMAX]; pIdx++;
  p.x = x; p.y = y;
  p.vx = (Math.random() - 0.5) * 30;
  p.vy = -Math.random() * 25 - 5;
  p.life = 1;
}

/* ===== 计算段状态 ===== */
function computeSegments() {
  for (let i = 0; i < CFG.numSeg; i++) {
    const phase = S.time - CFG.waveK * i;
    const cx = CFG.headX - i * CFG.segSpacing;
    const cy = CFG.centerY + CFG.amplitude * Math.sin(phase);
    const vy = CFG.amplitude * Math.cos(phase); // ∝ 横向速度

    // 切线角
    let angle;
    if (i === 0) {
      const cy1 = CFG.centerY + CFG.amplitude * Math.sin(S.time - CFG.waveK * 1);
      angle = Math.atan2(cy1 - cy, (CFG.headX - 1 * CFG.segSpacing) - cx);
    } else {
      const cxP = CFG.headX - (i - 1) * CFG.segSpacing;
      const cyP = CFG.centerY + CFG.amplitude * Math.sin(S.time - CFG.waveK * (i - 1));
      angle = Math.atan2(cyP - cy, cxP - cx);
    }

    // 摩擦状态: vy>0 向下→左侧抓地;vy<0 向上→右侧抓地
    const d = segData[i];
    d.cx = cx; d.cy = cy; d.angle = angle; d.vy = vy;
    d.leftHigh = vy > 0.1;
    d.rightHigh = vy < -0.1;
  }
}

/* ===== 渲染 ===== */
function render() {
  const rough = S.roughness;
  const asymmetry = (muHigh() - muLow()) / 0.7; // 0~1

  // --- 蛇段 ---
  for (let i = 0; i < CFG.numSeg; i++) {
    const d = segData[i];
    d.g.setAttribute('transform', `translate(${d.cx},${d.cy}) rotate(${deg(d.angle)})`);

    // 摩擦边颜色
    const leftColor = d.leftHigh ? `rgba(255,87,34,${0.5 + 0.5 * asymmetry})` : `rgba(0,229,160,${0.3 + 0.3 * asymmetry})`;
    const rightColor = d.rightHigh ? `rgba(255,87,34,${0.5 + 0.5 * asymmetry})` : `rgba(0,229,160,${0.3 + 0.3 * asymmetry})`;
    const leftOp = d.leftHigh ? 0.9 : 0.55;
    const rightOp = d.rightHigh ? 0.9 : 0.55;

    d.leftEdge.setAttribute('fill', leftColor);
    d.leftEdge.setAttribute('opacity', leftOp * asymmetry + 0.15);
    d.rightEdge.setAttribute('fill', rightColor);
    d.rightEdge.setAttribute('opacity', rightOp * asymmetry + 0.15);

    // 力箭头
    if (S.showForce && asymmetry > 0.05) {
      const fMag = Math.abs(d.vy) / CFG.amplitude * asymmetry * forwardFactor();
      const fLen = clamp(fMag * 30, 4, 35);
      d.arrowG.setAttribute('opacity', clamp(fMag * 1.5, 0, 0.85));
      d.arrowG.setAttribute('transform', `translate(${d.cx},${d.cy}) rotate(${deg(d.angle)})`);
      d.arrowLine.setAttribute('x2', fLen);
      d.arrowHead.setAttribute('points', `${fLen},-3.5 ${fLen + 7},0 ${fLen},3.5`);
    } else {
      d.arrowG.setAttribute('opacity', 0);
    }

    // 粒子发射
    if (asymmetry > 0.15) {
      const isGripLeft = d.leftHigh && Math.abs(d.vy) > CFG.amplitude * 0.5;
      const isGripRight = d.rightHigh && Math.abs(d.vy) > CFG.amplitude * 0.5;
      if (isGripLeft && Math.random() < 0.15 * asymmetry) {
        const nx = -Math.sin(d.angle) * CFG.segW / 2;
        const ny = Math.cos(d.angle) * CFG.segW / 2;
        emitParticle(d.cx + nx, d.cy - ny);
      }
      if (isGripRight && Math.random() < 0.15 * asymmetry) {
        const nx = Math.sin(d.angle) * CFG.segW / 2;
        const ny = -Math.cos(d.angle) * CFG.segW / 2;
        emitParticle(d.cx + nx, d.cy + ny);
      }
    }
  }

  // --- 前进大箭头 ---
  const thrustG = document.getElementById('thrust-arrow');
  const fSpeed = forwardSpeed();
  if (fSpeed > 2) {
    const arrLen = clamp(fSpeed * 0.5, 10, 60);
    const hx = segData[0].cx + 30;
    const hy = segData[0].cy;
    thrustG.querySelector('line').setAttribute('x1', hx);
    thrustG.querySelector('line').setAttribute('y1', hy);
    thrustG.querySelector('line').setAttribute('x2', hx + arrLen);
    thrustG.querySelector('line').setAttribute('y2', hy);
    thrustG.querySelector('polygon').setAttribute('points', `${hx + arrLen},${hy - 5} ${hx + arrLen + 10},${hy} ${hx + arrLen},${hy + 5}`);
    thrustG.setAttribute('opacity', clamp(fSpeed / 40, 0.2, 0.8));
  } else {
    thrustG.setAttribute('opacity', 0);
  }

  // --- 地面滚动 ---
  const gOffset = (S.dist * 0.5) % 40;
  for (let i = 0; i < groundLines.length; i++) {
    groundLines[i].setAttribute('x1', i * 40 - gOffset);
    groundLines[i].setAttribute('x2', i * 40 - gOffset);
  }
  // 刻度尺
  const rOffset = (S.dist * 0.5) % 40;
  for (let i = 0; i < rulerMarks.length; i++) {
    rulerMarks[i].setAttribute('x1', i * 40 - rOffset);
    rulerMarks[i].setAttribute('x2', i * 40 - rOffset);
  }

  // --- 轨迹 ---
  if (segData[0].cx && segData[0].cy) {
    S.trail.push({ x: segData[0].cx, y: segData[0].cy });
    if (S.trail.length > 300) S.trail.shift();
    if (S.trail.length > 2) {
      trailEl.setAttribute('points', S.trail.map(p => `${p.x},${p.y}`).join(' '));
    }
  }

  // --- 鳞片微观 ---
  renderInset();

  // --- 指标 ---
  document.getElementById('m-dist').textContent = (S.dist * 0.5).toFixed(1) + ' mm';
  document.getElementById('m-speed').textContent = (fSpeed * 0.5).toFixed(1) + ' mm/s';
  document.getElementById('m-mu-h').textContent = muHigh().toFixed(2);
  document.getElementById('m-mu-l').textContent = muLow().toFixed(2);
  const ratio = muLow() > 0.01 ? (muHigh() / muLow()).toFixed(1) : '—';
  document.getElementById('m-ratio').textContent = ratio;
  const surfEl = document.getElementById('m-surface');
  if (S.roughness < 0.15) { surfEl.textContent = '玻璃(失效)'; surfEl.style.color = '#FF5722'; }
  else if (S.roughness < 0.4) { surfEl.textContent = '光滑'; surfEl.style.color = '#FFA726'; }
  else { surfEl.textContent = '正常'; surfEl.style.color = '#00E5A0'; }

  // 警告
  document.getElementById('warn-glass').classList.toggle('show', S.roughness < 0.12);
}

/* ===== 鳞片微观渲染 ===== */
function renderInset() {
  const headD = segData[0];
  const isGripRight = headD.rightHigh; // 右侧抓地
  const isGripLeft = headD.leftHigh;   // 左侧抓地
  const isGrip = isGripLeft || isGripRight;
  const asym = (muHigh() - muLow()) / 0.7;
  const a = S.scaleAngle;
  const rad = a * Math.PI / 180;

  const modeLabel = document.getElementById('inset-mode-label');
  const muLabel = document.getElementById('inset-mu-label');

  if (isGrip) {
    modeLabel.textContent = '← 后退方向:鳞片倒刺扎入地面';
    modeLabel.setAttribute('fill', '#FF5722');
    muLabel.textContent = `μ_静 ≥ ${muHigh().toFixed(2)}  (高摩擦锚定)`;
    muLabel.setAttribute('fill', '#FF5722');
  } else {
    modeLabel.textContent = '→ 前进方向:鳞片顺滑放下';
    modeLabel.setAttribute('fill', '#00E5A0');
    muLabel.textContent = `μ_动 ≤ ${muLow().toFixed(2)}  (低摩擦滑行)`;
    muLabel.setAttribute('fill', '#00E5A0');
  }

  // 鳞片几何
  const baseY = 78; // 鳞片根部 y
  const sLen = 14;  // 鳞片长度
  const sW = 8;     // 鳞片宽度

  for (let i = 0; i < insetScaleEls.length; i++) {
    const sx = 80 + i * 34;
    // 鳞片倾斜方向:向左倾斜45°(倒刺朝左/后方)
    const tipX = sx - sLen * Math.cos(rad);
    const tipY = baseY + sLen * Math.sin(rad);

    const p1x = sx - sW / 2, p1y = baseY;
    const p2x = sx + sW / 2, p2y = baseY;
    const p3x = tipX + sW / 3 * Math.sin(rad), p3y = tipY + sW / 3 * Math.cos(rad);
    const p4x = tipX - sW / 3 * Math.sin(rad), p4y = tipY - sW / 3 * Math.cos(rad);

    insetScaleEls[i].setAttribute('points', `${p1x},${p1y} ${p2x},${p2y} ${p3x},${p3y} ${p4x},${p4y}`);

    if (isGrip) {
      insetScaleEls[i].setAttribute('fill', `rgba(255,87,34,${0.4 + 0.5 * asym})`);
      insetScaleEls[i].setAttribute('stroke', '#FF5722');
      // 鳞片尖端下压
      insetScaleEls[i].setAttribute('transform', `translate(0,${2 * asym})`);
    } else {
      insetScaleEls[i].setAttribute('fill', `rgba(0,229,160,${0.25 + 0.35 * asym})`);
      insetScaleEls[i].setAttribute('stroke', '#00E5A0');
      insetScaleEls[i].setAttribute('transform', 'translate(0,0)');
    }
  }

  // 运动箭头
  while (insetArrowsG.firstChild) insetArrowsG.removeChild(insetArrowsG.firstChild);
  if (isGrip) {
    // 身体向左(后退),地面反力向右
    const arr = el('line', { x1: 250, y1: 68, x2: 200, y2: 68, stroke: '#FF5722', 'stroke-width': 2.5, 'stroke-linecap': 'round', 'marker-end': '' });
    insetArrowsG.appendChild(arr);
    const arr2 = el('line', { x1: 130, y1: 100, x2: 190, y2: 100, stroke: '#FFD54F', 'stroke-width': 2, 'stroke-linecap': 'round' });
    insetArrowsG.appendChild(arr2);
    const tx1 = el('text', { x: 255, y: 66, 'font-size': 8, fill: '#FF5722', 'font-family': "'Fira Code',monospace" });
    tx1.textContent = '后退';
    insetArrowsG.appendChild(tx1);
    const tx2 = el('text', { x: 192, y: 98, 'font-size': 8, fill: '#FFD54F', 'font-family': "'Fira Code',monospace" });
    tx2.textContent = '地面反力 →';
    insetArrowsG.appendChild(tx2);
  } else {
    const arr = el('line', { x1: 130, y1: 68, x2: 190, y2: 68, stroke: '#00E5A0', 'stroke-width': 2, 'stroke-linecap': 'round' });
    insetArrowsG.appendChild(arr);
    const tx1 = el('text', { x: 100, y: 66, 'font-size': 8, fill: '#00E5A0', 'font-family': "'Fira Code',monospace" });
    tx1.textContent = '→ 前进滑行';
    insetArrowsG.appendChild(tx1);
    const tx2 = el('text', { x: 140, y: 100, 'font-size': 8, fill: '#5E5A54', 'font-family': "'Fira Code',monospace" });
    tx2.textContent = '极小阻力';
    insetArrowsG.appendChild(tx2);
  }

  // 角度标注
  const angleArc = el('path', {
    d: `M 80,${baseY} A 12,12 0 0,0 ${80 - 12 * Math.cos(rad)},${baseY + 12 * Math.sin(rad)}`,
    fill: 'none', stroke: '#FFD54F', 'stroke-width': 0.8, opacity: 0.7
  });
  insetArrowsG.appendChild(angleArc);
  const angleTx = el('text', { x: 56, y: baseY + 14, 'font-size': 8, fill: '#FFD54F', 'font-family': "'Fira Code',monospace" });
  angleTx.textContent = `${a}°`;
  insetArrowsG.appendChild(angleTx);
}

/* ===== 粒子更新 ===== */
function updateParticles(dt) {
  for (const p of particles) {
    if (p.life > 0) {
      p.life -= dt * 2.5;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 40 * dt; // 重力
      p.el.setAttribute('cx', p.x);
      p.el.setAttribute('cy', p.y);
      p.el.setAttribute('opacity', clamp(p.life, 0, 1));
      p.el.setAttribute('r', 1.2 + p.life * 1.2);
    }
  }
}

/* ===== 动画循环 ===== */
let lastTS = 0;
function animate(ts) {
  const dt = Math.min((ts - lastTS) / 1000, 0.05);
  lastTS = ts;

  if (!S.paused) {
    S.time += dt * S.waveSpeed;
    S.dist += forwardSpeed() * dt;
  }

  computeSegments();
  render();
  updateParticles(dt);

  requestAnimationFrame(animate);
}

/* ===== 控件 ===== */
function initControls() {
  const cSpeed = document.getElementById('c-speed');
  const cAngle = document.getElementById('c-angle');
  const cRough = document.getElementById('c-rough');

  cSpeed.addEventListener('input', () => {
    S.waveSpeed = parseFloat(cSpeed.value);
    document.getElementById('v-speed').textContent = S.waveSpeed.toFixed(1);
  });
  cAngle.addEventListener('input', () => {
    S.scaleAngle = parseInt(cAngle.value);
    document.getElementById('v-angle').textContent = S.scaleAngle + '°';
  });
  cRough.addEventListener('input', () => {
    S.roughness = parseFloat(cRough.value);
    document.getElementById('v-rough').textContent = S.roughness.toFixed(2);
  });

  document.getElementById('btn-pause').addEventListener('click', function () {
    S.paused = !S.paused;
    this.textContent = S.paused ? '继续' : '暂停';
    this.classList.toggle('active', S.paused);
  });
  document.getElementById('btn-force').addEventListener('click', function () {
    S.showForce = !S.showForce;
    this.classList.toggle('active', S.showForce);
  });
  document.getElementById('btn-reset').addEventListener('click', () => {
    S.time = 0; S.dist = 0; S.trail = [];
    trailEl.setAttribute('points', '');
  });
}

/* ===== 启动 ===== */
initControls();
requestAnimationFrame(animate);
</script>
</body>
</html>

实现说明

本动画以深色技术美学为基调,完整呈现了仿生鳞片单向推进的 IFR 理想解原理:

核心视觉编码

  • **橙红色(#FF5722)**标识高摩擦抓地侧——鳞片倒刺扎入地面,产生锚定力
  • **薄荷绿(#00E5A0)**标识低摩擦滑行侧——鳞片顺滑放下,几乎无阻力
  • 摩擦边颜色强度随摩擦不对称度(μ_高 - μ_低)动态缩放,粗糙度越低,两侧颜色越趋同

IFR 体现

  • 右上角 IFR 标注框强调"被动鳞片自调节 + 零额外动力"——同一结构根据运动方向自动切换抓地/滑行,无需额外执行器
  • 鳞片微观 inset 同步展示当前活跃侧的力学机制:后退时鳞片下压锁死,前进时鳞片翘起滑行

交互控制

  • 波速滑块:调节 S 形行波传播速度,直接影响前进推力
  • 鳞片角度滑块:15°~75° 可调,45° 时 sin(2α) 最大、推力最优,偏离则明显衰减
  • 表面粗糙度滑块:降至 0 附近时模拟玻璃表面,摩擦差异消失,蛇体停止前进并弹出失效警告
  • 力向量开关:金色箭头显示各段净前向推力分量
  • 暂停/重置:便于观察特定时刻的力学状态
积分规则:第一轮对话扣减6分,后续每轮扣4分