<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>套管式伞骨自动折叠原理 · TRIZ IFR</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=Fira+Code: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:#0c1422;--fg:#c0d0e0;--muted:#3e5570;--accent:#f0a820;--lock:#e84050;--unlock:#20d878;--motor:#f08030;--spring:#38a0e8;--border:#152535;--card:#0e1a2c}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Chakra Petch',sans-serif;min-height:100vh;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 25% 40%,rgba(240,168,32,.04) 0%,transparent 55%),radial-gradient(ellipse at 75% 70%,rgba(56,160,232,.03) 0%,transparent 50%);pointer-events:none;z-index:0}
.app{position:relative;z-index:1;max-width:1440px;margin:0 auto;padding:16px 20px;display:flex;flex-direction:column;height:100vh}
.hdr{text-align:center;padding:10px 0 6px;flex-shrink:0}
.hdr h1{font-size:1.65rem;font-weight:700;letter-spacing:.14em;color:var(--fg)}
.hdr h1 span{color:var(--accent)}
.hdr .sub{font-family:'Fira Code',monospace;font-size:.72rem;color:var(--muted);margin-top:4px;letter-spacing:.04em}
.ifr{display:inline-flex;align-items:center;gap:6px;background:rgba(240,168,32,.1);border:1px solid rgba(240,168,32,.28);border-radius:16px;padding:3px 12px;font-family:'Fira Code',monospace;font-size:.65rem;color:var(--accent);margin-top:6px}
.main{flex:1;display:flex;gap:16px;margin-top:10px;min-height:0}
.svg-wrap{flex:1;background:var(--panel);border:1px solid var(--border);border-radius:10px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
.svg-wrap svg{width:100%;height:100%}
.side{width:260px;display:flex;flex-direction:column;gap:10px;flex-shrink:0;overflow-y:auto}
.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 14px}
.card h3{font-size:.68rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.card h3 i{font-size:.62rem}
.mt{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.03)}
.mt:last-child{border-bottom:none}
.ml{font-family:'Fira Code',monospace;font-size:.68rem;color:var(--muted)}
.mv{font-family:'Fira Code',monospace;font-size:.76rem;font-weight:500;color:var(--fg)}
.mv.ac{color:var(--accent)}.mv.lk{color:var(--lock)}.mv.ul{color:var(--unlock)}.mv.mt2{color:var(--motor)}
.lk-row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;background:rgba(255,255,255,.015);margin-bottom:4px;transition:background .3s}
.lk-row.on{background:rgba(32,216,120,.07)}
.lk-dot{width:9px;height:9px;border-radius:50%;background:var(--lock);transition:all .3s;flex-shrink:0}
.lk-dot.open{background:var(--unlock);box-shadow:0 0 7px rgba(32,216,120,.45)}
.lk-txt{font-family:'Fira Code',monospace;font-size:.68rem;color:var(--fg)}
.lk-st{margin-left:auto;font-family:'Fira Code',monospace;font-size:.6rem;color:var(--muted)}
.pbar{width:100%;height:3px;background:rgba(255,255,255,.05);border-radius:2px;margin-top:8px;overflow:hidden}
.pfill{height:100%;background:var(--accent);border-radius:2px;width:0%;transition:width .08s}
.ctrl{display:flex;align-items:center;justify-content:center;gap:14px;padding:14px 0 6px;flex-wrap:wrap;flex-shrink:0}
.btn{font-family:'Chakra Petch',sans-serif;font-size:.8rem;font-weight:600;letter-spacing:.06em;padding:8px 20px;border:1px solid var(--border);border-radius:7px;background:var(--card);color:var(--fg);cursor:pointer;transition:all .18s;display:flex;align-items:center;gap:7px}
.btn:hover{border-color:var(--accent);background:rgba(240,168,32,.07)}
.btn:active{transform:scale(.97)}
.btn.pri{background:rgba(240,168,32,.12);border-color:var(--accent);color:var(--accent)}
.btn.pri:hover{background:rgba(240,168,32,.22)}
.btn:disabled{opacity:.35;cursor:not-allowed;pointer-events:none}
.spd{display:flex;align-items:center;gap:8px}
.spd label{font-family:'Fira Code',monospace;font-size:.68rem;color:var(--muted)}
input[type=range]{-webkit-appearance:none;width:100px;height:3px;background:rgba(255,255,255,.1);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--accent);cursor:pointer}
.tl{display:flex;align-items:center;gap:0;padding:0 8px}
.tl-d{width:10px;height:10px;border-radius:50%;border:2px solid var(--muted);background:transparent;transition:all .3s;position:relative;flex-shrink:0}
.tl-d.act{border-color:var(--accent);background:var(--accent);box-shadow:0 0 8px rgba(240,168,32,.4)}
.tl-d.done{border-color:var(--unlock);background:var(--unlock)}
.tl-l{width:28px;height:2px;background:var(--border);flex-shrink:0;transition:background .3s}
.tl-l.done{background:var(--unlock)}
.tl-lbl{position:absolute;top:14px;left:50%;transform:translateX(-50%);white-space:nowrap;font-family:'Fira Code',monospace;font-size:.52rem;color:var(--muted)}
@media(max-width:960px){.main{flex-direction:column}.side{width:100%;flex-direction:row;flex-wrap:wrap}.card{flex:1;min-width:180px}}
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
.hdr,.main,.ctrl{animation:fadeUp .6s ease-out both}
.main{animation-delay:.1s}.ctrl{animation-delay:.2s}
</style>
</head>
<body>
<div class="app">
<header class="hdr">
<h1>套管式伞骨<span>自动折叠</span>原理</h1>
<div class="sub">TRIZ 最终理想解 (IFR) 动态演示 · 三节嵌套薄壁钛合金套管</div>
<div class="ifr"><i class="fas fa-bolt"></i> IFR: 零增外部复杂度 · 内力自驱动坍缩 · 极小收纳体积</div>
</header>
<div class="main">
<div class="svg-wrap">
<svg id="svg" viewBox="0 0 960 720" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<aside class="side">
<div class="card">
<h3><i class="fas fa-play-circle"></i> 运行状态</h3>
<div class="mt"><span class="ml">阶段</span><span class="mv ac" id="uPhase">待机</span></div>
<div class="mt"><span class="ml">进度</span><span class="mv ac" id="uProg">0%</span></div>
<div class="pbar"><div class="pfill" id="uFill"></div></div>
</div>
<div class="card">
<h3><i class="fas fa-bolt"></i> 拉线参数</h3>
<div class="mt"><span class="ml">张力</span><span class="mv ac" id="uTen">0.0 N</span></div>
<div class="mt"><span class="ml">电机扭矩</span><span class="mv mt2" id="uTrq">0.00 N·m</span></div>
<div class="mt"><span class="ml">收线速度</span><span class="mv" id="uSpd">0 mm/s</span></div>
</div>
<div class="card">
<h3><i class="fas fa-lock"></i> 锁定节点</h3>
<div class="lk-row" id="lr1"><div class="lk-dot" id="ld1"></div><span class="lk-txt">内管→中管</span><span class="lk-st" id="ls1">锁定</span></div>
<div class="lk-row" id="lr2"><div class="lk-dot" id="ld2"></div><span class="lk-txt">中管→外管</span><span class="lk-st" id="ls2">锁定</span></div>
<div class="lk-row" id="lr3"><div class="lk-dot" id="ld3"></div><span class="lk-txt">末端卡扣</span><span class="lk-st" id="ls3">释放</span></div>
</div>
<div class="card">
<h3><i class="fas fa-microchip"></i> 关键参数</h3>
<div class="mt"><span class="ml">套管间隙</span><span class="mv">0.15 mm</span></div>
<div class="mt"><span class="ml">破断拉力</span><span class="mv ac">80 N</span></div>
<div class="mt"><span class="ml">电机扭矩</span><span class="mv mt2">0.5 N·m</span></div>
<div class="mt"><span class="ml">收纳总长</span><span class="mv">18 cm</span></div>
</div>
</aside>
</div>
<div class="ctrl">
<button class="btn pri" id="bRet" onclick="doRetract()"><i class="fas fa-compress-alt"></i> 收伞折叠</button>
<button class="btn" id="bDep" onclick="doDeploy()"><i class="fas fa-expand-alt"></i> 开伞伸展</button>
<button class="btn" onclick="doReset()"><i class="fas fa-undo"></i> 复位</button>
<div class="spd"><label>速度</label><input type="range" id="spdR" min="0.2" max="3" step="0.1" value="1" oninput="speed=+this.value;document.getElementById('spdV').textContent=speed.toFixed(1)+'x'"><label id="spdV">1.0x</label></div>
<div class="tl" id="timeline"></div>
</div>
</div>
<script>
/* ============ 常量配置 ============ */
const NS='http://www.w3.org/2000/svg';
const CX=310; // 机构中心X
const EPS=0.001;
/* 管体尺寸 */
const OUTER={x:CX-38,w:76,top:190,bot:660,wallW:8,fill:'#5a6a7a',stroke:'#7a8a9a',label:'外管'};
const MIDDLE={x:CX-29,w:58,topExt:58,botExt:620,wallW:6,fill:'#6a7a8a',stroke:'#8a9aaa',label:'中管',maxOff:155};
const INNER={x:CX-21,w:42,topExt:16,botExt:580,wallW:5,fill:'#7a8a9a',stroke:'#9aaaba',label:'内管',maxOff:56};
/* 动画相位映射 (progress 0=展开 1=收回) */
function innerT(p){if(p<.15)return 0;if(p>.48)return 1;return(p-.15)/.33}
function middleT(p){if(p<.54)return 0;if(p>.84)return 1;return(p-.54)/.30}
function motorOn(p){return p>.02&&p<.94}
function lock1Open(p){return p>.12} // 内管锁释放
function lock2Open(p){return p>.52} // 中管锁释放
function finalLock(p){return p>.88} // 末端卡扣锁死
function cableTension(p){
if(p<.04)return 0;if(p<.14)return(p-.04)/.10*42;
if(p<.48)return 42+innerT(p)*16;if(p<.54)return 58;
if(p<.84)return 58+middleT(p)*12;if(p<.90)return 70;
return 70*(1-clamp01((p-.90)/.10))}
function clamp01(v){return Math.max(0,Math.min(1,v))}
function lerp(a,b,t){return a+(b-a)*t}
function easeIO(t){t=clamp01(t);return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
/* ============ 状态 ============ */
let progress=0, dir=0, speed=1, lastT=0, spoolAngle=0;
/* ============ SVG辅助 ============ */
function el(tag,attrs,parent){
const e=document.createElementNS(NS,tag);
for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);
if(parent)parent.appendChild(e);return e}
function grp(attrs,parent){return el('g',attrs,parent)}
/* ============ 构建场景 ============ */
const svg=document.getElementById('svg');
let middleGrp,innerGrp,cablePath,canopyPath,spring1Path,spring2Path;
let lock1Circles,lock2Circles,spoolGrp,detailGrp;
let measureLine,measureText,ifrCallout;
let annoElements=[];
function buildScene(){
/* -- defs -- */
const defs=el('defs',null,svg);
// 金属渐变
[['mgO',OUTER.fill,OUTER.stroke],['mgM',MIDDLE.fill,MIDDLE.stroke],['mgI',INNER.fill,INNER.stroke]].forEach(([id,c1,c2])=>{
const g=el('linearGradient',{id,x1:'0',y1:'0',x2:'1',y2:'0'},defs);
el('stop',{offset:'0%','stop-color':c1},g);el('stop',{offset:'35%','stop-color':c2},g);
el('stop',{offset:'65%','stop-color':c2},g);el('stop',{offset:'100%','stop-color':c1},g)});
// 拉线发光
const gf=el('filter',{id:'cableGlow',x:'-50%',y:'-10%',width:'200%',height:'120%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'5',result:'b'},gf);
const gm=el('feMerge',null,gf);el('feMergeNode',{in:'b'},gm);el('feMergeNode',{in:'SourceGraphic'},gm);
// 阴影
const sf=el('filter',{id:'shadow',x:'-20%',y:'-5%',width:'140%',height:'115%'},defs);
el('feDropShadow',{dx:'0',dy:'3',stdDeviation:'4','flood-color':'rgba(0,0,0,0.35)'},sf);
// 网格图案
const pat=el('pattern',{id:'grid',width:30,height:30,patternUnits:'userSpaceOnUse'},defs);
el('path',{d:'M 30 0 L 0 0 0 30',fill:'none',stroke:'rgba(100,140,180,0.04)','stroke-width':'0.5'},pat);
/* -- 背景网格 -- */
el('rect',{x:0,y:0,width:960,height:720,fill:'url(#grid)'},svg);
/* -- 中心线 -- */
el('line',{x1:CX,y1:10,x2:CX,y2:700,stroke:'rgba(100,140,180,0.08)','stroke-width':'1','stroke-dasharray':'6,4'},svg);
/* -- 手柄 -- */
const hGrp=grp({},svg);
el('rect',{x:CX-32,y:660,width:64,height:55,rx:8,fill:'#2a3545',stroke:'#4a5a6a','stroke-width':'1.5',filter:'url(#shadow)'},hGrp);
el('rect',{x:CX-22,y:668,width:44,height:22,rx:4,fill:'#1a2535',stroke:'#3a4a5a','stroke-width':'1'},hGrp);
// 电机符号
const mCirc=el('circle',{cx:CX,cy:679,r:8,fill:'none',stroke:'#4a5a6a','stroke-width':'1'},hGrp);
el('text',{x:CX,y:683,fill:'#5a6a7a','font-size':'9','text-anchor':'middle','font-family':'Fira Code'},mCirc.parentNode).textContent='M';
spoolGrp=grp({},hGrp);
el('circle',{cx:CX,cy:653,r:12,fill:'#1a2535',stroke:'#3a4a5a','stroke-width':'1.5'},spoolGrp);
el('circle',{cx:CX,cy:653,r:5,fill:'#2a3545',stroke:OUTER.stroke,'stroke-width':'1'},spoolGrp);
// 拉线出发点标记
el('circle',{cx:CX,cy:653,r:2.5,fill:'var(--accent)',opacity:.7},spoolGrp);
// 手柄标注
addAnno(CX+42,688,'手柄 / 电机','right');
/* -- 外管 (静态) -- */
const oGrp=grp({},svg);
// 管壁
el('rect',{x:OUTER.x,y:OUTER.top,width:OUTER.wallW,height:OUTER.bot-OUTER.top,rx:2,fill:'url(#mgO)',opacity:.85},oGrp);
el('rect',{x:OUTER.x+OUTER.w-OUTER.wallW,y:OUTER.top,width:OUTER.wallW,height:OUTER.bot-OUTER.top,rx:2,fill:'url(#mgO)',opacity:.85},oGrp);
// 顶部封口
el('line',{x1:OUTER.x,y1:OUTER.top,x2:OUTER.x+OUTER.w,y2:OUTER.top,stroke:OUTER.stroke,'stroke-width':'2'},oGrp);
// 内腔半透明
el('rect',{x:OUTER.x+OUTER.wallW,y:OUTER.top,width:OUTER.w-OUTER.wallW*2,height:OUTER.bot-OUTER.top,fill:'rgba(15,25,40,0.5)'},oGrp);
// 外管标注
addAnno(OUTER.x-8,420,'外管','left');
/* -- 中管组 (动态平移) -- */
middleGrp=grp({transform:'translate(0,0)'},svg);
drawTube(middleGrp,MIDDLE,'url(#mgM)');
// 锁定钢珠 - 中管/外管交界
lock2Circles=drawLockPair(middleGrp,OUTER.top-3,OUTER.x+OUTER.wallW,MIDDLE.x,CX);
// 中管标注
addAnno(MIDDLE.x-8,340,'中管','left');
/* -- 内管组 (嵌套在中管组内, 动态平移) -- */
innerGrp=grp({transform:'translate(0,0)'},middleGrp);
drawTube(innerGrp,INNER,'url(#mgI)');
// 锁定钢珠 - 内管/中管交界
lock1Circles=drawLockPair(innerGrp,MIDDLE.topExt+2,MIDDLE.x+MIDDLE.wallW,INNER.x,CX);
// 内管标注
addAnno(INNER.x-8,300,'内管','left');
/* -- 伞尖 -- */
const tipGrp=grp({},innerGrp);
el('circle',{cx:CX,cy:INNER.topExt-2,r:5,fill:'#9aaaba',stroke:'#b0c0d0','stroke-width':'1.5'},tipGrp);
el('line',{x1:CX,y1:INNER.topExt-7,x2:CX,y2:INNER.topExt-18,stroke:'#b0c0d0','stroke-width':'2','stroke-linecap':'round'},tipGrp);
/* -- 拉线 -- */
cablePath=el('path',{d:'',fill:'none',stroke:'var(--accent)','stroke-width':'3','stroke-linecap':'round',filter:'url(#cableGlow)'},svg);
/* -- 弹簧 -- */
spring1Path=el('path',{d:'',fill:'none',stroke:'var(--spring)','stroke-width':'1.8','stroke-linecap':'round',opacity:.7},svg);
spring2Path=el('path',{d:'',fill:'none',stroke:'var(--spring)','stroke-width':'1.8','stroke-linecap':'round',opacity:.7},svg);
/* -- 伞面 -- */
canopyPath=el('path',{d:'',fill:'rgba(80,120,170,0.12)',stroke:'rgba(120,160,210,0.4)','stroke-width':'1.5'},svg);
/* -- 测量线 -- */
measureLine=grp({opacity:0},svg);
el('line',{x1:OUTER.x+OUTER.w+20,y1:OUTER.top,x2:OUTER.x+OUTER.w+20,y2:OUTER.bot,stroke:'var(--accent)','stroke-width':'1','stroke-dasharray':'3,2'},measureLine);
el('line',{x1:OUTER.x+OUTER.w+15,y1:OUTER.top,x2:OUTER.x+OUTER.w+25,y2:OUTER.top,stroke:'var(--accent)','stroke-width':'1'},measureLine);
el('line',{x1:OUTER.x+OUTER.w+15,y1:OUTER.bot,x2:OUTER.x+OUTER.w+25,y2:OUTER.bot,stroke:'var(--accent)','stroke-width':'1'},measureLine);
measureText=el('text',{x:OUTER.x+OUTER.w+28,y:(OUTER.top+OUTER.bot)/2+4,fill:'var(--accent)','font-size':'11','font-family':'Fira Code'},measureLine);
measureText.textContent='47 cm';
/* -- IFR呼出标注 -- */
ifrCallout=grp({opacity:0},svg);
el('rect',{x:540,y:530,width:380,height:110,rx:8,fill:'rgba(240,168,32,0.06)',stroke:'rgba(240,168,32,0.3)','stroke-width':'1'},ifrCallout);
const ifrT1=el('text',{x:555,y:558,fill:'var(--accent)','font-size':'13','font-weight':'700','font-family':'Chakra Petch','letter-spacing':'0.08em'},ifrCallout);
ifrT1.textContent='IFR 最终理想解实现';
const ifrT2=el('text',{x:555,y:580,fill:'var(--fg)','font-size':'11','font-family':'Fira Code','opacity':.8},ifrCallout);
ifrT2.textContent='系统以 0.5N·m 微型电机 + 内置拉线';
const ifrT3=el('text',{x:555,y:598,fill:'var(--fg)','font-size':'11','font-family':'Fira Code','opacity':.8},ifrCallout);
ifrT3.textContent='替代传统复杂外置驱动臂,实现';
const ifrT4=el('text',{x:555,y:616,fill:'var(--accent)','font-size':'12','font-weight':'600','font-family':'Chakra Petch'},ifrCallout);
ifrT4.textContent='自驱动坍缩 → 18 cm 极小收纳';
const ifrT5=el('text',{x:555,y:634,fill:'var(--muted)','font-size':'10','font-family':'Fira Code'},ifrCallout);
ifrT5.textContent='资源巧用: 拉线锥面 → 逐级解锁 → 重力辅助';
/* -- 锁定机构详图 (右侧插图) -- */
detailGrp=grp({},svg);
el('rect',{x:570,y:60,width:360,height:260,rx:6,fill:'rgba(10,18,30,0.92)',stroke:'var(--border)','stroke-width':'1'},detailGrp);
el('text',{x:590,y:85,fill:'var(--accent)','font-size':'11','font-weight':'600','font-family':'Chakra Petch','letter-spacing':'0.06em'},detailGrp).textContent='锁定机构径向剖视';
// 外管壁 (大圆环)
el('circle',{cx:740,cy:200,r:72,fill:'none',stroke:OUTER.stroke,'stroke-width':'10',opacity:.6},detailGrp);
// 中管壁 (小圆环)
el('circle',{cx:740,cy:200,r:50,fill:'none',stroke:MIDDLE.stroke,'stroke-width':'8',opacity:.6},detailGrp);
// 拉线 (中心)
el('circle',{cx:740,cy:200,r:5,fill:'var(--accent)',opacity:.8},detailGrp);
el('text',{x:740,y:220,fill:'var(--accent)','font-size':'9','text-anchor':'middle','font-family':'Fira Code'},detailGrp).textContent='拉线';
// 锥面示意 (梯形)
const coneGrp=grp({},detailGrp);
el('path',{d:'M730,180 L735,210 L745,210 L750,180 Z',fill:'rgba(240,168,32,0.2)',stroke:'var(--accent)','stroke-width':'1'},coneGrp);
el('text',{x:760,y:198,fill:'var(--accent)','font-size':'8','font-family':'Fira Code'},coneGrp).textContent='锥面';
// 钢珠 (4个, 均匀分布)
const ballAngles=[0,90,180,270];
const detailBalls=[];
ballAngles.forEach(a=>{
const rad=a*Math.PI/180;
const bx=740+Math.cos(rad)*61,by=200+Math.sin(rad)*61;
const bc=el('circle',{cx:bx,cy:by,r:6,fill:'var(--lock)',stroke:'#fff','stroke-width':'0.5'},detailGrp);
detailBalls.push(bc);
// 锁槽
const gx=740+Math.cos(rad)*72,gy=200+Math.sin(rad)*72;
el('rect',{x:gx-4,y:gy-4,width:8,height:8,rx:1,fill:'none',stroke:'rgba(255,255,255,0.2)','stroke-width':'0.8',transform:`rotate(${a},${gx},${gy})`},detailGrp);
});
detailGrp._balls=detailBalls;
// 标注
el('text',{x:820,y:140,fill:OUTER.stroke,'font-size':'9','font-family':'Fira Code'},detailGrp).textContent='← 外管壁';
el('text',{x:800,y:170,fill:MIDDLE.stroke,'font-size':'9','font-family':'Fira Code'},detailGrp).textContent='← 中管壁';
// 状态文本
detailGrp._stateText=el('text',{x:590,y:300,fill:'var(--fg)','font-size':'10','font-family':'Fira Code','opacity':.7},detailGrp);
detailGrp._stateText.textContent='状态: 钢珠嵌入锁槽 → 锁定';
/* -- 引线 (从主图到详图) -- */
el('path',{d:`M ${OUTER.x+OUTER.w} ${OUTER.top-5} C ${OUTER.x+OUTER.w+40} ${OUTER.top-30} 540 100 570 130`,fill:'none',stroke:'rgba(240,168,32,0.2)','stroke-width':'1','stroke-dasharray':'4,3'},svg);
/* -- 阶段时间线 (底部SVG内) -- */
const phases=['待机','电机收线','内管缩回','中管缩回','锁定完成'];
const tlY=705;
for(let i=0;i<5;i++){
const px=180+i*150;
el('circle',{cx:px,cy:tlY,r:4,fill:i===0?'var(--accent)':'var(--muted)',id:'tl'+i},svg);
el('text',{x:px,y:tlY+14,fill:'var(--muted)','font-size':'8','text-anchor':'middle','font-family':'Fira Code'},svg).textContent=phases[i];
if(i<4)el('line',{x1:px+6,y1:tlY,x2:px+144,y2:tlY,stroke:'var(--border)','stroke-width':'1.5'},svg);
}
render();
}
/* 绘制管体 */
function drawTube(parent,cfg,fillId){
const h=cfg.botExt-cfg.topExt;
el('rect',{x:cfg.x,y:cfg.topExt,width:cfg.wallW,height:h,rx:1.5,fill:fillId,opacity:.8},parent);
el('rect',{x:cfg.x+cfg.w-cfg.wallW,y:cfg.topExt,width:cfg.wallW,height:h,rx:1.5,fill:fillId,opacity:.8},parent);
el('line',{x1:cfg.x,y1:cfg.topExt,x2:cfg.x+cfg.w,y2:cfg.topExt,stroke:cfg.stroke,'stroke-width':'1.5'},parent);
el('rect',{x:cfg.x+cfg.wallW,y:cfg.topExt,width:cfg.w-cfg.wallW*2,height:h,fill:'rgba(12,20,34,0.45)'},parent);
}
/* 绘制锁定钢珠对 */
function drawLockPair(parent,y,leftWallX,innerLeftX,centerX){
const lx=leftWallX+3,rx=centerX*2-leftWallX-3;
const lc=el('circle',{cx:lx,cy:y,r:4.5,fill:'var(--lock)',stroke:'rgba(255,255,255,0.3)','stroke-width':'0.8'},parent);
const rc=el('circle',{cx:rx,cy:y,r:4.5,fill:'var(--lock)',stroke:'rgba(255,255,255,0.3)','stroke-width':'0.8'},parent);
return[lc,rc];
}
/* 标注 */
function addAnno(x,y,text,side){
const t=el('text',{x,y,fill:'var(--muted)','font-size':'10','font-family':'Fira Code','text-anchor':side==='right'?'start':'end',opacity:.6},svg);
t.textContent=text;annoElements.push(t);
}
/* 弹簧路径 */
function springD(x,y1,y2,coils,amp){
if(Math.abs(y2-y1)<5)return`M${x} ${y1}L${x} ${y2}`;
const dy=(y2-y1)/(coils*2);let d=`M${x} ${y1}`;
for(let i=0;i<coils*2;i++){d+=` L${x+(i%2===0?amp:-amp)} ${y1+dy*(i+1)}`}
d+=` L${x} ${y2}`;return d;
}
/* ============ 渲染 ============ */
function render(){
const p=progress;
const iT=easeIO(innerT(p));
const mT=easeIO(middleT(p));
const mOff=mT*MIDDLE.maxOff;
const iOff=iT*INNER.maxOff;
// 中管组平移
middleGrp.setAttribute('transform',`translate(0,${mOff})`);
// 内管组平移 (相对中管)
innerGrp.setAttribute('transform',`translate(0,${iOff})`);
// 拉线路径
const cableTopY=INNER.topExt+iOff+mOff;
const cableBotY=653;
cablePath.setAttribute('d',`M${CX} ${cableBotY} L${CX} ${Math.min(cableTopY,cableBotY)}`);
const ten=cableTension(p);
cablePath.setAttribute('stroke-width',2.5+ten/70*2);
cablePath.setAttribute('opacity',.5+ten/70*.5);
// 伞面
const tipY=cableTopY-18;
const droop=lerp(0,280,easeIO(Math.max(0,(p-.6)/.4)));
const canopyW=lerp(170,35,easeIO(p));
const canopyH=lerp(-40,30,easeIO(p));
canopyPath.setAttribute('d',
`M${CX-canopyW} ${tipY+15} Q${CX-canopyW*.6} ${tipY+canopyH} ${CX} ${tipY+canopyH-5} Q${CX+canopyW*.6} ${tipY+canopyH} ${CX+canopyW} ${tipY+15}`);
// 弹簧1: 内管底部与中管之间
const s1Top=INNER.botExt+iOff-10;
const s1Bot=MIDDLE.botExt-10;
spring1Path.setAttribute('d',springD(CX,Math.min(s1Top,s1Bot-5),s1Bot,5,7));
// 弹簧2: 中管底部与外管/手柄之间
const s2Top=MIDDLE.botExt+mOff-10;
const s2Bot=OUTER.bot-10;
spring2Path.setAttribute('d',springD(CX,Math.min(s2Top,s2Bot-5),s2Bot,6,8));
// 锁定钢珠颜色
const l1open=lock1Open(p),l2open=lock2Open(p),fLock=finalLock(p);
lock1Circles.forEach(c=>{c.setAttribute('fill',l1open?'var(--unlock)':'var(--lock)');c.setAttribute('r',l1open?3.5:4.5)});
lock2Circles.forEach(c=>{c.setAttribute('fill',l2open?'var(--unlock)':'var(--lock)');c.setAttribute('r',l2open?3.5:4.5)});
// 收线轮旋转
if(motorOn(p)){spoolAngle+=speed*3}
spoolGrp.setAttribute('transform',`rotate(${spoolAngle},${CX},653)`);
// 详图钢珠
const dBalls=detailGrp._balls;
const anyUnlocked=l1open||l2open;
dBalls.forEach(b=>{
b.setAttribute('fill',anyUnlocked?'var(--unlock)':'var(--lock)');
b.setAttribute('r',anyUnlocked?4.5:6)});
detailGrp._stateText.textContent=anyUnlocked?
(l1open&&l2open?'状态: 双级解锁 → 坍缩中':'状态: 首级解锁 → 内管缩回'):
'状态: 钢珠嵌入锁槽 → 锁定';
// 测量线
const totalLen=lerp(47,18,easeIO(p));
const mTop=lerp(OUTER.top,OUTER.top,mT);
measureText.textContent=totalLen.toFixed(0)+' cm';
measureLine.setAttribute('opacity',p>.3?.7:0);
// IFR呼出
ifrCallout.setAttribute('opacity',p>.92?1:0);
ifrCallout.style.transition='opacity 0.5s';
// 时间线点
const phaseIdx=p<.02?0:p<.15?1:p<.50?2:p<.85?3:4;
for(let i=0;i<5;i++){
const dot=document.getElementById('tl'+i);
if(!dot)continue;
dot.setAttribute('fill',i<phaseIdx?'var(--unlock)':i===phaseIdx?'var(--accent)':'var(--muted)');
dot.setAttribute('r',i===phaseIdx?5:4);
}
}
/* ============ UI更新 ============ */
function updateUI(){
const p=progress;
const ten=cableTension(p);
const phName=p<.02?'待机':p<.15?'电机收线':p<.50?'内管缩回':p<.85?'中管缩回':'锁定完成';
document.getElementById('uPhase').textContent=phName;
document.getElementById('uProg').textContent=(p*100).toFixed(0)+'%';
document.getElementById('uFill').style.width=(p*100)+'%';
document.getElementById('uTen').textContent=ten.toFixed(1)+' N';
document.getElementById('uTrq').textContent=(motorOn(p)?0.5*p:0).toFixed(2)+' N·m';
document.getElementById('uSpd').textContent=motorOn(p)?(speed*120).toFixed(0)+' mm/s':'0 mm/s';
const l1=lock1Open(p),l2=lock2Open(p),fl=finalLock(p);
document.getElementById('ld1').className='lk-dot'+(l1?' open':'');
document.getElementById('ls1').textContent=l1?'释放':'锁定';
document.getElementById('lr1').className='lk-row'+(l1?' on':'');
document.getElementById('ld2').className='lk-dot'+(l2?' open':'');
document.getElementById('ls2').textContent=l2?'释放':'锁定';
document.getElementById('lr2').className='lk-row'+(l2?' on':'');
document.getElementById('ld3').className='lk-dot'+(fl?' open':'');
document.getElementById('ls3').textContent=fl?'锁死':'释放';
document.getElementById('lr3').className='lk-row'+(fl?' on':'');
document.getElementById('bRet').disabled=dir===1||p>=1;
document.getElementById('bDep').disabled=dir===-1||p<=0;
}
/* ============ 动画循环 ============ */
function animate(t){
if(dir!==0){
const dt=Math.min((t-lastT)/1000,0.05);
progress+=dir*speed*0.28*dt;
progress=Math.max(0,Math.min(1,progress));
if(progress<=0||progress>=1){dir=0;progress=Math.max(0,Math.min(1,progress))}
render();updateUI();
}
lastT=t;requestAnimationFrame(animate);
}
/* ============ 控制 ============ */
function doRetract(){if(progress<1){dir=1}}
function doDeploy(){if(progress>0){dir=-1}}
function doReset(){dir=0;progress=0;spoolAngle=0;render();updateUI()}
/* ============ 初始化 ============ */
buildScene();updateUI();
requestAnimationFrame(t=>{lastT=t;requestAnimationFrame(animate)});
/* 滑块控制 */
const spdSlider=document.getElementById('spdR');
spdSlider.addEventListener('input',()=>{speed=+spdSlider.value;document.getElementById('spdV').textContent=speed.toFixed(1)+'x'});
</script>
</body>
</html>
实现说明
整体架构:单文件 HTML,SVG 承载全部动画图形,JavaScript 驱动动画循环与交互,右侧数据面板实时同步状态。
核心动画机制:
- 进度变量
progress从 0(完全展开)到 1(完全收回),通过requestAnimationFrame持续推进 - 三个关键映射函数
innerT()、middleT()将线性进度映射为各管体的分段缩回时序,配合easeIO缓动,实现"内管先缩 → 中管后缩"的逐级坍缩效果 - 中管组和内管组采用嵌套 SVG
<g>变换,内管的translate叠加在中管的translate之上,物理逻辑自洽
IFR 视觉引导:
- 拉线为视觉主角:琥珀金色 + SVG 辉光滤镜,张力越大线越粗越亮
- 锁定钢珠红/绿颜色切换标记解锁瞬间,详图插图同步展示径向剖视
- 收纳完成时自动弹出 IFR 呼出标注,强调"0.5N·m 微型电机替代复杂外置驱动臂"的核心矛盾破除
交互:收伞/开伞按钮、速度滑块、复位按钮,所有控件均连接真实动画逻辑;右侧面板实时显示张力、扭矩、锁定状态等参数。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
