<!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=Rajdhani:wght@300;400;600;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root {
--bg:#050a14;--grid:#0a1428;--pole:#1e3050;--bone:#00c8a0;--bone-hi:#00ffcc;
--spring-idle:#1a5a3a;--spring-act:#00ff88;--sma-cold:#3a2820;--sma-hot:#ff6b2b;
--canopy:#0a3555;--annot:#6090c0;--accent:#ffd700;--text:#c0d0e0;--dim:#3a5570;
--card:rgba(8,16,30,0.85);--border:rgba(0,200,160,0.15);
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Rajdhani',sans-serif;
overflow:hidden;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;
user-select:none}
.title-bar{position:fixed;top:0;left:0;right:0;padding:18px 32px;z-index:10;
background:linear-gradient(180deg,rgba(5,10,20,0.95),transparent);display:flex;align-items:baseline;gap:18px}
.title-bar h1{font-size:22px;font-weight:700;letter-spacing:2px;color:var(--bone-hi);text-transform:uppercase}
.title-bar p{font-size:13px;color:var(--dim);font-family:'Share Tech Mono',monospace;letter-spacing:1px}
.svg-wrap{flex:1;display:flex;align-items:center;justify-content:center;width:100%;padding:0 20px}
svg#mech{width:100%;max-width:1200px;height:auto;max-height:80vh}
/* 右侧参数面板 */
.param-panel{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:10;
display:flex;flex-direction:column;gap:10px;width:180px}
.param-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;
backdrop-filter:blur(8px);transition:border-color .3s}
.param-card.active{border-color:var(--spring-act)}
.param-card.active-sma{border-color:var(--sma-hot)}
.param-label{font-size:10px;color:var(--dim);font-family:'Share Tech Mono',monospace;letter-spacing:1px;text-transform:uppercase}
.param-value{font-size:22px;font-weight:700;color:var(--text);margin-top:2px;font-family:'Share Tech Mono',monospace}
.param-unit{font-size:11px;color:var(--dim);margin-left:4px}
/* 左侧阶段指示器 */
.phase-timeline{position:fixed;left:28px;top:50%;transform:translateY(-50%);z-index:10;
display:flex;flex-direction:column;gap:0;align-items:flex-start}
.phase-item{display:flex;align-items:center;gap:10px;padding:8px 0;opacity:0.35;transition:all .4s}
.phase-item.active{opacity:1}
.phase-dot{width:10px;height:10px;border-radius:50%;border:2px solid var(--dim);transition:all .4s;flex-shrink:0}
.phase-item.active .phase-dot{border-color:var(--bone-hi);background:var(--bone-hi);
box-shadow:0 0 10px var(--bone-hi)}
.phase-item.done .phase-dot{border-color:var(--spring-act);background:var(--spring-act)}
.phase-item.active-sma .phase-dot{border-color:var(--sma-hot);background:var(--sma-hot);
box-shadow:0 0 10px var(--sma-hot)}
.phase-line{width:2px;height:20px;background:var(--dim);margin-left:4px;opacity:0.3;transition:all .4s}
.phase-line.lit{background:var(--spring-act);opacity:0.7}
.phase-name{font-size:12px;font-family:'Share Tech Mono',monospace;color:var(--text);letter-spacing:0.5px;white-space:nowrap}
/* 底部控制栏 */
.controls{position:fixed;bottom:0;left:0;right:0;z-index:10;
background:linear-gradient(0deg,rgba(5,10,20,0.95),transparent);padding:16px 32px 22px;
display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap}
.btn{padding:8px 22px;border:1px solid var(--border);background:var(--card);color:var(--text);
border-radius:6px;font-family:'Rajdhani',sans-serif;font-size:14px;font-weight:600;
cursor:pointer;transition:all .25s;letter-spacing:1px;backdrop-filter:blur(6px)}
.btn:hover{border-color:var(--bone-hi);color:var(--bone-hi);box-shadow:0 0 12px rgba(0,255,204,0.15)}
.btn:active{transform:scale(0.96)}
.btn.primary{border-color:var(--bone);background:rgba(0,200,160,0.12);color:var(--bone-hi)}
.btn.primary:hover{background:rgba(0,200,160,0.22);box-shadow:0 0 16px rgba(0,255,204,0.2)}
.slider-wrap{display:flex;align-items:center;gap:10px}
.slider-wrap label{font-size:11px;color:var(--dim);font-family:'Share Tech Mono',monospace;white-space:nowrap}
input[type=range]{-webkit-appearance:none;width:180px;height:4px;background:var(--dim);
border-radius:2px;outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;
border-radius:50%;background:var(--bone-hi);border:2px solid var(--bg);cursor:pointer;
box-shadow:0 0 6px var(--bone-hi)}
.speed-label{font-size:11px;color:var(--dim);font-family:'Share Tech Mono',monospace;min-width:28px;text-align:center}
/* 响应式 */
@media(max-width:900px){
.param-panel{right:8px;width:140px}
.param-value{font-size:17px}
.phase-timeline{left:10px}
.phase-name{font-size:10px}
}
@media(max-width:600px){
.param-panel,.phase-timeline{display:none}
.title-bar h1{font-size:16px}
}
</style>
</head>
<body>
<div class="title-bar">
<h1>IFR 无源两段自动折叠</h1>
<p>利用现有资源破除矛盾的理想解</p>
</div>
<div class="svg-wrap">
<svg id="mech" viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 网格 -->
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M40 0L0 0 0 40" fill="none" stroke="#0b1528" stroke-width="0.5"/>
</pattern>
<pattern id="gridFine" width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M8 0L0 0 0 8" fill="none" stroke="#08101e" stroke-width="0.3"/>
</pattern>
<!-- 发光滤镜 -->
<filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" 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="7" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="12"/>
</filter>
<!-- 箭头标记 -->
<marker id="arrGreen" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,1 L9,5 L0,9Z" fill="#00ff88"/>
</marker>
<marker id="arrOrange" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,1 L9,5 L0,9Z" fill="#ff6b2b"/>
</marker>
<!-- SMA热辐射渐变 -->
<radialGradient id="smaHeatGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff6b2b" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#ff6b2b" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- 背景层 -->
<rect width="1200" height="800" fill="url(#gridFine)"/>
<rect width="1200" height="800" fill="url(#grid)"/>
<rect width="1200" height="800" fill="rgba(5,10,20,0.5)"/>
<!-- 聚光效果 -->
<ellipse cx="420" cy="340" rx="280" ry="200" fill="rgba(0,200,160,0.015)"/>
<!-- ===== 机构图层 ===== -->
<g id="layerMech">
<!-- 伞柄 -->
<rect id="handle" x="336" y="600" width="28" height="48" rx="6" fill="#141e30" stroke="#1e3050" stroke-width="1.5"/>
<rect x="340" y="608" width="20" height="6" rx="2" fill="#1e3050"/>
<!-- 主杆 -->
<line id="pole" x1="350" y1="600" x2="350" y2="220" stroke="#1a2d4a" stroke-width="6" stroke-linecap="round"/>
<line x1="350" y1="600" x2="350" y2="220" stroke="#223a5a" stroke-width="3" stroke-linecap="round"/>
<!-- 微动开关 -->
<g id="microSwitch">
<rect x="340" y="580" width="20" height="10" rx="2" fill="#1a1a2a" stroke="#3a3a5a" stroke-width="1"/>
<rect id="switchBtn" x="344" y="582" width="5" height="6" rx="1" fill="#5a5a7a"/>
</g>
<!-- 滑套(Runner) -->
<g id="runner">
<rect x="340" y="260" width="20" height="16" rx="3" fill="#12203a" stroke="#2a4a6a" stroke-width="1.5"/>
<line x1="343" y1="264" x2="357" y2="264" stroke="#3a6a8a" stroke-width="0.8"/>
<line x1="343" y1="268" x2="357" y2="268" stroke="#3a6a8a" stroke-width="0.8"/>
</g>
<!-- 撑骨(Stretcher) -->
<line id="stretcher" x1="350" y1="268" x2="420" y2="290" stroke="#1a3a5a" stroke-width="2.5" stroke-linecap="round"/>
<!-- 伞面(Canopy) -->
<path id="canopy" d="" fill="rgba(10,53,85,0.25)" stroke="rgba(0,200,160,0.12)" stroke-width="1"/>
<!-- 骨节轨迹 -->
<path id="traceMid" d="" fill="none" stroke="rgba(0,255,136,0.15)" stroke-width="1" stroke-dasharray="3,4"/>
<path id="traceDist" d="" fill="none" stroke="rgba(255,107,43,0.12)" stroke-width="1" stroke-dasharray="3,4"/>
<!-- 近端骨 -->
<line id="boneProx" x1="350" y1="220" x2="510" y2="150" stroke="#00c8a0" stroke-width="5" stroke-linecap="round"/>
<line id="boneProxHi" x1="350" y1="220" x2="510" y2="150" stroke="#00ffcc" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- 铰接点P-M (近端-中端) -->
<circle id="hingePM" cx="510" cy="150" r="5" fill="#0a1a2a" stroke="#00c8a0" stroke-width="2"/>
<!-- 双扭簧 -->
<g id="torsionSpring"></g>
<circle id="springGlow" cx="510" cy="150" r="20" fill="rgba(0,255,136,0)" filter="url(#softGlow)"/>
<!-- 中端骨 -->
<line id="boneMid" x1="510" y1="150" x2="640" y2="100" stroke="#00c8a0" stroke-width="4" stroke-linecap="round"/>
<line id="boneMidHi" x1="510" y1="150" x2="640" y2="100" stroke="#00ffcc" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<!-- 铰接点M-D (中端-远端) -->
<circle id="hingeMD" cx="640" cy="100" r="4.5" fill="#0a1a2a" stroke="#00c8a0" stroke-width="1.8"/>
<!-- SMA致动器 -->
<g id="smaActuator"></g>
<circle id="smaGlow" cx="640" cy="100" r="30" fill="url(#smaHeatGrad)" opacity="0"/>
<!-- 远端骨 -->
<line id="boneDist" x1="640" y1="100" x2="720" y2="60" stroke="#00c8a0" stroke-width="3" stroke-linecap="round"/>
<line id="boneDistHi" x1="640" y1="100" x2="720" y2="60" stroke="#00ffcc" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
<!-- 远端骨尖端 -->
<circle id="boneTip" cx="720" cy="60" r="2.5" fill="#00ffcc" opacity="0.6"/>
</g>
<!-- ===== 力箭头图层 ===== -->
<g id="layerArrows">
<line id="arrowSpring" x1="0" y1="0" x2="0" y2="0" stroke="#00ff88" stroke-width="2"
marker-end="url(#arrGreen)" opacity="0" filter="url(#glowGreen)"/>
<line id="arrowSMA" x1="0" y1="0" x2="0" y2="0" stroke="#ff6b2b" stroke-width="2"
marker-end="url(#arrOrange)" opacity="0" filter="url(#glowOrange)"/>
</g>
<!-- ===== 标注图层 ===== -->
<g id="layerAnnot" font-family="'Share Tech Mono',monospace" font-size="11" fill="#6090c0">
<g id="annotSpring" opacity="0">
<rect x="0" y="0" width="200" height="36" rx="4" fill="rgba(0,255,136,0.06)" stroke="rgba(0,255,136,0.2)" stroke-width="0.8"/>
<text x="10" y="15" fill="#00ff88" font-size="10">双扭簧释能</text>
<text x="10" y="29" fill="#6b8ab8" font-size="9.5">刚度 0.02 N·mm/deg → 第一阶段折叠</text>
</g>
<g id="annotSMA" opacity="0">
<rect x="0" y="0" width="220" height="36" rx="4" fill="rgba(255,107,43,0.06)" stroke="rgba(255,107,43,0.2)" stroke-width="0.8"/>
<text x="10" y="15" fill="#ff6b2b" font-size="10">SMA 形变致动</text>
<text x="10" y="29" fill="#6b8ab8" font-size="9.5">45℃相变 · 0.8A · 1.2s → 第二阶段折叠</text>
</g>
<g id="annotUnlock" opacity="0">
<rect x="0" y="0" width="160" height="24" rx="4" fill="rgba(0,200,160,0.06)" stroke="rgba(0,200,160,0.2)" stroke-width="0.8"/>
<text x="10" y="16" fill="#00ffcc" font-size="10">锁定解除 · 滑套下行</text>
</g>
</g>
</svg>
</div>
<!-- 左侧阶段指示器 -->
<div class="phase-timeline">
<div class="phase-item active" data-phase="0"><div class="phase-dot"></div><span class="phase-name">展开锁定</span></div>
<div class="phase-line" data-line="0"></div>
<div class="phase-item" data-phase="1"><div class="phase-dot"></div><span class="phase-name">解除锁定</span></div>
<div class="phase-line" data-line="1"></div>
<div class="phase-item" data-phase="2"><div class="phase-dot"></div><span class="phase-name">扭簧一次折叠</span></div>
<div class="phase-line" data-line="2"></div>
<div class="phase-item" data-phase="3"><div class="phase-dot"></div><span class="phase-name">SMA二次折叠</span></div>
<div class="phase-line" data-line="3"></div>
<div class="phase-item" data-phase="4"><div class="phase-dot"></div><span class="phase-name">收拢完成</span></div>
</div>
<!-- 右侧参数面板 -->
<div class="param-panel">
<div class="param-card" id="cardSpring">
<div class="param-label">扭簧角位移</div>
<div><span class="param-value" id="valSpring">10</span><span class="param-unit">deg</span></div>
</div>
<div class="param-card" id="cardSMA">
<div class="param-label">SMA 温度</div>
<div><span class="param-value" id="valSMA">25</span><span class="param-unit">℃</span></div>
</div>
<div class="param-card">
<div class="param-label">致动电流</div>
<div><span class="param-value" id="valCurrent">0.0</span><span class="param-unit">A</span></div>
</div>
<div class="param-card">
<div class="param-label">中端骨折叠角</div>
<div><span class="param-value" id="valMidAng">170</span><span class="param-unit">deg</span></div>
</div>
<div class="param-card">
<div class="param-label">远端骨折叠角</div>
<div><span class="param-value" id="valDistAng">180</span><span class="param-unit">deg</span></div>
</div>
</div>
<!-- 底部控制栏 -->
<div class="controls">
<button class="btn" id="btnOpen" onclick="startOpen()">◀ 开伞</button>
<button class="btn primary" id="btnAuto" onclick="toggleAuto()">自动演示</button>
<button class="btn" id="btnClose" onclick="startClose()">收伞 ▶</button>
<div class="slider-wrap">
<label>进度</label>
<input type="range" id="sliderProgress" min="0" max="1000" value="0" oninput="onSliderInput(this)">
</div>
<div class="slider-wrap">
<label>速度</label>
<input type="range" id="sliderSpeed" min="20" max="300" value="100">
<span class="speed-label" id="speedLabel">1.0x</span>
</div>
</div>
<script>
/* ===== 常量 ===== */
const DEG = Math.PI / 180;
const NOTCH = {x:350, y:220}; // 铰接点(伞顶)
const POLE_BOT = 600; // 主杆底端y
const L = {p:160, m:130, d:95}; // 骨节长度
const OPEN_A = {p:-28, mRel:-8, dRel:0}; // 展开角度
const FOLD_A = {mRel:140, dRel:130}; // 折叠终态相对角度
/* ===== 动画状态 ===== */
let progress = 0; // 0=展开 1=收拢
let target = 0; // 目标progress
let autoPlay = false;
let autoDir = 1; // 1=收拢 -1=展开
let manualMode= false;
let traceMid = []; // 中端骨末端轨迹
let traceDist = []; // 远端骨末端轨迹
/* ===== 缓动函数 ===== */
function easeInOutCubic(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}
function easeOutQuad(t){return 1-(1-t)*(1-t)}
/* ===== 角度与位置计算 ===== */
function getAngles(p){
// p: 0~1 总进度
let mRel = OPEN_A.mRel, dRel = OPEN_A.dRel, runnerOff = 0, smaHeat = 0, springAct = 0;
if(p <= 0.08){
// 滑套下行,解锁
runnerOff = (p/0.08)*50;
} else if(p <= 0.45){
// 第一阶段:扭簧折叠
const t = (p-0.08)/0.37;
const e = easeInOutCubic(t);
mRel = OPEN_A.mRel + (FOLD_A.mRel - OPEN_A.mRel)*e;
runnerOff = 50;
springAct = 1 - e*0.6;
} else if(p <= 0.55){
// SMA加热
const t = (p-0.45)/0.10;
mRel = FOLD_A.mRel;
runnerOff = 50;
smaHeat = easeOutQuad(t);
springAct = 0.4;
} else if(p <= 0.90){
// 第二阶段:SMA折叠
const t = (p-0.55)/0.35;
const e = easeInOutCubic(t);
mRel = FOLD_A.mRel;
dRel = OPEN_A.dRel + (FOLD_A.dRel - OPEN_A.dRel)*e;
runnerOff = 50;
smaHeat = 1 - e*0.3;
springAct = 0.4*(1-e);
} else {
// 收拢完成
const t = (p-0.90)/0.10;
mRel = FOLD_A.mRel;
dRel = FOLD_A.dRel;
runnerOff = 50 + t*10;
smaHeat = 0.7*(1-t);
}
return {pAngle:OPEN_A.p, mRel, dRel, runnerOff, smaHeat, springAct};
}
function computePos(a){
const p1 = {x:NOTCH.x, y:NOTCH.y};
const p2 = {x:p1.x + L.p*Math.cos(a.pAngle*DEG), y:p1.y + L.p*Math.sin(a.pAngle*DEG)};
const mAbs = a.pAngle + a.mRel;
const p3 = {x:p2.x + L.m*Math.cos(mAbs*DEG), y:p2.y + L.m*Math.sin(mAbs*DEG)};
const dAbs = mAbs + a.dRel;
const p4 = {x:p3.x + L.d*Math.cos(dAbs*DEG), y:p3.y + L.d*Math.sin(dAbs*DEG)};
return {p1,p2,p3,p4, mAbs, dAbs};
}
/* ===== SVG 元素引用 ===== */
const $ = id => document.getElementById(id);
const el = {
pole:$('pole'), handle:$('handle'),
runner:$('runner'), stretcher:$('stretcher'),
boneP:$('boneProx'), bonePHi:$('boneProxHi'),
boneM:$('boneMid'), boneMHi:$('boneMidHi'),
boneD:$('boneDist'), boneDHi:$('boneDistHi'),
hingePM:$('hingePM'), hingeMD:$('hingeMD'),
boneTip:$('boneTip'), canopy:$('canopy'),
springGlow:$('springGlow'), smaGlow:$('smaGlow'),
arrowSpring:$('arrowSpring'), arrowSMA:$('arrowSMA'),
annotSpring:$('annotSpring'), annotSMA:$('annotSMA'), annotUnlock:$('annotUnlock'),
traceMid:$('traceMid'), traceDist:$('traceDist'),
microSwitch:$('microSwitch'), switchBtn:$('switchBtn'),
torsionSpring:$('torsionSpring'), smaActuator:$('smaActuator'),
};
/* ===== 绘制函数 ===== */
function drawScene(a, pos){
// 主杆
// (静态,不更新)
// 滑套位置
const runnerY = 260 + a.runnerOff;
el.runner.setAttribute('transform', `translate(0,${a.runnerOff})`);
// 微动开关触发
const swActive = a.runnerOff > 30;
el.switchBtn.setAttribute('fill', swActive ? '#ff6b2b' : '#5a5a7a');
el.switchBtn.setAttribute('x', swActive ? '349' : '344');
// 撑骨 - 从滑套到近端骨40%处
const strutEnd = {
x: pos.p1.x + 0.4*(pos.p2.x - pos.p1.x),
y: pos.p1.y + 0.4*(pos.p2.y - pos.p1.y)
};
el.stretcher.setAttribute('x1', 350);
el.stretcher.setAttribute('y1', runnerY + 8);
el.stretcher.setAttribute('x2', strutEnd.x);
el.stretcher.setAttribute('y2', strutEnd.y);
// 近端骨
setLine(el.boneP, pos.p1, pos.p2);
setLine(el.bonePHi, pos.p1, pos.p2);
// 中端骨
setLine(el.boneM, pos.p2, pos.p3);
setLine(el.boneMHi, pos.p2, pos.p3);
// 远端骨
setLine(el.boneD, pos.p3, pos.p4);
setLine(el.boneDHi, pos.p3, pos.p4);
// 铰接点
setCircle(el.hingePM, pos.p2, 5);
setCircle(el.hingeMD, pos.p3, 4.5);
setCircle(el.boneTip, pos.p4, 2.5);
// 骨节高亮
const midFold = Math.abs(a.mRel - OPEN_A.mRel) / (FOLD_A.mRel - OPEN_A.mRel);
const distFold = Math.abs(a.dRel - OPEN_A.dRel) / (FOLD_A.dRel - OPEN_A.dRel);
el.boneMHi.setAttribute('opacity', 0.2 + midFold*0.5);
el.boneDHi.setAttribute('opacity', 0.2 + distFold*0.5);
// 扭簧发光
const sg = a.springAct;
el.springGlow.setAttribute('cx', pos.p2.x);
el.springGlow.setAttribute('cy', pos.p2.y);
el.springGlow.setAttribute('r', 18 + sg*12);
el.springGlow.setAttribute('fill', `rgba(0,255,136,${sg*0.25})`);
// SMA发光
const sh = a.smaHeat;
el.smaGlow.setAttribute('cx', pos.p3.x);
el.smaGlow.setAttribute('cy', pos.p3.y);
el.smaGlow.setAttribute('r', 25 + sh*25);
el.smaGlow.setAttribute('opacity', sh*0.7);
// 绘制扭簧
drawTorsionSpring(pos.p2, a.pAngle, a.pAngle + a.mRel, sg);
// 绘制SMA
drawSMA(pos.p3, pos.p2, pos.p4, a.pAngle + a.mRel, sh);
// 伞面
drawCanopy(pos);
// 力箭头
drawForceArrows(a, pos);
// 标注
drawAnnotations(a, pos);
// 铰接点颜色
el.hingePM.setAttribute('stroke', sg > 0.3 ? '#00ff88' : '#00c8a0');
el.hingeMD.setAttribute('stroke', sh > 0.3 ? '#ff6b2b' : '#00c8a0');
// 轨迹
drawTraces(pos);
}
function setLine(el, a, b){
el.setAttribute('x1',a.x); el.setAttribute('y1',a.y);
el.setAttribute('x2',b.x); el.setAttribute('y2',b.y);
}
function setCircle(el, c, r){
el.setAttribute('cx',c.x); el.setAttribute('cy',c.y);
if(r) el.setAttribute('r',r);
}
/* 扭簧绘制 - 在铰接点处画螺旋簧 */
function drawTorsionSpring(center, ang1, ang2, activation){
const g = el.torsionSpring;
let html = '';
const r1 = 10, r2 = 14;
const arm1Ang = ang1 * DEG;
const arm2Ang = ang2 * DEG;
const armLen = 18;
// 臂1 - 沿近端骨方向
const a1x = center.x + armLen*Math.cos(arm1Ang);
const a1y = center.y + armLen*Math.sin(arm1Ang);
// 臂2 - 沿中端骨方向
const a2x = center.x + armLen*Math.cos(arm2Ang);
const a2y = center.y + armLen*Math.sin(arm2Ang);
// 弹簧螺旋
const startA = ang1 + 180;
const endA = ang2;
const turns = 3;
const steps = turns * 24;
let d = `M${a1x},${a1y} L${center.x + r1*Math.cos(arm1Ang)},${center.y + r1*Math.sin(arm1Ang)}`;
for(let i=0;i<=steps;i++){
const t = i/steps;
const angle = (startA + t*(endA - startA + turns*360))*DEG;
const r = r1 + (r2-r1)*t;
const x = center.x + r*Math.cos(angle);
const y = center.y + r*Math.sin(angle);
d += ` L${x},${y}`;
}
d += ` L${a2x},${a2y}`;
const col = activation > 0.3 ? '#00ff88' : '#1a5a3a';
const w = activation > 0.3 ? 2 : 1.2;
const filt = activation > 0.5 ? ' filter="url(#glowGreen)"' : '';
html += `<path d="${d}" fill="none" stroke="${col}" stroke-width="${w}" stroke-linecap="round"${filt}/>`;
g.innerHTML = html;
}
/* SMA致动器绘制 */
function drawSMA(hinge, midStart, distEnd, midAbsAngle, heat){
const g = el.smaActuator;
let html = '';
const ang = midAbsAngle * DEG;
const perpAng = ang + Math.PI/2;
const w = 14, h = 3;
// SMA矩形位于铰接点旁
const cx = hinge.x + 8*Math.cos(perpAng);
const cy = hinge.y + 8*Math.sin(perpAng);
const col = heat > 0.3 ? `rgb(${Math.round(60+195*heat)},${Math.round(40+67*heat)},${Math.round(30-10*heat)})` : '#3a2820';
const filt = heat > 0.5 ? ' filter="url(#glowOrange)"' : '';
// 画旋转的矩形
const deg = midAbsAngle;
html += `<rect x="${cx-w/2}" y="${cy-h/2}" width="${w}" height="${h}" rx="1" fill="${col}" stroke="${heat>0.3?'#ff6b2b':'#5a3a2a'}" stroke-width="0.8" transform="rotate(${deg},${cx},${cy})"${filt}/>`;
// 加热时画弯曲方向指示
if(heat > 0.4){
const bendAng = (midAbsAngle + 90) * DEG;
const arrowLen = 16 * heat;
const ax = hinge.x + arrowLen*Math.cos(bendAng);
const ay = hinge.y + arrowLen*Math.sin(bendAng);
html += `<line x1="${hinge.x}" y1="${hinge.y}" x2="${ax}" y2="${ay}" stroke="#ff6b2b" stroke-width="1.2" stroke-dasharray="3,2" opacity="${heat*0.6}"/>`;
}
g.innerHTML = html;
}
/* 伞面绘制 */
function drawCanopy(pos){
// 从远端尖端到伞顶的弧线
const cp1x = (pos.p1.x + pos.p4.x)/2 + 30;
const cp1y = Math.min(pos.p1.y, pos.p4.y) - 40;
const d = `M${pos.p1.x},${pos.p1.y} Q${cp1x},${cp1y} ${pos.p4.x},${pos.p4.y}`;
el.canopy.setAttribute('d', d);
}
/* 力箭头绘制 */
function drawForceArrows(a, pos){
// 扭簧力箭头 - 在中端骨方向上
const sf = a.springAct;
if(sf > 0.2){
const mAbs = a.pAngle + a.mRel;
const perpAng = (mAbs + 90) * DEG;
const len = 30 * sf;
const sx = pos.p2.x + 10*Math.cos(perpAng);
const sy = pos.p2.y + 10*Math.sin(perpAng);
// 指向折叠方向
const foldAng = (a.pAngle + FOLD_A.mRel) * DEG;
const ex = sx + len*Math.cos(foldAng);
const ey = sy + len*Math.sin(foldAng);
el.arrowSpring.setAttribute('x1',sx);
el.arrowSpring.setAttribute('y1',sy);
el.arrowSpring.setAttribute('x2',ex);
el.arrowSpring.setAttribute('y2',ey);
el.arrowSpring.setAttribute('opacity', sf*0.8);
} else {
el.arrowSpring.setAttribute('opacity', 0);
}
// SMA力箭头
const sh = a.smaHeat;
if(sh > 0.3){
const mAbs = a.pAngle + a.mRel;
const perpAng = (mAbs + 90) * DEG;
const len = 25 * sh;
const sx = pos.p3.x + 8*Math.cos(perpAng);
const sy = pos.p3.y + 8*Math.sin(perpAng);
const foldAng = (mAbs + FOLD_A.dRel) * DEG;
const ex = sx + len*Math.cos(foldAng);
const ey = sy + len*Math.sin(foldAng);
el.arrowSMA.setAttribute('x1',sx);
el.arrowSMA.setAttribute('y1',sy);
el.arrowSMA.setAttribute('x2',ex);
el.arrowSMA.setAttribute('y2',ey);
el.arrowSMA.setAttribute('opacity', sh*0.7);
} else {
el.arrowSMA.setAttribute('opacity', 0);
}
}
/* 标注绘制 */
function drawAnnotations(a, pos){
// 解锁标注
const showUnlock = progress > 0.02 && progress < 0.18;
el.annotUnlock.setAttribute('opacity', showUnlock ? 0.9 : 0);
if(showUnlock){
const bx = 370, by = 280 + a.runnerOff;
el.annotUnlock.querySelector('rect').setAttribute('x', bx);
el.annotUnlock.querySelector('rect').setAttribute('y', by);
const texts = el.annotUnlock.querySelectorAll('text');
texts[0].setAttribute('x', bx+10);
texts[0].setAttribute('y', by+16);
}
// 扭簧标注
const showSpring = a.springAct > 0.3;
el.annotSpring.setAttribute('opacity', showSpring ? Math.min(1, a.springAct*1.5) : 0);
if(showSpring){
const bx = pos.p2.x + 20, by = pos.p2.y - 50;
el.annotSpring.querySelector('rect').setAttribute('x', bx);
el.annotSpring.querySelector('rect').setAttribute('y', by);
const texts = el.annotSpring.querySelectorAll('text');
texts[0].setAttribute('x', bx+10);
texts[0].setAttribute('y', by+15);
texts[1].setAttribute('x', bx+10);
texts[1].setAttribute('y', by+29);
}
// SMA标注
const showSMA = a.smaHeat > 0.2;
el.annotSMA.setAttribute('opacity', showSMA ? Math.min(1, a.smaHeat*1.5) : 0);
if(showSMA){
const bx = pos.p3.x + 15, by = pos.p3.y - 50;
el.annotSMA.querySelector('rect').setAttribute('x', bx);
el.annotSMA.querySelector('rect').setAttribute('y', by);
const texts = el.annotSMA.querySelectorAll('text');
texts[0].setAttribute('x', bx+10);
texts[0].setAttribute('y', by+15);
texts[1].setAttribute('x', bx+10);
texts[1].setAttribute('y', by+29);
}
}
/* 轨迹绘制 */
function drawTraces(pos){
// 采样轨迹点
traceMid.push({x:pos.p3.x, y:pos.p3.y});
traceDist.push({x:pos.p4.x, y:pos.p4.y});
const maxPts = 120;
if(traceMid.length > maxPts) traceMid.shift();
if(traceDist.length > maxPts) traceDist.shift();
if(traceMid.length > 2){
let d = `M${traceMid[0].x},${traceMid[0].y}`;
for(let i=1;i<traceMid.length;i++) d += ` L${traceMid[i].x},${traceMid[i].y}`;
el.traceMid.setAttribute('d', d);
}
if(traceDist.length > 2){
let d = `M${traceDist[0].x},${traceDist[0].y}`;
for(let i=1;i<traceDist.length;i++) d += ` L${traceDist[i].x},${traceDist[i].y}`;
el.traceDist.setAttribute('d', d);
}
}
/* ===== 参数面板更新 ===== */
function updateParams(a){
// 扭簧角位移(相对170°自由态的偏移)
const springDisp = Math.abs(a.mRel - OPEN_A.mRel);
$('valSpring').textContent = springDisp.toFixed(0);
// SMA温度
const smaTemp = 25 + a.smaHeat * 55;
$('valSMA').textContent = smaTemp.toFixed(0);
// 电流
const current = a.smaHeat > 0.2 ? 0.8 : 0;
$('valCurrent').textContent = current.toFixed(1);
// 中端骨折叠角(两骨间角度)
const midAng = 180 - a.mRel + OPEN_A.mRel;
$('valMidAng').textContent = Math.max(0, midAng).toFixed(0);
// 远端骨折叠角
const distAng = 180 - a.dRel;
$('valDistAng').textContent = Math.max(0, distAng).toFixed(0);
// 高亮卡片
const cardSpring = $('cardSpring');
const cardSMA = $('cardSMA');
cardSpring.className = 'param-card' + (a.springAct > 0.3 ? ' active' : '');
cardSMA.className = 'param-card' + (a.smaHeat > 0.3 ? ' active-sma' : '');
}
/* ===== 阶段指示器更新 ===== */
function updatePhases(){
const phases = document.querySelectorAll('.phase-item');
const lines = document.querySelectorAll('.phase-line');
let activePhase = 0;
if(progress < 0.05) activePhase = 0;
else if(progress < 0.15) activePhase = 1;
else if(progress < 0.50) activePhase = 2;
else if(progress < 0.90) activePhase = 3;
else activePhase = 4;
// 判断是否SMA阶段
const isSMA = progress >= 0.45 && progress < 0.90;
phases.forEach((el, i) => {
el.className = 'phase-item';
if(i < activePhase) el.classList.add('done');
if(i === activePhase){
el.classList.add('active');
if(isSMA && i === 3) el.classList.add('active-sma');
}
});
lines.forEach((el, i) => {
el.className = 'phase-line' + (i < activePhase ? ' lit' : '');
});
}
/* ===== 动画主循环 ===== */
let lastTime = 0;
function animate(time){
if(!lastTime) lastTime = time;
const dt = (time - lastTime) / 1000;
lastTime = time;
// 速度
const speed = $('sliderSpeed').value / 100;
$('speedLabel').textContent = speed.toFixed(1) + 'x';
if(autoPlay && !manualMode){
const dir = autoDir;
const step = dt * 0.25 * speed * dir;
progress += step;
if(progress >= 1){ progress = 1; autoDir = -1; }
if(progress <= 0){ progress = 0; autoDir = 1; }
$('sliderProgress').value = Math.round(progress * 1000);
} else if(!manualMode){
// 缓动到目标
const diff = target - progress;
progress += diff * Math.min(1, dt * 4 * speed);
if(Math.abs(diff) < 0.001) progress = target;
$('sliderProgress').value = Math.round(progress * 1000);
}
// 计算
const a = getAngles(progress);
const pos = computePos(a);
// 绘制
drawScene(a, pos);
updateParams(a);
updatePhases();
requestAnimationFrame(animate);
}
/* ===== 交互 ===== */
function startClose(){
autoPlay = false;
manualMode = false;
target = 1;
updateBtnState();
}
function startOpen(){
autoPlay = false;
manualMode = false;
target = 0;
updateBtnState();
}
function toggleAuto(){
autoPlay = !autoPlay;
manualMode = false;
if(autoPlay && progress >= 0.99) autoDir = -1;
if(autoPlay && progress <= 0.01) autoDir = 1;
updateBtnState();
}
function onSliderInput(slider){
manualMode = true;
autoPlay = false;
progress = slider.value / 1000;
target = progress;
// 清空轨迹(避免跳跃连线)
traceMid = [];
traceDist = [];
updateBtnState();
}
function updateBtnState(){
$('btnAuto').textContent = autoPlay ? '暂停演示' : '自动演示';
$('btnAuto').classList.toggle('primary', !autoPlay);
}
/* ===== 启动 ===== */
// 初始轨迹采样(展开态)
const initA = getAngles(0);
const initPos = computePos(initA);
for(let i=0;i<3;i++){
traceMid.push({x:initPos.p3.x, y:initPos.p3.y});
traceDist.push({x:initPos.p4.x, y:initPos.p4.y});
}
requestAnimationFrame(animate);
/* 键盘快捷键 */
document.addEventListener('keydown', e => {
if(e.key === ' ' || e.code === 'Space'){ e.preventDefault(); toggleAuto(); }
if(e.key === 'ArrowRight'){ startClose(); }
if(e.key === 'ArrowLeft'){ startOpen(); }
});
</script>
</body>
</html>
这个实现包含以下核心要素:
视觉设计
- 深蓝黑色技术蓝图风格背景,搭配精密网格
- 骨节采用青绿色(#00c8a0)主体 + 高亮描边双层绘制,折叠时亮度递增
- 双扭簧以螺旋线绘制,激活时发出绿色辉光(SVG filter glow)
- SMA 致动器以矩形色块呈现,加热时从暗棕渐变至橙红并辐射热光晕
- 骨端运动轨迹以虚线残影实时描绘,中端骨绿色、远端骨橙色
动画机理
- 进度变量
progress(0→1)驱动全部状态,通过分段映射实现:- 0~8%:滑套下行、锁定解除
- 8~45%:扭簧释能、中端骨自动下折(第一阶段)
- 45~55%:微动开关触发、SMA 通电升温
- 55~90%:SMA 相变致动、远端骨二次折叠(第二阶段)
- 90~100%:收拢就位、SMA 冷却
- 三次缓动函数使折叠运动符合物理加速-减速特征
交互控制
- 「开伞 / 收伞」按钮带缓动过渡
- 「自动演示」循环播放开合全过程
- 进度滑块支持手动逐帧拖拽
- 速度滑块调节 0.2x ~ 3.0x
- 键盘快捷键:空格=演示切换,←→=开收伞
信息层
- 左侧五阶段时间线指示器,实时高亮当前阶段(扭簧阶段绿色、SMA 阶段橙色)
- 右侧参数面板实时显示扭簧角位移、SMA 温度、致动电流、两段折叠角
- 机构上浮动标注框在关键瞬间出现,说明机理参数
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
