分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEA腿部原理 · 结构即控制</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{
  --bg:#060c18;--card:rgba(10,22,40,.88);--border:#1a2d4d;
  --fg:#c8d6e5;--dim:#5a7a99;
  --flex:#00e5ff;--trans:#ffab00;--rigid:#ff5722;--green:#00e676;
  --struct:#2a4a6f;--struct-l:#4a7aad;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Rajdhani',sans-serif;height:100vh;overflow:hidden;display:flex;flex-direction:column}
/* 背景网格 */
body::before{content:'';position:fixed;inset:0;background-image:
  linear-gradient(rgba(26,45,77,.18) 1px,transparent 1px),
  linear-gradient(90deg,rgba(26,45,77,.18) 1px,transparent 1px);
  background-size:48px 48px;pointer-events:none;z-index:0}
/* 主布局 */
.main-wrap{flex:1;display:flex;position:relative;z-index:1;min-height:0}
.svg-area{flex:1;display:flex;align-items:center;justify-content:center;position:relative}
.svg-area svg{width:100%;height:100%;max-height:calc(100vh - 140px)}
/* 右侧信息面板 */
.info-panel{width:310px;padding:20px 18px;display:flex;flex-direction:column;gap:16px;
  overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;
  backdrop-filter:blur(12px)}
.card-title{font-size:11px;font-weight:600;letter-spacing:1.6px;text-transform:uppercase;
  color:var(--dim);margin-bottom:10px;font-family:'JetBrains Mono',monospace}
/* 相位指示器 */
.phase-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;
  border-radius:6px;font-weight:600;font-size:17px;letter-spacing:.5px;
  transition:background .4s,color .4s}
.phase-dot{width:10px;height:10px;border-radius:50%;transition:background .3s,box-shadow .3s}
.phase-desc{font-size:13.5px;color:var(--dim);margin-top:8px;line-height:1.5;font-weight:400}
/* 状态条 */
.state-row{display:flex;align-items:center;gap:10px;margin-bottom:8px;font-size:13px}
.state-label{color:var(--dim);min-width:76px;font-family:'JetBrains Mono',monospace;font-size:11px}
.state-bar{flex:1;height:6px;background:var(--border);border-radius:3px;overflow:hidden}
.state-bar-fill{height:100%;border-radius:3px;transition:width .15s,background .3s}
.state-val{font-family:'JetBrains Mono',monospace;font-size:12px;min-width:70px;text-align:right}
/* 参数表 */
.param-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:13px}
.param-row:last-child{border-bottom:none}
.param-row span:first-child{color:var(--dim)}
.param-row span:last-child{font-family:'JetBrains Mono',monospace;font-size:12px}
/* 力流路径 */
.force-flow-list{display:flex;flex-direction:column;gap:4px}
.ff-item{display:flex;align-items:center;gap:6px;font-size:12px;font-family:'JetBrains Mono',monospace;
  padding:3px 8px;border-radius:4px;transition:background .3s,color .3s}
.ff-item.active{background:rgba(255,87,34,.12);color:var(--rigid)}
.ff-item.flex-active{background:rgba(0,229,255,.1);color:var(--flex)}
.ff-arrow{font-size:10px;opacity:.5}
/* 刚度曲线 */
.chart-wrap{position:relative}
.chart-wrap svg{width:100%;height:auto}
.chart-dot{transition:cx .15s,cy .15s,fill .3s}
/* 底部控制栏 */
.ctrl-bar{height:72px;background:var(--card);border-top:1px solid var(--border);
  display:flex;align-items:center;padding:0 24px;gap:20px;z-index:2;backdrop-filter:blur(12px)}
.timeline-wrap{flex:1;display:flex;flex-direction:column;gap:6px}
.timeline-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dim);
  font-family:'JetBrains Mono',monospace}
.timeline-track{position:relative;height:8px;background:var(--border);border-radius:4px;cursor:pointer;overflow:visible}
.timeline-fill{height:100%;border-radius:4px;transition:width .05s}
.timeline-thumb{position:absolute;top:50%;width:16px;height:16px;border-radius:50%;
  transform:translate(-50%,-50%);border:2px solid var(--fg);background:var(--bg);
  cursor:grab;transition:box-shadow .2s;z-index:2}
.timeline-thumb:hover,.timeline-thumb:active{box-shadow:0 0 0 4px rgba(200,214,229,.2)}
/* 相位色带 */
.phase-bands{position:absolute;top:0;left:0;right:0;bottom:0;border-radius:4px;overflow:hidden;display:flex}
.phase-band{height:100%;opacity:.35}
/* 按钮 */
.btn{width:40px;height:40px;border-radius:8px;border:1px solid var(--border);background:transparent;
  color:var(--fg);cursor:pointer;display:flex;align-items:center;justify-content:center;
  font-size:16px;transition:background .2s,border-color .2s}
.btn:hover{background:rgba(200,214,229,.06);border-color:var(--dim)}
.btn.active{border-color:var(--flex);color:var(--flex)}
.speed-btns{display:flex;gap:4px}
.speed-btn{padding:4px 10px;border-radius:5px;border:1px solid var(--border);background:transparent;
  color:var(--dim);font-family:'JetBrains Mono',monospace;font-size:11px;cursor:pointer;transition:all .2s}
.speed-btn:hover{border-color:var(--dim)}
.speed-btn.active{border-color:var(--flex);color:var(--flex);background:rgba(0,229,255,.08)}
/* 刚度滑块 */
.stiffness-control{display:flex;align-items:center;gap:10px}
.stiffness-control label{font-size:11px;color:var(--dim);font-family:'JetBrains Mono',monospace;white-space:nowrap}
.stiffness-control input[type=range]{width:100px;accent-color:var(--flex);cursor:pointer}
.stiffness-val{font-family:'JetBrains Mono',monospace;font-size:12px;min-width:60px}
/* 响应式 */
@media(max-width:900px){.info-panel{width:240px;padding:14px 10px}}
@media(max-width:700px){.info-panel{display:none}.ctrl-bar{padding:0 12px;gap:12px}}
</style>
</head>
<body>

<div class="main-wrap">
  <div class="svg-area">
    <svg id="mech" viewBox="0 0 860 700" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <!-- 发光滤镜 -->
        <filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glowOrange" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
          <feFlood flood-color="#ff5722" flood-opacity=".4" result="c"/>
          <feComposite in="c" in2="b" operator="in" result="cb"/>
          <feMerge><feMergeNode in="cb"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="softGlow" x="-30%" y="-30%" width="160%" height="160%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
        </filter>
        <!-- 地面渐变 -->
        <linearGradient id="groundGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#1a3050" stop-opacity=".9"/>
          <stop offset="100%" stop-color="#0a1628" stop-opacity="1"/>
        </linearGradient>
        <!-- 金属渐变 -->
        <linearGradient id="metalGrad" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stop-color="#2a4a6f"/>
          <stop offset="50%" stop-color="#3d6090"/>
          <stop offset="100%" stop-color="#2a4a6f"/>
        </linearGradient>
        <!-- 剪裁区域:大腿管内部 -->
        <clipPath id="thighClip">
          <rect x="278" y="172" width="84" height="290"/>
        </clipPath>
        <!-- 箭头标记 -->
        <marker id="arrowCyan" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
          <path d="M0,0 L10,5 L0,10 z" fill="#00e5ff"/>
        </marker>
        <marker id="arrowOrange" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
          <path d="M0,0 L10,5 L0,10 z" fill="#ff5722"/>
        </marker>
      </defs>

      <!-- 背景氛围 -->
      <rect x="0" y="590" width="860" height="110" fill="url(#groundGrad)"/>
      <line x1="40" y1="590" x2="820" y2="590" stroke="#2a4a6f" stroke-width="2" stroke-dasharray="6,4" id="groundLine"/>
      <text x="55" y="615" fill="#3d5a80" font-size="11" font-family="JetBrains Mono,monospace">GROUND</text>

      <!-- 力流箭头组(在腿后面) -->
      <g id="forceArrows" opacity="0"></g>

      <!-- === 腿部总成 === -->
      <g id="legAssembly">
        <!-- 身体段 -->
        <rect x="290" y="42" width="60" height="55" rx="6" fill="#1a2d45" stroke="#4a7aad" stroke-width="1.5"/>
        <text x="320" y="75" text-anchor="middle" fill="#5a7a99" font-size="10" font-family="JetBrains Mono,monospace">BODY</text>

        <!-- 髋关节电机 -->
        <g id="hipMotors">
          <!-- 俯仰电机 -->
          <rect x="238" y="62" width="48" height="36" rx="5" fill="#152238" stroke="#4a7aad" stroke-width="1.5"/>
          <circle cx="262" cy="80" r="12" fill="none" stroke="#5c7da0" stroke-width="1.5"/>
          <circle cx="262" cy="80" r="5" fill="#3d6090"/>
          <text x="262" y="55" text-anchor="middle" fill="#5a7a99" font-size="8" font-family="JetBrains Mono,monospace">PITCH</text>
          <!-- 横滚电机 -->
          <rect x="354" y="62" width="48" height="36" rx="5" fill="#152238" stroke="#4a7aad" stroke-width="1.5"/>
          <circle cx="378" cy="80" r="12" fill="none" stroke="#5c7da0" stroke-width="1.5"/>
          <circle cx="378" cy="80" r="5" fill="#3d6090"/>
          <text x="378" y="55" text-anchor="middle" fill="#5a7a99" font-size="8" font-family="JetBrains Mono,monospace">ROLL</text>
          <!-- 髋关节轴 -->
          <circle cx="320" cy="105" r="14" fill="#1a2d45" stroke="#5c7da0" stroke-width="2"/>
          <circle cx="320" cy="105" r="6" fill="#3d6090" stroke="#5c7da0" stroke-width="1"/>
        </g>

        <!-- 膝关节电机 -->
        <g id="kneeMotor">
          <rect x="340" y="112" width="36" height="30" rx="4" fill="#152238" stroke="#4a7aad" stroke-width="1.2"/>
          <circle cx="358" cy="127" r="9" fill="none" stroke="#5c7da0" stroke-width="1.2"/>
          <circle cx="358" cy="127" r="4" fill="#3d6090"/>
          <text x="358" y="152" text-anchor="middle" fill="#5a7a99" font-size="7.5" font-family="JetBrains Mono,monospace">KNEA</text>
        </g>

        <!-- 同步带 -->
        <g id="timingBelt" opacity="0.7">
          <line x1="350" y1="135" x2="348" y2="175" stroke="#5c7da0" stroke-width="1.5" stroke-dasharray="3,2"/>
          <line x1="362" y1="135" x2="360" y2="175" stroke="#5c7da0" stroke-width="1.5" stroke-dasharray="3,2"/>
          <!-- 滑轮 -->
          <circle cx="354" cy="178" r="7" fill="#1a2d45" stroke="#5c7da0" stroke-width="1.5"/>
          <circle cx="354" cy="178" r="3" fill="#3d6090"/>
        </g>

        <!-- 大腿管(外壳) -->
        <g id="thighTube">
          <!-- 管壁 -->
          <rect x="278" y="100" width="84" height="320" rx="4" fill="rgba(15,29,53,.5)" stroke="#4a7aad" stroke-width="2"/>
          <!-- 内台阶(弹簧上座) -->
          <line x1="278" y1="175" x2="298" y2="175" stroke="#5c7da0" stroke-width="3"/>
          <line x1="342" y1="175" x2="362" y2="175" stroke="#5c7da0" stroke-width="3"/>
          <!-- 台阶标注 -->
          <text x="252" y="179" text-anchor="end" fill="#5a7a99" font-size="8" font-family="JetBrains Mono,monospace">台阶</text>
          <line x1="254" y1="175" x2="276" y2="175" stroke="#5a7a99" stroke-width=".5" stroke-dasharray="2,2"/>
        </g>

        <!-- 弹簧(动态路径) -->
        <g id="springGroup" clip-path="url(#thighClip)">
          <path id="springGlow" d="" stroke="#00e5ff" stroke-width="8" fill="none" opacity=".25" filter="url(#softGlow)"/>
          <path id="springPath" d="" stroke="#00e5ff" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
        </g>

        <!-- 小腿杆 -->
        <g id="calfGroup">
          <!-- 推力轴承 -->
          <rect id="thrustBearing" x="296" y="0" width="48" height="6" rx="2" fill="#5c7da0" stroke="#7aaad0" stroke-width="1"/>
          <!-- 小腿杆肩部 -->
          <rect id="calfShoulder" x="292" y="0" width="56" height="10" rx="2" fill="#2a4a6f" stroke="#4a7aad" stroke-width="1.5"/>
          <!-- 小腿杆体 -->
          <rect id="calfRod" x="302" y="0" width="36" height="230" rx="2" fill="#1e3450" stroke="#3d6090" stroke-width="1.5"/>
          <!-- 足端 -->
          <g id="footGroup">
            <rect id="footPad" x="292" y="0" width="56" height="14" rx="4" fill="#2a4a6f" stroke="#5c7da0" stroke-width="1.5"/>
            <rect x="296" y="10" width="48" height="6" rx="2" fill="#3d5a80"/>
          </g>
        </g>

        <!-- 电机拉索 -->
        <line id="motorCable" x1="354" y1="178" x2="320" y2="400" stroke="#5c7da0" stroke-width="1" stroke-dasharray="4,3" opacity=".6"/>
      </g>

      <!-- 标注 -->
      <g id="annotations" font-family="JetBrains Mono,monospace" font-size="9" fill="#5a7a99">
        <text x="400" y="120" id="annSpring">k = 80 N/mm</text>
        <text x="400" y="135" id="annPreload">预压缩 5mm</text>
        <text x="400" y="150" id="annRatio">减速比 1:2</text>
      </g>

      <!-- 冲击粒子组 -->
      <g id="particles"></g>

      <!-- 状态文字 -->
      <g id="phaseText" font-family="Rajdhani,sans-serif">
        <text id="phaseLabel" x="80" y="390" font-size="28" font-weight="700" fill="#00e5ff" opacity="0">腾空期</text>
        <text id="phaseSublabel" x="80" y="415" font-size="14" font-weight="400" fill="#5a7a99" opacity="0">弹簧释放,小腿弹伸</text>
      </g>
    </svg>
  </div>

  <!-- 右侧信息面板 -->
  <div class="info-panel">
    <!-- 当前相位 -->
    <div class="card">
      <div class="card-title">当前相位</div>
      <div class="phase-badge" id="phaseBadge" style="background:rgba(0,229,255,.1);color:#00e5ff">
        <span class="phase-dot" id="phaseDot" style="background:#00e5ff;box-shadow:0 0 8px #00e5ff"></span>
        <span id="phaseName">腾空期</span>
      </div>
      <p class="phase-desc" id="phaseDesc">弹簧释放,小腿弹伸至最大行程</p>
    </div>

    <!-- 弹簧状态 -->
    <div class="card">
      <div class="card-title">弹簧状态</div>
      <div class="state-row">
        <span class="state-label">压缩量</span>
        <div class="state-bar"><div class="state-bar-fill" id="compressBar" style="width:10%;background:#00e5ff"></div></div>
        <span class="state-val" id="compressVal">5 mm</span>
      </div>
      <div class="state-row">
        <span class="state-label">有效刚度</span>
        <div class="state-bar"><div class="state-bar-fill" id="stiffBar" style="width:15%;background:#00e5ff"></div></div>
        <span class="state-val" id="stiffVal">80 N/mm</span>
      </div>
      <div class="state-row">
        <span class="state-label">弹簧力</span>
        <span class="state-val" id="forceVal">400 N</span>
      </div>
      <div class="state-row">
        <span class="state-label">状态</span>
        <span class="state-val" id="stateLabel" style="color:#00e5ff">柔性</span>
      </div>
    </div>

    <!-- 力流路径 -->
    <div class="card">
      <div class="card-title">力流路径</div>
      <div class="force-flow-list" id="forceFlowList">
        <div class="ff-item" data-idx="0">足底</div>
        <div class="ff-item" data-idx="1"><span class="ff-arrow">↓</span> 小腿杆</div>
        <div class="ff-item" data-idx="2"><span class="ff-arrow">↓</span> <span id="ffSpring">弹簧(柔性)</span></div>
        <div class="ff-item" data-idx="3"><span class="ff-arrow">↓</span> 大腿管</div>
        <div class="ff-item" data-idx="4"><span class="ff-arrow">↓</span> 髋关节</div>
        <div class="ff-item" data-idx="5"><span class="ff-arrow">↓</span> 机体</div>
      </div>
    </div>

    <!-- 刚度特性曲线 -->
    <div class="card chart-wrap">
      <div class="card-title">刚度特性曲线</div>
      <svg viewBox="0 0 260 140" xmlns="http://www.w3.org/2000/svg">
        <!-- 坐标轴 -->
        <line x1="35" y1="115" x2="245" y2="115" stroke="#2a4a6f" stroke-width="1"/>
        <line x1="35" y1="115" x2="35" y2="15" stroke="#2a4a6f" stroke-width="1"/>
        <text x="140" y="132" text-anchor="middle" fill="#5a7a99" font-size="9" font-family="JetBrains Mono,monospace">位移 (mm)</text>
        <text x="12" y="65" text-anchor="middle" fill="#5a7a99" font-size="9" font-family="JetBrains Mono,monospace" transform="rotate(-90,12,65)">力 (N)</text>
        <!-- 刻度 -->
        <text x="35" y="125" text-anchor="middle" fill="#3d5a80" font-size="7" font-family="JetBrains Mono,monospace">0</text>
        <text x="95" y="125" text-anchor="middle" fill="#3d5a80" font-size="7" font-family="JetBrains Mono,monospace">10</text>
        <text x="155" y="125" text-anchor="middle" fill="#3d5a80" font-size="7" font-family="JetBrains Mono,monospace">20</text>
        <text x="215" y="125" text-anchor="middle" fill="#3d5a80" font-size="7" font-family="JetBrains Mono,monospace">30</text>
        <!-- 线性区 -->
        <line x1="35" y1="112" x2="155" y2="40" stroke="#00e5ff" stroke-width="2" opacity=".7"/>
        <!-- 底并区(刚度突增) -->
        <line x1="155" y1="40" x2="175" y2="22" stroke="#ff5722" stroke-width="2.5"/>
        <line x1="175" y1="22" x2="180" y2="18" stroke="#ff5722" stroke-width="3"/>
        <!-- 底并标注 -->
        <text x="185" y="24" fill="#ff5722" font-size="8" font-family="JetBrains Mono,monospace">底并</text>
        <!-- 预压缩标注 -->
        <line x1="55" y1="100" x2="55" y2="108" stroke="#ffab00" stroke-width="1.5"/>
        <text x="55" y="98" text-anchor="middle" fill="#ffab00" font-size="7" font-family="JetBrains Mono,monospace">预压</text>
        <!-- 当前工作点 -->
        <circle id="chartDot" cx="55" cy="100" r="5" fill="#00e5ff" stroke="#fff" stroke-width="1.5" class="chart-dot"/>
      </svg>
    </div>

    <!-- 关键参数 -->
    <div class="card">
      <div class="card-title">关键参数</div>
      <div class="param-row"><span>弹簧刚度</span><span id="paramK">80 N/mm</span></div>
      <div class="param-row"><span>预压缩量</span><span>5 mm</span></div>
      <div class="param-row"><span>同步带减速比</span><span>1:2</span></div>
      <div class="param-row"><span>髋关节峰值扭矩</span><span>12 Nm</span></div>
    </div>

    <!-- 刚度调节 -->
    <div class="card">
      <div class="card-title">交互调节</div>
      <div class="stiffness-control">
        <label>弹簧刚度</label>
        <input type="range" id="stiffSlider" min="30" max="180" value="80" step="5"/>
        <span class="stiffness-val" id="stiffSliderVal">80 N/mm</span>
      </div>
    </div>
  </div>
</div>

<!-- 底部控制栏 -->
<div class="ctrl-bar">
  <button class="btn active" id="playBtn" title="播放/暂停">▶</button>
  <div class="timeline-wrap">
    <div class="timeline-track" id="timelineTrack">
      <div class="phase-bands">
        <div class="phase-band" style="width:22%;background:#00e5ff"></div>
        <div class="phase-band" style="width:16%;background:#00b8d4"></div>
        <div class="phase-band" style="width:24%;background:#ffab00"></div>
        <div class="phase-band" style="width:16%;background:#ff5722"></div>
        <div class="phase-band" style="width:22%;background:#00e676"></div>
      </div>
      <div class="timeline-fill" id="timelineFill" style="width:0%;background:#00e5ff"></div>
      <div class="timeline-thumb" id="timelineThumb" style="left:0%"></div>
    </div>
    <div class="timeline-labels">
      <span>腾空</span><span>触地</span><span>刚性化</span><span>蹬地</span><span>释放</span>
    </div>
  </div>
  <div class="speed-btns">
    <button class="speed-btn" data-speed="0.3">0.3x</button>
    <button class="speed-btn active" data-speed="0.6">1x</button>
    <button class="speed-btn" data-speed="1.2">2x</button>
  </div>
</div>

<script>
/* ============================
   动画引擎与状态管理
   ============================ */
const S = {
  time: 0, playing: true, speed: 0.6, lastTS: 0,
  stiffness: 80, // N/mm
  preCompress: 5, // mm
  maxSpringMM: 30, // mm 最大行程(视觉映射)
};

/* 相位定义 */
const PHASES = [
  { id:'flight',  name:'腾空期',   s:0,    e:0.22, color:'#00e5ff', desc:'弹簧释放,小腿弹伸至最大行程' },
  { id:'touch',   name:'触地缓冲', s:0.22, e:0.38, color:'#00b8d4', desc:'地面反力推小腿上行,弹簧压缩吸能' },
  { id:'rigidify',name:'支撑刚性化',s:0.38,e:0.62, color:'#ffab00', desc:'电机收拉滑轮,弹簧压并趋向刚性' },
  { id:'pushoff', name:'刚性蹬地', s:0.62, e:0.78, color:'#ff5722', desc:'弹簧底并锁定,力流直接传导至机体' },
  { id:'liftoff', name:'离地释放', s:0.78, e:1.0,  color:'#00e676', desc:'弹簧释放弹性势能,助力抬腿' },
];

function getPhase(t) {
  for (const p of PHASES) if (t >= p.s && t < p.e) return p;
  return PHASES[0];
}

/* 弹簧压缩量(mm),0=自然长度 */
function getCompression(t) {
  const k = S.stiffness / 80; // 归一化刚度因子
  if (t < 0.22) return S.preCompress;
  if (t < 0.38) {
    const p = (t - 0.22) / 0.16;
    return S.preCompress + p * 12 * k;
  }
  if (t < 0.62) {
    const p = (t - 0.38) / 0.24;
    const start = S.preCompress + 12 * k;
    return start + p * (S.maxSpringMM - start);
  }
  if (t < 0.78) return S.maxSpringMM;
  const p = (t - 0.78) / 0.22;
  return S.maxSpringMM * (1 - p * p) + S.preCompress * p * p;
}

/* 有效刚度(随压缩量非线性增长,底并时趋于无穷) */
function getEffStiffness(comp) {
  const ratio = comp / S.maxSpringMM;
  if (ratio < 0.7) return S.stiffness;
  // 底并区:刚度急剧上升
  const t = (ratio - 0.7) / 0.3;
  return S.stiffness * (1 + t * t * 20);
}

/* 髋关节角度(度) */
function getHipAngle(t) {
  if (t < 0.22) return -15 + (t / 0.22) * 30;
  if (t < 0.38) return 15 - ((t - 0.22) / 0.16) * 5;
  if (t < 0.62) return 10 + ((t - 0.38) / 0.24) * 20;
  if (t < 0.78) return 30 + ((t - 0.62) / 0.16) * 5;
  return 35 - ((t - 0.78) / 0.22) * 50;
}

/* 足端是否触地 */
function isGrounded(t) { return t >= 0.22 && t < 0.82; }

/* ============================
   SVG 元素引用
   ============================ */
const svg = document.getElementById('mech');
const springPath = document.getElementById('springPath');
const springGlow = document.getElementById('springGlow');
const calfShoulder = document.getElementById('calfShoulder');
const calfRod = document.getElementById('calfRod');
const thrustBearing = document.getElementById('thrustBearing');
const footGroup = document.getElementById('footGroup');
const footPad = document.getElementById('footPad');
const motorCable = document.getElementById('motorCable');
const forceArrowsG = document.getElementById('forceArrows');
const particlesG = document.getElementById('particles');
const legAssembly = document.getElementById('legAssembly');
const phaseLabel = document.getElementById('phaseLabel');
const phaseSublabel = document.getElementById('phaseSublabel');
const chartDot = document.getElementById('chartDot');

/* 信息面板元素 */
const phaseBadge = document.getElementById('phaseBadge');
const phaseDot = document.getElementById('phaseDot');
const phaseNameEl = document.getElementById('phaseName');
const phaseDescEl = document.getElementById('phaseDesc');
const compressBar = document.getElementById('compressBar');
const compressVal = document.getElementById('compressVal');
const stiffBar = document.getElementById('stiffBar');
const stiffVal = document.getElementById('stiffVal');
const forceVal = document.getElementById('forceVal');
const stateLabel = document.getElementById('stateLabel');
const ffSpring = document.getElementById('ffSpring');
const timelineFill = document.getElementById('timelineFill');
const timelineThumb = document.getElementById('timelineThumb');
const playBtn = document.getElementById('playBtn');
const stiffSlider = document.getElementById('stiffSlider');
const stiffSliderVal = document.getElementById('stiffSliderVal');
const paramK = document.getElementById('paramK');

/* ============================
   弹簧路径生成
   ============================ */
const SPRING_CX = 320;
const SPRING_YTOP = 182;
const SPRING_MAX_PX = 230;
const SPRING_MIN_PX = 40;
const SPRING_COILS = 9;
const SPRING_HW = 24;

function makeSpringPath(yTop, yBottom) {
  const h = yBottom - yTop;
  if (h < 8) return `M ${SPRING_CX} ${yTop} L ${SPRING_CX} ${yBottom}`;
  const segH = h / (SPRING_COILS * 2);
  let d = `M ${SPRING_CX} ${yTop}`;
  for (let i = 0; i < SPRING_COILS * 2; i++) {
    const y = yTop + segH * (i + 1);
    const x = (i % 2 === 0) ? SPRING_CX + SPRING_HW : SPRING_CX - SPRING_HW;
    d += ` L ${x} ${y}`;
  }
  d += ` L ${SPRING_CX} ${yBottom}`;
  return d;
}

/* ============================
   力流箭头绘制
   ============================ */
let forceArrowPaths = [];
function createForceArrows() {
  forceArrowsG.innerHTML = '';
  forceArrowPaths = [];
  const pts = [
    [320, 560], [320, 500], [320, 420], [320, 340], [320, 260], [320, 180], [320, 120]
  ];
  for (let i = 0; i < pts.length - 1; i++) {
    const p = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    p.setAttribute('x1', pts[i][0] - 55);
    p.setAttribute('y1', pts[i][1]);
    p.setAttribute('x2', pts[i + 1][0] - 55);
    p.setAttribute('y2', pts[i + 1][1]);
    p.setAttribute('stroke', '#00e5ff');
    p.setAttribute('stroke-width', '2.5');
    p.setAttribute('stroke-dasharray', '8,5');
    p.setAttribute('marker-end', 'url(#arrowCyan)');
    p.setAttribute('opacity', '0');
    forceArrowsG.appendChild(p);
    forceArrowPaths.push(p);
  }
}
createForceArrows();

/* ============================
   粒子系统
   ============================ */
let particles = [];
function spawnParticles(x, y, count, color) {
  for (let i = 0; i < count; i++) {
    const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.8;
    const speed = 40 + Math.random() * 80;
    const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    el.setAttribute('cx', x);
    el.setAttribute('cy', y);
    el.setAttribute('r', 2 + Math.random() * 2.5);
    el.setAttribute('fill', color);
    el.setAttribute('opacity', '1');
    particlesG.appendChild(el);
    particles.push({
      el, x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      life: 0.6 + Math.random() * 0.5,
      age: 0
    });
  }
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.age += dt;
    if (p.age >= p.life) {
      p.el.remove();
      particles.splice(i, 1);
      continue;
    }
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 120 * dt; // 重力
    const alpha = 1 - p.age / p.life;
    p.el.setAttribute('cx', p.x);
    p.el.setAttribute('cy', p.y);
    p.el.setAttribute('opacity', alpha.toFixed(2));
  }
}

/* ============================
   主动画循环
   ============================ */
let prevGrounded = false;
let impactSpawned = false;

function animate(ts) {
  if (!S.lastTS) S.lastTS = ts;
  const dt = Math.min((ts - S.lastTS) / 1000, 0.05);
  S.lastTS = ts;

  if (S.playing) {
    S.time = (S.time + dt * S.speed * 0.35) % 1;
  }

  const t = S.time;
  const phase = getPhase(t);
  const comp = getCompression(t);
  const compRatio = comp / S.maxSpringMM;
  const effStiff = getEffStiffness(comp);
  const springForce = S.stiffness * comp;
  const grounded = isGrounded(t);

  // 冲击粒子
  if (grounded && !prevGrounded) impactSpawned = false;
  if (grounded && !impactSpawned && t > 0.22 && t < 0.3) {
    spawnParticles(320, 585, 14, '#00e5ff');
    impactSpawned = true;
  }
  // 刚性化粒子
  if (compRatio > 0.92 && t > 0.5 && t < 0.78) {
    if (Math.random() < 0.15) spawnParticles(320, 300, 3, '#ff5722');
  }
  prevGrounded = grounded;
  updateParticles(dt);

  // 弹簧长度映射
  const springPx = SPRING_MAX_PX - compRatio * (SPRING_MAX_PX - SPRING_MIN_PX);
  const springYBottom = SPRING_YTOP + springPx;

  // 弹簧路径
  const spD = makeSpringPath(SPRING_YTOP, springYBottom);
  springPath.setAttribute('d', spD);
  springGlow.setAttribute('d', spD);

  // 弹簧颜色插值
  let springColor;
  if (compRatio < 0.5) {
    springColor = lerpColor('#00e5ff', '#ffab00', compRatio * 2);
  } else {
    springColor = lerpColor('#ffab00', '#ff5722', (compRatio - 0.5) * 2);
  }
  springPath.setAttribute('stroke', springColor);
  springGlow.setAttribute('stroke', springColor);

  // 弹簧发光强度
  const glowOpacity = 0.15 + compRatio * 0.35;
  springGlow.setAttribute('opacity', glowOpacity.toFixed(2));
  if (compRatio > 0.85) {
    springPath.setAttribute('filter', 'url(#glowOrange)');
  } else {
    springPath.removeAttribute('filter');
  }

  // 小腿杆位置
  const shoulderY = springYBottom;
  const rodTopY = shoulderY + 10;
  const rodLen = 230;
  const footY = rodTopY + rodLen;

  calfShoulder.setAttribute('y', shoulderY);
  thrustBearing.setAttribute('y', shoulderY - 6);
  calfRod.setAttribute('y', rodTopY);
  calfRod.setAttribute('height', rodLen);
  footGroup.setAttribute('transform', `translate(0, ${footY})`);

  // 电机拉索
  motorCable.setAttribute('x2', 320);
  motorCable.setAttribute('y2', shoulderY);
  // 拉索颜色随电机激活变化
  const motorActive = t > 0.35 && t < 0.82;
  motorCable.setAttribute('stroke', motorActive ? '#ffab00' : '#5c7da0');
  motorCable.setAttribute('opacity', motorActive ? '0.9' : '0.4');
  motorCable.setAttribute('stroke-dasharray', motorActive ? '6,3' : '4,3');

  // 髋关节角度 → 整体旋转(微幅)
  const hipAngle = getHipAngle(t);
  legAssembly.setAttribute('transform', `rotate(${hipAngle * 0.3}, 320, 105)`);

  // 足端位置调整(让脚触地)
  // 不需要额外偏移,因为弹簧压缩自然使腿变短

  // 力流箭头更新
  const showForce = grounded && t > 0.24;
  forceArrowsG.setAttribute('opacity', showForce ? '0.8' : '0');
  if (showForce) {
    const arrowColor = compRatio > 0.85 ? '#ff5722' : (compRatio > 0.5 ? '#ffab00' : '#00e5ff');
    const arrowMarker = compRatio > 0.85 ? 'url(#arrowOrange)' : 'url(#arrowCyan)';
    const dashLen = compRatio > 0.85 ? '12,0' : '8,5';
    forceArrowPaths.forEach((p, i) => {
      p.setAttribute('stroke', arrowColor);
      p.setAttribute('marker-end', arrowMarker);
      p.setAttribute('stroke-dasharray', dashLen);
      // 弹簧段的箭头特殊处理
      if (i === 2 || i === 3) {
        p.setAttribute('stroke-width', compRatio > 0.85 ? '3.5' : '2.5');
      }
    });
  }

  // 力流箭头动画偏移
  const dashOffset = -(ts / 40) % 13;
  forceArrowPaths.forEach(p => {
    p.setAttribute('stroke-dashoffset', dashOffset.toFixed(1));
  });

  // 相位文字
  phaseLabel.textContent = phase.name;
  phaseLabel.setAttribute('fill', phase.color);
  phaseLabel.setAttribute('opacity', '1');
  phaseSublabel.textContent = phase.desc;
  phaseSublabel.setAttribute('opacity', '1');

  // ===== 信息面板更新 =====
  phaseBadge.style.background = hexToRGBA(phase.color, 0.12);
  phaseBadge.style.color = phase.color;
  phaseDot.style.background = phase.color;
  phaseDot.style.boxShadow = `0 0 8px ${phase.color}`;
  phaseNameEl.textContent = phase.name;
  phaseDescEl.textContent = phase.desc;

  // 压缩量
  compressBar.style.width = `${compRatio * 100}%`;
  compressBar.style.background = springColor;
  compressVal.textContent = `${comp.toFixed(1)} mm`;

  // 有效刚度
  const stiffRatio = Math.min(effStiff / (S.stiffness * 20), 1);
  stiffBar.style.width = `${stiffRatio * 100}%`;
  stiffBar.style.background = compRatio > 0.85 ? '#ff5722' : springColor;
  if (effStiff > S.stiffness * 10) {
    stiffVal.textContent = '→ ∞';
    stiffVal.style.color = '#ff5722';
  } else {
    stiffVal.textContent = `${effStiff.toFixed(0)} N/mm`;
    stiffVal.style.color = '';
  }

  // 弹簧力
  forceVal.textContent = `${springForce.toFixed(0)} N`;

  // 状态标签
  if (compRatio > 0.85) {
    stateLabel.textContent = '刚性(底并)';
    stateLabel.style.color = '#ff5722';
  } else if (compRatio > 0.4) {
    stateLabel.textContent = '过渡中';
    stateLabel.style.color = '#ffab00';
  } else {
    stateLabel.textContent = '柔性';
    stateLabel.style.color = '#00e5ff';
  }

  // 力流路径高亮
  const ffItems = document.querySelectorAll('.ff-item');
  ffItems.forEach(item => {
    item.classList.remove('active', 'flex-active');
  });
  if (showForce) {
    ffItems.forEach(item => {
      if (compRatio > 0.85) item.classList.add('active');
      else item.classList.add('flex-active');
    });
  }
  ffSpring.textContent = compRatio > 0.85 ? '弹簧(刚性)' : '弹簧(柔性)';

  // 刚度曲线工作点
  const dotX = 35 + (comp / S.maxSpringMM) * 150;
  const dotY = 115 - (springForce / (S.stiffness * S.maxSpringMM)) * 95;
  chartDot.setAttribute('cx', dotX);
  chartDot.setAttribute('cy', Math.max(dotY, 18));
  chartDot.setAttribute('fill', springColor);

  // 时间线
  timelineFill.style.width = `${t * 100}%`;
  timelineFill.style.background = phase.color;
  timelineThumb.style.left = `${t * 100}%`;

  requestAnimationFrame(animate);
}

/* ============================
   辅助函数
   ============================ */
function lerpColor(a, b, t) {
  t = Math.max(0, Math.min(1, t));
  const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16);
  const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16);
  const r = Math.round(ar + (br - ar) * t);
  const g = Math.round(ag + (bg - ag) * t);
  const bl = Math.round(ab + (bb - ab) * t);
  return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${bl.toString(16).padStart(2,'0')}`;
}

function hexToRGBA(hex, alpha) {
  const r = parseInt(hex.slice(1,3),16);
  const g = parseInt(hex.slice(3,5),16);
  const b = parseInt(hex.slice(5,7),16);
  return `rgba(${r},${g},${b},${alpha})`;
}

/* ============================
   交互控制
   ============================ */
// 播放/暂停
playBtn.addEventListener('click', () => {
  S.playing = !S.playing;
  playBtn.textContent = S.playing ? '⏸' : '▶';
  playBtn.classList.toggle('active', S.playing);
});

// 速度按钮
document.querySelectorAll('.speed-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    S.speed = parseFloat(btn.dataset.speed);
  });
});

// 时间线拖拽
const timelineTrack = document.getElementById('timelineTrack');
let dragging = false;

function setTimeFromEvent(e) {
  const rect = timelineTrack.getBoundingClientRect();
  const x = (e.clientX || e.touches[0].clientX) - rect.left;
  S.time = Math.max(0, Math.min(0.999, x / rect.width));
}

timelineTrack.addEventListener('mousedown', e => { dragging = true; setTimeFromEvent(e); });
window.addEventListener('mousemove', e => { if (dragging) setTimeFromEvent(e); });
window.addEventListener('mouseup', () => { dragging = false; });
timelineTrack.addEventListener('touchstart', e => { dragging = true; setTimeFromEvent(e); }, {passive:true});
window.addEventListener('touchmove', e => { if (dragging) setTimeFromEvent(e); }, {passive:true});
window.addEventListener('touchend', () => { dragging = false; });

// 刚度滑块
stiffSlider.addEventListener('input', () => {
  S.stiffness = parseInt(stiffSlider.value);
  stiffSliderVal.textContent = `${S.stiffness} N/mm`;
  paramK.textContent = `${S.stiffness} N/mm`;
});

/* ============================
   启动
   ============================ */
// 初始播放按钮状态
playBtn.textContent = '⏸';
requestAnimationFrame(animate);
</script>
</body>
</html>

实现说明

这个高保真 SVG 原理动画围绕 TRIZ 最终理想解(IFR) 思想——"结构即控制"进行设计,核心展示串联弹性驱动器(SEA)如何在无需复杂主动控制的前提下,通过同一个弹簧元件自动实现柔性缓冲与刚性发力。

关键视觉设计:

  1. 弹簧颜色渐变:从青色(柔性)→ 琥珀色(过渡)→ 橙红色(刚性底并),直观呈现同一结构在不同压缩阶段的角色转换,这是破除矛盾的核心视觉暗示。

  2. 力流箭头动画:触地后左侧出现动态虚线箭头,柔性阶段为虚线流动(能量被吸收),刚性底并后变为实线粗箭头(力直接传导),清晰展示力流传导路径的本质变化。

  3. 刚度特性曲线:右面板实时绘制力-位移曲线,工作点随动画移动,在底并区可观察到刚度突增趋于无穷——这就是"结构即控制"的数学本质。

  4. 冲击粒子:触地瞬间在足端爆发青色粒子,刚性化阶段弹簧附近偶发橙色粒子,强化关键物理事件的感知。

交互功能:

  • 时间线拖拽:可手动 scrub 整个步态周期,逐帧观察弹簧状态变化
  • 播放速度:0.3x / 1x / 2x 三档变速
  • 刚度调节滑块:可实时改变弹簧刚度(30~180 N/mm),直观体验刚度如何影响柔性-刚性转换的时机和效果
积分规则:第一轮对话扣减6分,后续每轮扣4分