分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMA仿生蛇 · 无驱动器蜿蜒爬行原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
  --bg: #060a14;
  --surface: #0c1424;
  --surface-2: #111d30;
  --border: #1a2d48;
  --text: #c8d8ea;
  --text-dim: #4a6080;
  --accent: #00e8a2;
  --accent-dim: rgba(0,232,162,0.12);
  --hot: #ff6b35;
  --hot-glow: rgba(255,107,53,0.45);
  --cool-left: #3d5a78;
  --cool-right: #3d5a78;
  --active-left: #ff6b35;
  --active-right: #00b8d4;
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;overflow-x:hidden;}
body{
  background:var(--bg);
  color:var(--text);
  font-family:'Noto Sans SC','Rajdhani',sans-serif;
  display:flex;flex-direction:column;align-items:center;
  min-height:100vh;
}
header{
  width:100%;padding:18px 32px 10px;
  display:flex;align-items:baseline;gap:18px;flex-wrap:wrap;
  border-bottom:1px solid var(--border);
  background:linear-gradient(180deg,rgba(12,20,36,0.95),transparent);
  position:relative;z-index:2;
}
header h1{
  font-family:'Rajdhani','Noto Sans SC',sans-serif;
  font-weight:700;font-size:clamp(22px,3.2vw,34px);
  letter-spacing:1px;color:#e8f0fa;
}
header h1 .dot{color:var(--accent);}
header p{
  font-size:clamp(12px,1.4vw,15px);color:var(--text-dim);
  font-weight:300;letter-spacing:0.5px;
}
.main-wrap{
  flex:1;width:100%;max-width:1440px;
  display:flex;flex-direction:column;
  padding:0 16px 16px;
}
.canvas-container{
  position:relative;width:100%;
  aspect-ratio:2.6/1;min-height:320px;max-height:560px;
  border-radius:12px;overflow:hidden;
  border:1px solid var(--border);
  background:var(--surface);
  margin-top:12px;
}
.canvas-container canvas{width:100%;height:100%;display:block;}
.ifs-tag{
  position:absolute;top:14px;right:18px;
  background:rgba(0,232,162,0.08);border:1px solid rgba(0,232,162,0.25);
  border-radius:6px;padding:5px 14px;
  font-family:'Rajdhani',sans-serif;font-size:13px;font-weight:600;
  color:var(--accent);letter-spacing:1.5px;text-transform:uppercase;
  pointer-events:none;
}
.bottom-panel{
  display:grid;grid-template-columns:220px 1fr 260px;gap:14px;
  margin-top:14px;width:100%;
}
@media(max-width:900px){
  .bottom-panel{grid-template-columns:1fr;max-width:520px;}
}
.panel-card{
  background:var(--surface);border:1px solid var(--border);
  border-radius:10px;padding:14px;overflow:hidden;
}
.panel-card h3{
  font-family:'Rajdhani',sans-serif;font-size:13px;font-weight:600;
  color:var(--accent);letter-spacing:1.5px;text-transform:uppercase;
  margin-bottom:10px;
}
.cross-svg{width:100%;max-width:190px;display:block;margin:0 auto;}
.phase-canvas{width:100%;height:70px;display:block;border-radius:6px;}
.ctrl-group{margin-bottom:12px;}
.ctrl-group:last-child{margin-bottom:0;}
.ctrl-label{
  display:flex;justify-content:space-between;align-items:center;
  font-size:12px;color:var(--text-dim);margin-bottom:4px;
}
.ctrl-label span.val{color:var(--accent);font-family:'Rajdhani',sans-serif;font-weight:600;}
input[type=range]{
  -webkit-appearance:none;width:100%;height:4px;border-radius:2px;
  background:var(--border);outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;border-radius:50%;
  background:var(--accent);border:2px solid var(--bg);cursor:pointer;
}
.toggle-row{
  display:flex;align-items:center;gap:8px;
  font-size:12px;color:var(--text-dim);margin-bottom:8px;
}
.toggle-row input[type=checkbox]{accent-color:var(--accent);cursor:pointer;}
.legend-bar{
  display:flex;align-items:center;gap:20px;flex-wrap:wrap;
  margin-top:14px;padding:10px 16px;
  background:var(--surface);border:1px solid var(--border);
  border-radius:8px;font-size:12px;color:var(--text-dim);
}
.legend-item{display:flex;align-items:center;gap:6px;}
.legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.legend-line{width:18px;height:3px;border-radius:2px;flex-shrink:0;}

@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
.main-wrap{animation:fadeIn 0.8s ease-out both;}
.bottom-panel .panel-card:nth-child(1){animation:fadeIn 0.6s 0.2s ease-out both;}
.bottom-panel .panel-card:nth-child(2){animation:fadeIn 0.6s 0.35s ease-out both;}
.bottom-panel .panel-card:nth-child(3){animation:fadeIn 0.6s 0.5s ease-out both;}
</style>
</head>
<body>

<header>
  <h1><span class="dot">⬡</span> SMA仿生蛇 · 蜿蜒爬行原理</h1>
  <p>形状记忆合金弹簧 × 碳纤维骨架 — 消除一切宏观驱动器的最终理想解</p>
</header>

<div class="main-wrap">
  <div class="canvas-container">
    <canvas id="mainCanvas"></canvas>
    <div class="ifs-tag">IFR · Ideal Final Result</div>
  </div>

  <div class="bottom-panel">
    <!-- 截面图 -->
    <div class="panel-card">
      <h3>Cross Section · 截面构型</h3>
      <svg class="cross-svg" viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg">
        <!-- 扁椭圆轮廓 -->
        <ellipse cx="100" cy="90" rx="72" ry="36" fill="#141e30" stroke="#2a3e58" stroke-width="1.5"/>
        <!-- 碳纤维脊柱 -->
        <rect x="38" y="82" width="124" height="6" rx="2" fill="#0e1520" stroke="#1a2d44" stroke-width="0.8"/>
        <line x1="50" y1="82" x2="50" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="65" y1="82" x2="65" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="80" y1="82" x2="80" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="95" y1="82" x2="95" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="110" y1="82" x2="110" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="125" y1="82" x2="125" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="140" y1="82" x2="140" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <line x1="155" y1="82" x2="155" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
        <!-- 左SMA弹簧 -->
        <path d="M34,72 L40,68 L34,64 L40,60 L34,56" fill="none" stroke="#ff6b35" stroke-width="1.8" stroke-linecap="round"/>
        <circle cx="34" cy="72" r="2" fill="#ff6b35"/>
        <circle cx="34" cy="56" r="2" fill="#ff6b35"/>
        <!-- 右SMA弹簧 -->
        <path d="M166,72 L160,68 L166,64 L160,60 L166,56" fill="none" stroke="#00b8d4" stroke-width="1.8" stroke-linecap="round"/>
        <circle cx="166" cy="72" r="2" fill="#00b8d4"/>
        <circle cx="166" cy="56" r="2" fill="#00b8d4"/>
        <!-- 腹部倒刺 -->
        <line x1="60" y1="124" x2="55" y2="130" stroke="#2a3e58" stroke-width="1"/>
        <line x1="72" y1="125" x2="67" y2="132" stroke="#2a3e58" stroke-width="1"/>
        <line x1="84" y1="126" x2="79" y2="133" stroke="#2a3e58" stroke-width="1"/>
        <line x1="96" y1="126" x2="91" y2="133" stroke="#2a3e58" stroke-width="1"/>
        <line x1="108" y1="126" x2="103" y2="133" stroke="#2a3e58" stroke-width="1"/>
        <line x1="120" y1="125" x2="115" y2="132" stroke="#2a3e58" stroke-width="1"/>
        <line x1="132" y1="124" x2="127" y2="130" stroke="#2a3e58" stroke-width="1"/>
        <!-- 标注 -->
        <text x="16" y="50" font-size="8" fill="#ff6b35" font-family="Rajdhani,sans-serif" font-weight="600">SMA-L</text>
        <text x="162" y="50" font-size="8" fill="#00b8d4" font-family="Rajdhani,sans-serif" font-weight="600">SMA-R</text>
        <text x="74" y="79" font-size="7" fill="#4a6080" font-family="Rajdhani,sans-serif">CARBON SPINE</text>
        <text x="68" y="146" font-size="7" fill="#4a6080" font-family="Rajdhani,sans-serif">TEFLON BARBS ▸</text>
      </svg>
    </div>

    <!-- 相位图 -->
    <div class="panel-card">
      <h3>Phase · SMA激活相位</h3>
      <canvas id="phaseCanvas" class="phase-canvas"></canvas>
    </div>

    <!-- 控制 -->
    <div class="panel-card">
      <h3>Controls · 变量控制</h3>
      <div class="ctrl-group">
        <div class="ctrl-label"><span>波速 Wave Speed</span><span class="val" id="speedVal">1.0x</span></div>
        <input type="range" id="speedSlider" min="0.2" max="3" step="0.1" value="1">
      </div>
      <div class="ctrl-group">
        <div class="ctrl-label"><span>波幅 Amplitude</span><span class="val" id="ampVal">1.0x</span></div>
        <input type="range" id="ampSlider" min="0.2" max="2" step="0.1" value="1">
      </div>
      <div class="ctrl-group">
        <div class="ctrl-label"><span>蜿蜒波数 Waves</span><span class="val" id="waveVal">2.5</span></div>
        <input type="range" id="waveSlider" min="1" max="4" step="0.5" value="2.5">
      </div>
      <div class="toggle-row">
        <input type="checkbox" id="showAnnot" checked>
        <label for="showAnnot">显示标注 Annotations</label>
      </div>
      <div class="toggle-row">
        <input type="checkbox" id="showForce" checked>
        <label for="showForce">显示收缩力 Force Arrows</label>
      </div>
    </div>
  </div>

  <div class="legend-bar">
    <div class="legend-item"><div class="legend-dot" style="background:#ff6b35;box-shadow:0 0 6px #ff6b35;"></div>左侧SMA收缩 (加热态)</div>
    <div class="legend-item"><div class="legend-dot" style="background:#00b8d4;box-shadow:0 0 6px #00b8d4;"></div>右侧SMA收缩 (加热态)</div>
    <div class="legend-item"><div class="legend-dot" style="background:#3d5a78;"></div>SMA冷却态 (马氏体)</div>
    <div class="legend-item"><div class="legend-line" style="background:#0e1520;border:1px solid #1a2d44;"></div>碳纤维脊柱</div>
    <div class="legend-item"><div class="legend-line" style="background:repeating-linear-gradient(90deg,#2a3e58 0 2px,transparent 2px 5px);"></div>腹部倒刺 (特氟龙)</div>
  </div>
</div>

<script>
/* ==============================
   配置与状态
   ============================== */
const CFG = {
  numSegments: 50,     // 蛇体采样点数
  snakeLength: 0,      // 将在resize时计算
  amplitude: 50,       // 蜿蜒幅度(px)
  numWaves: 2.5,       // 蜿蜒波数
  omega: 2.0,          // 角频率(rad/s)
  maxBodyWidth: 15,    // 最大半宽(px)
  groundSpeed: 40,     // 地面滚动速度(px/s)
  particleRate: 0.35,  // 粒子生成概率
};

const STATE = {
  time: 0,
  speedMul: 1.0,
  ampMul: 1.0,
  showAnnot: true,
  showForce: true,
  particles: [],
  dpr: 1,
  cw: 0, ch: 0, // canvas逻辑尺寸
};

/* ==============================
   Canvas初始化
   ============================== */
const mainCanvas = document.getElementById('mainCanvas');
const ctx = mainCanvas.getContext('2d');
const phaseCanvas = document.getElementById('phaseCanvas');
const pctx = phaseCanvas.getContext('2d');

function resizeCanvas() {
  STATE.dpr = window.devicePixelRatio || 1;
  const container = mainCanvas.parentElement;
  const rect = container.getBoundingClientRect();
  STATE.cw = rect.width;
  STATE.ch = rect.height;
  mainCanvas.width = rect.width * STATE.dpr;
  mainCanvas.height = rect.height * STATE.dpr;
  mainCanvas.style.width = rect.width + 'px';
  mainCanvas.style.height = rect.height + 'px';
  ctx.setTransform(STATE.dpr, 0, 0, STATE.dpr, 0, 0);

  // 蛇体长度自适应
  CFG.snakeLength = Math.min(STATE.cw * 0.68, 800);

  // 相位canvas
  const pr = phaseCanvas.parentElement.getBoundingClientRect();
  const pw = pr.width - 28;
  phaseCanvas.width = pw * STATE.dpr;
  phaseCanvas.height = 70 * STATE.dpr;
  phaseCanvas.style.width = pw + 'px';
  phaseCanvas.style.height = '70px';
  pctx.setTransform(STATE.dpr, 0, 0, STATE.dpr, 0, 0);
}

/* ==============================
   蛇体计算
   ============================== */
function computeSnake(time) {
  const N = CFG.numSegments;
  const L = CFG.snakeLength;
  const A = CFG.amplitude * STATE.ampMul;
  const kW = CFG.numWaves;
  const w = CFG.omega * STATE.speedMul;
  const pts = [];

  for (let i = 0; i <= N; i++) {
    const s = i / N;
    const phase = 2 * Math.PI * kW * s - w * time;

    const x = s * L;
    const y = A * Math.sin(phase);

    // 切线方向
    const dyds = A * 2 * Math.PI * kW * Math.cos(phase);
    const heading = Math.atan2(dyds, L / N * N);

    // 曲率符号 → SMA激活
    const curvSign = -Math.sin(phase);

    pts.push({
      x, y, heading, s,
      leftAct:  Math.max(0, curvSign),
      rightAct: Math.max(0, -curvSign),
    });
  }
  return pts;
}

/* 身体半宽(头尾渐缩) */
function bodyHalfW(s) {
  const headT = Math.min(1, s * 7);
  const tailT = Math.min(1, (1 - s) * 4.5);
  return CFG.maxBodyWidth * headT * tailT;
}

/* ==============================
   绘图工具
   ============================== */

/* 绘制平滑闭合路径(左轮廓 → 右轮廓反向) */
function drawSmoothClosedPath(c, leftPts, rightPts) {
  c.beginPath();
  // 左侧 head→tail
  smoothLineTo(c, leftPts);
  // 右侧 tail→head
  smoothLineTo(c, rightPts.reverse());
  c.closePath();
}

function smoothLineTo(c, pts) {
  if (pts.length < 2) return;
  c.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) {
    const prev = pts[i - 1];
    const curr = pts[i];
    const mx = (prev.x + curr.x) / 2;
    const my = (prev.y + curr.y) / 2;
    c.quadraticCurveTo(prev.x, prev.y, mx, my);
  }
  const last = pts[pts.length - 1];
  c.lineTo(last.x, last.y);
}

/* 颜色插值 */
function lerpColor(a, b, t) {
  t = Math.max(0, Math.min(1, t));
  const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16);
  const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16);
  const r = Math.round(ar + (br - ar) * t);
  const g = Math.round(ag + (bg - ag) * t);
  const bl = Math.round(ab + (bb - ab) * t);
  return `rgb(${r},${g},${bl})`;
}

/* ==============================
   粒子系统(SMA加热产生的微光粒子)
   ============================== */
function spawnParticle(x, y, side) {
  STATE.particles.push({
    x, y,
    vx: (Math.random() - 0.5) * 12,
    vy: -Math.random() * 18 - 6,
    life: 1.0,
    decay: 0.6 + Math.random() * 0.8,
    size: 1.5 + Math.random() * 2,
    side, // 'left' or 'right'
  });
}

function updateParticles(dt) {
  for (let i = STATE.particles.length - 1; i >= 0; i--) {
    const p = STATE.particles[i];
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 10 * dt; // 微重力
    p.life -= p.decay * dt;
    if (p.life <= 0) STATE.particles.splice(i, 1);
  }
}

/* ==============================
   主绘图
   ============================== */
function drawScene(pts, time) {
  const W = STATE.cw, H = STATE.ch;
  const ox = (W - CFG.snakeLength) / 2; // 居中偏移
  const oy = H * 0.48;

  ctx.clearRect(0, 0, W, H);

  // —— 背景渐变 ——
  const bgGrad = ctx.createRadialGradient(W/2, H*0.45, 0, W/2, H*0.45, W*0.6);
  bgGrad.addColorStop(0, '#0f1a2c');
  bgGrad.addColorStop(1, '#060a14');
  ctx.fillStyle = bgGrad;
  ctx.fillRect(0, 0, W, H);

  // —— 地面网格(显示前进运动) ——
  drawGround(time, W, H, oy);

  ctx.save();
  ctx.translate(ox, oy);

  // —— 蛇体阴影 ——
  drawShadow(pts);

  // —— 蛇体轮廓 ——
  drawBody(pts);

  // —— 碳纤维脊柱 ——
  drawSpine(pts);

  // —— SMA弹簧 ——
  drawSMASprings(pts, time);

  // —— 腹部倒刺 ——
  drawBarbs(pts);

  // —— 收缩力箭头 ——
  if (STATE.showForce) drawForceArrows(pts);

  // —— 粒子 ——
  drawParticles();

  // —— 标注 ——
  if (STATE.showAnnot) drawAnnotations(pts, time);

  ctx.restore();

  // —— 前进方向指示 ——
  drawForwardIndicator(W, H, oy, time);
}

/* 地面网格 */
function drawGround(time, W, H, oy) {
  const spacing = 32;
  const offset = (time * CFG.groundSpeed * STATE.speedMul) % spacing;
  ctx.save();
  ctx.globalAlpha = 0.08;
  ctx.strokeStyle = '#3a6090';
  ctx.lineWidth = 0.5;
  // 竖线
  for (let x = -offset; x < W + spacing; x += spacing) {
    ctx.beginPath();
    ctx.moveTo(x, oy + 50);
    ctx.lineTo(x, H);
    ctx.stroke();
  }
  // 横线
  for (let y = oy + 50; y < H; y += spacing) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(W, y);
    ctx.stroke();
  }
  ctx.restore();
}

/* 阴影 */
function drawShadow(pts) {
  if (pts.length < 2) return;
  const leftP = [], rightP = [];
  for (const p of pts) {
    const hw = bodyHalfW(p.s);
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);
    leftP.push({ x: p.x + nx * hw, y: p.y + ny * hw + 18 });
    rightP.push({ x: p.x - nx * hw, y: p.y - ny * hw + 18 });
  }
  ctx.save();
  ctx.globalAlpha = 0.18;
  drawSmoothClosedPath(ctx, leftP, rightP.slice());
  ctx.fillStyle = '#000000';
  ctx.filter = 'blur(8px)';
  ctx.fill();
  ctx.filter = 'none';
  ctx.restore();
}

/* 蛇体 */
function drawBody(pts) {
  if (pts.length < 2) return;
  const leftP = [], rightP = [];
  for (const p of pts) {
    const hw = bodyHalfW(p.s);
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);
    leftP.push({ x: p.x + nx * hw, y: p.y + ny * hw });
    rightP.push({ x: p.x - nx * hw, y: p.y - ny * hw });
  }

  // 主填充
  drawSmoothClosedPath(ctx, leftP, rightP.slice());
  const bGrad = ctx.createLinearGradient(0, -CFG.maxBodyWidth, 0, CFG.maxBodyWidth);
  bGrad.addColorStop(0, '#263548');
  bGrad.addColorStop(0.4, '#1a2838');
  bGrad.addColorStop(1, '#15202e');
  ctx.fillStyle = bGrad;
  ctx.fill();

  // 边缘高光
  ctx.strokeStyle = '#2e4462';
  ctx.lineWidth = 0.8;
  ctx.stroke();

  // 段分割线
  ctx.save();
  ctx.globalAlpha = 0.15;
  ctx.strokeStyle = '#3a5a7a';
  ctx.lineWidth = 0.5;
  for (let i = 2; i < pts.length - 2; i += 3) {
    const p = pts[i];
    const hw = bodyHalfW(p.s);
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);
    ctx.beginPath();
    ctx.moveTo(p.x + nx * hw, p.y + ny * hw);
    ctx.lineTo(p.x - nx * hw, p.y - ny * hw);
    ctx.stroke();
  }
  ctx.restore();
}

/* 碳纤维脊柱 */
function drawSpine(pts) {
  if (pts.length < 2) return;
  ctx.save();
  // 主线
  ctx.beginPath();
  smoothLineTo(ctx, pts.map(p => ({ x: p.x, y: p.y })));
  ctx.strokeStyle = '#0c1420';
  ctx.lineWidth = 2.5;
  ctx.stroke();

  // 交叉纹
  ctx.globalAlpha = 0.25;
  ctx.strokeStyle = '#1a2d44';
  ctx.lineWidth = 0.6;
  for (let i = 2; i < pts.length - 2; i += 2) {
    const p = pts[i];
    const tx = Math.cos(p.heading);
    const ty = Math.sin(p.heading);
    const hw = bodyHalfW(p.s) * 0.6;
    ctx.beginPath();
    ctx.moveTo(p.x - ty * hw, p.y + tx * hw);
    ctx.lineTo(p.x + ty * hw, p.y - tx * hw);
    ctx.stroke();
  }
  ctx.restore();
}

/* SMA弹簧 */
function drawSMASprings(pts, time) {
  for (let i = 1; i < pts.length - 1; i += 2) {
    const p = pts[i];
    const pn = pts[Math.min(i + 2, pts.length - 1)];
    const hw = bodyHalfW(p.s) * 0.85;

    // 法线方向
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);

    // 左侧弹簧
    const lx1 = p.x + nx * hw;
    const ly1 = p.y + ny * hw;
    const lx2 = pn.x + nx * hw;
    const ly2 = pn.y + ny * hw;
    drawSpring(lx1, ly1, lx2, ly2, p.leftAct, 'left');

    // 右侧弹簧
    const rx1 = p.x - nx * hw;
    const ry1 = p.y - ny * hw;
    const rx2 = pn.x - nx * hw;
    const ry2 = pn.y - ny * hw;
    drawSpring(rx1, ry1, rx2, ry2, p.rightAct, 'right');

    // 粒子生成
    if (p.leftAct > 0.5 && Math.random() < CFG.particleRate) {
      spawnParticle(lx1, ly1, 'left');
    }
    if (p.rightAct > 0.5 && Math.random() < CFG.particleRate) {
      spawnParticle(rx1, ry1, 'right');
    }
  }
}

function drawSpring(x1, y1, x2, y2, activation, side) {
  const dx = x2 - x1;
  const dy = y2 - y1;
  const len = Math.max(1, Math.sqrt(dx * dx + dy * dy));
  const ux = dx / len;
  const uy = dy / len;
  const nx = -uy;
  const ny = ux;

  const coils = 4;
  const ampBase = 2.5;
  const ampAct = activation * 2.5;
  const sAmp = ampBase + ampAct;

  // 颜色
  const coolColor = '#3d5a78';
  const hotColor = side === 'left' ? '#ff6b35' : '#00b8d4';
  const color = lerpColor(coolColor, hotColor, activation);

  ctx.save();
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  for (let c = 1; c <= coils * 2; c++) {
    const t = c / (coils * 2 + 1);
    const sign = c % 2 === 1 ? 1 : -1;
    const px = x1 + dx * t + nx * sAmp * sign;
    const py = y1 + dy * t + ny * sAmp * sign;
    ctx.lineTo(px, py);
  }
  ctx.lineTo(x2, y2);
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.3 + activation * 0.8;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.stroke();

  // 激活发光
  if (activation > 0.3) {
    ctx.shadowColor = side === 'left' ? 'rgba(255,107,53,0.6)' : 'rgba(0,184,212,0.6)';
    ctx.shadowBlur = 6 + activation * 8;
    ctx.stroke();
    ctx.shadowBlur = 0;
  }
  ctx.restore();
}

/* 腹部倒刺 */
function drawBarbs(pts) {
  ctx.save();
  ctx.globalAlpha = 0.3;
  ctx.strokeStyle = '#2e4a62';
  ctx.lineWidth = 0.8;
  for (let i = 3; i < pts.length - 3; i += 4) {
    const p = pts[i];
    const hw = bodyHalfW(p.s);
    // 腹部 = 身体下方
    const bx = p.x + Math.sin(p.heading) * hw * 0.7;
    const by = p.y + Math.cos(p.heading) * hw * 0.7;
    // 倒刺方向:指向尾部+下方
    const tx = -Math.cos(p.heading);
    const ty = -Math.sin(p.heading);
    ctx.beginPath();
    ctx.moveTo(bx, by);
    ctx.lineTo(bx + tx * 4 + ty * 2, by + ty * 4 - tx * 2);
    ctx.stroke();
  }
  ctx.restore();
}

/* 收缩力箭头 */
function drawForceArrows(pts) {
  ctx.save();
  const step = 6;
  for (let i = step; i < pts.length - step; i += step) {
    const p = pts[i];
    const hw = bodyHalfW(p.s);

    // 左侧
    if (p.leftAct > 0.4) {
      const nx = -Math.sin(p.heading);
      const ny =  Math.cos(p.heading);
      const sx = p.x + nx * (hw + 10);
      const sy = p.y + ny * (hw + 10);
      const ex = p.x + nx * (hw + 2);
      const ey = p.y + ny * (hw + 2);
      drawArrow(sx, sy, ex, ey, p.leftAct, '#ff6b35');
    }

    // 右侧
    if (p.rightAct > 0.4) {
      const nx = -Math.sin(p.heading);
      const ny =  Math.cos(p.heading);
      const sx = p.x - nx * (hw + 10);
      const sy = p.y - ny * (hw + 10);
      const ex = p.x - nx * (hw + 2);
      const ey = p.y - ny * (hw + 2);
      drawArrow(sx, sy, ex, ey, p.rightAct, '#00b8d4');
    }
  }
  ctx.restore();
}

function drawArrow(x1, y1, x2, y2, alpha, color) {
  const dx = x2 - x1, dy = y2 - y1;
  const len = Math.max(1, Math.sqrt(dx*dx + dy*dy));
  const ux = dx/len, uy = dy/len;

  ctx.save();
  ctx.globalAlpha = alpha * 0.7;
  ctx.strokeStyle = color;
  ctx.fillStyle = color;
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();

  // 箭头
  const aLen = 4;
  ctx.beginPath();
  ctx.moveTo(x2, y2);
  ctx.lineTo(x2 - ux*aLen + uy*2.5, y2 - uy*aLen - ux*2.5);
  ctx.lineTo(x2 - ux*aLen - uy*2.5, y2 - uy*aLen + ux*2.5);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

/* 粒子绘制 */
function drawParticles() {
  for (const p of STATE.particles) {
    ctx.save();
    const alpha = p.life * 0.8;
    const color = p.side === 'left' ? `rgba(255,130,60,${alpha})` : `rgba(0,200,220,${alpha})`;
    ctx.fillStyle = color;
    ctx.shadowColor = color;
    ctx.shadowBlur = 4;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

/* 标注 */
function drawAnnotations(pts, time) {
  ctx.save();
  ctx.font = '600 11px Rajdhani, Noto Sans SC, sans-serif';

  // 找到激活最强的左侧和右侧段
  let maxL = 0, maxLI = 0, maxR = 0, maxRI = 0;
  for (let i = 5; i < pts.length - 5; i++) {
    if (pts[i].leftAct > maxL) { maxL = pts[i].leftAct; maxLI = i; }
    if (pts[i].rightAct > maxR) { maxR = pts[i].rightAct; maxRI = i; }
  }

  // 左侧SMA标注
  if (maxL > 0.6) {
    const p = pts[maxLI];
    const hw = bodyHalfW(p.s);
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);
    const lx = p.x + nx * (hw + 26);
    const ly = p.y + ny * (hw + 26);
    ctx.fillStyle = '#ff6b35';
    ctx.textAlign = 'center';
    ctx.fillText('SMA-L 收缩', lx, ly - 2);
    ctx.globalAlpha = 0.5;
    ctx.strokeStyle = '#ff6b35';
    ctx.lineWidth = 0.6;
    ctx.setLineDash([3,3]);
    ctx.beginPath();
    ctx.moveTo(p.x + nx * (hw + 4), p.y + ny * (hw + 4));
    ctx.lineTo(lx, ly + 4);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.globalAlpha = 1;
  }

  // 右侧SMA标注
  if (maxR > 0.6) {
    const p = pts[maxRI];
    const hw = bodyHalfW(p.s);
    const nx = -Math.sin(p.heading);
    const ny =  Math.cos(p.heading);
    const rx = p.x - nx * (hw + 26);
    const ry = p.y - ny * (hw + 26);
    ctx.fillStyle = '#00b8d4';
    ctx.textAlign = 'center';
    ctx.fillText('SMA-R 收缩', rx, ry - 2);
    ctx.globalAlpha = 0.5;
    ctx.strokeStyle = '#00b8d4';
    ctx.lineWidth = 0.6;
    ctx.setLineDash([3,3]);
    ctx.beginPath();
    ctx.moveTo(p.x - nx * (hw + 4), p.y - ny * (hw + 4));
    ctx.lineTo(rx, ry + 4);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.globalAlpha = 1;
  }

  // 脊柱标注
  const si = Math.floor(pts.length * 0.2);
  const sp = pts[si];
  ctx.fillStyle = '#4a6080';
  ctx.textAlign = 'left';
  ctx.fillText('碳纤维脊柱', sp.x + 8, sp.y - 18);
  ctx.globalAlpha = 0.35;
  ctx.strokeStyle = '#4a6080';
  ctx.lineWidth = 0.5;
  ctx.setLineDash([2,2]);
  ctx.beginPath();
  ctx.moveTo(sp.x, sp.y);
  ctx.lineTo(sp.x + 6, sp.y - 12);
  ctx.stroke();
  ctx.setLineDash([]);
  ctx.globalAlpha = 1;

  // 波传播方向
  const wi = Math.floor(pts.length * 0.75);
  const wp = pts[wi];
  ctx.fillStyle = '#00e8a2';
  ctx.font = '600 10px Rajdhani, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('WAVE →', wp.x, wp.y + bodyHalfW(wp.s) + 28);

  ctx.restore();
}

/* 前进方向指示 */
function drawForwardIndicator(W, H, oy, time) {
  const cx = W - 70;
  const cy = oy - 20;
  ctx.save();
  ctx.fillStyle = '#00e8a2';
  ctx.font = '600 11px Rajdhani, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('FWD', cx, cy - 8);

  // 动态箭头
  const pulse = 0.5 + 0.5 * Math.sin(time * 3);
  ctx.globalAlpha = 0.5 + pulse * 0.4;
  ctx.strokeStyle = '#00e8a2';
  ctx.lineWidth = 2;
  ctx.lineCap = 'round';
  const ax = cx - 8 + pulse * 6;
  ctx.beginPath();
  ctx.moveTo(ax, cy);
  ctx.lineTo(ax + 12, cy);
  ctx.moveTo(ax + 8, cy - 4);
  ctx.lineTo(ax + 12, cy);
  ctx.lineTo(ax + 8, cy + 4);
  ctx.stroke();
  ctx.restore();
}

/* ==============================
   相位图绘制
   ============================== */
function drawPhaseDiagram(pts) {
  const w = phaseCanvas.width / STATE.dpr;
  const h = 70;
  pctx.clearRect(0, 0, w, h);

  // 背景
  pctx.fillStyle = '#0a1220';
  pctx.fillRect(0, 0, w, h);

  if (pts.length < 2) return;

  const barW = Math.max(1, (w - 20) / pts.length);
  const midY = h / 2;
  const maxH = midY - 6;

  for (let i = 0; i < pts.length; i++) {
    const p = pts[i];
    const x = 10 + i * barW;

    // 左侧SMA (上半)
    if (p.leftAct > 0.05) {
      const bh = p.leftAct * maxH;
      const alpha = 0.3 + p.leftAct * 0.7;
      pctx.fillStyle = `rgba(255,107,53,${alpha})`;
      pctx.fillRect(x, midY - bh, barW - 0.5, bh);
    }

    // 右侧SMA (下半)
    if (p.rightAct > 0.05) {
      const bh = p.rightAct * maxH;
      const alpha = 0.3 + p.rightAct * 0.7;
      pctx.fillStyle = `rgba(0,184,212,${alpha})`;
      pctx.fillRect(x, midY, barW - 0.5, bh);
    }
  }

  // 中线
  pctx.strokeStyle = '#1a2d48';
  pctx.lineWidth = 0.5;
  pctx.beginPath();
  pctx.moveTo(10, midY);
  pctx.lineTo(w - 10, midY);
  pctx.stroke();

  // 标签
  pctx.font = '600 9px Rajdhani, sans-serif';
  pctx.fillStyle = '#ff6b35';
  pctx.textAlign = 'left';
  pctx.fillText('L', 2, 12);
  pctx.fillStyle = '#00b8d4';
  pctx.fillText('R', 2, h - 4);

  // HEAD/TAIL标签
  pctx.fillStyle = '#4a6080';
  pctx.font = '500 8px Rajdhani, sans-serif';
  pctx.textAlign = 'left';
  pctx.fillText('HEAD', 12, h - 3);
  pctx.textAlign = 'right';
  pctx.fillText('TAIL', w - 12, h - 3);
}

/* ==============================
   控件绑定
   ============================== */
function bindControls() {
  const speedSlider = document.getElementById('speedSlider');
  const ampSlider = document.getElementById('ampSlider');
  const waveSlider = document.getElementById('waveSlider');
  const showAnnot = document.getElementById('showAnnot');
  const showForce = document.getElementById('showForce');

  speedSlider.addEventListener('input', () => {
    STATE.speedMul = parseFloat(speedSlider.value);
    document.getElementById('speedVal').textContent = STATE.speedMul.toFixed(1) + 'x';
  });
  ampSlider.addEventListener('input', () => {
    STATE.ampMul = parseFloat(ampSlider.value);
    document.getElementById('ampVal').textContent = STATE.ampMul.toFixed(1) + 'x';
  });
  waveSlider.addEventListener('input', () => {
    CFG.numWaves = parseFloat(waveSlider.value);
    document.getElementById('waveVal').textContent = CFG.numWaves.toFixed(1);
  });
  showAnnot.addEventListener('change', () => { STATE.showAnnot = showAnnot.checked; });
  showForce.addEventListener('change', () => { STATE.showForce = showForce.checked; });
}

/* ==============================
   动画主循环
   ============================== */
let lastTime = 0;
let startTime = 0;
let animStarted = false;

function animate(timestamp) {
  if (!animStarted) {
    startTime = timestamp;
    lastTime = timestamp;
    animStarted = true;
  }

  const dt = Math.min(0.05, (timestamp - lastTime) / 1000);
  lastTime = timestamp;

  // 入场渐显 (前2秒)
  const elapsed = (timestamp - startTime) / 1000;
  STATE.time = elapsed;

  // 入场波幅渐增
  const introFade = Math.min(1, elapsed / 1.8);
  const savedAmpMul = STATE.ampMul;
  STATE.ampMul *= introFade;

  const pts = computeSnake(STATE.time);
  STATE.ampMul = savedAmpMul; // 恢复

  updateParticles(dt);
  drawScene(pts, STATE.time);
  drawPhaseDiagram(pts);

  requestAnimationFrame(animate);
}

/* ==============================
   初始化
   ============================== */
function init() {
  resizeCanvas();
  bindControls();
  window.addEventListener('resize', () => {
    resizeCanvas();
  });
  requestAnimationFrame(animate);
}

// 页面加载后自动启动
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}
</script>
</body>
</html>

这个实现包含以下核心要素:

IFR(最终理想解)可视化策略

  • 直接展示消除电机/气泵后的理想工作状态——蛇体仅靠SMA弹簧的交替收缩实现蜿蜒波,无需任何传统驱动器
  • 资源利用层面,SMA弹簧同时充当"执行器+结构"的双重角色,碳纤维薄片同时提供背腹柔性与侧向约束,通过截面图和脊柱交叉纹清晰呈现
  • 视觉引导上,激活态的SMA弹簧以醒目的暖橙(左侧)/冷青(右侧)发光标记,并伴有微光粒子上浮效果,冷却态回归灰蓝,形成强烈的对比引导

动画核心机制

  • 蛇体中心线由正弦行波 y = A·sin(2πk·s − ωt) 驱动,曲率符号决定左右SMA的激活状态
  • 弹簧以锯齿线绘制,激活时增粗、加光晕、并发射热辐射粒子;冷却时收细变灰
  • 地面网格持续左移暗示蛇体前进,腹部倒刺以小斜线标示单向摩擦
  • 收缩力箭头从弹簧外侧指向蛇体,直观展示"通电→收缩→拉弯脊柱"的因果链

交互控制

  • 波速、波幅、波数三个滑块可实时调节蜿蜒参数
  • 标注与力箭头可独立开关
  • 相位图实时显示从头到尾的SMA激活分布,左右分色清晰呈现行波传递

自动播放:页面加载后1.8秒内波幅从零渐增至满幅,蛇体自然"苏醒"开始蜿蜒爬行,刷新/重新加载即重新播放。

积分规则:第一轮对话扣减8分,后续每轮扣6分