分享图
A
动画渲染工坊
就绪
<!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=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#060a14;--fg:#d8e2ec;--muted:#4a5e72;
  --cyan:#00e5ff;--green:#00ff88;--amber:#ffb800;
  --red:#ff4422;--spring:#c8e600;--core:#5a9ae6;
  --card:rgba(8,16,34,0.92);--border:rgba(0,229,255,0.15);
}
body{background:var(--bg);color:var(--fg);font-family:'Share Tech Mono',monospace;overflow:hidden;height:100vh}
canvas#main{position:fixed;top:0;left:0;width:100%;height:100%}

/* 顶部阶段条 */
.phase-bar{
  position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:10;
  display:flex;align-items:center;gap:14px;
  background:var(--card);border:1px solid var(--border);border-radius:10px;
  padding:10px 22px;backdrop-filter:blur(14px);pointer-events:none;
}
.phase-name{font-family:'Orbitron',sans-serif;font-weight:700;font-size:16px;min-width:110px;transition:color .3s}
.phase-dots{display:flex;gap:8px}
.phase-dot{width:10px;height:10px;border-radius:50%;border:2px solid var(--muted);transition:all .35s}
.phase-dot.active{transform:scale(1.4);box-shadow:0 0 8px currentColor}
.phase-progress{width:180px;height:3px;background:rgba(255,255,255,.08);border-radius:2px;overflow:hidden}
.phase-progress-fill{height:100%;border-radius:2px;transition:width .06s linear}

/* 左侧描述面板 */
.desc-panel{
  position:fixed;top:90px;left:22px;width:232px;z-index:10;
  background:var(--card);border:1px solid var(--border);border-radius:10px;
  padding:14px 16px;backdrop-filter:blur(14px);pointer-events:none;
}
.sec-title{font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:2.5px;margin-bottom:8px;text-transform:uppercase}
.desc-text{font-size:12.5px;line-height:1.65;color:var(--fg)}
.chain-text{font-size:10.5px;color:var(--muted);line-height:1.55}

/* 右侧参数面板 */
.params-panel{
  position:fixed;top:90px;right:22px;width:210px;z-index:10;
  background:var(--card);border:1px solid var(--border);border-radius:10px;
  padding:14px 16px;backdrop-filter:blur(14px);pointer-events:none;
}
.param-row{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.param-label{font-size:11px;color:var(--muted)}
.param-value{font-size:12px;font-weight:bold;transition:color .3s}
.lock-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;transition:all .3s}

/* 底部力曲线 */
.force-panel{
  position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:480px;height:110px;z-index:10;
  background:var(--card);border:1px solid var(--border);border-radius:10px;
  padding:8px 14px;backdrop-filter:blur(14px);pointer-events:none;
}
.force-title{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;color:var(--cyan);margin-bottom:3px}
canvas#force{width:100%;height:calc(100% - 18px)}

/* 左上标题 */
.title-block{position:fixed;top:22px;left:22px;z-index:10;pointer-events:none}
.title-main{font-family:'Orbitron',sans-serif;font-size:13px;font-weight:900;letter-spacing:3px;color:var(--cyan)}
.title-sub{font-size:10.5px;color:var(--muted);margin-top:3px}

@media(max-width:900px){
  .desc-panel,.params-panel{display:none}
  .force-panel{width:calc(100% - 40px)}
}
</style>
</head>
<body>

<canvas id="main"></canvas>

<div class="title-block">
  <div class="title-main">PASSIVE SLIDER LEG</div>
  <div class="title-sub">被动伸缩滑块小腿 · 自适应缓冲原理</div>
</div>

<div class="phase-bar">
  <div class="phase-name" id="pName">悬空摆动</div>
  <div class="phase-dots">
    <div class="phase-dot" id="d0"></div>
    <div class="phase-dot" id="d1"></div>
    <div class="phase-dot" id="d2"></div>
    <div class="phase-dot" id="d3"></div>
  </div>
  <div class="phase-progress"><div class="phase-progress-fill" id="pFill"></div></div>
</div>

<div class="desc-panel">
  <div class="sec-title" style="color:var(--cyan)">当前时序</div>
  <div class="desc-text" id="descTxt">电磁锁松开,滑块自由伸缩</div>
  <div style="margin-top:12px">
    <div class="sec-title" style="color:var(--amber)">机理作用链</div>
    <div class="chain-text">摆动解锁 → 触地吸能 → 力矩阈值锁死 → 刚性支撑 → 离地解锁弹起</div>
  </div>
  <div style="margin-top:12px">
    <div class="sec-title" style="color:var(--red)">失效条件</div>
    <div class="chain-text">极高频冲击下电磁锁响应滞后,导致支撑刚性不足</div>
  </div>
</div>

<div class="params-panel">
  <div class="sec-title" style="color:var(--cyan)">实时参数</div>
  <div class="param-row"><span class="param-label">弹簧刚度</span><span class="param-value" style="color:var(--spring)">15 N/mm</span></div>
  <div class="param-row"><span class="param-label">最大行程</span><span class="param-value" style="color:var(--cyan)">30 mm</span></div>
  <div class="param-row"><span class="param-label">锁紧响应</span><span class="param-value" style="color:var(--green)">&lt;10 ms</span></div>
  <div class="param-row"><span class="param-label">当前压缩</span><span class="param-value" id="vComp" style="color:var(--amber)">0.0 mm</span></div>
  <div class="param-row"><span class="param-label">弹簧力</span><span class="param-value" id="vForce" style="color:var(--spring)">0 N</span></div>
  <div class="param-row"><span class="param-label">膝力矩</span><span class="param-value" id="vTorq" style="color:var(--fg)">0.0 Nm</span></div>
  <div class="param-row"><span class="param-label">支撑阈值</span><span class="param-value" style="color:var(--red)">5 Nm</span></div>
  <div class="param-row"><span class="param-label">电磁锁</span><span class="param-value" id="vLock"><span class="lock-dot" id="lockDot"></span><span id="lockTxt">松开</span></span></div>
</div>

<div class="force-panel">
  <div class="force-title">FORCE-TIME 力-时曲线</div>
  <canvas id="force"></canvas>
</div>

<script>
/* =========================================
   被动伸缩滑块小腿 · 原理动画
   ========================================= */

const C = document.getElementById('main');
const X = C.getContext('2d');
const FC = document.getElementById('force');
const FX = FC.getContext('2d');

// ─── 阶段定义 ───
const PH = [
  { name:'悬空摆动', sub:'SWING',   dur:2000, color:'#00ff88', desc:'电磁锁松开,滑块自由伸缩' },
  { name:'触地缓冲', sub:'IMPACT',  dur:1500, color:'#ffb800', desc:'地面反力压缩弹簧吸收冲击能' },
  { name:'刚性支撑', sub:'STANCE',  dur:2000, color:'#ff4422', desc:'电磁锁锁死,滑块变刚性连杆' },
  { name:'蹬地离地', sub:'PUSH-OFF', dur:1500, color:'#00e5ff', desc:'解锁释放弹簧余能弹起离地' }
];
const CYCLE = PH.reduce((s,p)=>s+p.dur,0);

// ─── 机构尺寸(以800高为基准) ───
const M = {
  sTopOff:28, sLen:200, sOW:78, sIW:56,
  spTopOff:36, spNatLen:120,
  coreLen:155, coreW:44,
  footW:92, footH:14,
  maxComp:42
};
M.natRel = M.spTopOff + M.spNatLen + M.coreLen; // 自然足距

// ─── 状态 ───
let S = { ct:0, pi:0, pp:0, by:0, comp:0, compMm:0, lock:false, fog:false, sf:0, kt:0, pc:'#00ff88' };
let particles = [];
let fHist = [];
const FH_MAX = 350;
let lastT = 0;

// ─── 画布尺寸 ───
function resize(){
  C.width = innerWidth; C.height = innerHeight;
  FC.width = FC.parentElement.clientWidth - 28;
  FC.height = FC.parentElement.clientHeight - 22;
}
addEventListener('resize', resize); resize();

// ─── 工具函数 ───
const lerp = (a,b,t)=>a+(b-a)*t;
const clamp = (v,lo,hi)=>Math.max(lo,Math.min(hi,v));
const eio = t=>t<.5?2*t*t:-1+(4-2*t)*t;
const ein = t=>t*t;
const eout = t=>t*(2-t);

// ─── 更新逻辑 ───
function update(dt){
  S.ct = (S.ct + dt) % CYCLE;
  let el = 0;
  for(let i=0;i<4;i++){
    if(S.ct < el + PH[i].dur){ S.pi=i; S.pp=(S.ct-el)/PH[i].dur; break; }
    el += PH[i].dur;
  }
  const t = S.pp;
  const sc = C.height / 800;
  const GY = C.height * 0.78;
  const natRel = M.natRel * sc;
  const maxC = M.maxComp * sc;
  const byContact = GY - natRel; // 足刚好触地时bodyY

  switch(S.pi){
    case 0: // 悬空摆动
      S.by = lerp(byContact - 100*sc, byContact, eio(t));
      S.comp = 0; S.lock = false; S.fog = false;
      break;
    case 1: // 触地缓冲
      S.by = lerp(byContact, byContact + maxC, ein(t));
      S.comp = S.by - byContact; S.lock = false; S.fog = true;
      break;
    case 2: // 刚性支撑
      S.by = byContact + maxC + Math.sin(t*Math.PI*10)*0.6*sc;
      S.comp = maxC; S.lock = true; S.fog = true;
      break;
    case 3: // 蹬地离地
      S.by = lerp(byContact + maxC, byContact - 100*sc, eout(t));
      S.lock = false;
      if(S.by > byContact){ S.fog=true; S.comp = S.by - byContact; }
      else{ S.fog=false; S.comp=0; }
      break;
  }
  S.comp = clamp(S.comp, 0, maxC);
  S.compMm = (S.comp / maxC) * 30;
  S.sf = 15 * S.compMm;
  S.kt = S.fog ? S.sf * 0.12 : 0;
  S.pc = PH[S.pi].color;

  // 粒子
  if(S.pi===2 && t<0.04) spawnLock();
  if(S.pi===3 && t>0.1 && t<0.15) spawnRelease();
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i]; p.x+=p.vx; p.y+=p.vy; p.vy+=0.02; p.life-=p.dec;
    if(p.life<=0) particles.splice(i,1);
  }

  // 力历史
  fHist.push({ sf:S.sf, kt:S.kt, ph:S.pi });
  if(fHist.length>FH_MAX) fHist.shift();
}

function spawnLock(){
  const sc=C.height/800, cx=C.width*0.55, ly=S.by+(M.sTopOff+M.sLen*0.55)*sc;
  for(let i=0;i<20;i++) particles.push({
    x:cx+(Math.random()-.5)*80*sc, y:ly+(Math.random()-.5)*16*sc,
    vx:(Math.random()-.5)*2.5, vy:(Math.random()-.5)*3,
    life:1, dec:.015+Math.random()*.02, color:'#ff4422', sz:2+Math.random()*3
  });
}
function spawnRelease(){
  const sc=C.height/800, cx=C.width*0.55, ly=S.by+(M.sTopOff+M.sLen*0.55)*sc;
  for(let i=0;i<10;i++) particles.push({
    x:cx+(Math.random()-.5)*60*sc, y:ly+(Math.random()-.5)*12*sc,
    vx:(Math.random()-.5)*1.5, vy:(Math.random()-.5)*2,
    life:1, dec:.025+Math.random()*.02, color:'#00e5ff', sz:1.5+Math.random()*2
  });
}

// ─── 绘制:网格背景 ───
function drawGrid(){
  const w=C.width, h=C.height;
  X.strokeStyle='rgba(0,229,255,0.025)'; X.lineWidth=1;
  const sp=50;
  for(let x=0;x<w;x+=sp){ X.beginPath(); X.moveTo(x,0); X.lineTo(x,h); X.stroke(); }
  for(let y=0;y<h;y+=sp){ X.beginPath(); X.moveTo(0,y); X.lineTo(w,y); X.stroke(); }
}

// ─── 绘制:地面 ───
function drawGround(){
  const gy=C.height*0.78, w=C.width;
  // 地面填充
  const grd=X.createLinearGradient(0,gy,0,C.height);
  grd.addColorStop(0,'#111827'); grd.addColorStop(1,'#060a14');
  X.fillStyle=grd; X.fillRect(0,gy,w,C.height-gy);
  // 地面线
  X.strokeStyle='#1e2d44'; X.lineWidth=2;
  X.beginPath(); X.moveTo(0,gy); X.lineTo(w,gy); X.stroke();
  // 纹理斜线
  X.strokeStyle='rgba(255,255,255,0.02)'; X.lineWidth=1;
  for(let x=-C.height;x<w+C.height;x+=14){
    X.beginPath(); X.moveTo(x,gy+2); X.lineTo(x+(C.height-gy),C.height); X.stroke();
  }
  // 地面冲击波纹
  if(S.pi===1 && S.pp<0.5){
    const cx=C.width*0.55;
    const r = S.pp * 120 * (C.height/800);
    const a = 1 - S.pp*2;
    X.strokeStyle=`rgba(255,184,0,${a*0.3})`; X.lineWidth=1.5;
    X.beginPath(); X.ellipse(cx,gy,r,r*0.3,0,0,Math.PI*2); X.stroke();
  }
}

// ─── 绘制:完整机构 ───
function drawMech(){
  const sc=C.height/800, cx=C.width*0.55, by=S.by, comp=S.comp;
  const sT=by+M.sTopOff*sc, sB=sT+M.sLen*sc;
  const spT=by+M.spTopOff*sc, spL=M.spNatLen*sc-comp, spB=spT+spL;
  const cT=spB, cB=cT+M.coreLen*sc;
  const sOW=M.sOW*sc, sIW=M.sIW*sc, cW=M.coreW*sc, fW=M.footW*sc, fH=M.footH*sc;

  // ── 机体连接 ──
  X.strokeStyle='#00e5ff'; X.lineWidth=2.5*sc;
  X.beginPath(); X.moveTo(cx-30*sc,by); X.lineTo(cx-20*sc,by+15*sc);
  X.lineTo(cx+20*sc,by+15*sc); X.lineTo(cx+30*sc,by); X.closePath();
  X.fillStyle='rgba(0,229,255,0.04)'; X.fill(); X.stroke();
  // 机体横线
  X.lineWidth=2*sc;
  X.beginPath(); X.moveTo(cx-55*sc,by-5*sc); X.lineTo(cx+55*sc,by-5*sc); X.stroke();
  // 连接到套筒
  X.lineWidth=2*sc;
  X.beginPath(); X.moveTo(cx,by+15*sc); X.lineTo(cx,sT); X.stroke();
  // 标注
  X.fillStyle='rgba(0,229,255,0.5)'; X.font=`${10*sc}px "Share Tech Mono"`; X.textAlign='center';
  X.fillText('BODY 机体',cx,by-14*sc);

  // ── 套筒(左侧完整,右侧剖切)──
  const hO=sOW/2, hI=sIW/2;
  const cutS=sT+40*sc, cutE=sB-28*sc;
  X.strokeStyle='#00e5ff'; X.lineWidth=2*sc;
  // 左外壁
  X.beginPath(); X.moveTo(cx-hO,sT); X.lineTo(cx-hO,sB); X.stroke();
  // 左内壁
  X.lineWidth=1.2*sc;
  X.beginPath(); X.moveTo(cx-hI,sT+8*sc); X.lineTo(cx-hI,sB-8*sc); X.stroke();
  // 右外壁(上段+下段)
  X.lineWidth=2*sc;
  X.beginPath(); X.moveTo(cx+hO,sT); X.lineTo(cx+hO,cutS); X.stroke();
  X.beginPath(); X.moveTo(cx+hO,cutE); X.lineTo(cx+hO,sB); X.stroke();
  // 右内壁
  X.lineWidth=1.2*sc;
  X.beginPath(); X.moveTo(cx+hI,sT+8*sc); X.lineTo(cx+hI,cutS-4*sc); X.stroke();
  X.beginPath(); X.moveTo(cx+hI,cutE+4*sc); X.lineTo(cx+hI,sB-8*sc); X.stroke();
  // 顶盖
  X.lineWidth=2*sc;
  X.beginPath(); X.moveTo(cx-hO,sT); X.lineTo(cx+hO,sT); X.stroke();
  // 底盖(留芯轴孔)
  X.beginPath(); X.moveTo(cx-hO,sB); X.lineTo(cx-hI-2*sc,sB); X.stroke();
  X.beginPath(); X.moveTo(cx+hI+2*sc,sB); X.lineTo(cx+hO,sB); X.stroke();
  // 剖切虚线
  X.setLineDash([4*sc,4*sc]); X.strokeStyle='rgba(0,229,255,0.2)'; X.lineWidth=1*sc;
  X.beginPath(); X.moveTo(cx+hO,cutS); X.lineTo(cx+hO,cutE); X.stroke();
  X.setLineDash([]);
  // 剖面线(斜线纹理)
  X.strokeStyle='rgba(0,229,255,0.06)'; X.lineWidth=1*sc;
  for(let y=cutS;y<cutE;y+=8*sc){
    X.beginPath(); X.moveTo(cx+hI,y); X.lineTo(cx+hO,y+6*sc); X.stroke();
  }
  // 套筒标注
  X.fillStyle='rgba(0,229,255,0.45)'; X.font=`${9.5*sc}px "Share Tech Mono"`; X.textAlign='left';
  X.fillText('SLEEVE 套筒',cx+hO+10*sc,sT+18*sc);

  // ── 弹簧 ──
  const spW=sIW*0.72, coils=8;
  const glow=S.comp/(M.maxComp*sc);
  if(glow>0.05){ X.shadowColor='#c8e600'; X.shadowBlur=12*glow*sc; }
  X.strokeStyle='#c8e600'; X.lineWidth=2.5*sc; X.lineJoin='round';
  const segH=spL/(coils*2);
  X.beginPath(); X.moveTo(cx,spT);
  for(let i=0;i<coils*2;i++){
    const ny=spT+(i+1)*segH;
    const xo=(i%2===0)?spW/2:-spW/2;
    X.lineTo(cx+xo,ny);
  }
  X.lineTo(cx,spB); X.stroke();
  X.shadowColor='transparent'; X.shadowBlur=0;
  // 弹簧顶板
  X.fillStyle='rgba(200,230,0,0.1)'; X.strokeStyle='#c8e600'; X.lineWidth=1.5*sc;
  X.fillRect(cx-spW/2-5*sc,spT-3*sc,spW+10*sc,6*sc);
  X.strokeRect(cx-spW/2-5*sc,spT-3*sc,spW+10*sc,6*sc);
  // 弹簧底板
  X.fillRect(cx-spW/2-5*sc,spB-3*sc,spW+10*sc,6*sc);
  X.strokeRect(cx-spW/2-5*sc,spB-3*sc,spW+10*sc,6*sc);
  // 弹簧标注
  X.fillStyle='rgba(200,230,0,0.55)'; X.font=`${9.5*sc}px "Share Tech Mono"`; X.textAlign='right';
  X.fillText('SPRING 弹簧',cx-hI-12*sc,(spT+spB)/2-4*sc);
  X.fillText('k=15N/mm',cx-hI-12*sc,(spT+spB)/2+10*sc);

  // ── 芯轴 ──
  const hcW=cW/2;
  X.strokeStyle='#5a9ae6'; X.lineWidth=2*sc;
  X.fillStyle='rgba(90,154,230,0.06)';
  X.fillRect(cx-hcW,cT,cW,cB-cT);
  X.strokeRect(cx-hcW,cT,cW,cB-cT);
  // 芯轴刻度
  X.strokeStyle='rgba(90,154,230,0.2)'; X.lineWidth=1*sc;
  const marks=Math.floor((cB-cT)/(14*sc));
  for(let i=1;i<=marks;i++){
    const my=cT+(cB-cT)*i/(marks+1);
    X.beginPath(); X.moveTo(cx-hcW+3*sc,my); X.lineTo(cx+hcW-3*sc,my); X.stroke();
  }
  // 芯轴标注
  X.fillStyle='rgba(90,154,230,0.5)'; X.font=`${9.5*sc}px "Share Tech Mono"`; X.textAlign='left';
  X.fillText('CORE 芯轴',cx+hcW+10*sc,cT+28*sc);

  // ── 电磁锁 ──
  const lockY=sT+(sB-sT)*0.55;
  const engaged=S.lock;
  const lc=engaged?'#ff4422':'#00ff88';
  const pinExt=engaged?hI-hcW-2*sc:4*sc;
  const lkW=14*sc, lkH=11*sc;
  // 左锁销
  X.fillStyle=engaged?'rgba(255,68,34,0.25)':'rgba(0,255,136,0.08)';
  X.strokeStyle=lc; X.lineWidth=1.5*sc;
  X.beginPath(); X.rect(cx-hI-lkW,lockY-lkH/2,lkW+pinExt,lkH); X.fill(); X.stroke();
  // 右锁销(剖切区域内半透明)
  X.globalAlpha=0.5;
  X.beginPath(); X.rect(cx+hI-pinExt,lockY-lkH/2,lkW+pinExt,lkH); X.fill(); X.stroke();
  X.globalAlpha=1;
  // 锁紧指示灯
  if(engaged){
    X.shadowColor='#ff4422'; X.shadowBlur=16*sc;
    X.fillStyle='#ff4422'; X.beginPath(); X.arc(cx,lockY,3.5*sc,0,Math.PI*2); X.fill();
    X.shadowColor='transparent'; X.shadowBlur=0;
  } else {
    X.fillStyle='#00ff88'; X.beginPath(); X.arc(cx,lockY,2.5*sc,0,Math.PI*2); X.fill();
  }
  // 锁标注
  X.fillStyle='rgba(255,255,255,0.35)'; X.font=`${9*sc}px "Share Tech Mono"`; X.textAlign='left';
  X.fillText('EM-LOCK 电磁锁',cx+hO+10*sc,lockY-8*sc);
  X.fillStyle=lc; X.font=`${9*sc}px "Orbitron"`;
  X.fillText(engaged?'LOCKED':'UNLOCKED',cx+hO+10*sc,lockY+6*sc);
  // 电磁线圈符号
  X.strokeStyle='rgba(255,255,255,0.15)'; X.lineWidth=1*sc;
  const coilX=cx-hI-lkW-8*sc;
  for(let i=0;i<3;i++){
    const cy_=lockY-6*sc+i*5*sc;
    X.beginPath(); X.arc(coilX,cy_,3*sc,-Math.PI/2,Math.PI/2); X.stroke();
  }

  // ── 足垫 ──
  const fpY=cB;
  X.fillStyle='rgba(106,159,181,0.06)'; X.strokeStyle='#6a9fb5'; X.lineWidth=2*sc;
  X.beginPath();
  X.moveTo(cx-fW/2,fpY); X.lineTo(cx-fW/2,fpY+fH*0.6);
  X.quadraticCurveTo(cx-fW/2,fpY+fH,cx-fW/4,fpY+fH);
  X.lineTo(cx+fW/4,fpY+fH);
  X.quadraticCurveTo(cx+fW/2,fpY+fH,cx+fW/2,fpY+fH*0.6);
  X.lineTo(cx+fW/2,fpY); X.closePath(); X.fill(); X.stroke();
  // 足垫纹路
  X.strokeStyle='rgba(106,159,181,0.2)'; X.lineWidth=1*sc;
  for(let i=0;i<5;i++){
    const lx=cx-fW/2+10*sc+i*16*sc;
    X.beginPath(); X.moveTo(lx,fpY+4*sc); X.lineTo(lx,fpY+fH-4*sc); X.stroke();
  }
  X.fillStyle='rgba(106,159,181,0.45)'; X.font=`${9*sc}px "Share Tech Mono"`; X.textAlign='center';
  X.fillText('FOOT PAD 仿生足垫',cx,fpY+fH+14*sc);

  // ── 力箭头 ──
  if(S.fog && S.sf>5){
    const arrH=clamp(S.sf/450*65*sc,10*sc,70*sc);
    drawArrow(cx-fW/2-22*sc,cB+fH+8*sc,cx-fW/2-22*sc,cB+fH+8*sc-arrH,'#00ff88',2*sc);
    X.fillStyle='rgba(0,255,136,0.5)'; X.font=`${8.5*sc}px "Share Tech Mono"`; X.textAlign='right';
    X.fillText('GRF',cx-fW/2-26*sc,cB+fH+8*sc-arrH/2+3*sc);
  }
  if(S.comp>3*sc){
    const arrH2=clamp(S.sf/450*55*sc,8*sc,55*sc);
    const sy=(spT+spB)/2;
    drawArrow(cx+hO+8*sc,sy+arrH2/2,cx+hO+8*sc,sy-arrH2/2,'#c8e600',1.5*sc);
    X.fillStyle='rgba(200,230,0,0.5)'; X.font=`${8.5*sc}px "Share Tech Mono"`; X.textAlign='left';
    X.fillText('Fs',cx+hO+12*sc,sy+3*sc);
  }
  // 锁死力指示
  if(S.lock){
    drawArrow(cx-hO-8*sc,lockY-18*sc,cx-hO-8*sc,lockY+18*sc,'#ff4422',1.5*sc);
    X.fillStyle='rgba(255,68,34,0.5)'; X.font=`${8*sc}px "Share Tech Mono"`; X.textAlign='right';
    X.fillText('Rigid',cx-hO-12*sc,lockY+3*sc);
  }

  // ── 压缩量标注 ──
  if(S.comp>4*sc){
    const dimX=cx-hO-30*sc;
    const natB=by+M.spTopOff*sc+M.spNatLen*sc;
    X.strokeStyle='rgba(255,184,0,0.4)'; X.lineWidth=1*sc;
    X.setLineDash([3*sc,3*sc]);
    X.beginPath(); X.moveTo(dimX,spB); X.lineTo(dimX,natB); X.stroke();
    X.setLineDash([]);
    // 端点标记
    X.beginPath(); X.moveTo(dimX-4*sc,spB); X.lineTo(dimX+4*sc,spB); X.stroke();
    X.beginPath(); X.moveTo(dimX-4*sc,natB); X.lineTo(dimX+4*sc,natB); X.stroke();
    X.fillStyle='#ffb800'; X.font=`${10*sc}px "Share Tech Mono"`; X.textAlign='right';
    X.fillText(S.compMm.toFixed(1)+'mm',dimX-6*sc,(spB+natB)/2+4*sc);
  }

  // ── 阶段标签 ──
  X.fillStyle=S.pc; X.font=`bold ${12*sc}px "Orbitron"`; X.textAlign='center';
  X.fillText(PH[S.pi].sub,cx,by-32*sc);

  // ── 摆动相运动线 ──
  if(S.pi===0){
    X.strokeStyle='rgba(0,255,136,0.12)'; X.lineWidth=1.5*sc;
    for(let i=0;i<3;i++){
      const ox=(i-1)*18*sc;
      X.beginPath(); X.moveTo(cx+ox,cB+fH+5*sc);
      X.lineTo(cx+ox-8*sc,cB+fH+25*sc); X.stroke();
    }
  }
  // 蹬地相运动线
  if(S.pi===3 && S.pp>0.3){
    X.strokeStyle='rgba(0,229,255,0.15)'; X.lineWidth=1.5*sc;
    for(let i=0;i<3;i++){
      const ox=(i-1)*18*sc;
      X.beginPath(); X.moveTo(cx+ox,cB+fH+5*sc);
      X.lineTo(cx+ox+8*sc,cB+fH+25*sc); X.stroke();
    }
  }
}

function drawArrow(x1,y1,x2,y2,col,w){
  const hl=8*(C.height/800), ang=Math.atan2(y2-y1,x2-x1);
  X.strokeStyle=col; X.lineWidth=w;
  X.beginPath(); X.moveTo(x1,y1); X.lineTo(x2,y2); X.stroke();
  X.fillStyle=col; X.beginPath(); X.moveTo(x2,y2);
  X.lineTo(x2-hl*Math.cos(ang-Math.PI/6),y2-hl*Math.sin(ang-Math.PI/6));
  X.lineTo(x2-hl*Math.cos(ang+Math.PI/6),y2-hl*Math.sin(ang+Math.PI/6));
  X.closePath(); X.fill();
}

// ─── 绘制:粒子 ───
function drawParticles(){
  for(const p of particles){
    X.globalAlpha=p.life; X.fillStyle=p.color;
    X.beginPath(); X.arc(p.x,p.y,p.sz*p.life*(C.height/800),0,Math.PI*2); X.fill();
  }
  X.globalAlpha=1;
}

// ─── 绘制:小型犬腿上下文图 ───
function drawDogCtx(){
  const sc=C.height/800;
  const ox=140*sc, oy=C.height*0.34;
  const s=0.55*sc;
  X.save(); X.translate(ox,oy); X.scale(s,s);

  // 机体躯干
  const bob=Math.sin(S.ct/CYCLE*Math.PI*2)*4;
  X.strokeStyle='rgba(0,229,255,0.35)'; X.lineWidth=3;
  X.beginPath();
  X.moveTo(-55,-12+bob); X.quadraticCurveTo(0,-28+bob,55,-12+bob);
  X.stroke();
  // 头
  X.beginPath(); X.ellipse(75,-18+bob,22,14,0.2,0,Math.PI*2); X.stroke();
  // 尾巴
  X.beginPath(); X.moveTo(-55,-16+bob); X.quadraticCurveTo(-72,-38+bob,-60,-48+bob); X.stroke();

  // 四条腿 - 前右腿高亮
  drawMiniLeg(25,-10+bob, S.pi, true);
  drawMiniLeg(12,-10+bob, (S.pi+2)%4, false);
  drawMiniLeg(-28,-10+bob, (S.pi+1)%4, false);
  drawMiniLeg(-40,-10+bob, (S.pi+3)%4, false);

  X.restore();

  // 标注文字
  X.fillStyle='rgba(0,229,255,0.3)'; X.font=`${9*sc}px "Share Tech Mono"`; X.textAlign='center';
  X.fillText('LEG CONTEXT',ox,oy+110*sc);
}

function drawMiniLeg(hx,hy,ph,hl){
  const thL=55, shL=65;
  const t=S.pp;
  let ta,ka;
  switch(ph){
    case 0: ta=lerp(-25,18,eio(t))*Math.PI/180; ka=lerp(8,3,t)*Math.PI/180; break;
    case 1: ta=lerp(18,5,ein(t))*Math.PI/180; ka=lerp(3,0,t)*Math.PI/180; break;
    case 2: ta=lerp(5,-8,eio(t))*Math.PI/180; ka=0; break;
    case 3: ta=lerp(-8,-25,eout(t))*Math.PI/180; ka=lerp(0,8,t)*Math.PI/180; break;
  }
  const kx=hx+Math.sin(ta)*thL, ky=hy+Math.cos(ta)*thL;
  const sa=ta+ka, fx=kx+Math.sin(sa)*shL, fy=ky+Math.cos(sa)*shL;

  const col=hl?'#00e5ff':'rgba(0,229,255,0.18)';
  X.strokeStyle=col; X.lineWidth=hl?3:2;
  X.beginPath(); X.moveTo(hx,hy); X.lineTo(kx,ky); X.stroke();
  X.beginPath(); X.moveTo(kx,ky); X.lineTo(fx,fy); X.stroke();
  X.fillStyle=col;
  X.beginPath(); X.arc(hx,hy,3,0,Math.PI*2); X.fill();
  X.beginPath(); X.arc(kx,ky,hl?4:2.5,0,Math.PI*2); X.fill();

  if(hl){
    // 高亮框
    const mx=(kx+fx)/2, my=(ky+fy)/2;
    X.strokeStyle='#ff4422'; X.lineWidth=1; X.setLineDash([3,3]);
    X.beginPath(); X.rect(mx-10,my-6,20,12); X.stroke();
    X.setLineDash([]);
    // 箭头指向主视图
    X.strokeStyle='rgba(255,68,34,0.25)'; X.lineWidth=1;
    X.beginPath(); X.moveTo(mx+10,my); X.lineTo(mx+40,my); X.stroke();
    drawArrow(mx+40,my,mx+55,my,'rgba(255,68,34,0.25)',1);
  }
}

// ─── 力曲线 ───
function drawForceGraph(){
  const w=FC.width, h=FC.height;
  FX.clearRect(0,0,w,h);
  FX.fillStyle='rgba(0,0,0,0.15)'; FX.fillRect(0,0,w,h);
  // 网格
  FX.strokeStyle='rgba(255,255,255,0.04)'; FX.lineWidth=1;
  for(let y=0;y<h;y+=h/4){ FX.beginPath(); FX.moveTo(0,y); FX.lineTo(w,y); FX.stroke(); }
  // 相位底色
  const phColors=['0,255,136','255,184,0','255,68,34','0,229,255'];
  for(let i=0;i<fHist.length;i++){
    const x=(i/FH_MAX)*w;
    FX.fillStyle=`rgba(${phColors[fHist[i].ph]},0.06)`;
    FX.fillRect(x,0,w/FH_MAX+1,h);
  }
  // 弹簧力曲线
  const maxF=450;
  FX.strokeStyle='#c8e600'; FX.lineWidth=1.5;
  FX.beginPath();
  for(let i=0;i<fHist.length;i++){
    const x=(i/FH_MAX)*w, y=h-(fHist[i].sf/maxF)*(h-10)-5;
    i===0?FX.moveTo(x,y):FX.lineTo(x,y);
  }
  FX.stroke();
  // 膝力矩曲线
  const maxT=55;
  FX.strokeStyle='#ff4422'; FX.lineWidth=1.5;
  FX.beginPath();
  for(let i=0;i<fHist.length;i++){
    const x=(i/FH_MAX)*w, y=h-(fHist[i].kt/maxT)*(h-10)-5;
    i===0?FX.moveTo(x,y):FX.lineTo(x,y);
  }
  FX.stroke();
  // 阈值线
  const thY=h-(5/maxT)*(h-10)-5;
  FX.strokeStyle='rgba(255,68,34,0.3)'; FX.lineWidth=1; FX.setLineDash([4,4]);
  FX.beginPath(); FX.moveTo(0,thY); FX.lineTo(w,thY); FX.stroke();
  FX.setLineDash([]);
  // 图例
  FX.font='9px "Share Tech Mono"';
  FX.fillStyle='#c8e600'; FX.textAlign='left'; FX.fillText('Spring Force',5,10);
  FX.fillStyle='#ff4422'; FX.fillText('Knee Torque',105,10);
  FX.fillStyle='rgba(255,68,34,0.5)'; FX.fillText('Threshold 5Nm',220,10);
}

// ─── UI同步 ───
function syncUI(){
  const ph=PH[S.pi];
  document.getElementById('pName').textContent=ph.name;
  document.getElementById('pName').style.color=ph.color;
  for(let i=0;i<4;i++){
    const d=document.getElementById('d'+i);
    d.style.borderColor=i===S.pi?PH[i].color:'#4a5e72';
    d.style.backgroundColor=i===S.pi?PH[i].color:'transparent';
    d.className='phase-dot'+(i===S.pi?' active':'');
  }
  document.getElementById('pFill').style.width=(S.pp*100)+'%';
  document.getElementById('pFill').style.backgroundColor=ph.color;
  document.getElementById('descTxt').textContent=ph.desc;
  document.getElementById('vComp').textContent=S.compMm.toFixed(1)+' mm';
  document.getElementById('vForce').textContent=S.sf.toFixed(0)+' N';
  document.getElementById('vTorq').textContent=S.kt.toFixed(1)+' Nm';
  const ld=document.getElementById('lockDot'), lt=document.getElementById('lockTxt');
  if(S.lock){
    ld.style.backgroundColor='#ff4422'; ld.style.boxShadow='0 0 8px #ff4422';
    lt.textContent='锁死'; lt.style.color='#ff4422';
  } else {
    ld.style.backgroundColor='#00ff88'; ld.style.boxShadow='0 0 4px #00ff88';
    lt.textContent='松开'; lt.style.color='#00ff88';
  }
}

// ─── 主循环 ───
function loop(ts){
  const dt=lastT?ts-lastT:16; lastT=ts;
  update(dt);
  X.clearRect(0,0,C.width,C.height);
  drawGrid();
  drawGround();
  drawDogCtx();
  drawMech();
  drawParticles();
  drawForceGraph();
  syncUI();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>

实现说明

这是一个完整的被动伸缩滑块小腿自适应缓冲原理动画,采用深色技术蓝图美学风格,核心特点:

视觉设计

  • 深海蓝黑底色 + 青色结构线 + 黄绿弹簧 + 蓝色芯轴 + 红/绿锁状态色,形成清晰的功能色彩语义
  • 套筒右侧采用剖切视图,内部弹簧、芯轴、锁销一目了然
  • 锁紧时红色脉冲发光 + 粒子爆裂,解锁时青色粒子释放,增强状态转换的感知冲击

四阶段动画循环(7秒一轮)

  1. 悬空摆动(2s):机体下降,足垫悬空,弹簧自然长度,锁松开
  2. 触地缓冲(1.5s):足垫触地,地面冲击波纹,弹簧压缩吸能
  3. 刚性支撑(2s):电磁锁锁死(粒子闪烁),芯轴被夹紧,微振动表现刚性传力
  4. 蹬地离地(1.5s):解锁释放,弹簧余能弹起,运动线表示离地

交互信息层

  • 左上角:小型犬腿上下文图,高亮腿对应主视图,红色虚线框标注机构位置
  • 右侧面板:实时参数(压缩量、弹簧力、膝力矩、锁状态)
  • 底部:力-时曲线同步绘制,含5Nm阈值线标示锁紧触发点
  • 压缩量标注随动画动态显示,力箭头大小随实际力值缩放
积分规则:第一轮对话扣减6分,后续每轮扣4分