<!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)为叙事主线,动画直接展示"矛盾已消除"的理想运行状态——机构无需额外驱动与控制,仅凭地面反力自身即可触发刚度跃变。
视觉编码体系:
- 🟢 绿色 → 低刚度/解耦状态(腾空摆动期)
- 🟠 橙色 → 高刚度/锁死状态(支撑蹬地期)
- 🟡 黄色 → 过渡瞬间(触地冲击/离地过渡)
- 所有颜色连续插值,状态切换丝滑可感知
三层动画架构:
- 机构轴向剖面(中央主视觉)— 楔块径向伸缩、弹簧压缩/释放、内外圈相对转动,全部实时联动
- 腿部侧视(左侧)— 髋膝角度随步态周期变化,膝关节光圈颜色同步反映刚度状态
- 刚度仪表(右侧)— 弧形仪表实时映射归一化刚度值 0.08→0.95
交互设计亮点:
- 楔块锥角滑块(6°~22°):当锥角超过摩擦角(≈14°)时,自锁条件破坏,楔块无法完全锁死,刚度无法跃升——直接可视化 IFR 的核心物理条件
- 阶段滑块:手动拖拽步态周期任意时刻,逐帧审视楔块动作细节
- 因果链面板:四步自锁因果链在触地冲击时逐级点亮,清晰展示"地面反力→相对转动→楔块挤入→刚度跃升"的资源自利用路径
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
