<!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)如何在无需复杂主动控制的前提下,通过同一个弹簧元件自动实现柔性缓冲与刚性发力。
关键视觉设计:
弹簧颜色渐变:从青色(柔性)→ 琥珀色(过渡)→ 橙红色(刚性底并),直观呈现同一结构在不同压缩阶段的角色转换,这是破除矛盾的核心视觉暗示。
力流箭头动画:触地后左侧出现动态虚线箭头,柔性阶段为虚线流动(能量被吸收),刚性底并后变为实线粗箭头(力直接传导),清晰展示力流传导路径的本质变化。
刚度特性曲线:右面板实时绘制力-位移曲线,工作点随动画移动,在底并区可观察到刚度突增趋于无穷——这就是"结构即控制"的数学本质。
冲击粒子:触地瞬间在足端爆发青色粒子,刚性化阶段弹簧附近偶发橙色粒子,强化关键物理事件的感知。
交互功能:
- 时间线拖拽:可手动 scrub 整个步态周期,逐帧观察弹簧状态变化
- 播放速度:0.3x / 1x / 2x 三档变速
- 刚度调节滑块:可实时改变弹簧刚度(30~180 N/mm),直观体验刚度如何影响柔性-刚性转换的时机和效果
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
