分享图
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 href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;600;800;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root{--bg:#080c18;--panel:#0d1525;--fg:#e2e8f0;--muted:#64748b;--accent:#00e5a0;--accent2:#ff8c00;--accent3:#00b4d8;--danger:#ef4444;--card:#111b30;--border:#1c2a45;--glow:0 0 20px rgba(0,229,160,.3)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Noto Sans SC','Exo 2',sans-serif;height:100vh;display:flex;flex-direction:column;overflow:hidden;user-select:none}
header{padding:12px 24px;display:flex;align-items:center;gap:16px;background:linear-gradient(180deg,rgba(13,21,37,.95),rgba(8,12,24,.8));border-bottom:1px solid var(--border);z-index:10;flex-shrink:0}
header .logo{width:36px;height:36px;border-radius:8px;background:linear-gradient(135deg,var(--accent),var(--accent3));display:flex;align-items:center;justify-content:center;font-family:'Exo 2';font-weight:900;font-size:18px;color:#080c18;flex-shrink:0}
header h1{font-family:'Exo 2',sans-serif;font-weight:800;font-size:18px;letter-spacing:1px;background:linear-gradient(90deg,var(--accent),var(--accent3));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
header .tag{font-size:11px;padding:2px 10px;border-radius:20px;background:rgba(0,229,160,.12);color:var(--accent);border:1px solid rgba(0,229,160,.25);font-weight:500;letter-spacing:.5px}
.viewport{flex:1;position:relative;min-height:0}
.viewport canvas{width:100%;height:100%;display:block}
.controls{padding:14px 20px;background:var(--panel);border-top:1px solid var(--border);display:flex;gap:16px;align-items:center;flex-wrap:wrap;flex-shrink:0}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{font-size:12px;color:var(--muted);white-space:nowrap;font-weight:500}
.ctrl-group .val{font-family:'Exo 2';font-size:13px;color:var(--accent);min-width:42px;text-align:right;font-weight:600}
input[type=range]{-webkit-appearance:none;height:4px;border-radius:2px;background:var(--border);outline:none;cursor:pointer;width:110px}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px rgba(0,229,160,.5);cursor:pointer}
.btn{padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--fg);font-size:12px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:6px;font-family:'Noto Sans SC',sans-serif}
.btn:hover{border-color:var(--accent);background:rgba(0,229,160,.08)}
.btn.active{border-color:var(--accent);background:rgba(0,229,160,.15);color:var(--accent)}
.btn i{font-size:12px}
.steer-btns{display:flex;gap:4px}
.steer-btns .btn{padding:6px 12px;font-size:14px}
.legend{display:flex;gap:14px;margin-left:auto}
.legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)}
.legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
@media(max-width:900px){.controls{gap:10px;padding:10px 14px}input[type=range]{width:80px}.legend{display:none}}
</style>
</head>
<body>
<header>
  <div class="logo">S</div>
  <h1>MECH-SERPENT IFR</h1>
  <span class="tag">被动蜿蜒波涌现</span>
</header>
<div class="viewport">
  <canvas id="canvas"></canvas>
</div>
<div class="controls">
  <div class="ctrl-group">
    <label>转速 RPM</label>
    <input type="range" id="rpmSlider" min="30" max="180" value="90">
    <span class="val" id="rpmVal">90</span>
  </div>
  <div class="ctrl-group">
    <label>阻尼系数</label>
    <input type="range" id="dampSlider" min="5" max="60" value="20">
    <span class="val" id="dampVal">0.20</span>
  </div>
  <div class="ctrl-group">
    <label>摩擦系数</label>
    <input type="range" id="fricSlider" min="10" max="100" value="60">
    <span class="val" id="fricVal">0.60</span>
  </div>
  <div class="ctrl-group steer-btns">
    <button class="btn" id="steerLeft"><i class="fa-solid fa-arrow-left"></i></button>
    <button class="btn" id="steerRight"><i class="fa-solid fa-arrow-right"></i></button>
  </div>
  <button class="btn" id="playBtn"><i class="fa-solid fa-pause"></i> 暂停</button>
  <button class="btn" id="forceBtn"><i class="fa-solid fa-arrows-up-down"></i> 力矢量</button>
  <button class="btn" id="cableBtn" class="active"><i class="fa-solid fa-link"></i> 拉索</button>
  <div class="legend">
    <div class="legend-item"><div class="legend-dot" style="background:#ff8c00"></div>柔性拉索</div>
    <div class="legend-item"><div class="legend-dot" style="background:#00e676"></div>偏心驱动</div>
    <div class="legend-item"><div class="legend-dot" style="background:#00b4d8"></div>阻尼轴承</div>
    <div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div>横向摩擦</div>
    <div class="legend-item"><div class="legend-dot" style="background:#a78bfa"></div>纵向推力</div>
  </div>
</div>

<script>
/* ============================================================
   机械蛇 IFR 原理动画 —— 被动蜿蜒波涌现
   核心思想:仅头部1个主动驱动,通过柔性拉索+单向阻尼轴承
   让蜿蜒波形从蛇体中自动"涌现",实现持续前进
   ============================================================ */

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W, H, dpr;

// ---- 全局参数 ----
const CFG = {
  numSeg: 9,          // 1头+8被动
  segLen: 52,         // 每节长度(px)
  segW: 24,           // 每节宽度(px)
  eccRadius: 14,      // 偏心轮视觉半径
  eccOffset: 8,       // 偏心距(px,映射)
  phaseDelay: Math.PI / 3.2,
  ampBase: 0.36,      // 基础摆幅(rad)
  rpm: 90,
  damping: 0.20,
  friction: 0.60,
  steering: 0,
  showForces: true,
  showCable: true,
  running: true,
};

// ---- 状态 ----
let time = 0, headX = 300, headY = 0, cameraX = 0;
let joints = [], angles = [], eccAngle = 0;
let trail = [];

// ---- UI 绑定 ----
const $=id=>document.getElementById(id);
function bindSlider(id, valId, fn){
  const s=$(id), v=$(valId);
  s.addEventListener('input',()=>{fn(s,v)});
  fn(s,v); // init
}
bindSlider('rpmSlider','rpmVal',(s,v)=>{CFG.rpm=+s.value;v.textContent=s.value});
bindSlider('dampSlider','dampVal',(s,v)=>{CFG.damping=+s.value/100;v.textContent=CFG.damping.toFixed(2)});
bindSlider('fricSlider','fricVal',(s,v)=>{CFG.friction=+s.value/100;v.textContent=CFG.friction.toFixed(2)});

// 转向按钮
let steerInterval=null;
function startSteer(dir){
  CFG.steering=dir*0.6;
  steerInterval=setInterval(()=>{CFG.steering=dir*0.6},50);
}
function stopSteer(){CFG.steering=0;clearInterval(steerInterval)}
$('steerLeft').addEventListener('mousedown',()=>startSteer(-1));
$('steerLeft').addEventListener('mouseup',stopSteer);
$('steerLeft').addEventListener('mouseleave',stopSteer);
$('steerRight').addEventListener('mousedown',()=>startSteer(1));
$('steerRight').addEventListener('mouseup',stopSteer);
$('steerRight').addEventListener('mouseleave',stopSteer);
// 触屏
$('steerLeft').addEventListener('touchstart',e=>{e.preventDefault();startSteer(-1)});
$('steerLeft').addEventListener('touchend',stopSteer);
$('steerRight').addEventListener('touchstart',e=>{e.preventDefault();startSteer(1)});
$('steerRight').addEventListener('touchend',stopSteer);

$('playBtn').addEventListener('click',function(){
  CFG.running=!CFG.running;
  this.innerHTML=CFG.running?'<i class="fa-solid fa-pause"></i> 暂停':'<i class="fa-solid fa-play"></i> 播放';
});
$('forceBtn').addEventListener('click',function(){
  CFG.showForces=!CFG.showForces;
  this.classList.toggle('active',CFG.showForces);
});
$('cableBtn').addEventListener('click',function(){
  CFG.showCable=!CFG.showCable;
  this.classList.toggle('active',CFG.showCable);
});

// ---- 尺寸 ----
function resize(){
  const r=canvas.parentElement.getBoundingClientRect();
  dpr=window.devicePixelRatio||1;
  W=r.width; H=r.height;
  canvas.width=W*dpr; canvas.height=H*dpr;
  canvas.style.width=W+'px'; canvas.style.height=H+'px';
  headY=H*0.48;
}
window.addEventListener('resize',resize);
resize();

// ---- 物理更新 ----
function update(dt){
  if(!CFG.running) return;
  time+=dt;
  const omega=CFG.rpm*2*Math.PI/60;
  eccAngle=omega*time;

  // 阻尼影响相位延迟:阻尼越大,相位延迟越大
  const effectivePhase=CFG.phaseDelay*(0.6+CFG.damping*3);
  // 阻尼也略微减小摆幅
  const effectiveAmp=CFG.ampBase*Math.min(1, 0.5+CFG.damping*2.5);

  // 前进速度:摩擦×振幅²×角速度×相位因子
  const vFwd=CFG.friction*0.12*effectiveAmp*omega*CFG.segLen;
  headX+=vFwd*dt;

  // 各节角度
  angles=[];
  for(let i=0;i<CFG.numSeg;i++){
    const ph=omega*time - i*effectivePhase;
    let a=effectiveAmp*Math.sin(ph);
    // 转向偏置:从头到尾衰减
    const biasDecay=Math.exp(-i*0.28);
    a+=CFG.steering*0.35*biasDecay;
    angles.push(a);
  }

  // 计算关节位置(从头部向尾部链式推导)
  joints=[{x:headX,y:headY}];
  for(let i=0;i<CFG.numSeg;i++){
    const p=joints[i];
    joints.push({
      x: p.x - CFG.segLen*Math.cos(angles[i]),
      y: p.y - CFG.segLen*Math.sin(angles[i])
    });
  }

  // 相机跟随
  const targetCam=headX - W*0.32;
  cameraX+=(targetCam-cameraX)*0.06;

  // 尾迹
  const tail=joints[joints.length-1];
  trail.push({x:tail.x,y:tail.y,t:time});
  while(trail.length>0 && time-trail[0].t>3) trail.shift();
}

// ---- 绘制辅助 ----
function roundRect(ctx,x,y,w,h,r){
  r=Math.min(r,w/2,h/2);
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);
  ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r);
  ctx.arcTo(x,y,x+w,y,r);
  ctx.closePath();
}

// 绘制箭头
function drawArrow(ctx,x1,y1,x2,y2,color,lw){
  const dx=x2-x1, dy=y2-y1;
  const len=Math.sqrt(dx*dx+dy*dy);
  if(len<2) return;
  const ux=dx/len, uy=dy/len;
  ctx.save();
  ctx.strokeStyle=color;
  ctx.fillStyle=color;
  ctx.lineWidth=lw||2;
  ctx.lineCap='round';
  ctx.beginPath();
  ctx.moveTo(x1,y1);
  ctx.lineTo(x2,y2);
  ctx.stroke();
  // 箭头
  const hs=Math.min(8,len*0.35);
  ctx.beginPath();
  ctx.moveTo(x2,y2);
  ctx.lineTo(x2-ux*hs-uy*hs*0.45, y2-uy*hs+ux*hs*0.45);
  ctx.lineTo(x2-ux*hs+uy*hs*0.45, y2-uy*hs-ux*hs*0.45);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

// ---- 主绘制 ----
function draw(){
  ctx.save();
  ctx.scale(dpr,dpr);
  ctx.clearRect(0,0,W,H);

  // 背景
  const bgGrad=ctx.createLinearGradient(0,0,0,H);
  bgGrad.addColorStop(0,'#060a14');
  bgGrad.addColorStop(0.5,'#0a1020');
  bgGrad.addColorStop(1,'#0c1428');
  ctx.fillStyle=bgGrad;
  ctx.fillRect(0,0,W,H);

  // 背景网格(随相机滚动)
  ctx.save();
  ctx.translate(-cameraX,0);
  const gridSize=60;
  const startX=Math.floor(cameraX/gridSize)*gridSize;
  ctx.strokeStyle='rgba(30,42,69,0.5)';
  ctx.lineWidth=0.5;
  for(let x=startX;x<cameraX+W+gridSize;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(cameraX,y);ctx.lineTo(cameraX+W,y);ctx.stroke();
  }

  // 地面
  const groundY=headY+CFG.segW*0.7;
  const gGrad=ctx.createLinearGradient(0,groundY,0,groundY+80);
  gGrad.addColorStop(0,'rgba(0,229,160,0.08)');
  gGrad.addColorStop(1,'rgba(0,229,160,0)');
  ctx.fillStyle=gGrad;
  ctx.fillRect(cameraX,groundY,W,80);
  ctx.strokeStyle='rgba(0,229,160,0.2)';
  ctx.lineWidth=1;
  ctx.beginPath();ctx.moveTo(cameraX,groundY);ctx.lineTo(cameraX+W,groundY);ctx.stroke();

  // 地面摩擦方向标记
  if(CFG.showForces){
    for(let i=1;i<CFG.numSeg;i+=2){
      const j=joints[i], jn=joints[i+1];
      const mx=(j.x+jn.x)/2, my=(j.y+jn.y)/2;
      const vy=my-j.y; // 横向分量方向
      if(Math.abs(vy)>2){
        // 横向摩擦(与运动方向相反)
        const fy=vy>0?-1:1;
        const fLen=Math.abs(vy)*2.5*CFG.friction;
        drawArrow(ctx,mx,my+fLen*0.3,mx,my+fLen*0.3+fy*fLen,'rgba(239,68,68,0.6)',1.5);
      }
    }
  }

  // 尾迹
  if(trail.length>2){
    ctx.beginPath();
    ctx.moveTo(trail[0].x,trail[0].y);
    for(let i=1;i<trail.length;i++){
      ctx.lineTo(trail[i].x,trail[i].y);
    }
    ctx.strokeStyle='rgba(0,229,160,0.08)';
    ctx.lineWidth=3;
    ctx.stroke();
  }

  // ========== 拉索 ==========
  if(CFG.showCable && joints.length>1){
    // 拉索贯穿各节中心
    ctx.beginPath();
    ctx.moveTo(joints[0].x,joints[0].y);
    for(let i=1;i<joints.length;i++){
      ctx.lineTo(joints[i].x,joints[i].y);
    }
    // 发光效果
    ctx.shadowColor='#ff8c00';
    ctx.shadowBlur=12;
    ctx.strokeStyle='rgba(255,140,0,0.7)';
    ctx.lineWidth=3;
    ctx.stroke();
    ctx.shadowBlur=0;
    // 实线
    ctx.strokeStyle='#ff8c00';
    ctx.lineWidth=1.8;
    ctx.stroke();

    // 拉索偏移方向指示(虚线从中心到偏心位置)
    const ex=joints[0].x + CFG.eccOffset*Math.cos(eccAngle);
    const ey=joints[0].y + CFG.eccOffset*Math.sin(eccAngle);
    ctx.setLineDash([3,3]);
    ctx.strokeStyle='rgba(255,140,0,0.4)';
    ctx.lineWidth=1;
    ctx.beginPath();
    ctx.moveTo(joints[0].x,joints[0].y);
    ctx.lineTo(ex,ey);
    ctx.stroke();
    ctx.setLineDash([]);
  }

  // ========== 蛇体各节 ==========
  for(let i=CFG.numSeg-1;i>=0;i--){
    const ja=joints[i], jb=joints[i+1];
    const cx=(ja.x+jb.x)/2, cy=(ja.y+jb.y)/2;
    const angle=angles[i];
    const isHead=i===0;
    const sw=CFG.segW + (isHead?4:0);
    const sl=CFG.segLen;

    ctx.save();
    ctx.translate(cx,cy);
    ctx.rotate(angle);

    // 段体
    const bodyGrad=ctx.createLinearGradient(0,-sw/2,0,sw/2);
    if(isHead){
      bodyGrad.addColorStop(0,'#1a3a4a');
      bodyGrad.addColorStop(0.3,'#1f4a5a');
      bodyGrad.addColorStop(0.7,'#1a4050');
      bodyGrad.addColorStop(1,'#152e3e');
    } else {
      bodyGrad.addColorStop(0,'#1e2d42');
      bodyGrad.addColorStop(0.3,'#263850');
      bodyGrad.addColorStop(0.7,'#223448');
      bodyGrad.addColorStop(1,'#1a2a3c');
    }

    roundRect(ctx,-sl/2,-sw/2,sl,sw,6);
    ctx.fillStyle=bodyGrad;
    ctx.fill();

    // 边框
    ctx.strokeStyle=isHead?'rgba(0,230,118,0.5)':'rgba(0,180,216,0.25)';
    ctx.lineWidth=isHead?1.5:1;
    ctx.stroke();

    // 腹部弧面指示(底部高亮线)
    ctx.beginPath();
    ctx.moveTo(-sl/2+4,sw/2-1);
    ctx.quadraticCurveTo(0,sw/2+3,sl/2-4,sw/2-1);
    ctx.strokeStyle='rgba(0,229,160,0.3)';
    ctx.lineWidth=1.5;
    ctx.stroke();

    // 头部:偏心轮 + 电机
    if(isHead){
      // 左电机
      const motorY1=-sw/2+4;
      ctx.fillStyle='rgba(100,116,139,0.6)';
      roundRect(ctx,-8,motorY1-5,16,10,2);
      ctx.fill();
      ctx.strokeStyle='rgba(148,163,184,0.4)';
      ctx.lineWidth=0.8;
      ctx.stroke();
      // 右电机
      const motorY2=sw/2-4;
      ctx.fillStyle='rgba(100,116,139,0.6)';
      roundRect(ctx,-8,motorY2-5,16,10,2);
      ctx.fill();
      ctx.strokeStyle='rgba(148,163,184,0.4)';
      ctx.lineWidth=0.8;
      ctx.stroke();

      // 偏心轮
      const eAngle=eccAngle;
      // 轮体
      ctx.beginPath();
      ctx.arc(0,0,CFG.eccRadius,0,Math.PI*2);
      ctx.fillStyle='rgba(0,230,118,0.12)';
      ctx.fill();
      ctx.strokeStyle='rgba(0,230,118,0.5)';
      ctx.lineWidth=1.5;
      ctx.stroke();
      // 偏心点
      const epx=CFG.eccOffset*Math.cos(eAngle);
      const epy=CFG.eccOffset*Math.sin(eAngle);
      ctx.beginPath();
      ctx.arc(epx,epy,4,0,Math.PI*2);
      ctx.fillStyle='#00e676';
      ctx.shadowColor='#00e676';
      ctx.shadowBlur=10;
      ctx.fill();
      ctx.shadowBlur=0;
      // 偏心轨迹
      ctx.beginPath();
      ctx.arc(0,0,CFG.eccOffset,0,Math.PI*2);
      ctx.strokeStyle='rgba(0,230,118,0.2)';
      ctx.lineWidth=0.8;
      ctx.setLineDash([2,3]);
      ctx.stroke();
      ctx.setLineDash([]);

      // 头部眼睛
      ctx.fillStyle='rgba(0,229,160,0.9)';
      ctx.beginPath();ctx.arc(sl/2-6,-sw/4,2.5,0,Math.PI*2);ctx.fill();
      ctx.beginPath();ctx.arc(sl/2-6,sw/4,2.5,0,Math.PI*2);ctx.fill();
    }

    // 被动节内部:阻尼轴承标记
    if(!isHead){
      // 轴承外圈
      ctx.beginPath();
      ctx.arc(0,0,5,0,Math.PI*2);
      ctx.fillStyle='rgba(0,180,216,0.15)';
      ctx.fill();
      ctx.strokeStyle='rgba(0,180,216,0.5)';
      ctx.lineWidth=1.2;
      ctx.stroke();
      // 旋转指示(单向箭头弧)
      const bAngle=angles[i]*2; // 夸张化
      ctx.beginPath();
      ctx.arc(0,0,5,bAngle,bAngle+Math.PI*1.2);
      ctx.strokeStyle='rgba(0,180,216,0.7)';
      ctx.lineWidth=1;
      ctx.stroke();
      // 箭头尖
      const tipAngle=bAngle+Math.PI*1.2;
      const tx=5*Math.cos(tipAngle), ty=5*Math.sin(tipAngle);
      ctx.beginPath();
      ctx.arc(tx,ty,1.5,0,Math.PI*2);
      ctx.fillStyle='#00b4d8';
      ctx.fill();

      // 节段编号
      ctx.fillStyle='rgba(100,116,139,0.35)';
      ctx.font='9px "Exo 2"';
      ctx.textAlign='center';
      ctx.fillText(i,0,3.5);
    }

    ctx.restore();
  }

  // ========== 关节连接线 ==========
  for(let i=1;i<joints.length-1;i++){
    const j=joints[i];
    ctx.beginPath();
    ctx.arc(j.x,j.y,3,0,Math.PI*2);
    ctx.fillStyle='rgba(0,180,216,0.4)';
    ctx.fill();
  }

  // ========== 力矢量 ==========
  if(CFG.showForces){
    for(let i=0;i<CFG.numSeg;i++){
      const ja=joints[i], jb=joints[i+1];
      const cx=(ja.x+jb.x)/2, cy=(ja.y+jb.y)/2;
      const angle=angles[i];
      const isHead=i===0;

      // 计算横向速度分量(y方向的变化率近似)
      const omega=CFG.rpm*2*Math.PI/60;
      const effectivePhase=CFG.phaseDelay*(0.6+CFG.damping*3);
      const effectiveAmp=CFG.ampBase*Math.min(1,0.5+CFG.damping*2.5);
      const lateralVel=effectiveAmp*omega*CFG.segLen*Math.cos(omega*time-i*effectivePhase);
      
      // 横向摩擦力(与横向速度反向)
      const fricForce=CFG.friction*Math.abs(lateralVel)*0.04;
      if(fricForce>1){
        const fDir=lateralVel>0?-1:1;
        // 分解为横向摩擦和纵向推力
        const fLat=fricForce*0.7;
        const fFwd=fricForce*0.5;
        // 横向摩擦箭头
        drawArrow(ctx,cx,cy,cx,cy+fDir*fLat*15,'rgba(239,68,68,0.7)',1.8);
        // 纵向推力箭头
        drawArrow(ctx,cx,cy,cx+fFwd*12,cy,'rgba(167,139,250,0.7)',1.8);
      }
    }
  }

  // ========== 波形传播指示 ==========
  if(joints.length>2){
    // 在蛇体上方画一条S波曲线
    const waveY=headY-60;
    ctx.beginPath();
    ctx.moveTo(joints[0].x, waveY + angles[0]*80);
    for(let i=1;i<joints.length;i++){
      ctx.lineTo(joints[i].x, waveY + angles[i]*80);
    }
    ctx.strokeStyle='rgba(0,229,160,0.25)';
    ctx.lineWidth=1.5;
    ctx.stroke();
    // 波形标签
    ctx.fillStyle='rgba(0,229,160,0.4)';
    ctx.font='10px "Exo 2"';
    ctx.textAlign='left';
    ctx.fillText('S-WAVE PROPAGATION →',joints[joints.length-1].x, waveY-10);
    // 波峰波谷标记
    for(let i=0;i<CFG.numSeg;i++){
      const wx=joints[i].x, wy=waveY+angles[i]*80;
      ctx.beginPath();
      ctx.arc(wx,wy,2.5,0,Math.PI*2);
      const hue=i===0?'#00e676':'#00b4d8';
      ctx.fillStyle=hue;
      ctx.fill();
    }
  }

  ctx.restore(); // 相机还原

  // ========== 右上角:偏心轮详图 ==========
  drawDetailInset();

  // ========== 左下角:相位图 ==========
  drawPhaseDiagram();

  // ========== 信息标注 ==========
  drawAnnotations();
}

// ---- 偏心轮详图 ----
function drawDetailInset(){
  const ix=W-195, iy=15, iw=180, ih=140;
  // 背景
  ctx.save();
  ctx.globalAlpha=0.92;
  roundRect(ctx,ix,iy,iw,ih,10);
  ctx.fillStyle='#0c1424';
  ctx.fill();
  ctx.strokeStyle='rgba(0,229,160,0.3)';
  ctx.lineWidth=1;
  ctx.stroke();
  ctx.globalAlpha=1;

  // 标题
  ctx.fillStyle='rgba(0,229,160,0.8)';
  ctx.font='bold 11px "Exo 2"';
  ctx.textAlign='left';
  ctx.fillText('ECCENTRIC MECHANISM',ix+12,iy+18);

  const cx=ix+iw/2, cy=iy+75;

  // 外壳
  ctx.beginPath();
  ctx.arc(cx,cy,35,0,Math.PI*2);
  ctx.strokeStyle='rgba(148,163,184,0.3)';
  ctx.lineWidth=1;
  ctx.stroke();

  // 偏心轮
  ctx.beginPath();
  ctx.arc(cx,cy,22,0,Math.PI*2);
  ctx.fillStyle='rgba(0,230,118,0.08)';
  ctx.fill();
  ctx.strokeStyle='rgba(0,230,118,0.5)';
  ctx.lineWidth=1.5;
  ctx.stroke();

  // 旋转标记线
  for(let a=0;a<4;a++){
    const ra=eccAngle+a*Math.PI/2;
    ctx.beginPath();
    ctx.moveTo(cx+8*Math.cos(ra),cy+8*Math.sin(ra));
    ctx.lineTo(cx+20*Math.cos(ra),cy+20*Math.sin(ra));
    ctx.strokeStyle='rgba(0,230,118,0.2)';
    ctx.lineWidth=1;
    ctx.stroke();
  }

  // 偏心点
  const epx=cx+CFG.eccOffset*1.8*Math.cos(eccAngle);
  const epy=cy+CFG.eccOffset*1.8*Math.sin(eccAngle);
  ctx.beginPath();
  ctx.arc(epx,epy,5,0,Math.PI*2);
  ctx.fillStyle='#00e676';
  ctx.shadowColor='#00e676';
  ctx.shadowBlur=12;
  ctx.fill();
  ctx.shadowBlur=0;

  // 拉索连接线
  ctx.beginPath();
  ctx.moveTo(epx,epy);
  ctx.lineTo(cx+iw/2+30,epy);
  ctx.strokeStyle='#ff8c00';
  ctx.lineWidth=2;
  ctx.shadowColor='#ff8c00';
  ctx.shadowBlur=8;
  ctx.stroke();
  ctx.shadowBlur=0;

  // 偏心轨迹
  ctx.beginPath();
  ctx.arc(cx,cy,CFG.eccOffset*1.8,0,Math.PI*2);
  ctx.strokeStyle='rgba(0,230,118,0.2)';
  ctx.lineWidth=0.8;
  ctx.setLineDash([2,3]);
  ctx.stroke();
  ctx.setLineDash([]);

  // 标注
  ctx.fillStyle='rgba(148,163,184,0.6)';
  ctx.font='9px "Noto Sans SC"';
  ctx.textAlign='center';
  ctx.fillText('偏心距 8mm',cx,cy+46);
  ctx.fillText('伺服电机 ×2',cx,cy+58);

  // 转速显示
  ctx.fillStyle='#00e676';
  ctx.font='bold 13px "Exo 2"';
  ctx.fillText(CFG.rpm+' RPM',cx,iy+ih-8);

  ctx.restore();
}

// ---- 相位图 ----
function drawPhaseDiagram(){
  const px=15, py=H-145, pw=200, ph=130;
  ctx.save();
  ctx.globalAlpha=0.88;
  roundRect(ctx,px,py,pw,ph,10);
  ctx.fillStyle='#0c1424';
  ctx.fill();
  ctx.strokeStyle='rgba(0,180,216,0.3)';
  ctx.lineWidth=1;
  ctx.stroke();
  ctx.globalAlpha=1;

  ctx.fillStyle='rgba(0,180,216,0.8)';
  ctx.font='bold 10px "Exo 2"';
  ctx.textAlign='left';
  ctx.fillText('PHASE DELAY DIAGRAM',px+10,py+16);

  const ox=px+20, oy=py+ph/2+8, gW=pw-40, gH=ph-45;

  // 坐标轴
  ctx.strokeStyle='rgba(100,116,139,0.3)';
  ctx.lineWidth=0.5;
  ctx.beginPath();
  ctx.moveTo(ox,oy-gH/2);ctx.lineTo(ox,oy+gH/2);
  ctx.moveTo(ox,oy);ctx.lineTo(ox+gW,oy);
  ctx.stroke();

  // 各节角度正弦曲线
  const omega=CFG.rpm*2*Math.PI/60;
  const effectivePhase=CFG.phaseDelay*(0.6+CFG.damping*3);
  const effectiveAmp=CFG.ampBase*Math.min(1,0.5+CFG.damping*2.5);
  const colors=['#00e676','#00d4aa','#00c4d8','#00b4e0','#38a2e0','#6e90d0','#9880c0','#b870b0','#d060a0'];

  for(let i=0;i<CFG.numSeg;i++){
    ctx.beginPath();
    for(let x=0;x<=gW;x++){
      const tLocal=(x/gW)*4*Math.PI;
      const val=effectiveAmp*Math.sin(tLocal - i*effectivePhase);
      const sy=oy - val*gH*1.3;
      if(x===0) ctx.moveTo(ox+x,sy);
      else ctx.lineTo(ox+x,sy);
    }
    ctx.strokeStyle=colors[i%colors.length];
    ctx.globalAlpha=i===0?0.9:0.35;
    ctx.lineWidth=i===0?1.5:0.8;
    ctx.stroke();
  }
  ctx.globalAlpha=1;

  // 当前时刻标记
  const curX=ox+(((omega*time)%(4*Math.PI))/(4*Math.PI))*gW;
  ctx.strokeStyle='rgba(255,255,255,0.5)';
  ctx.lineWidth=1;
  ctx.setLineDash([2,2]);
  ctx.beginPath();
  ctx.moveTo(curX,oy-gH/2);
  ctx.lineTo(curX,oy+gH/2);
  ctx.stroke();
  ctx.setLineDash([]);

  // 节号图例
  ctx.font='8px "Exo 2"';
  ctx.textAlign='right';
  for(let i=0;i<Math.min(5,CFG.numSeg);i++){
    ctx.fillStyle=colors[i];
    ctx.fillText('S'+i,px+pw-6,py+28+i*11);
  }

  ctx.restore();
}

// ---- 标注文字 ----
function drawAnnotations(){
  ctx.save();
  // 头部标注
  const hsx=headX-cameraX;
  const hsy=headY-95;

  ctx.fillStyle='rgba(0,229,160,0.6)';
  ctx.font='bold 12px "Noto Sans SC"';
  ctx.textAlign='center';
  ctx.fillText('主动驱动节',hsx,hsy);
  ctx.font='10px "Noto Sans SC"';
  ctx.fillStyle='rgba(148,163,184,0.5)';
  ctx.fillText('2×伺服电机 + 偏心轮',hsx,hsy+14);

  // 被动节标注
  if(joints.length>5){
    const midIdx=5;
    const mx=joints[midIdx].x-cameraX;
    const my=joints[midIdx].y+50;
    ctx.fillStyle='rgba(0,180,216,0.6)';
    ctx.font='bold 11px "Noto Sans SC"';
    ctx.textAlign='center';
    ctx.fillText('被动随动节',mx,my);
    ctx.font='9px "Noto Sans SC"';
    ctx.fillStyle='rgba(148,163,184,0.5)';
    ctx.fillText('无电机 · 单向阻尼轴承',mx,my+13);
  }

  // IFR 标注
  ctx.fillStyle='rgba(0,229,160,0.15)';
  ctx.font='bold 28px "Exo 2"';
  ctx.textAlign='right';
  ctx.fillText('IFR',W-20,H-20);
  ctx.font='11px "Noto Sans SC"';
  ctx.fillStyle='rgba(148,163,184,0.25)';
  ctx.fillText('最终理想解:结构自组织涌现蜿蜒波',W-20,H-6);

  // 速度显示
  const omega=CFG.rpm*2*Math.PI/60;
  const effectiveAmp=CFG.ampBase*Math.min(1,0.5+CFG.damping*2.5);
  const vDisplay=(CFG.friction*0.12*effectiveAmp*omega*CFG.segLen).toFixed(1);
  ctx.fillStyle='rgba(167,139,250,0.7)';
  ctx.font='bold 14px "Exo 2"';
  ctx.textAlign='left';
  ctx.fillText('v = '+vDisplay+' px/s',15,25);
  ctx.font='9px "Noto Sans SC"';
  ctx.fillStyle='rgba(148,163,184,0.4)';
  ctx.fillText('前进速度',15,38);

  ctx.restore();
}

// ---- 主循环 ----
let lastTime=0;
function loop(ts){
  const dt=Math.min((ts-lastTime)/1000, 0.05);
  lastTime=ts;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(ts=>{lastTime=ts;loop(ts)});
</script>
</body>
</html>

实现说明

核心设计思路

本动画严格遵循 TRIZ 的**最终理想解(IFR)**思想,直接展示"系统自身结构在极少增加复杂度的前提下自动涌现蜿蜒波"的理想状态,而非前后对比。

关键可视化要素

  1. 偏心轮驱动(绿色高亮):右上角详图实时展示偏心轮旋转——这是整条蛇唯一的主动输入,两个伺服电机的偏心旋转通过橙色柔性拉索传递运动

  2. 柔性拉索(橙色发光线):贯穿蛇体全长的亮橙色线条,是核心传动资源的可视化。拉索将头部偏心运动传递至每一节,无需各节独立电机

  3. 单向阻尼轴承(青色标记):每个被动节中心的青色圆弧箭头,展示阻尼轴承"顺转自由、回弹受阻"的单向特性——这是波形涌现的关键约束

  4. S波传播曲线:蛇体上方的绿色连线实时展示相位延迟如何自动形成从头到尾传播的蜿蜒波

  5. 力矢量分解:红色箭头=横向摩擦力(与横向摆动反向),紫色箭头=纵向推进力(净前进推力),直观呈现摩擦各向异性如何将横向摆动转化为前进动力

交互控制

  • RPM 滑块:调节偏心轮转速,观察波形频率与前进速度的关系
  • 阻尼系数:调节单向阻尼强度,影响相位延迟和波形幅度
  • 摩擦系数:模拟不同地面条件——降低至冰面级别时可观察到推进失效
  • 左右转向:按住方向按钮改变头部电机占空比不对称性,迫使蛇身弯曲转向
  • 力矢量/拉索:切换显示核心创新要素的视觉强调
积分规则:第一轮对话扣减6分,后续每轮扣4分