分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机械蛇 · 单向阻力鳞片推进原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;500;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#060a14;--fg:#d0d8e8;--muted:#3e4d65;--accent:#f97316;--card:#0b1018;--border:#1a2540;--grip:#f97316;--slide:#06b6d4;--thrust:#eab308;--body-c:#5a7090}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:1.5rem;letter-spacing:.08em;color:var(--fg);margin:18px 0 6px;text-align:center}
.sub{font-size:.72rem;color:var(--muted);text-align:center;margin-bottom:10px;letter-spacing:.03em;max-width:700px;line-height:1.5}
#wrap{width:96vw;max-width:1280px;aspect-ratio:16/9;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--card);box-shadow:0 0 50px rgba(249,115,22,.04),0 0 100px rgba(6,182,212,.03)}
#main-svg{width:100%;height:100%;display:block}
.ctrls{display:flex;gap:20px;flex-wrap:wrap;justify-content:center;margin:14px 0 6px;padding:0 16px;max-width:1280px}
.cg{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:130px}
.cg label{font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
.cg input[type=range]{width:110px;accent-color:var(--accent);cursor:pointer}
.cg .v{font-size:.8rem;color:var(--accent);font-weight:500}
.drow{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-bottom:14px}
.di{font-size:.72rem;color:var(--muted);display:flex;align-items:center;gap:5px}
.di .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.di .vl{color:var(--fg);font-weight:500}
</style>
</head>
<body>
<h1>机械蛇 · 单向阻力鳞片推进原理</h1>
<p class="sub">各向异性摩擦将双向弯曲单向转化为前进推力 — TRIZ 最终理想解 (IFR) 演示</p>
<div id="wrap"><svg id="main-svg" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg"></svg></div>
<div class="ctrls">
  <div class="cg"><label>波速 ω</label><input type="range" id="c-spd" min="0.5" max="4" step="0.1" value="2.0"><span class="v" id="v-spd">2.0</span></div>
  <div class="cg"><label>振幅 A</label><input type="range" id="c-amp" min="15" max="65" step="5" value="45"><span class="v" id="v-amp">45</span></div>
  <div class="cg"><label>短节数 N</label><input type="range" id="c-seg" min="8" max="15" step="1" value="12"><span class="v" id="v-seg">12</span></div>
  <div class="cg"><label>摩擦比 μ</label><input type="range" id="c-fr" min="2" max="6" step="0.5" value="3"><span class="v" id="v-fr">3.0</span></div>
</div>
<div class="drow">
  <div class="di"><span class="dot" style="background:var(--grip)"></span>抓地: <span class="vl" id="d-grip">0</span>节</div>
  <div class="di"><span class="dot" style="background:var(--slide)"></span>滑行: <span class="vl" id="d-slide">0</span>节</div>
  <div class="di"><span class="dot" style="background:var(--thrust)"></span>推进: <span class="vl" id="d-spd">0</span>mm/s</div>
  <div class="di">行程: <span class="vl" id="d-dist">0</span>mm</div>
</div>

<script>
(function(){
/* ====== 配置 ====== */
const CFG={numSeg:12,segLen:46,segW:26,amp:45,waveLen:280,omega:2.0,fwdSpd:22,friction:3,cx:580,cy:260};
let groundOffset=0,totalDist=0;

/* ====== SVG 工具 ====== */
const NS="http://www.w3.org/2000/svg";
const svg=document.getElementById("main-svg");
function el(tag,attrs,parent){
  const e=document.createElementNS(NS,tag);
  for(const k in attrs)e.setAttribute(k,attrs[k]);
  if(parent)parent.appendChild(e);
  return e;
}

/* ====== Defs ====== */
const defs=el("defs",{},svg);
// 发光滤镜-抓地
const fGrip=el("filter",{id:"fg",x:"-60%",y:"-60%",width:"220%",height:"220%"},defs);
el("feGaussianBlur",{stdDeviation:"5",result:"b"},fGrip);
const mGrip=el("feMerge",{},fGrip);el("feMergeNode",{in:"b"},mGrip);el("feMergeNode",{in:"SourceGraphic"},mGrip);
// 发光滤镜-推力
const fThrust=el("filter",{id:"ft",x:"-60%",y:"-60%",width:"220%",height:"220%"},defs);
el("feGaussianBlur",{stdDeviation:"3",result:"b"},fThrust);
const mThrust=el("feMerge",{},fThrust);el("feMergeNode",{in:"b"},mThrust);el("feMergeNode",{in:"SourceGraphic"},mThrust);
// 阴影
const fShadow=el("filter",{id:"fs",x:"-20%",y:"-20%",width:"140%",height:"160%"},defs);
el("feDropShadow",{dx:"0",dy:"4",stdDeviation:"6","flood-color":"#000000","flood-opacity":"0.5"},fShadow);
// 渐变-身体
const gBody=el("linearGradient",{id:"gb",x1:"0",y1:"0",x2:"0",y2:"1"},defs);
el("stop",{offset:"0%","stop-color":"#7a90aa"},gBody);el("stop",{offset:"100%","stop-color":"#3e5470"},gBody);
// 渐变-地面
const gGnd=el("linearGradient",{id:"gg",x1:"0",y1:"0",x2:"0",y2:"1"},defs);
el("stop",{offset:"0%","stop-color":"#141e30"},gGnd);el("stop",{offset:"100%","stop-color":"#0a0f18"},gGnd);
// 箭头标记
const mkT=el("marker",{id:"at",viewBox:"0 0 10 6",refX:"10",refY:"3",markerWidth:"8",markerHeight:"5",orient:"auto"},defs);
el("path",{d:"M0,0 L10,3 L0,6 Z",fill:"#eab308"},mkT);
const mkF=el("marker",{id:"af",viewBox:"0 0 10 6",refX:"10",refY:"3",markerWidth:"7",markerHeight:"4",orient:"auto"},defs);
el("path",{d:"M0,0 L10,3 L0,6 Z",fill:"#f97316"},mkF);

/* ====== 静态背景 ====== */
el("rect",{width:1280,height:720,fill:"#0b1018"},svg);
// 网格
const gridG=el("g",{opacity:"0.08"},svg);
for(let x=0;x<=1280;x+=40)el("line",{x1:x,y1:0,x2:x,y2:720,stroke:"#4080c0","stroke-width":"0.5"},gridG);
for(let y=0;y<=720;y+=40)el("line",{x1:0,y1:y,x2:1280,y2:y,stroke:"#4080c0","stroke-width":"0.5"},gridG);
// 地面
el("rect",{x:0,y:385,width:1280,height:335,fill:"url(#gg)",opacity:"0.6"},svg);
el("line",{x1:0,y1:385,x2:1280,y2:385,stroke:"#1e3050","stroke-width":"1.5"},svg);

/* ====== 动态层 ====== */
const gMarkers=el("g",{},svg);     // 地面滚动标记
const gShadow=el("g",{opacity:"0.25",filter:"url(#fs)"},svg);
const gBody=el("g",{},svg);        // 蛇身
const gScales=el("g",{},svg);      // 鳞片
const gArrows=el("g",{},svg);      // 推力箭头
const gParts=el("g",{},svg);       // 抓地粒子
const gPhase=el("g",{transform:"translate(70,460)"},svg); // 相位条
const gDetail=el("g",{transform:"translate(850,500)"},svg); // 截面详图
const gIFR=el("g",{transform:"translate(40,500)"},svg);    // IFR说明

/* ====== 预创建元素 ====== */
// 地面标记线
const markers=[];
for(let i=0;i<40;i++){
  const m=el("line",{y1:387,y2:405,stroke:"#2a3a58","stroke-width":"1.5"},gMarkers);
  markers.push(m);
}
// 身体路径
const bodyPath=el("path",{fill:"url(#gb)",stroke:"#3a506e","stroke-width":"1.5",d:""},gBody);
const shadowPath=el("path",{fill:"#000000",d:"",opacity:"0.35"},gShadow);
// 身体脊线
const spinePath=el("path",{fill:"none",stroke:"#8aa0ba","stroke-width":"1","stroke-dasharray":"4 3",d:"",opacity:"0.4"},gBody);

// 鳞片元素(最多15段×每段4个鳞片×2侧)
const MAX_SEG=15, SCALES_PER_SEG=4;
const scaleEls=[];
for(let i=0;i<MAX_SEG;i++){
  const row=[];
  for(let j=0;j<SCALES_PER_SEG;j++){
    const s=el("polygon",{points:"0,0 -5,4 0,3 5,4",fill:"#06b6d4",opacity:"0.8"},gScales);
    row.push(s);
  }
  scaleEls.push(row);
}

// 关节圆
const jointEls=[];
for(let i=0;i<MAX_SEG;i++){
  const j=el("circle",{r:"4",fill:"#2a3a58",stroke:"#4a6080","stroke-width":"1"},gBody);
  jointEls.push(j);
}
// 头部标记
const headEl=el("ellipse",{rx:"14",ry:"10",fill:"#5a7090",stroke:"#8aa0ba","stroke-width":"1.5"},gBody);
const headEye1=el("circle",{r:"2.5",fill:"#e0e8f0"},gBody);
const headEye2=el("circle",{r:"2.5",fill:"#e0e8f0"},gBody);

// 推力箭头
const arrowEls=[];
for(let i=0;i<MAX_SEG;i++){
  const a=el("line",{x1:"0",y1:"0",x2:"0",y2:"0",stroke:"#eab308","stroke-width":"2.5","marker-end":"url(#at)",opacity:"0",filter:"url(#ft)"},gArrows);
  arrowEls.push(a);
}

// 抓地粒子
const particles=[];
for(let i=0;i<60;i++){
  const p=el("circle",{r:"1.5",fill:"#f97316",opacity:"0"},gParts);
  particles.push({el:p,life:0,x:0,y:0,vx:0,vy:0});
}

/* ====== 相位图 ====== */
el("rect",{x:0,y:0,width:1100,height:50,rx:"6",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1"},gPhase);
el("text",{x:"8",y:"15","font-size":"10",fill:"#4a5e78","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gPhase).textContent="鳞片状态相位图  ▸ 波形传播方向";
const phaseBars=[];
for(let i=0;i<MAX_SEG;i++){
  const b=el("rect",{x:0,y:20,width:0,height:22,rx:"3",fill:"#06b6d4"},gPhase);
  const t=el("text",{y:"35","font-size":"8",fill:"#c0d0e0","text-anchor":"middle","font-family":"'IBM Plex Mono'"},gPhase);
  t.textContent=""+(i+1);
  phaseBars.push({bar:b,label:t});
}

/* ====== 截面详图 ====== */
el("rect",{x:0,y:0,width:390,height:200,rx:"8",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1.5"},gDetail);
el("text",{x:"12",y:"18","font-size":"11",fill:"#5a7090","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gDetail).textContent="鳞片截面原理 (侧视)";
// 地面线
el("line",{x1:30,y1:160,x2:360,y2:160,stroke:"#2a3a58","stroke-width":"2"},gDetail);
el("text",{x:45,y:178,"font-size":"9",fill:"#3e5068","font-family":"'IBM Plex Mono'"},gDetail).textContent="地面";
// 段体
const detailSeg=el("rect",{x:100,y:105,width:160,height:35,rx:"4",fill:"#4a5e78",stroke:"#6a8098","stroke-width":"1"},gDetail);
// 鳞片组(2个状态)
const detailScaleGrip=el("g",{},gDetail);
const detailScaleSlide=el("g",{opacity:"0"},gDetail);
// 抓地鳞片
const scG1=el("polygon",{points:"120,140 108,155 120,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG2=el("polygon",{points:150,140 138,155 150,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG3=el("polygon",{points:180,140 168,155 180,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG4=el("polygon",{points:210,140 198,155 210,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
// 抓地标注
el("text",{x:260,y:132,"font-size":"10",fill:"#f97316","font-family":"'Chakra Petch',sans-serif","font-weight":"700"},detailScaleGrip).textContent="GRIP 抓地";
el("text",{x:260,y:148,"font-size":"8",fill:"#8a6040","font-family":"'IBM Plex Mono'"},detailScaleGrip).textContent="鳞片刚硬卡地";
el("text",{x:260,y:160,"font-size":"8",fill:"#8a6040","font-family":"'IBM Plex Mono'"},detailScaleGrip).textContent="→ 高摩擦防后退";
// 抓地力箭头
const gripForceArr=el("line",{x1:140,y1:155,x2:140,y2:172,stroke:"#f97316","stroke-width":"2","marker-end":"url(#af)"},detailScaleGrip);
// 滑行鳞片
const scS1=el("polygon",{points:"120,138 115,143 120,140 125,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS2=el("polygon",{points:"150,138 145,143 150,140 155,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS3=el("polygon",{points:"180,138 175,143 180,140 185,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS4=el("polygon",{points:"210,138 205,143 210,140 215,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
// 滑行标注
el("text",{x:260,y:132,"font-size":"10",fill:"#06b6d4","font-family":"'Chakra Petch',sans-serif","font-weight":"700"},detailScaleSlide).textContent="SLIDE 滑行";
el("text",{x:260,y:148,"font-size":"8",fill:"#2a6a7a","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="鳞片顺势倒伏";
el("text",{x:260,y:160,"font-size":"8",fill:"#2a6a7a","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="→ 低摩擦易前滑";
// 运动方向箭头
const slideDirArr=el("line",{x1:105,y1:95,x2:75,y2:95,stroke:"#06b6d4","stroke-width":"1.5","marker-end":"url(#af)","stroke-dasharray":"4 2"},detailScaleSlide);
el("text",{x:80,y:88,"font-size":"8",fill:"#06b6d4","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="前滑方向";

/* ====== IFR说明框 ====== */
el("rect",{x:0,y:0,width:380,height:200,rx:"8",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1.5"},gIFR);
el("text",{x:"12",y:"18","font-size":"11",fill:"#5a7090","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gIFR).textContent="IFR 最终理想解核心";
const ifrLines=[
  {t:"矛盾:弯曲运动双向,推力需单向",y:40,c:"#8a9ab0"},
  {t:"理想解:鳞片 = 零成本单向阀",y:60,c:"#f97316"},
  {t:"",y:78,c:""},
  {t:"▸ 复用资源:地面摩擦力",y:95,c:"#eab308"},
  {t:"  (原本是阻力 → 转为推力来源)",y:112,c:"#8a7a40"},
  {t:"▸ 极简增量:仅改变表面纹理方向",y:130,c:"#eab308"},
  {t:"  (不增加主动机构或能源消耗)",y:147,c:"#8a7a40"},
  {t:"▸ 自消除矛盾:后退时高阻/前滑时低阻",y:165,c:"#06b6d4"},
  {t:"  (同一结构在不同运动方向自动切换)",y:182,c:"#2a6a7a"},
];
ifrLines.forEach(l=>{
  if(l.t)el("text",{x:"16",y:l.y,"font-size":l.c==="#f97316"||l.c==="#eab308"?"10":"9",fill:l.c,"font-family":"'IBM Plex Mono'","font-weight":l.c==="#f97316"?"700":"400"},gIFR).textContent=l.t;
});

/* ====== 波形方向标注 ====== */
const waveLabel=el("text",{x:CFG.cx+80,y:CFG.cy-80,"font-size":"11",fill:"#4a6080","font-family":"'Chakra Petch',sans-serif","font-weight":"500",opacity:"0.7"},svg);
waveLabel.textContent="▸ 波形传播方向  头 → 尾";

/* ====== 状态计算 ====== */
function computeSnake(t){
  const N=CFG.numSeg, L=CFG.segLen, A=CFG.amp, wl=CFG.waveLen, w=CFG.omega;
  const segs=[];
  const totalLen=N*L;
  for(let i=0;i<N;i++){
    const s=i*L;
    const phase=(2*Math.PI*s/wl)-w*t;
    const x=CFG.cx-s;
    const y=CFG.cy+A*Math.sin(phase);
    const dyds=A*(2*Math.PI/wl)*Math.cos(phase);
    const angle=Math.atan2(dyds,-1);
    const dydt=-A*w*Math.cos(phase);
    const yPos=A*Math.sin(phase);
    const product=yPos*dydt; // >0 推离中心=抓地, <0 回归中心=滑行
    const maxProd=A*A*w*0.5;
    const gripI=Math.max(0,product/maxProd);
    const slideI=Math.max(0,-product/maxProd);
    segs.push({x,y,angle,phase,gripI,slideI,isGrip:gripI>0.25});
  }
  return segs;
}

/* ====== 身体轮廓 ====== */
function buildOutline(segs){
  const hw=CFG.segW/2;
  const pts=segs.length;
  // 增加首尾锥形
  const left=[],right=[];
  for(let i=0;i<pts;i++){
    const s=segs[i];
    const nx=-Math.sin(s.angle),ny=Math.cos(s.angle);
    const taper=i<2?(0.6+0.2*i):i>pts-3?(0.6+0.2*(pts-1-i)):1;
    const w=hw*taper;
    left.push({x:s.x+nx*w,y:s.y+ny*w});
    right.push({x:s.x-nx*w,y:s.y-ny*w});
  }
  // 头部尖端
  const h=segs[0];
  const hx=h.x+Math.cos(h.angle)*18,hy=h.y+Math.sin(h.angle)*18;
  // 尾部尖端
  const tl=segs[pts-1];
  const tx=tl.x-Math.cos(tl.angle)*14,ty=tl.y-Math.sin(tl.angle)*14;
  let d=`M${tx} ${ty} `;
  for(let i=pts-1;i>=0;i--)d+=`L${left[i].x} ${left[i].y} `;
  d+=`L${hx} ${hy} `;
  for(let i=0;i<pts;i++)d+=`L${right[i].x} ${right[i].y} `;
  d+="Z";
  return d;
}

/* ====== 更新函数 ====== */
let prevTime=0, particleIdx=0;

function update(ts){
  const t=ts/1000;
  const dt=Math.min(t-prevTime,0.05);
  prevTime=t;
  const N=CFG.numSeg;
  const segs=computeSnake(t);

  // 推进距离
  const fwdInc=CFG.fwdSpd*(CFG.amp/45)*(CFG.omega/2)*dt;
  totalDist+=fwdInc;
  groundOffset+=fwdInc*2;

  // 地面标记
  const spacing=60;
  for(let i=0;i<markers.length;i++){
    const rawX=((i*spacing-groundOffset)%1280+1280)%1280;
    markers[i].setAttribute("x1",rawX);
    markers[i].setAttribute("x2",rawX);
    // 颜色根据蛇下方的位置变化
    markers[i].setAttribute("stroke","#2a3a58");
  }

  // 身体轮廓
  const outlineD=buildOutline(segs);
  bodyPath.setAttribute("d",outlineD);
  // 阴影(向下偏移)
  const shadowSegs=segs.map(s=>({...s,y:s.y+12}));
  shadowPath.setAttribute("d",buildOutline(shadowSegs));

  // 脊线
  let spineD=`M${segs[0].x} ${segs[0].y}`;
  for(let i=1;i<N;i++)spineD+=` L${segs[i].x} ${segs[i].y}`;
  spinePath.setAttribute("d",spineD);

  // 关节
  for(let i=0;i<MAX_SEG;i++){
    if(i<N){
      jointEls[i].setAttribute("cx",segs[i].x);
      jointEls[i].setAttribute("cy",segs[i].y);
      jointEls[i].setAttribute("opacity","1");
    }else{
      jointEls[i].setAttribute("opacity","0");
    }
  }

  // 头部
  headEl.setAttribute("cx",segs[0].x+Math.cos(segs[0].angle)*8);
  headEl.setAttribute("cy",segs[0].y+Math.sin(segs[0].angle)*8);
  headEl.setAttribute("transform",`rotate(${segs[0].angle*180/Math.PI},${segs[0].x+Math.cos(segs[0].angle)*8},${segs[0].y+Math.sin(segs[0].angle)*8})`);
  const ex1=segs[0].x+Math.cos(segs[0].angle)*14-Math.sin(segs[0].angle)*5;
  const ey1=segs[0].y+Math.sin(segs[0].angle)*14+Math.cos(segs[0].angle)*5;
  const ex2=segs[0].x+Math.cos(segs[0].angle)*14+Math.sin(segs[0].angle)*5;
  const ey2=segs[0].y+Math.sin(segs[0].angle)*14-Math.cos(segs[0].angle)*5;
  headEye1.setAttribute("cx",ex1);headEye1.setAttribute("cy",ey1);
  headEye2.setAttribute("cx",ex2);headEye2.setAttribute("cy",ey2);

  // 鳞片
  for(let i=0;i<MAX_SEG;i++){
    for(let j=0;j<SCALES_PER_SEG;j++){
      const s=scaleEls[i][j];
      if(i>=N){s.setAttribute("opacity","0");continue;}
      const seg=segs[i];
      // 在段体底部排列鳞片(沿段体方向等间距)
      const along=-0.3+0.2*j; // 沿体轴的偏移比例
      const sx=seg.x+Math.cos(seg.angle)*along*CFG.segLen;
      const sy=seg.y+Math.sin(seg.angle)*along*CFG.segLen;
      // 法向偏移(身体下方)
      const bnx=Math.sin(seg.angle),bny=-Math.cos(seg.angle);
      const offY=8;
      const px=sx+bnx*offY;
      const py=sy+bny*offY;
      const rot=seg.angle*180/Math.PI;
      // 颜色
      const gI=seg.gripI,sI=seg.slideI;
      let fill,op;
      if(gI>0.25){
        const r=Math.round(6+249*gI),g=Math.round(182-67*gI),b=Math.round(212-190*gI);
        fill=`rgb(${r},${g},${b})`;
        op=0.5+0.5*gI;
      }else if(sI>0.15){
        fill="#06b6d4";
        op=0.4+0.4*sI;
      }else{
        fill="#2a4a5a";op=0.3;
      }
      s.setAttribute("transform",`translate(${px},${py}) rotate(${rot})`);
      s.setAttribute("fill",fill);
      s.setAttribute("opacity",op);
    }
  }

  // 推力箭头
  let gripCount=0,slideCount=0;
  for(let i=0;i<MAX_SEG;i++){
    if(i>=N){arrowEls[i].setAttribute("opacity","0");continue;}
    const seg=segs[i];
    if(seg.isGrip){
      gripCount++;
      const len=20+30*seg.gripI;
      const ax=seg.x+Math.cos(seg.angle)*len;
      const ay=seg.y+Math.sin(seg.angle)*len;
      arrowEls[i].setAttribute("x1",seg.x);
      arrowEls[i].setAttribute("y1",seg.y);
      arrowEls[i].setAttribute("x2",ax);
      arrowEls[i].setAttribute("y2",ay);
      arrowEls[i].setAttribute("opacity",""+(0.4+0.6*seg.gripI));
      // 抓地粒子
      if(seg.gripI>0.6&&Math.random()<0.3){
        const pi=particleIdx%particles.length;
        particles[pi].x=seg.x+Math.sin(seg.angle)*(5+Math.random()*10);
        particles[pi].y=seg.y-Math.cos(seg.angle)*(5+Math.random()*10);
        particles[pi].vx=(Math.random()-0.5)*15;
        particles[pi].vy=-Math.random()*10-5;
        particles[pi].life=1;
        particleIdx++;
      }
    }else{
      slideCount++;
      arrowEls[i].setAttribute("opacity","0");
    }
  }

  // 粒子更新
  for(const p of particles){
    if(p.life>0){
      p.life-=dt*2;
      p.x+=p.vx*dt;
      p.y+=p.vy*dt;
      p.vy+=30*dt;
      p.el.setAttribute("cx",p.x);
      p.el.setAttribute("cy",p.y);
      p.el.setAttribute("opacity",""+Math.max(0,p.life*0.8));
      p.el.setAttribute("r",""+(1+p.life));
    }else{
      p.el.setAttribute("opacity","0");
    }
  }

  // 相位图
  const barW=Math.min(80,1000/N-4);
  const barGap=4;
  const totalW=N*(barW+barGap);
  const startX=(1100-totalW)/2;
  for(let i=0;i<MAX_SEG;i++){
    if(i<N){
      const seg=segs[i];
      const bx=startX+i*(barW+barGap);
      phaseBars[i].bar.setAttribute("x",bx);
      phaseBars[i].bar.setAttribute("width",barW);
      phaseBars[i].bar.setAttribute("y","20");
      phaseBars[i].bar.setAttribute("height","22");
      // 颜色
      if(seg.isGrip){
        phaseBars[i].bar.setAttribute("fill","#f97316");
        phaseBars[i].bar.setAttribute("opacity",""+(0.5+0.5*seg.gripI));
      }else{
        phaseBars[i].bar.setAttribute("fill","#06b6d4");
        phaseBars[i].bar.setAttribute("opacity",""+(0.3+0.5*seg.slideI));
      }
      phaseBars[i].label.setAttribute("x",bx+barW/2);
      phaseBars[i].label.setAttribute("opacity","1");
    }else{
      phaseBars[i].bar.setAttribute("width","0");
      phaseBars[i].label.setAttribute("opacity","0");
    }
  }

  // 截面详图 - 切换抓地/滑行状态
  const detailPhase=Math.sin(t*CFG.omega);
  if(detailPhase>0.1){
    detailScaleGrip.setAttribute("opacity","1");
    detailScaleSlide.setAttribute("opacity","0");
  }else if(detailPhase<-0.1){
    detailScaleGrip.setAttribute("opacity","0");
    detailScaleSlide.setAttribute("opacity","1");
  }else{
    detailScaleGrip.setAttribute("opacity","0.5");
    detailScaleSlide.setAttribute("opacity","0.5");
  }

  // 数据显示
  document.getElementById("d-grip").textContent=gripCount;
  document.getElementById("d-slide").textContent=slideCount;
  document.getElementById("d-spd").textContent=Math.round(fwdInc/dt*10||0);
  document.getElementById("d-dist").textContent=Math.round(totalDist);

  requestAnimationFrame(update);
}

/* ====== 控件绑定 ====== */
function bindCtrl(id,valId,fn){
  const inp=document.getElementById(id);
  const val=document.getElementById(valId);
  inp.addEventListener("input",()=>{val.textContent=inp.value;fn(parseFloat(inp.value));});
}
bindCtrl("c-spd","v-spd",v=>{CFG.omega=v;});
bindCtrl("c-amp","v-amp",v=>{CFG.amp=v;});
bindCtrl("c-seg","v-seg",v=>{CFG.numSeg=Math.round(v);});
bindCtrl("c-fr","v-fr",v=>{CFG.friction=v;});

/* ====== 启动 ====== */
requestAnimationFrame(update);

})();
</script>
</body>
</html>

实现说明:

  1. 核心物理建模:蛇身由 N 个刚性短节通过正弦行波串联而成。每节的位置由 y = A·sin(2πs/λ − ωt) 决定,通过 y × dy/dt 的符号判断鳞片状态——乘积为正表示推离中心线(抓地发力),为负表示回归中心线(滑行复位),由此实现各向异性摩擦的可视化。

  2. IFR 理想解聚焦:动画直接展示理想工作状态——鳞片在侧推时自动刚硬卡地(橙色高亮+推力箭头),前滑时自动倒伏(青色低阻),无需额外主动机构。左下角 IFR 框用文字明确标注"零成本单向阀"的核心创新:复用地面摩擦力资源、仅改变表面纹理方向即消除矛盾。

  3. 视觉引导

    • 颜色编码:橙色 = 抓地高摩擦,青色 = 滑行低摩擦,金色箭头 = 推力方向
    • 粒子特效:抓地瞬间溅出橙色粒子,强化"力传递到地面"的直觉
    • 相位图:底部条形图实时显示每节鳞片状态,波形传播一目了然
    • 截面详图:右下角侧视动画在抓地/滑行两种状态间切换,展示鳞片倾角变化的物理机制
  4. 交互控制:四个滑块(波速 ω、振幅 A、短节数 N、摩擦比 μ)允许用户实时调节参数,观察推进效果的变化——例如降低摩擦比后抓地力减弱、减小振幅后推力下降等。

  5. 自动播放requestAnimationFrame 在页面加载后立即启动动画循环,无需任何手动触发。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>单向阻力鳞片底盘 — 机械蛇推进原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;500;700;900&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
  :root {
    --bg: #060a12;
    --surface: #0c1220;
    --card: #0f1829;
    --border: #1a2a42;
    --fg: #c8d6e5;
    --muted: #5a6e84;
    --accent: #00e5c7;
    --accent-dim: rgba(0,229,199,0.15);
    --amber: #ff9f1c;
    --amber-dim: rgba(255,159,28,0.15);
    --red: #ff4757;
    --green: #2ed573;
    --green-dim: rgba(46,213,115,0.2);
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  body {
    background: var(--bg);
    color: var(--fg);
    font-family: 'IBM Plex Mono', monospace;
    min-height: 100vh;
    overflow-x: hidden;
  }
  /* 噪点纹理叠加 */
  body::before {
    content: '';
    position: fixed; inset: 0;
    background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
    pointer-events: none;
    z-index: 9999;
  }
  .app-container {
    max-width: 1400px;
    margin: 0 auto;
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 20px;
    min-height: 100vh;
  }
  header {
    display: flex;
    align-items: baseline;
    gap: 16px;
    flex-wrap: wrap;
  }
  header h1 {
    font-family: 'Outfit', sans-serif;
    font-weight: 900;
    font-size: 28px;
    letter-spacing: -0.5px;
    color: var(--accent);
    line-height: 1.1;
  }
  header .subtitle {
    font-size: 13px;
    color: var(--muted);
    font-weight: 400;
  }
  .ifrs-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--amber-dim);
    border: 1px solid rgba(255,159,28,0.3);
    color: var(--amber);
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.5px;
  }
  .main-layout {
    display: grid;
    grid-template-columns: 1fr 320px;
    gap: 20px;
    flex: 1;
  }
  @media (max-width: 960px) {
    .main-layout {
      grid-template-columns: 1fr;
    }
  }
  .canvas-wrapper {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    min-height: 480px;
  }
  .canvas-wrapper canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
  /* 左上角图例 */
  .legend {
    position: absolute;
    top: 12px; left: 12px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    z-index: 10;
  }
  .legend-item {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11px;
    color: var(--muted);
  }
  .legend-dot {
    width: 10px; height: 10px;
    border-radius: 2px;
    flex-shrink: 0;
  }
  /* 侧面板 */
  .side-panel {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }
  .panel-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 16px;
  }
  .panel-card h3 {
    font-family: 'Outfit', sans-serif;
    font-weight: 700;
    font-size: 14px;
    color: var(--fg);
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .panel-card h3 i {
    color: var(--accent);
    font-size: 13px;
  }
  .detail-canvas-wrapper {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
    aspect-ratio: 16/10;
  }
  .detail-canvas-wrapper canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
  .detail-label {
    text-align: center;
    font-size: 10px;
    color: var(--muted);
    margin-top: 8px;
    letter-spacing: 0.5px;
  }
  /* 滑块控件 */
  .control-group {
    margin-bottom: 14px;
  }
  .control-group:last-child { margin-bottom: 0; }
  .control-label {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 6px;
  }
  .control-label span:first-child {
    font-size: 12px;
    color: var(--fg);
  }
  .control-label .value {
    font-size: 12px;
    color: var(--accent);
    font-weight: 600;
  }
  input[type="range"] {
    -webkit-appearance: none;
    width: 100%;
    height: 6px;
    background: var(--border);
    border-radius: 3px;
    outline: none;
  }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px; height: 16px;
    background: var(--accent);
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 0 8px rgba(0,229,199,0.4);
  }
  /* 指标 */
  .metrics-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }
  .metric-item {
    background: var(--surface);
    border-radius: 8px;
    padding: 10px;
    text-align: center;
  }
  .metric-item .metric-value {
    font-family: 'Outfit', sans-serif;
    font-weight: 700;
    font-size: 22px;
    color: var(--accent);
    line-height: 1.2;
  }
  .metric-item .metric-label {
    font-size: 10px;
    color: var(--muted);
    margin-top: 2px;
    letter-spacing: 0.3px;
  }
  .metric-item.amber .metric-value { color: var(--amber); }
  .metric-item.green .metric-value { color: var(--green); }
  /* IFR 注释条 */
  .ifr-insight {
    background: linear-gradient(135deg, var(--amber-dim), var(--accent-dim));
    border: 1px solid rgba(255,159,28,0.2);
    border-radius: 10px;
    padding: 14px 16px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--fg);
  }
  .ifr-insight strong {
    color: var(--amber);
  }
  .ifr-insight .highlight {
    color: var(--accent);
    font-weight: 600;
  }
  /* 底部说明 */
  .footer-note {
    text-align: center;
    font-size: 11px;
    color: var(--muted);
    padding: 8px 0;
    letter-spacing: 0.3px;
  }
  /* 动画 - 脉冲指示 */
  @keyframes pulse-glow {
    0%, 100% { box-shadow: 0 0 0 0 rgba(0,229,199,0.2); }
    50% { box-shadow: 0 0 12px 2px rgba(0,229,199,0.3); }
  }
  .pulse-border {
    animation: pulse-glow 2s ease-in-out infinite;
  }
  /* 减弱动画偏好 */
  @media (prefers-reduced-motion: reduce) {
    .pulse-border { animation: none; }
  }
</style>
</head>
<body>

<div class="app-container">
  <header>
    <h1>单向阻力鳞片底盘</h1>
    <span class="subtitle">机械蛇各向异性摩擦推进原理</span>
    <span class="ifrs-badge"><i class="fas fa-bolt"></i> IFRS — 理想最终解</span>
  </header>

  <div class="main-layout">
    <!-- 主画布 -->
    <div class="canvas-wrapper" id="canvasWrapper">
      <canvas id="mainCanvas"></canvas>
      <div class="legend">
        <div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div>鳞片抓地(高摩擦)</div>
        <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>鳞片倒伏(低摩擦)</div>
        <div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div>推进力方向</div>
      </div>
    </div>

    <!-- 侧面板 -->
    <div class="side-panel">
      <!-- 鳞片细节 -->
      <div class="panel-card pulse-border">
        <h3><i class="fas fa-microscope"></i>鳞片机构剖面</h3>
        <div class="detail-canvas-wrapper">
          <canvas id="detailCanvas"></canvas>
        </div>
        <div class="detail-label">侧视图 · 鳞片棘爪状态切换</div>
      </div>

      <!-- 控制面板 -->
      <div class="panel-card">
        <h3><i class="fas fa-sliders-h"></i>参数控制</h3>
        <div class="control-group">
          <div class="control-label"><span>波幅</span><span class="value" id="ampVal">0.35 rad</span></div>
          <input type="range" id="ampSlider" min="10" max="55" value="35" step="1">
        </div>
        <div class="control-group">
          <div class="control-label"><span>波速</span><span class="value" id="speedVal">1.8 rad/s</span></div>
          <input type="range" id="speedSlider" min="5" max="35" value="18" step="1">
        </div>
        <div class="control-group">
          <div class="control-label"><span>鳞片摩擦比</span><span class="value" id="fricVal">3.0 : 1</span></div>
          <input type="range" id="fricSlider" min="15" max="60" value="30" step="1">
        </div>
      </div>

      <!-- 实时指标 -->
      <div class="panel-card">
        <h3><i class="fas fa-tachometer-alt"></i>实时指标</h3>
        <div class="metrics-grid">
          <div class="metric-item">
            <div class="metric-value" id="metricSpeed">0</div>
            <div class="metric-label">前进速度 mm/s</div>
          </div>
          <div class="metric-item amber">
            <div class="metric-value" id="metricDist">0</div>
            <div class="metric-label">总行程 mm</div>
          </div>
          <div class="metric-item green">
            <div class="metric-value" id="metricGrip">0</div>
            <div class="metric-label">当前抓地段</div>
          </div>
          <div class="metric-item">
            <div class="metric-value" id="metricWave">0</div>
            <div class="metric-label">波形位置</div>
          </div>
        </div>
      </div>

      <!-- IFR 核心洞察 -->
      <div class="ifr-insight">
        <strong>最终理想解:</strong>蛇体弯曲运动<em>已存在</em>,鳞片仅以<strong>零能耗被动几何</strong>将侧向力<strong>单向转化</strong>为前向推力——<span class="highlight">不增加主动机构,矛盾自解</span>。
      </div>
    </div>
  </div>

  <div class="footer-note">各向异性摩擦鳞片 · 仿生腹鳞力学 · 被动式单向推进</div>
</div>

<script>
// ==============================
// 全局参数
// ==============================
const SEG_COUNT = 12;
const SEG_LEN = 46;
const SEG_WID = 24;
const SCALES_PER_SEG = 4;
const GROUND_OFFSET = 32; // 蛇身中心到地面的距离

let amplitude = 0.35;
let waveSpeed = 1.8;
let waveNumber = 0.72;
let frictionRatio = 3.0;

let time = 0;
let headX = 0;
let headY = 0;
let totalDist = 0;
let segments = [];
let lastTs = 0;

// 画布引用
let mc, mctx, dc, dctx;

// ==============================
// 初始化
// ==============================
function init() {
  mc = document.getElementById('mainCanvas');
  mctx = mc.getContext('2d');
  dc = document.getElementById('detailCanvas');
  dctx = dc.getContext('2d');

  resizeCanvas();
  window.addEventListener('resize', resizeCanvas);

  // 滑块绑定
  document.getElementById('ampSlider').addEventListener('input', e => {
    amplitude = parseInt(e.target.value) / 100;
    document.getElementById('ampVal').textContent = amplitude.toFixed(2) + ' rad';
  });
  document.getElementById('speedSlider').addEventListener('input', e => {
    waveSpeed = parseInt(e.target.value) / 10;
    document.getElementById('speedVal').textContent = waveSpeed.toFixed(1) + ' rad/s';
  });
  document.getElementById('fricSlider').addEventListener('input', e => {
    frictionRatio = parseInt(e.target.value) / 10;
    document.getElementById('fricVal').textContent = frictionRatio.toFixed(1) + ' : 1';
  });

  requestAnimationFrame(loop);
}

function resizeCanvas() {
  const wrapper = document.getElementById('canvasWrapper');
  const dpr = window.devicePixelRatio || 1;
  mc.width = wrapper.clientWidth * dpr;
  mc.height = wrapper.clientHeight * dpr;
  mctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  mc.style.width = wrapper.clientWidth + 'px';
  mc.style.height = wrapper.clientHeight + 'px';

  headY = wrapper.clientHeight / 2;
  if (headX === 0) headX = wrapper.clientWidth * 0.3;

  const dw = dc.parentElement;
  dc.width = dw.clientWidth * dpr;
  dc.height = dw.clientHeight * dpr;
  dctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

// ==============================
// 蛇体计算
// ==============================
function computeSnake() {
  segments = [];
  let cumAngle = 0;

  for (let i = 0; i < SEG_COUNT; i++) {
    const relAngle = amplitude * Math.sin(waveNumber * i - waveSpeed * time);
    cumAngle += relAngle;

    let x, y;
    if (i === 0) {
      x = headX;
      y = headY;
    } else {
      const prev = segments[i - 1];
      x = prev.x - SEG_LEN * Math.cos(prev.angle);
      y = prev.y - SEG_LEN * Math.sin(prev.angle);
    }

    // 抓地因子:角速度越大 → 越在主动摆动 → 鳞片抓地
    const angVel = Math.abs(amplitude * waveSpeed * Math.cos(waveNumber * i - waveSpeed * time));
    const maxAngVel = Math.max(amplitude * waveSpeed, 0.001);
    const gripFactor = Math.pow(angVel / maxAngVel, 0.7);

    segments.push({ x, y, angle: cumAngle, relAngle, gripFactor });
  }
}

// ==============================
// 主循环
// ==============================
function loop(ts) {
  if (!lastTs) lastTs = ts;
  const dt = Math.min((ts - lastTs) / 1000, 0.05);
  lastTs = ts;

  // 更新状态
  time += dt;
  const fwdSpeed = 55 * amplitude * waveSpeed;
  totalDist += fwdSpeed * dt;
  headX += fwdSpeed * dt;

  computeSnake();
  renderMain();
  renderDetail();
  updateMetrics(fwdSpeed);

  requestAnimationFrame(loop);
}

// ==============================
// 主画布渲染
// ==============================
function renderMain() {
  const W = mc.width / (window.devicePixelRatio || 1);
  const H = mc.height / (window.devicePixelRatio || 1);
  const ctx = mctx;
  const camX = headX - W * 0.35;

  // 背景
  ctx.fillStyle = '#060a12';
  ctx.fillRect(0, 0, W, H);

  // 网格
  drawGrid(ctx, W, H, camX);

  // 地面
  const groundY = headY + GROUND_OFFSET;
  drawGround(ctx, W, H, camX, groundY);

  ctx.save();
  ctx.translate(-camX, 0);

  // 绘制蛇体(从尾到头,头在最上层)
  for (let i = SEG_COUNT - 1; i >= 0; i--) {
    drawSegment(ctx, segments[i], i);
  }

  // 绘制头部
  drawHead(ctx, segments[0]);

  // 推进力箭头
  drawForceArrows(ctx, camX, W);

  // 波形传播指示
  drawWaveIndicator(ctx, camX, W);

  ctx.restore();

  // 前进距离标尺
  drawRuler(ctx, W, H, camX);
}

function drawGrid(ctx, W, H, camX) {
  ctx.strokeStyle = 'rgba(26,42,66,0.4)';
  ctx.lineWidth = 0.5;
  const step = 60;
  const offX = camX % step;
  for (let x = -offX; x < W; x += step) {
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
  }
  for (let y = 0; y < H; y += step) {
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
  }
}

function drawGround(ctx, W, H, camX, groundY) {
  // 地面填充
  const grd = ctx.createLinearGradient(0, groundY, 0, groundY + 80);
  grd.addColorStop(0, '#0e1628');
  grd.addColorStop(1, '#060a12');
  ctx.fillStyle = grd;
  ctx.fillRect(0, groundY, W, H - groundY);

  // 地面线
  ctx.strokeStyle = '#1e3050';
  ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();

  // 距离刻度
  ctx.fillStyle = '#2a3a5a';
  ctx.font = '9px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  const step = 80;
  const offX = camX % step;
  for (let x = -offX; x < W; x += step) {
    const worldX = Math.round((camX + x) / 10) * 10;
    ctx.fillRect(x, groundY - 4, 1, 8);
    ctx.fillText(worldX + '', x, groundY + 18);
  }

  // 抓地痕迹
  for (let i = 0; i < SEG_COUNT; i++) {
    const seg = segments[i];
    if (seg.gripFactor > 0.6) {
      const gx = seg.x;
      const gy = groundY;
      ctx.fillStyle = `rgba(255,159,28,${seg.gripFactor * 0.25})`;
      for (let s = 0; s < SCALES_PER_SEG; s++) {
        const sx = gx - SEG_LEN * 0.4 + (SEG_LEN * 0.8 / SCALES_PER_SEG) * (s + 0.5);
        ctx.fillRect(sx - 2, gy, 4, 3);
      }
    }
  }
}

function drawSegment(ctx, seg, idx) {
  ctx.save();
  ctx.translate(seg.x, seg.y);
  ctx.rotate(seg.angle);

  const w = SEG_LEN;
  const h = SEG_WID;
  const gf = seg.gripFactor;

  // 阴影
  ctx.shadowColor = gf > 0.5 ? `rgba(255,159,28,${gf * 0.3})` : 'rgba(0,229,199,0.08)';
  ctx.shadowBlur = gf > 0.5 ? 14 : 6;
  ctx.shadowOffsetY = 4;

  // 段体填充
  const bodyGrd = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
  bodyGrd.addColorStop(0, '#162a3a');
  bodyGrd.addColorStop(0.5, '#0e1e2e');
  bodyGrd.addColorStop(1, '#0a1622');
  ctx.fillStyle = bodyGrd;
  roundRect(ctx, -w / 2, -h / 2, w, h, 5);
  ctx.fill();

  ctx.shadowColor = 'transparent';
  ctx.shadowBlur = 0;
  ctx.shadowOffsetY = 0;

  // 段体描边
  const borderColor = gf > 0.5
    ? `rgba(255,159,28,${0.3 + gf * 0.5})`
    : `rgba(0,229,199,${0.2 + (1 - gf) * 0.2})`;
  ctx.strokeStyle = borderColor;
  ctx.lineWidth = 1.5;
  roundRect(ctx, -w / 2, -h / 2, w, h, 5);
  ctx.stroke();

  // 鳞片(底部)
  drawScales(ctx, w, h, gf);

  // 关节圆点
  ctx.beginPath();
  ctx.arc(-w / 2, 0, 3.5, 0, Math.PI * 2);
  ctx.fillStyle = gf > 0.5 ? '#ff9f1c' : '#1a3a4a';
  ctx.fill();
  ctx.strokeStyle = gf > 0.5 ? '#ff9f1c' : '#00e5c7';
  ctx.lineWidth = 1;
  ctx.stroke();

  // 段号
  ctx.fillStyle = 'rgba(200,214,229,0.25)';
  ctx.font = '8px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(idx, 0, 0);

  ctx.restore();
}

function drawScales(ctx, w, h, gripFactor) {
  const scaleW = 7;
  const scaleH = 5;
  const spacing = (w * 0.7) / SCALES_PER_SEG;
  const startX = -w * 0.35;

  for (let s = 0; s < SCALES_PER_SEG; s++) {
    const sx = startX + spacing * (s + 0.5);
    const sy = h / 2;

    ctx.save();
    ctx.translate(sx, sy);

    if (gripFactor > 0.5) {
      // 抓地状态:鳞片竖起
      const tilt = -0.3 - gripFactor * 0.4; // 向后倾斜
      ctx.rotate(tilt);

      // 鳞片体
      const sGrd = ctx.createLinearGradient(0, 0, 0, scaleH);
      sGrd.addColorStop(0, `rgba(255,159,28,${0.6 + gripFactor * 0.4})`);
      sGrd.addColorStop(1, `rgba(255,71,87,${0.3 + gripFactor * 0.3})`);
      ctx.fillStyle = sGrd;

      ctx.beginPath();
      ctx.moveTo(-scaleW / 2, 0);
      ctx.lineTo(0, scaleH);
      ctx.lineTo(scaleW / 2, 0);
      ctx.closePath();
      ctx.fill();

      // 发光
      ctx.shadowColor = `rgba(255,159,28,${gripFactor * 0.5})`;
      ctx.shadowBlur = 6;
      ctx.fill();
      ctx.shadowColor = 'transparent';
      ctx.shadowBlur = 0;
    } else {
      // 倒伏状态:鳞片平贴
      const flatFactor = 1 - gripFactor;
      ctx.fillStyle = `rgba(46,213,115,${0.2 + flatFactor * 0.25})`;

      ctx.beginPath();
      ctx.moveTo(-scaleW / 2, 0);
      ctx.lineTo(-scaleW / 2 - 1, scaleH * 0.3);
      ctx.lineTo(scaleW / 2 + 1, scaleH * 0.3);
      ctx.lineTo(scaleW / 2, 0);
      ctx.closePath();
      ctx.fill();
    }

    ctx.restore();
  }
}

function drawHead(ctx, seg) {
  ctx.save();
  ctx.translate(seg.x, seg.y);
  ctx.rotate(seg.angle);

  // 头部形状(略尖)
  ctx.beginPath();
  ctx.moveTo(SEG_LEN / 2 + 14, 0);
  ctx.lineTo(SEG_LEN / 2, -SEG_WID / 2 - 2);
  ctx.lineTo(SEG_LEN / 2 - 6, -SEG_WID / 2);
  ctx.lineTo(SEG_LEN / 2 - 6, SEG_WID / 2);
  ctx.lineTo(SEG_LEN / 2, SEG_WID / 2 + 2);
  ctx.closePath();

  const headGrd = ctx.createLinearGradient(0, -SEG_WID / 2, 0, SEG_WID / 2);
  headGrd.addColorStop(0, '#1a4050');
  headGrd.addColorStop(1, '#0e2838');
  ctx.fillStyle = headGrd;
  ctx.fill();

  ctx.strokeStyle = 'rgba(0,229,199,0.6)';
  ctx.lineWidth = 1.5;
  ctx.stroke();

  // 眼睛
  ctx.beginPath();
  ctx.arc(SEG_LEN / 2 + 4, -6, 3, 0, Math.PI * 2);
  ctx.fillStyle = '#00e5c7';
  ctx.fill();
  ctx.beginPath();
  ctx.arc(SEG_LEN / 2 + 4, 6, 3, 0, Math.PI * 2);
  ctx.fillStyle = '#00e5c7';
  ctx.fill();

  // 眼睛高光
  ctx.beginPath();
  ctx.arc(SEG_LEN / 2 + 5, -7, 1.2, 0, Math.PI * 2);
  ctx.fillStyle = '#fff';
  ctx.fill();
  ctx.beginPath();
  ctx.arc(SEG_LEN / 2 + 5, 5, 1.2, 0, Math.PI * 2);
  ctx.fillStyle = '#fff';
  ctx.fill();

  ctx.restore();
}

function drawForceArrows(ctx, camX, viewW) {
  // 在抓地段显示推进力箭头
  for (let i = 0; i < SEG_COUNT; i++) {
    const seg = segments[i];
    if (seg.gripFactor < 0.6) continue;
    if (seg.x < camX - 50 || seg.x > camX + viewW + 50) continue;

    const arrowLen = 20 + seg.gripFactor * 25;
    const ax = seg.x + Math.cos(seg.angle) * (SEG_LEN / 2 + 10);
    const ay = seg.y + Math.sin(seg.angle) * (SEG_LEN / 2 + 10);

    // 推进力箭头(沿蛇身方向前进方向)
    ctx.save();
    ctx.translate(ax, ay);
    ctx.rotate(seg.angle);

    ctx.strokeStyle = `rgba(0,229,199,${seg.gripFactor * 0.8})`;
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(arrowLen, 0);
    ctx.stroke();

    // 箭头头部
    ctx.fillStyle = `rgba(0,229,199,${seg.gripFactor * 0.8})`;
    ctx.beginPath();
    ctx.moveTo(arrowLen + 6, 0);
    ctx.lineTo(arrowLen - 3, -4);
    ctx.lineTo(arrowLen - 3, 4);
    ctx.closePath();
    ctx.fill();

    // 力值标签
    const forceVal = (seg.gripFactor * frictionRatio * 0.8).toFixed(1);
    ctx.fillStyle = `rgba(0,229,199,${seg.gripFactor * 0.7})`;
    ctx.font = '9px "IBM Plex Mono"';
    ctx.textAlign = 'center';
    ctx.fillText('F=' + forceVal + 'N', arrowLen / 2, -8);

    ctx.restore();
  }
}

function drawWaveIndicator(ctx, camX, viewW) {
  // 波形传播方向指示(沿蛇身上方)
  if (segments.length < 2) return;

  const headSeg = segments[0];
  const tailSeg = segments[SEG_COUNT - 1];
  const midX = (headSeg.x + tailSeg.x) / 2;
  const topY = Math.min(...segments.map(s => s.y)) - 50;

  if (midX < camX - 100 || midX > camX + viewW + 100) return;

  // 波形传播箭头(从尾到头表示波向后传,但实际是蛇向前)
  const wavePhase = (time * waveSpeed) % (Math.PI * 2);
  const pulseX = headSeg.x - ((wavePhase / (Math.PI * 2)) * (headSeg.x - tailSeg.x));

  ctx.save();
  ctx.setLineDash([4, 4]);
  ctx.strokeStyle = 'rgba(255,159,28,0.35)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(headSeg.x, topY);
  ctx.lineTo(tailSeg.x, topY);
  ctx.stroke();
  ctx.setLineDash([]);

  // 脉冲点
  ctx.beginPath();
  ctx.arc(pulseX, topY, 5, 0, Math.PI * 2);
  ctx.fillStyle = 'rgba(255,159,28,0.8)';
  ctx.fill();

  // 标签
  ctx.fillStyle = 'rgba(255,159,28,0.7)';
  ctx.font = '10px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.fillText('← 波形传播方向', midX, topY - 10);

  ctx.restore();
}

function drawRuler(ctx, W, H, camX) {
  const y = H - 20;
  ctx.fillStyle = 'rgba(200,214,229,0.3)';
  ctx.font = '10px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.fillText('前进距离: ' + (totalDist / 10).toFixed(1) + ' cm', W / 2, y);
}

// ==============================
// 辅助:圆角矩形
// ==============================
function roundRect(ctx, x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.arcTo(x + w, y, x + w, y + r, r);
  ctx.lineTo(x + w, y + h - r);
  ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
  ctx.lineTo(x + r, y + h);
  ctx.arcTo(x, y + h, x, y + h - r, r);
  ctx.lineTo(x, y + r);
  ctx.arcTo(x, y, x + r, y, r);
  ctx.closePath();
}

// ==============================
// 鳞片细节剖面渲染
// ==============================
function renderDetail() {
  const W = dc.width / (window.devicePixelRatio || 1);
  const H = dc.height / (window.devicePixelRatio || 1);
  const ctx = dctx;

  // 取第2段的抓地因子作为示例
  const refGrip = segments.length > 2 ? segments[2].gripFactor : 0;

  ctx.fillStyle = '#080c14';
  ctx.fillRect(0, 0, W, H);

  const cx = W / 2;
  const groundY = H * 0.65;

  // 地面
  ctx.fillStyle = '#0e1628';
  ctx.fillRect(0, groundY, W, H - groundY);
  ctx.strokeStyle = '#1e3050';
  ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();

  // 地面纹理
  ctx.fillStyle = '#162240';
  for (let i = 0; i < 12; i++) {
    const bx = 10 + i * (W - 20) / 11;
    ctx.fillRect(bx, groundY + 2, 8, 3);
  }

  // 蛇身截面(底部弧形)
  const bodyW = W * 0.5;
  const bodyH = 28;
  const bodyX = cx - bodyW / 2;
  const bodyY = groundY - bodyH - 8;

  const bodyGrd = ctx.createLinearGradient(0, bodyY, 0, bodyY + bodyH);
  bodyGrd.addColorStop(0, '#1a3a4a');
  bodyGrd.addColorStop(1, '#0d2030');
  ctx.fillStyle = bodyGrd;
  roundRect(ctx, bodyX, bodyY, bodyW, bodyH, 6);
  ctx.fill();

  ctx.strokeStyle = refGrip > 0.5
    ? `rgba(255,159,28,${0.4 + refGrip * 0.4})`
    : 'rgba(0,229,199,0.3)';
  ctx.lineWidth = 1.5;
  roundRect(ctx, bodyX, bodyY, bodyW, bodyH, 6);
  ctx.stroke();

  // 标签:蛇身
  ctx.fillStyle = 'rgba(200,214,229,0.5)';
  ctx.font = '9px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.fillText('短节截面', cx, bodyY + bodyH / 2 + 3);

  // 鳞片(3个)
  const scalePositions = [cx - bodyW * 0.25, cx, cx + bodyW * 0.25];
  scalePositions.forEach((sx, si) => {
    ctx.save();
    ctx.translate(sx, bodyY + bodyH);

    if (refGrip > 0.5) {
      // 抓地:鳞片竖起,向后倾斜
      const tiltAngle = -0.4 - refGrip * 0.5;
      ctx.rotate(tiltAngle);

      const sLen = 18;
      const sWid = 6;

      // 鳞片体
      const sGrd = ctx.createLinearGradient(0, 0, 0, sLen);
      sGrd.addColorStop(0, `rgba(255,159,28,${0.7 + refGrip * 0.3})`);
      sGrd.addColorStop(1, `rgba(255,71,87,${0.4 + refGrip * 0.3})`);
      ctx.fillStyle = sGrd;

      ctx.beginPath();
      ctx.moveTo(-sWid / 2, 0);
      ctx.lineTo(0, sLen);
      ctx.lineTo(sWid / 2, 0);
      ctx.closePath();
      ctx.fill();

      ctx.shadowColor = `rgba(255,159,28,${refGrip * 0.5})`;
      ctx.shadowBlur = 8;
      ctx.fill();
      ctx.shadowColor = 'transparent';
      ctx.shadowBlur = 0;

      // 棘爪尖端标记
      ctx.beginPath();
      ctx.arc(0, sLen, 2, 0, Math.PI * 2);
      ctx.fillStyle = '#ff4757';
      ctx.fill();

      // 与地面的接触力
      if (si === 1) {
        ctx.rotate(-tiltAngle); // 恢复旋转以画水平箭头
        // 摩擦力(水平,向前)
        ctx.strokeStyle = 'rgba(0,229,199,0.8)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(0, groundY - bodyY - bodyH + 8);
        ctx.lineTo(30, groundY - bodyY - bodyH + 8);
        ctx.stroke();

        ctx.fillStyle = 'rgba(0,229,199,0.8)';
        ctx.beginPath();
        ctx.moveTo(36, groundY - bodyY - bodyH + 8);
        ctx.lineTo(28, groundY - bodyY - bodyH + 4);
        ctx.lineTo(28, groundY - bodyY - bodyH + 12);
        ctx.closePath();
        ctx.fill();

        // 法向力(竖直向上)
        ctx.strokeStyle = 'rgba(255,159,28,0.6)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(0, -20);
        ctx.stroke();

        ctx.fillStyle = 'rgba(255,159,28,0.6)';
        ctx.beginPath();
        ctx.moveTo(0, -24);
        ctx.lineTo(-3, -17);
        ctx.lineTo(3, -17);
        ctx.closePath();
        ctx.fill();
      }
    } else {
      // 倒伏:鳞片平贴蛇身底部
      const flatLen = 14;
      const flatH = 3;

      ctx.fillStyle = `rgba(46,213,115,${0.3 + (1 - refGrip) * 0.3})`;
      ctx.fillRect(-flatLen / 2, 0, flatLen, flatH);

      // 滑动方向小箭头
      if (si === 1) {
        ctx.fillStyle = `rgba(46,213,115,0.6)`;
        ctx.font = '8px "IBM Plex Mono"';
        ctx.textAlign = 'left';
        ctx.fillText('→ 滑行', flatLen / 2 + 4, flatH + 8);
      }
    }

    ctx.restore();
  });

  // 状态标签
  const stateLabel = refGrip > 0.5 ? '抓地模式' : '滑行模式';
  const stateColor = refGrip > 0.5 ? '#ff9f1c' : '#2ed573';
  ctx.fillStyle = stateColor;
  ctx.font = 'bold 11px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.fillText(stateLabel, cx, 16);

  // 摩擦系数
  if (refGrip > 0.5) {
    ctx.fillStyle = 'rgba(255,159,28,0.7)';
    ctx.font = '9px "IBM Plex Mono"';
    ctx.fillText('μ = ' + (frictionRatio * 0.5).toFixed(1) + '(高摩擦)', cx, groundY + 20);
  } else {
    ctx.fillStyle = 'rgba(46,213,115,0.6)';
    ctx.font = '9px "IBM Plex Mono"';
    ctx.fillText('μ = ' + (frictionRatio * 0.5 / frictionRatio).toFixed(2) + '(低摩擦)', cx, groundY + 20);
  }

  // 力分解标注
  if (refGrip > 0.5) {
    ctx.fillStyle = 'rgba(0,229,199,0.6)';
    ctx.font = '8px "IBM Plex Mono"';
    ctx.textAlign = 'left';
    ctx.fillText('推进力 F∥', cx + 38, groundY - bodyY - bodyH + 10);

    ctx.fillStyle = 'rgba(255,159,28,0.5)';
    ctx.fillText('法向力 F⊥', 6, bodyY - 6);
  }

  // IFR标注
  ctx.fillStyle = 'rgba(200,214,229,0.3)';
  ctx.font = '8px "IBM Plex Mono"';
  ctx.textAlign = 'center';
  ctx.fillText('被动几何 · 零能耗 · 各向异性', cx, H - 6);
}

// ==============================
// 指标更新
// ==============================
function updateMetrics(fwdSpeed) {
  document.getElementById('metricSpeed').textContent = (fwdSpeed * 1.2).toFixed(0);
  document.getElementById('metricDist').textContent = (totalDist / 10).toFixed(1);
  const gripCount = segments.filter(s => s.gripFactor > 0.6).length;
  document.getElementById('metricGrip').textContent = gripCount + ' / ' + SEG_COUNT;
  const wavePos = ((time * waveSpeed) % (Math.PI * 2) / (Math.PI * 2) * 100).toFixed(0);
  document.getElementById('metricWave').textContent = wavePos + '%';
}

// ==============================
// 启动
// ==============================
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

这个动画完整实现了机械蛇"单向阻力鳞片底盘"的推进原理演示,核心设计思路如下:

动画核心机制:

  • 12 段刚性短节通过铰链串联,正弦波从头部向尾部传播
  • 每段底部的仿生棘爪鳞片根据角速度自动判断抓地/滑行状态——角速度大时鳞片竖起(琥珀色高亮,高摩擦),角速度小时鳞片倒伏(绿色,低摩擦)
  • 抓地段显示推进力矢量箭头和力值,地面留有抓地痕迹

IFR 理想解视觉引导:

  • 侧面板的鳞片剖面图实时展示"被动几何·零能耗"的核心创新——鳞片无需额外动力,仅靠各向异性摩擦将侧向摆动单向转化为前向推力
  • 力分解箭头(推进力 F∥ 与法向力 F⊥)清晰展示转化机理
  • 抓地/滑行双色系统(琥珀 vs 翠绿)+ 段体描边辉光变化,引导用户关注矛盾破解的关键动作

交互控制: 三个滑块可实时调节波幅、波速和摩擦比,观察参数变化对推进效果的影响;四项实时指标监控速度、行程、抓地段数和波形位置。

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