分享图
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=Oxanium:wght@300;400;600;700;800&family=IBM+Plex+Mono:wght@300;400;500&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; --panel: #0b1120; --border: #152040;
    --text: #c0d0e4; --muted: #4a5e7a; --accent-lo: #00e676;
    --accent-hi: #ff6d00; --accent-mid: #ffd600; --spring: #00bcd4;
    --metal1: #1e2e4a; --metal2: #2a3e62; --metal3: #3a5070;
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  body {
    background: var(--bg); color: var(--text); font-family:'IBM Plex Mono',monospace;
    min-height:100vh; display:flex; flex-direction:column; overflow:hidden;
    background-image: radial-gradient(circle at 50% 40%, #0d1a30 0%, var(--bg) 70%);
  }
  header {
    padding: 14px 28px; display:flex; align-items:center; gap:16px;
    border-bottom: 1px solid var(--border);
    background: linear-gradient(180deg, rgba(10,18,34,0.95), rgba(6,10,18,0.8));
  }
  header .logo { color: var(--accent-lo); font-size:14px; }
  header h1 {
    font-family:'Oxanium',sans-serif; font-weight:700; font-size:18px;
    letter-spacing:1px; color:#e0eaf4;
  }
  header .tag {
    font-size:11px; padding:2px 10px; border-radius:4px;
    background: rgba(0,230,118,0.12); color:var(--accent-lo); border:1px solid rgba(0,230,118,0.25);
  }
  .main-wrap {
    flex:1; display:grid; grid-template-columns: 220px 1fr 240px;
    grid-template-rows: 1fr auto; gap:0; overflow:hidden;
  }
  .left-panel {
    border-right:1px solid var(--border); padding:16px;
    display:flex; flex-direction:column; gap:12px; background:var(--panel);
  }
  .center-panel {
    display:flex; align-items:center; justify-content:center;
    position:relative; overflow:hidden;
  }
  .right-panel {
    border-left:1px solid var(--border); padding:16px;
    display:flex; flex-direction:column; gap:16px; background:var(--panel);
  }
  .panel-title {
    font-family:'Oxanium',sans-serif; font-weight:600; font-size:12px;
    text-transform:uppercase; letter-spacing:2px; color:var(--muted); margin-bottom:4px;
  }
  .phase-bar {
    grid-column: 1/-1; border-top:1px solid var(--border);
    padding:10px 28px; background:var(--panel);
  }
  svg text { font-family:'IBM Plex Mono',monospace; }
  /* 刚度仪表 */
  .gauge-wrap { position:relative; width:100%; aspect-ratio:1; }
  .gauge-value {
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    font-family:'Oxanium',sans-serif; font-weight:800; font-size:32px;
    text-align:center; line-height:1.1;
  }
  .gauge-label { font-size:11px; font-weight:400; color:var(--muted); }
  /* 阶段指示 */
  .phase-steps { display:flex; gap:4px; }
  .phase-step {
    flex:1; height:6px; border-radius:3px; background:var(--border);
    position:relative; overflow:hidden; transition:background 0.3s;
  }
  .phase-step .fill {
    position:absolute; left:0; top:0; height:100%; border-radius:3px;
    transition: width 0.1s linear, background 0.3s;
  }
  .phase-step.active { box-shadow: 0 0 8px rgba(0,230,118,0.3); }
  .phase-labels { display:flex; gap:4px; margin-top:4px; }
  .phase-label {
    flex:1; font-size:10px; text-align:center; color:var(--muted);
    transition: color 0.3s;
  }
  .phase-label.active { color:var(--text); }
  /* 控制栏 */
  .controls {
    grid-column:1/-1; border-top:1px solid var(--border);
    padding:12px 28px; display:flex; align-items:center; gap:20px;
    background:var(--panel);
  }
  .ctrl-btn {
    width:36px; height:36px; border-radius:8px; border:1px solid var(--border);
    background:var(--metal1); color:var(--text); cursor:pointer;
    display:flex; align-items:center; justify-content:center; font-size:14px;
    transition: all 0.2s;
  }
  .ctrl-btn:hover { background:var(--metal2); border-color:var(--accent-lo); }
  .ctrl-btn.active { background:rgba(0,230,118,0.15); border-color:var(--accent-lo); color:var(--accent-lo); }
  .slider-group { display:flex; align-items:center; gap:8px; }
  .slider-group label { font-size:11px; color:var(--muted); white-space:nowrap; }
  .slider-group .val { font-family:'Oxanium',sans-serif; font-weight:600; font-size:13px; min-width:36px; }
  input[type=range] {
    -webkit-appearance:none; width:120px; height:4px; border-radius:2px;
    background:var(--border); outline:none;
  }
  input[type=range]::-webkit-slider-thumb {
    -webkit-appearance:none; width:14px; height:14px; border-radius:50%;
    background:var(--accent-lo); cursor:pointer; border:2px solid var(--bg);
  }
  /* 信息卡 */
  .info-card {
    padding:12px; border-radius:8px; border:1px solid var(--border);
    background: rgba(15,25,45,0.6);
  }
  .info-card .ic-title { font-size:11px; color:var(--muted); margin-bottom:6px; }
  .info-card .ic-value {
    font-family:'Oxanium',sans-serif; font-weight:700; font-size:20px;
  }
  .info-card .ic-desc { font-size:10px; color:var(--muted); margin-top:4px; line-height:1.4; }
  /* 参数面板 */
  .param-row {
    display:flex; align-items:center; justify-content:space-between;
    padding:8px 0; border-bottom:1px solid rgba(21,32,64,0.6);
  }
  .param-row:last-child { border-bottom:none; }
  .param-name { font-size:11px; color:var(--muted); }
  .param-val { font-family:'Oxanium',sans-serif; font-weight:600; font-size:14px; }
  .lock-indicator {
    display:inline-block; width:8px; height:8px; border-radius:50%;
    margin-right:6px; transition: background 0.3s, box-shadow 0.3s;
  }
  /* 因果链 */
  .chain-step {
    display:flex; align-items:flex-start; gap:8px; padding:6px 0;
    font-size:11px; color:var(--muted); transition: color 0.3s;
  }
  .chain-step.active { color:var(--text); }
  .chain-dot {
    width:8px; height:8px; border-radius:50%; background:var(--border);
    margin-top:3px; flex-shrink:0; transition: background 0.3s, box-shadow 0.3s;
  }
  .chain-step.active .chain-dot { background:var(--accent-hi); box-shadow:0 0 6px var(--accent-hi); }
  .chain-arrow { text-align:center; color:var(--border); font-size:10px; padding:2px 0; }
  /* 提示 */
  .tooltip {
    position:absolute; padding:6px 10px; border-radius:6px; font-size:11px;
    background:rgba(10,18,34,0.95); border:1px solid var(--border);
    color:var(--text); pointer-events:none; opacity:0; transition:opacity 0.3s;
    z-index:10; white-space:nowrap;
  }
  @keyframes pulse-lo { 0%,100%{box-shadow:0 0 4px var(--accent-lo)} 50%{box-shadow:0 0 12px var(--accent-lo)} }
  @keyframes pulse-hi { 0%,100%{box-shadow:0 0 4px var(--accent-hi)} 50%{box-shadow:0 0 12px var(--accent-hi)} }
  @media (max-width:900px) {
    .main-wrap { grid-template-columns:1fr; grid-template-rows:auto 1fr auto auto; }
    .left-panel,.right-panel { flex-direction:row; flex-wrap:wrap; border:none; border-bottom:1px solid var(--border); }
  }
</style>
</head>
<body>
<header>
  <span class="logo"><i class="fas fa-cog fa-spin" style="animation-duration:4s"></i></span>
  <h1>楔块自锁变刚度串联弹性机构</h1>
  <span class="tag">IFR 原理演示</span>
</header>

<div class="main-wrap">
  <!-- 左侧:腿部侧视 + 因果链 -->
  <div class="left-panel">
    <div>
      <div class="panel-title">腿部侧视</div>
      <svg id="legSvg" viewBox="0 0 200 240" width="100%" style="max-width:200px;display:block;margin:0 auto"></svg>
    </div>
    <div>
      <div class="panel-title">自锁因果链</div>
      <div id="chainContainer">
        <div class="chain-step" data-idx="0"><span class="chain-dot"></span><span>足端触地冲击</span></div>
        <div class="chain-arrow"><i class="fas fa-arrow-down"></i></div>
        <div class="chain-step" data-idx="1"><span class="chain-dot"></span><span>输出轴相对逆时针转动</span></div>
        <div class="chain-arrow"><i class="fas fa-arrow-down"></i></div>
        <div class="chain-step" data-idx="2"><span class="chain-dot"></span><span>楔块挤入锥面间隙</span></div>
        <div class="chain-arrow"><i class="fas fa-arrow-down"></i></div>
        <div class="chain-step" data-idx="3"><span class="chain-dot"></span><span>自锁 → 刚度跃升</span></div>
      </div>
    </div>
  </div>

  <!-- 中央:机构剖面动画 -->
  <div class="center-panel">
    <svg id="mechSvg" viewBox="-220 -220 440 440" width="100%" style="max-width:min(70vh,600px);max-height:min(70vh,600px)"></svg>
  </div>

  <!-- 右侧:刚度仪表 + 参数 -->
  <div class="right-panel">
    <div>
      <div class="panel-title">有效刚度</div>
      <div class="gauge-wrap">
        <svg id="gaugeSvg" viewBox="0 0 200 200" width="100%"></svg>
        <div class="gauge-value" id="gaugeVal">0.1<span class="gauge-label"><br>低刚度</span></div>
      </div>
    </div>
    <div class="info-card" id="phaseCard">
      <div class="ic-title">当前阶段</div>
      <div class="ic-value" id="phaseName" style="color:var(--accent-lo)">腾空摆动</div>
      <div class="ic-desc" id="phaseDesc">内外圈解耦,串联弹簧主导,关节低刚度顺应</div>
    </div>
    <div>
      <div class="panel-title">关键参数</div>
      <div class="param-row">
        <span class="param-name">楔块锥角</span>
        <span class="param-val" id="paramCone">12°</span>
      </div>
      <div class="param-row">
        <span class="param-name">摩擦角 (钢-钢)</span>
        <span class="param-val">≈14°</span>
      </div>
      <div class="param-row">
        <span class="param-name">自锁条件</span>
        <span class="param-val" id="paramLock" style="color:var(--accent-lo)">锥角 < 摩擦角 ✓</span>
      </div>
      <div class="param-row">
        <span class="param-name">最大相对转角</span>
        <span class="param-val">8°</span>
      </div>
      <div class="param-row">
        <span class="param-name">楔块状态</span>
        <span class="param-val"><span class="lock-indicator" id="lockDot"></span><span id="lockText">解耦</span></span>
      </div>
    </div>
  </div>

  <!-- 阶段进度条 -->
  <div class="phase-bar">
    <div class="phase-steps">
      <div class="phase-step" data-phase="0"><div class="fill"></div></div>
      <div class="phase-step" data-phase="1"><div class="fill"></div></div>
      <div class="phase-step" data-phase="2"><div class="fill"></div></div>
      <div class="phase-step" data-phase="3"><div class="fill"></div></div>
    </div>
    <div class="phase-labels">
      <div class="phase-label" data-phase="0">腾空摆动</div>
      <div class="phase-label" data-phase="1">触地冲击</div>
      <div class="phase-label" data-phase="2">支撑蹬地</div>
      <div class="phase-label" data-phase="3">离地过渡</div>
    </div>
  </div>
</div>

<!-- 控制栏 -->
<div class="controls">
  <button class="ctrl-btn active" id="btnPlay" title="播放/暂停"><i class="fas fa-pause"></i></button>
  <button class="ctrl-btn" id="btnReset" title="重置"><i class="fas fa-redo"></i></button>
  <div class="slider-group">
    <label>速度</label>
    <input type="range" id="speedSlider" min="0.2" max="3" step="0.1" value="1">
    <span class="val" id="speedVal">1.0x</span>
  </div>
  <div class="slider-group">
    <label>阶段</label>
    <input type="range" id="phaseSlider" min="0" max="100" step="0.5" value="0">
    <span class="val" id="phaseSliderVal">0%</span>
  </div>
  <div class="slider-group">
    <label>楔块锥角</label>
    <input type="range" id="coneSlider" min="6" max="22" step="0.5" value="12">
    <span class="val" id="coneVal" style="color:var(--accent-lo)">12°</span>
  </div>
</div>

<script>
// ===================== 工具函数 =====================
const SVG_NS = 'http://www.w3.org/2000/svg';
const DEG = Math.PI / 180;
function el(tag, attrs, parent) {
  const e = document.createElementNS(SVG_NS, tag);
  for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
  if (parent) parent.appendChild(e);
  return e;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function smoothstep(t) { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); }
function lerpColor(c1, c2, t) {
  // c1, c2 为 [r,g,b]
  return `rgb(${Math.round(lerp(c1[0],c2[0],t))},${Math.round(lerp(c1[1],c2[1],t))},${Math.round(lerp(c1[2],c2[2],t))})`;
}
const COL_LO = [0, 230, 118];   // 绿色 - 低刚度
const COL_HI = [255, 109, 0];   // 橙色 - 高刚度
const COL_MID = [255, 214, 0];  // 黄色 - 过渡

// ===================== 全局状态 =====================
let time = 0;            // 0-1 归一化步态周期
let speed = 1;
let playing = true;
let manualPhase = false;
let coneAngle = 12;      // 楔块锥角
const FRICTION_ANGLE = 14; // 摩擦角
const CYCLE_SEC = 5;     // 一个完整步态周期秒数

// 阶段定义 [start, end] in 0-1
const PHASES = [
  { name:'腾空摆动', s:0, e:0.38, color:COL_LO,
    desc:'内外圈解耦,串联弹簧主导,关节低刚度顺应' },
  { name:'触地冲击', s:0.38, e:0.50, color:COL_MID,
    desc:'地面反力触发楔块自锁,刚度瞬间跃升,吸收冲击' },
  { name:'支撑蹬地', s:0.50, e:0.82, color:COL_HI,
    desc:'楔块保持锁死,刚性传动链发力蹬地' },
  { name:'离地过渡', s:0.82, e:1.0, color:COL_MID,
    desc:'载荷消失,楔块复位,恢复低刚度顺应' },
];

function getPhaseIdx(t) {
  t = t % 1;
  for (let i = 0; i < PHASES.length; i++) {
    if (t >= PHASES[i].s && t < PHASES[i].e) return i;
  }
  return 0;
}

// 刚度函数 (0~1)
function getStiffness(t) {
  t = t % 1;
  if (t < 0.33) return 0.08;
  if (t < 0.45) return lerp(0.08, 0.95, smoothstep((t - 0.33) / 0.12));
  if (t < 0.78) return 0.95;
  if (t < 0.90) return lerp(0.95, 0.08, smoothstep((t - 0.78) / 0.12));
  return 0.08;
}

// 楔块径向位置 (0=收回, 1=完全伸出锁死)
function getWedgePos(t) {
  t = t % 1;
  if (t < 0.33) return 0;
  if (t < 0.45) return smoothstep((t - 0.33) / 0.12);
  if (t < 0.78) return 1;
  if (t < 0.90) return 1 - smoothstep((t - 0.78) / 0.12);
  return 0;
}

// 内圈相对转角 (度)
function getInnerRotation(t) {
  t = t % 1;
  if (t < 0.33) return 0;
  if (t < 0.42) return lerp(0, -8, smoothstep((t - 0.33) / 0.09));
  if (t < 0.78) return -8;
  if (t < 0.88) return lerp(-8, 0, smoothstep((t - 0.78) / 0.10));
  return 0;
}

// 自锁是否有效
function isSelfLocking() { return coneAngle < FRICTION_ANGLE; }

// ===================== 机构 SVG 构建 =====================
const mechSvg = document.getElementById('mechSvg');
const legSvg = document.getElementById('legSvg');
const gaugeSvg = document.getElementById('gaugeSvg');

// --- 定义渐变和滤镜 ---
const defs = el('defs', {}, mechSvg);

// 金属渐变
const metalGrad = el('linearGradient', { id:'metalGrad', x1:'0%', y1:'0%', x2:'100%', y2:'100%' }, defs);
el('stop', { offset:'0%', 'stop-color':'#1e2e4a' }, metalGrad);
el('stop', { offset:'50%', 'stop-color':'#2a3e62' }, metalGrad);
el('stop', { offset:'100%', 'stop-color':'#1a2844' }, metalGrad);

// 发光滤镜
function makeGlow(id, color, std) {
  const f = el('filter', { id, x:'-50%', y:'-50%', width:'200%', height:'200%' }, defs);
  const gb = el('feGaussianBlur', { stdDeviation:std, result:'blur' }, f);
  const fm = el('feMerge', {}, f);
  el('feMergeNode', { in:'blur' }, fm);
  el('feMergeNode', { in:'SourceGraphic' }, fm);
}
makeGlow('glowLo', '#00e676', 6);
makeGlow('glowHi', '#ff6d00', 8);
makeGlow('glowCyan', '#00bcd4', 4);
makeGlow('glowWhite', '#ffffff', 3);

// 背景网格
const gridG = el('g', { opacity:'0.08' }, mechSvg);
for (let x = -200; x <= 200; x += 40) {
  el('line', { x1:x, y1:-220, x2:x, y2:220, stroke:'#4080c0', 'stroke-width':'0.5' }, gridG);
}
for (let y = -200; y <= 200; y += 40) {
  el('line', { x1:-220, y1:y, x2:220, y2:y, stroke:'#4080c0', 'stroke-width':'0.5' }, gridG);
}

// 背景光晕
const bgGlow = el('circle', { cx:0, cy:0, r:160, fill:'none', stroke:'#00e676', 'stroke-width':'1', opacity:'0.05' }, mechSvg);

// --- 机构层次 ---
const outerRingG = el('g', {}, mechSvg);     // 外圈驱动齿轮
const gapG = el('g', {}, mechSvg);           // 锥面间隙
const wedgeG = el('g', {}, mechSvg);         // 楔块组
const springG = el('g', {}, mechSvg);        // 串联弹簧
const innerRingG = el('g', {}, mechSvg);     // 内圈保持架
const shaftG = el('g', {}, mechSvg);         // 中心输出轴
const labelG = el('g', {}, mechSvg);         // 标注
const arrowG = el('g', {}, mechSvg);         // 力箭头
const effectG = el('g', {}, mechSvg);        // 特效层

// 尺寸参数
const R_OUTER = 165, R_OUTER_IN = 140;
const R_GAP_OUT = 138, R_GAP_IN = 110;
const R_INNER_OUT = 108, R_INNER_IN = 72;
const R_SHAFT = 28;
const NUM_WEDGES = 6;
const WEDGE_ARC = 28; // 每个楔块所占角度

// 绘制外圈
function drawOuterRing() {
  // 主体环
  el('circle', { cx:0, cy:0, r:R_OUTER, fill:'url(#metalGrad)', stroke:'#3a5070', 'stroke-width':'1.5' }, outerRingG);
  el('circle', { cx:0, cy:0, r:R_OUTER_IN, fill:'var(--bg)', stroke:'#2a3e5a', 'stroke-width':'1' }, outerRingG);
  // 齿形标记
  const numTeeth = 36;
  for (let i = 0; i < numTeeth; i++) {
    const a = (i / numTeeth) * 360 * DEG;
    const x1 = R_OUTER * Math.cos(a), y1 = R_OUTER * Math.sin(a);
    const x2 = (R_OUTER + 8) * Math.cos(a), y2 = (R_OUTER + 8) * Math.sin(a);
    el('line', { x1, y1, x2, y2, stroke:'#3a5070', 'stroke-width':'2.5', 'stroke-linecap':'round' }, outerRingG);
  }
  // 锥面标记 (内表面锥角指示)
  for (let i = 0; i < 12; i++) {
    const a = (i / 12) * 360 * DEG + 15 * DEG;
    const x1 = R_OUTER_IN * Math.cos(a), y1 = R_OUTER_IN * Math.sin(a);
    const x2 = (R_OUTER_IN - 6) * Math.cos(a), y2 = (R_OUTER_IN - 6) * Math.sin(a);
    el('line', { x1, y1, x2, y2, stroke:'#2a3e5a', 'stroke-width':'1', opacity:'0.6' }, outerRingG);
  }
}

// 绘制内圈
function drawInnerRing() {
  el('circle', { cx:0, cy:0, r:R_INNER_OUT, fill:'url(#metalGrad)', stroke:'#2a3e5a', 'stroke-width':'1' }, innerRingG);
  el('circle', { cx:0, cy:0, r:R_INNER_IN, fill:'var(--bg)', stroke:'#2a3e5a', 'stroke-width':'1' }, innerRingG);
  // 保持架槽口标记
  for (let i = 0; i < NUM_WEDGES; i++) {
    const a = (i / NUM_WEDGES) * 360;
    const slotG = el('g', { transform:`rotate(${a})` }, innerRingG);
    el('rect', { x:-8, y:-R_INNER_OUT+2, width:16, height:14, rx:2, fill:'var(--bg)', stroke:'#2a3e5a', 'stroke-width':'0.5' }, slotG);
  }
}

// 绘制中心轴
function drawShaft() {
  el('circle', { cx:0, cy:0, r:R_SHAFT, fill:'#1a2844', stroke:'#3a5070', 'stroke-width':'1.5' }, shaftG);
  // 十字标记
  el('line', { x1:-R_SHAFT+6, y1:0, x2:R_SHAFT-6, y2:0, stroke:'#4a6080', 'stroke-width':'1.5' }, shaftG);
  el('line', { x1:0, y1:-R_SHAFT+6, x2:0, y2:R_SHAFT-6, stroke:'#4a6080', 'stroke-width':'1.5' }, shaftG);
  // 输出轴键槽
  el('rect', { x:-3, y:-R_SHAFT, width:6, height:8, fill:'#4a6080' }, shaftG);
}

// 创建楔块和弹簧的引用(动画更新用)
const wedgeEls = [];
const springEls = [];

// 绘制楔块
function drawWedges() {
  for (let i = 0; i < NUM_WEDGES; i++) {
    const a = (i / NUM_WEDGES) * 360;
    const g = el('g', { transform:`rotate(${a})` }, wedgeG);
    // 楔块主体 - 梯形
    const w = el('path', {
      d: wedgePath(0), fill:'#00e676', stroke:'#00c853', 'stroke-width':'1',
      opacity:'0.9', filter:'url(#glowLo)'
    }, g);
    wedgeEls.push({ el: w, group: g, baseAngle: a });
  }
}

function wedgePath(pos) {
  // pos: 0=收回(靠近内圈), 1=伸出(填满间隙)
  const innerR = R_INNER_OUT + 2;
  const outerR = lerp(R_INNER_OUT + 18, R_OUTER_IN - 2, pos);
  const halfArc = WEDGE_ARC / 2;
  // 楔块形状:内侧窄、外侧宽的梯形扇区
  const iw = halfArc * 0.5; // 内侧半角
  const ow = halfArc * 0.8; // 外侧半角
  const p1 = polar(innerR, -iw * DEG);
  const p2 = polar(outerR, -ow * DEG);
  const p3 = polar(outerR, ow * DEG);
  const p4 = polar(innerR, iw * DEG);
  return `M${p1.x},${p1.y} L${p2.x},${p2.y} L${p3.x},${p3.y} L${p4.x},${p4.y} Z`;
}

function polar(r, angle) { return { x: r * Math.sin(angle), y: -r * Math.cos(angle) }; }

// 绘制弹簧
function drawSprings() {
  for (let i = 0; i < NUM_WEDGES; i++) {
    const a = (i / NUM_WEDGES) * 360 + 360 / NUM_WEDGES / 2;
    const g = el('g', { transform:`rotate(${a})` }, springG);
    const sp = el('path', {
      d: springPath(0), fill:'none', stroke:'#00bcd4', 'stroke-width':'2',
      'stroke-linecap':'round', opacity:'0.7'
    }, g);
    springEls.push({ el: sp, group: g });
  }
}

function springPath(compression) {
  // compression: 0=自由, 1=完全压缩
  const coils = 5;
  const innerR = R_INNER_IN + 4;
  const outerR = R_INNER_OUT - 4;
  const amp = lerp(8, 2, compression);
  const halfArc = 12;
  let d = `M0,${-innerR}`;
  const steps = coils * 2;
  for (let i = 1; i <= steps; i++) {
    const frac = i / steps;
    const r = lerp(innerR, outerR, frac);
    const side = (i % 2 === 0) ? 1 : -1;
    const dx = amp * side;
    const dy = -r;
    d += ` L${dx},${dy}`;
  }
  return d;
}

// 标注
function drawLabels() {
  const labels = [
    { text:'外圈驱动齿轮', angle:-30, r:R_OUTER + 30, anchor:'start', dy:-8 },
    { text:'(大腿电机)', angle:-30, r:R_OUTER + 30, anchor:'start', dy:6 },
    { text:'摩擦楔块', angle:20, r:(R_INNER_OUT + R_OUTER_IN)/2, anchor:'start', dy:0 },
    { text:'内圈保持架', angle:160, r:(R_INNER_IN + R_INNER_OUT)/2, anchor:'middle', dy:0 },
    { text:'中心输出轴', angle:-90, r:R_SHAFT + 5, anchor:'middle', dy:14 },
    { text:'(小腿连杆)', angle:-90, r:R_SHAFT + 5, anchor:'middle', dy:28 },
  ];
  labels.forEach(l => {
    const a = l.angle * DEG;
    const x = l.r * Math.cos(a), y = l.r * Math.sin(a);
    el('text', {
      x, y: y + l.dy, fill:'#5a7090', 'font-size':'9', 'text-anchor':l.anchor,
      'font-family':'IBM Plex Mono, monospace'
    }, labelG).textContent = l.text;
  });
  // 连接线
  const leaderLines = [
    { x1: R_OUTER + 6, y1: -(R_OUTER+6)*Math.sin(30*DEG), x2: R_OUTER + 26, y2: -(R_OUTER+26)*Math.sin(30*DEG) - 8 },
  ];
  leaderLines.forEach(l => {
    el('line', { x1:l.x1, y1:l.y1, x2:l.x2, y2:l.y2, stroke:'#3a5070', 'stroke-width':'0.8', 'stroke-dasharray':'3,2' }, labelG);
  });
}

// 力箭头
const forceArrows = [];
function drawForceArrows() {
  // 地面反力箭头 (出现在触地/支撑期)
  const grf = el('g', { opacity:'0', transform:'rotate(180)' }, arrowG);
  el('line', { x1:0, y1:R_OUTER+20, x2:0, y2:R_OUTER+65, stroke:'#ff6d00', 'stroke-width':'3', 'stroke-linecap':'round' }, grf);
  el('polygon', { points:`0,${R_OUTER+18} -6,${R_OUTER+30} 6,${R_OUTER+30}`, fill:'#ff6d00' }, grf);
  el('text', { x:10, y:R_OUTER+50, fill:'#ff6d00', 'font-size':'9' }, grf).textContent = '地面反力';
  forceArrows.push({ el:grf, type:'grf' });

  // 电机扭矩箭头
  const motor = el('g', { opacity:'0.6' }, arrowG);
  const arcR = R_OUTER + 16;
  const a1 = -50 * DEG, a2 = -10 * DEG;
  const ax1 = arcR*Math.cos(a1), ay1 = arcR*Math.sin(a1);
  const ax2 = arcR*Math.cos(a2), ay2 = arcR*Math.sin(a2);
  el('path', {
    d: `M${ax1},${ay1} A${arcR},${arcR} 0 0,1 ${ax2},${ay2}`,
    fill:'none', stroke:'#4a80c0', 'stroke-width':'2', 'stroke-linecap':'round'
  }, motor);
  // 箭头
  const ha = a2, hr = arcR;
  el('polygon', {
    points: `${hr*Math.cos(ha)},${hr*Math.sin(ha)} ${(hr-5)*Math.cos(ha-0.15)},${(hr-5)*Math.sin(ha-0.15)} ${(hr+5)*Math.cos(ha-0.15)},${(hr+5)*Math.sin(ha-0.15)}`,
    fill:'#4a80c0'
  }, motor);
  el('text', { x:arcR*Math.cos(-30*DEG)+8, y:arcR*Math.sin(-30*DEG)-4, fill:'#4a80c0', 'font-size':'9' }, motor).textContent = '电机驱动';
  forceArrows.push({ el:motor, type:'motor' });

  // 相对转动指示
  const relRot = el('g', { opacity:'0' }, arrowG);
  el('path', {
    d: `M${R_INNER_IN+4},0 A${R_INNER_IN+4},${R_INNER_IN+4} 0 0,1 ${(-R_INNER_IN-4)*Math.sin(8*DEG)},${(-R_INNER_IN-4)*Math.cos(8*DEG)}`,
    fill:'none', stroke:'#ffd600', 'stroke-width':'2', 'stroke-dasharray':'4,2'
  }, relRot);
  el('text', { x:-R_INNER_IN+10, y:-10, fill:'#ffd600', 'font-size':'8' }, relRot).textContent = '相对转动8°';
  forceArrows.push({ el:relRot, type:'relRot' });
}

// 锁定特效粒子
const particles = [];
function spawnParticles(stiffness) {
  if (stiffness > 0.5 && Math.random() < 0.3) {
    for (let i = 0; i < NUM_WEDGES; i++) {
      const a = (i / NUM_WEDGES) * 360 * DEG;
      const r = (R_INNER_OUT + R_OUTER_IN) / 2;
      const px = r * Math.cos(a), py = r * Math.sin(a);
      const p = el('circle', {
        cx: px + (Math.random()-0.5)*10,
        cy: py + (Math.random()-0.5)*10,
        r: 1.5 + Math.random()*2,
        fill: lerpColor(COL_LO, COL_HI, stiffness),
        opacity: 0.8
      }, effectG);
      particles.push({ el:p, life:1, vx:(Math.random()-0.5)*30, vy:(Math.random()-0.5)*30, decay:0.02+Math.random()*0.03 });
    }
  }
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.life -= p.decay;
    if (p.life <= 0) {
      p.el.remove();
      particles.splice(i, 1);
    } else {
      const cx = parseFloat(p.el.getAttribute('cx')) + p.vx * dt;
      const cy = parseFloat(p.el.getAttribute('cy')) + p.vy * dt;
      p.el.setAttribute('cx', cx);
      p.el.setAttribute('cy', cy);
      p.el.setAttribute('opacity', p.life * 0.8);
      p.el.setAttribute('r', parseFloat(p.el.getAttribute('r')) * 0.98);
    }
  }
}

// 初始化机构
drawOuterRing();
drawSprings();
drawWedges();
drawInnerRing();
drawShaft();
drawLabels();
drawForceArrows();

// ===================== 腿部侧视动画 =====================
const HIP = { x:100, y:50 };
const THIGH_L = 80, SHIN_L = 80;

function getLegAngles(t) {
  t = t % 1;
  let hip, knee, onGround;
  if (t < 0.38) {
    const s = t / 0.38;
    hip = lerp(-18, 28, smoothstep(s));
    knee = 15 + 30 * Math.sin(s * Math.PI);
    onGround = false;
  } else if (t < 0.50) {
    const s = (t - 0.38) / 0.12;
    hip = lerp(28, 22, smoothstep(s));
    knee = lerp(15, 40, smoothstep(s));
    onGround = true;
  } else if (t < 0.82) {
    const s = (t - 0.50) / 0.32;
    hip = lerp(22, -22, smoothstep(s));
    knee = lerp(40, 12, smoothstep(s));
    onGround = true;
  } else {
    const s = (t - 0.82) / 0.18;
    hip = lerp(-22, -18, smoothstep(s));
    knee = lerp(12, 15, smoothstep(s));
    onGround = false;
  }
  return { hip, knee, onGround };
}

function legPoints(hipA, kneeA) {
  const hR = hipA * DEG;
  const kR = kneeA * DEG;
  const kx = HIP.x + THIGH_L * Math.sin(hR);
  const ky = HIP.y + THIGH_L * Math.cos(hR);
  const fx = kx + SHIN_L * Math.sin(hR - kR);
  const fy = ky + SHIN_L * Math.cos(hR - kR);
  return { hip:HIP, knee:{x:kx,y:ky}, foot:{x:fx,y:fy} };
}

// 绘制腿
function initLegSvg() {
  // 地面
  el('line', { x1:0, y1:210, x2:200, y2:210, stroke:'#1a2a44', 'stroke-width':'2' }, legSvg);
  for (let x = 0; x < 200; x += 12) {
    el('line', { x1:x, y1:210, x2:x-6, y2:218, stroke:'#1a2a44', 'stroke-width':'1' }, legSvg);
  }
  // 髋关节
  el('circle', { cx:HIP.x, cy:HIP.y, r:6, fill:'#1a2844', stroke:'#3a5070', 'stroke-width':'1.5' }, legSvg);
  // 大腿
  legEls.thigh = el('line', { x1:HIP.x, y1:HIP.y, x2:HIP.x, y2:HIP.y+THIGH_L, stroke:'#4a6a90', 'stroke-width':'6', 'stroke-linecap':'round' }, legSvg);
  // 膝关节
  legEls.knee = el('circle', { cx:0, cy:0, r:8, fill:'#0d1425', stroke:'#00e676', 'stroke-width':'2' }, legSvg);
  // 小腿
  legEls.shin = el('line', { x1:0, y1:0, x2:0, y2:80, stroke:'#4a6a90', 'stroke-width':'5', 'stroke-linecap':'round' }, legSvg);
  // 足端
  legEls.foot = el('circle', { cx:0, cy:0, r:4, fill:'#3a5070' }, legSvg);
  // 力箭头
  legEls.grfArrow = el('g', { opacity:'0' }, legSvg);
  el('line', { x1:0, y1:0, x2:0, y2:-35, stroke:'#ff6d00', 'stroke-width':'2.5', 'stroke-linecap':'round' }, legEls.grfArrow);
  el('polygon', { points:'0,-38 -4,-30 4,-30', fill:'#ff6d00' }, legEls.grfArrow);
  // 刚度指示圈
  legEls.stiffRing = el('circle', { cx:0, cy:0, r:12, fill:'none', stroke:'#00e676', 'stroke-width':'1.5', opacity:'0.6' }, legSvg);
}
const legEls = {};
initLegSvg();

// ===================== 刚度仪表 =====================
function initGauge() {
  const cx=100, cy=100, r=80;
  // 背景弧
  el('circle', { cx, cy, r, fill:'none', stroke:'#152040', 'stroke-width':'12', 'stroke-linecap':'round',
    'stroke-dasharray':`${r*Math.PI*1.5} ${r*Math.PI*0.5}`, transform:`rotate(135,${cx},${cy})`
  }, gaugeSvg);
  // 值弧
  gaugeEl.arc = el('circle', { cx, cy, r, fill:'none', stroke:'#00e676', 'stroke-width':'12', 'stroke-linecap':'round',
    'stroke-dasharray':`0 ${r*Math.PI*2}`, transform:`rotate(135,${cx},${cy})`
  }, gaugeSvg);
  // 刻度标记
  for (let i = 0; i <= 10; i++) {
    const a = (135 + i * 27) * DEG;
    const x1 = cx + (r-8)*Math.cos(a), y1 = cy + (r-8)*Math.sin(a);
    const x2 = cx + (r+8)*Math.cos(a), y2 = cy + (r+8)*Math.sin(a);
    el('line', { x1, y1, x2, y2, stroke:'#2a3e5a', 'stroke-width': i%5===0?'2':'1' }, gaugeSvg);
    if (i%5===0) {
      el('text', { x:cx+(r+20)*Math.cos(a), y:cy+(r+20)*Math.sin(a)+4, fill:'#5a7090', 'font-size':'10', 'text-anchor':'middle' }, gaugeSvg).textContent = (i/10).toFixed(1);
    }
  }
  // 标签
  el('text', { x:cx, y:cy+50, fill:'#4a5e7a', 'font-size':'10', 'text-anchor':'middle' }, gaugeSvg).textContent = '归一化刚度 K/K_max';
}
const gaugeEl = {};
initGauge();

// ===================== 主动画循环 =====================
let lastTimestamp = 0;
let prevStiffness = 0;

function updateAnimation(t) {
  const stiffness = getStiffness(t);
  const wedgePos = isSelfLocking() ? getWedgePos(t) : getWedgePos(t) * 0.3;
  const innerRot = getInnerRotation(t);
  const phaseIdx = getPhaseIdx(t);
  const colT = smoothstep((stiffness - 0.08) / 0.87);

  // --- 更新机构剖面 ---
  // 背景光晕
  bgGlow.setAttribute('r', lerp(155, 175, colT));
  bgGlow.setAttribute('stroke', lerpColor(COL_LO, COL_HI, colT));
  bgGlow.setAttribute('opacity', lerp(0.04, 0.12, colT));

  // 楔块
  wedgeEls.forEach((w, i) => {
    w.el.setAttribute('d', wedgePath(wedgePos));
    const wCol = lerpColor(COL_LO, COL_HI, colT);
    w.el.setAttribute('fill', wCol);
    w.el.setAttribute('stroke', lerpColor([0,200,83], [230,81,0], colT));
    w.el.setAttribute('filter', colT > 0.5 ? 'url(#glowHi)' : 'url(#glowLo)');
    w.el.setAttribute('opacity', lerp(0.7, 1.0, colT));
  });

  // 弹簧
  springEls.forEach((s, i) => {
    const compress = wedgePos;
    s.el.setAttribute('d', springPath(compress));
    s.el.setAttribute('opacity', lerp(0.7, 0.1, wedgePos));
    s.el.setAttribute('stroke-width', lerp(2, 1, wedgePos));
  });

  // 内圈旋转
  innerRingG.setAttribute('transform', `rotate(${innerRot})`);
  shaftG.setAttribute('transform', `rotate(${innerRot})`);

  // 力箭头
  forceArrows.forEach(fa => {
    if (fa.type === 'grf') {
      fa.el.setAttribute('opacity', colT > 0.3 ? lerp(0, 0.9, smoothstep((colT-0.3)/0.4)) : 0);
    }
    if (fa.type === 'relRot') {
      fa.el.setAttribute('opacity', (colT > 0.1 && colT < 0.7) ? 0.8 : 0);
    }
  });

  // 粒子
  spawnParticles(stiffness);

  // --- 更新腿部侧视 ---
  const la = getLegAngles(t);
  const lp = legPoints(la.hip, la.knee);
  legEls.thigh.setAttribute('x2', lp.knee.x);
  legEls.thigh.setAttribute('y2', lp.knee.y);
  legEls.knee.setAttribute('cx', lp.knee.x);
  legEls.knee.setAttribute('cy', lp.knee.y);
  legEls.shin.setAttribute('x1', lp.knee.x);
  legEls.shin.setAttribute('y1', lp.knee.y);
  legEls.shin.setAttribute('x2', lp.foot.x);
  legEls.shin.setAttribute('y2', lp.foot.y);
  legEls.foot.setAttribute('cx', lp.foot.x);
  legEls.foot.setAttribute('cy', lp.foot.y);
  // 地面反力箭头
  if (la.onGround) {
    legEls.grfArrow.setAttribute('opacity', colT > 0.3 ? 0.9 : 0.3);
    legEls.grfArrow.setAttribute('transform', `translate(${lp.foot.x},${lp.foot.y})`);
  } else {
    legEls.grfArrow.setAttribute('opacity', '0');
  }
  // 刚度圈
  legEls.stiffRing.setAttribute('cx', lp.knee.x);
  legEls.stiffRing.setAttribute('cy', lp.knee.y);
  legEls.stiffRing.setAttribute('stroke', lerpColor(COL_LO, COL_HI, colT));
  legEls.stiffRing.setAttribute('r', lerp(10, 16, colT));
  legEls.stiffRing.setAttribute('opacity', lerp(0.4, 0.9, colT));
  // 膝关节颜色
  legEls.knee.setAttribute('stroke', lerpColor(COL_LO, COL_HI, colT));

  // --- 更新刚度仪表 ---
  const gaugeR = 80;
  const arcLen = gaugeR * Math.PI * 1.5 * Math.max(0.01, stiffness);
  const arcGap = gaugeR * Math.PI * 2 - arcLen;
  gaugeEl.arc.setAttribute('stroke-dasharray', `${arcLen} ${arcGap}`);
  gaugeEl.arc.setAttribute('stroke', lerpColor(COL_LO, COL_HI, colT));
  const gaugeValEl = document.getElementById('gaugeVal');
  gaugeValEl.innerHTML = `${stiffness.toFixed(2)}<span class="gauge-label"><br>${stiffness > 0.5 ? '高刚度' : '低刚度'}</span>`;
  gaugeValEl.style.color = lerpColor(COL_LO, COL_HI, colT);

  // --- 更新阶段信息 ---
  const phase = PHASES[phaseIdx];
  document.getElementById('phaseName').textContent = phase.name;
  document.getElementById('phaseName').style.color = lerpColor(COL_LO, COL_HI, colT);
  document.getElementById('phaseDesc').textContent = phase.desc;

  // --- 更新阶段进度条 ---
  document.querySelectorAll('.phase-step').forEach((ps, i) => {
    const p = PHASES[i];
    const fill = ps.querySelector('.fill');
    if (t >= p.s && t < p.e) {
      const progress = (t - p.s) / (p.e - p.s) * 100;
      fill.style.width = progress + '%';
      fill.style.background = lerpColor(COL_LO, COL_HI, i >= 2 ? 1 : (i === 1 ? 0.5 : 0));
      ps.classList.add('active');
    } else if (t >= p.e) {
      fill.style.width = '100%';
      fill.style.background = lerpColor(COL_LO, COL_HI, i >= 2 ? 1 : 0.5);
      ps.classList.remove('active');
    } else {
      fill.style.width = '0%';
      ps.classList.remove('active');
    }
  });
  document.querySelectorAll('.phase-label').forEach((pl, i) => {
    pl.classList.toggle('active', i === phaseIdx);
  });

  // --- 更新参数面板 ---
  const lockDot = document.getElementById('lockDot');
  const lockText = document.getElementById('lockText');
  const lockValid = isSelfLocking();
  lockDot.style.background = lockValid ? (stiffness > 0.5 ? '#ff6d00' : '#00e676') : '#ff1744';
  lockDot.style.boxShadow = lockValid ? (stiffness > 0.5 ? '0 0 8px #ff6d00' : '0 0 6px #00e676') : '0 0 6px #ff1744';
  lockText.textContent = stiffness > 0.5 ? '锁死' : '解耦';
  lockText.style.color = stiffness > 0.5 ? '#ff6d00' : '#00e676';
  if (!lockValid) { lockText.textContent = '失效'; lockText.style.color = '#ff1744'; }

  const paramLock = document.getElementById('paramLock');
  paramLock.textContent = lockValid ? '锥角 < 摩擦角 ✓' : '锥角 ≥ 摩擦角 ✗ 自锁失效';
  paramLock.style.color = lockValid ? '#00e676' : '#ff1744';

  // --- 更新因果链 ---
  const chainSteps = document.querySelectorAll('.chain-step');
  if (stiffness > 0.5) {
    chainSteps.forEach(s => s.classList.add('active'));
  } else if (colT > 0.1) {
    chainSteps[0].classList.add('active');
    chainSteps[1].classList.add('active');
    chainSteps[2].classList.remove('active');
    chainSteps[3].classList.remove('active');
  } else {
    chainSteps.forEach(s => s.classList.remove('active'));
  }

  // --- 相位滑块 ---
  if (!manualPhase) {
    document.getElementById('phaseSlider').value = t * 100;
    document.getElementById('phaseSliderVal').textContent = Math.round(t * 100) + '%';
  }
}

function animate(timestamp) {
  const dt = Math.min((timestamp - lastTimestamp) / 1000, 0.05);
  lastTimestamp = timestamp;

  if (playing && !manualPhase) {
    time = (time + dt * speed / CYCLE_SEC) % 1;
  }

  updateAnimation(time);
  updateParticles(dt);

  requestAnimationFrame(animate);
}

// ===================== 控制绑定 =====================
const btnPlay = document.getElementById('btnPlay');
btnPlay.addEventListener('click', () => {
  playing = !playing;
  btnPlay.innerHTML = playing ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
  btnPlay.classList.toggle('active', playing);
});

document.getElementById('btnReset').addEventListener('click', () => {
  time = 0; playing = true; manualPhase = false;
  btnPlay.innerHTML = '<i class="fas fa-pause"></i>';
  btnPlay.classList.add('active');
});

const speedSlider = document.getElementById('speedSlider');
speedSlider.addEventListener('input', () => {
  speed = parseFloat(speedSlider.value);
  document.getElementById('speedVal').textContent = speed.toFixed(1) + 'x';
});

const phaseSlider = document.getElementById('phaseSlider');
phaseSlider.addEventListener('mousedown', () => { manualPhase = true; });
phaseSlider.addEventListener('touchstart', () => { manualPhase = true; });
phaseSlider.addEventListener('input', () => {
  time = parseFloat(phaseSlider.value) / 100;
  document.getElementById('phaseSliderVal').textContent = Math.round(time * 100) + '%';
});
phaseSlider.addEventListener('mouseup', () => { manualPhase = false; });
phaseSlider.addEventListener('touchend', () => { manualPhase = false; });

const coneSlider = document.getElementById('coneSlider');
coneSlider.addEventListener('input', () => {
  coneAngle = parseFloat(coneSlider.value);
  const coneValEl = document.getElementById('coneVal');
  coneValEl.textContent = coneAngle.toFixed(1) + '°';
  coneValEl.style.color = isSelfLocking() ? '#00e676' : '#ff1744';
  document.getElementById('paramCone').textContent = coneAngle.toFixed(1) + '°';
});

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

实现说明

核心设计思路: 以 TRIZ 最终理想解(IFR)为叙事主线,动画直接展示"矛盾已消除"的理想运行状态——机构无需额外驱动与控制,仅凭地面反力自身即可触发刚度跃变。

视觉编码体系:

  • 🟢 绿色 → 低刚度/解耦状态(腾空摆动期)
  • 🟠 橙色 → 高刚度/锁死状态(支撑蹬地期)
  • 🟡 黄色 → 过渡瞬间(触地冲击/离地过渡)
  • 所有颜色连续插值,状态切换丝滑可感知

三层动画架构:

  1. 机构轴向剖面(中央主视觉)— 楔块径向伸缩、弹簧压缩/释放、内外圈相对转动,全部实时联动
  2. 腿部侧视(左侧)— 髋膝角度随步态周期变化,膝关节光圈颜色同步反映刚度状态
  3. 刚度仪表(右侧)— 弧形仪表实时映射归一化刚度值 0.08→0.95

交互设计亮点:

  • 楔块锥角滑块(6°~22°):当锥角超过摩擦角(≈14°)时,自锁条件破坏,楔块无法完全锁死,刚度无法跃升——直接可视化 IFR 的核心物理条件
  • 阶段滑块:手动拖拽步态周期任意时刻,逐帧审视楔块动作细节
  • 因果链面板:四步自锁因果链在触地冲击时逐级点亮,清晰展示"地面反力→相对转动→楔块挤入→刚度跃升"的资源自利用路径
积分规则:第一轮对话扣减6分,后续每轮扣4分