这是一个极具创新性的 TRIZ IFR 方案——用气动场替代刚性铰链结构,让伞骨在充气时刚化挺直、泄压时柔性坍缩。我将用高保真 SVG 动画完整呈现这一原理。
<!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@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
background:#060A12;
color:#C8D6E5;
font-family:'Share Tech Mono',monospace;
min-height:100vh;
display:flex;flex-direction:column;align-items:center;
overflow-x:hidden;
position:relative;
}
body::before{
content:'';position:fixed;inset:0;
background-image:
linear-gradient(rgba(0,229,255,0.025) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,229,255,0.025) 1px,transparent 1px);
background-size:50px 50px;
pointer-events:none;z-index:0;
}
body::after{
content:'';position:fixed;inset:0;
background:radial-gradient(ellipse at 40% 30%,rgba(0,229,255,0.04) 0%,transparent 60%),
radial-gradient(ellipse at 80% 70%,rgba(255,109,0,0.03) 0%,transparent 50%);
pointer-events:none;z-index:0;
}
.container{position:relative;z-index:1;width:100%;max-width:1400px;padding:16px 20px}
header{text-align:center;padding:18px 0 10px}
header h1{
font-family:'Rajdhani',sans-serif;font-weight:700;font-size:2.2rem;
letter-spacing:6px;color:#00E5FF;
text-shadow:0 0 40px rgba(0,229,255,0.25),0 0 80px rgba(0,229,255,0.1);
}
header .sub{
font-size:0.78rem;color:#3E5060;letter-spacing:3px;margin-top:4px;
font-family:'Share Tech Mono',monospace;
}
.svg-wrap{display:flex;justify-content:center;width:100%}
.svg-wrap svg{width:100%;max-width:1300px;height:auto}
.controls{
display:flex;justify-content:center;align-items:center;gap:18px;
padding:14px 0 8px;flex-wrap:wrap;
}
.btn{
font-family:'Rajdhani',sans-serif;font-weight:700;font-size:0.95rem;
letter-spacing:3px;padding:12px 30px;border:2px solid;
border-radius:3px;cursor:pointer;transition:all .3s;
text-transform:uppercase;background:transparent;position:relative;
}
.btn-inflate{color:#00E5FF;border-color:#00E5FF}
.btn-inflate:hover{background:rgba(0,229,255,0.12);box-shadow:0 0 30px rgba(0,229,255,0.25)}
.btn-inflate.active{background:rgba(0,229,255,0.18);box-shadow:0 0 50px rgba(0,229,255,0.35)}
.btn-deflate{color:#FF6D00;border-color:#FF6D00}
.btn-deflate:hover{background:rgba(255,109,0,0.12);box-shadow:0 0 30px rgba(255,109,0,0.25)}
.btn-deflate.active{background:rgba(255,109,0,0.18);box-shadow:0 0 50px rgba(255,109,0,0.35)}
.slider-grp{display:flex;align-items:center;gap:10px}
.slider-grp label{font-size:0.7rem;color:#3E5060;letter-spacing:1px;white-space:nowrap}
.slider-grp input[type=range]{
-webkit-appearance:none;width:180px;height:3px;
background:linear-gradient(90deg,#FF6D00,#00E5FF);border-radius:2px;outline:none;
}
.slider-grp input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;border-radius:50%;
background:#00E5FF;cursor:pointer;box-shadow:0 0 10px rgba(0,229,255,0.5);
}
.pressure-val{
font-family:'Rajdhani',sans-serif;font-size:1.15rem;font-weight:700;
color:#00E5FF;min-width:95px;text-align:right;
}
.timeline{
display:flex;justify-content:center;align-items:center;gap:0;
padding:10px 0 6px;flex-wrap:wrap;
}
.tl-step{
font-size:0.68rem;letter-spacing:1.5px;padding:5px 14px;
color:#2A3545;border:1px solid #1A2535;border-radius:2px;
transition:all .4s;white-space:nowrap;
}
.tl-step.active{color:#00E5FF;border-color:rgba(0,229,255,0.4);background:rgba(0,229,255,0.08)}
.tl-step.active-warn{color:#FF6D00;border-color:rgba(255,109,0,0.4);background:rgba(255,109,0,0.08)}
.tl-arrow{color:#1A2535;font-size:0.7rem;padding:0 6px}
.status-bar{
text-align:center;padding:6px;font-size:0.72rem;letter-spacing:2px;color:#2A3545;
}
.status-bar .hl{color:#00E5FF}
.status-bar .wr{color:#FF6D00}
@media(max-width:768px){
header h1{font-size:1.4rem;letter-spacing:3px}
.controls{gap:10px}
.btn{padding:10px 18px;font-size:0.8rem}
.slider-grp input[type=range]{width:120px}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>PNEUMATIC FLEX-RIB</h1>
<div class="sub">气动柔性伞骨 · TRIZ 最终理想解 (IFR) 原理演示</div>
</header>
<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="gCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gCyanS" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gOrange" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gStrong" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="10" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<radialGradient id="canopyG" cx="50%" cy="20%" r="70%">
<stop offset="0%" stop-color="rgba(0,229,255,0.18)"/>
<stop offset="100%" stop-color="rgba(0,229,255,0.03)"/>
</radialGradient>
<radialGradient id="canopyG2" cx="50%" cy="20%" r="70%">
<stop offset="0%" stop-color="rgba(0,229,255,0.08)"/>
<stop offset="100%" stop-color="rgba(0,229,255,0.01)"/>
</radialGradient>
<linearGradient id="shaftG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3E5060"/><stop offset="100%" stop-color="#1A2535"/>
</linearGradient>
<pattern id="kevlarP" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="6" y2="0" stroke="#FF6D00" stroke-width="0.6" opacity="0.5"/>
<line x1="0" y1="3" x2="6" y2="3" stroke="#FF6D00" stroke-width="0.4" opacity="0.3"/>
</pattern>
<pattern id="siliconeP" x="0" y="0" width="4" height="4" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="0.5" fill="#8B9AAF" opacity="0.15"/>
</pattern>
</defs>
<!-- Background decorative arcs -->
<circle cx="420" cy="340" r="320" fill="none" stroke="rgba(0,229,255,0.02)" stroke-width="1"/>
<circle cx="420" cy="340" r="220" fill="none" stroke="rgba(0,229,255,0.015)" stroke-width="1"/>
<!-- ===== UMBRELLA GROUP ===== -->
<g id="umbrellaG">
<!-- Canopy fill -->
<path id="canopyFill" d="" fill="url(#canopyG)" stroke="none"/>
<!-- Canopy outline -->
<path id="canopyOutline" d="" fill="none" stroke="rgba(0,229,255,0.25)" stroke-width="1.2"/>
<!-- Canopy inner ribs (thin lines) -->
<g id="canopyInnerRibs"></g>
<!-- Main ribs - Kevlar layer (wider, orange dashed) -->
<g id="kevlarRibs"></g>
<!-- Main ribs - Silicone core -->
<g id="mainRibs"></g>
<!-- Central shaft -->
<rect id="shaft" x="415" y="195" width="10" height="460" rx="5" fill="url(#shaftG)"/>
<!-- Shaft air channel -->
<rect id="airChannel" x="418" y="200" width="4" height="450" rx="2" fill="rgba(0,229,255,0.05)"/>
<!-- Hub -->
<circle cx="420" cy="195" r="14" fill="#0D1420" stroke="#5C6B7A" stroke-width="2"/>
<circle cx="420" cy="195" r="7" fill="rgba(0,229,255,0.15)" stroke="rgba(0,229,255,0.4)" stroke-width="1"/>
<circle id="hubCore" cx="420" cy="195" r="3" fill="#00E5FF" opacity="0.3"/>
<!-- Slider -->
<g id="sliderG">
<rect id="sliderBody" x="406" y="400" width="28" height="26" rx="5" fill="#0D1420" stroke="#5C6B7A" stroke-width="1.5"/>
<circle id="sliderDot" cx="420" cy="413" r="4" fill="rgba(0,229,255,0.3)"/>
</g>
<!-- Handle -->
<g id="handleG">
<rect x="403" y="645" width="34" height="65" rx="10" fill="#0D1420" stroke="#2A3545" stroke-width="1.5"/>
<!-- Pump chamber -->
<rect x="410" y="658" width="20" height="16" rx="3" fill="rgba(0,229,255,0.04)" stroke="rgba(0,229,255,0.15)" stroke-width="0.8"/>
<text x="420" y="670" text-anchor="middle" font-size="6" fill="rgba(0,229,255,0.3)" font-family="Share Tech Mono">PUMP</text>
<!-- Valve -->
<rect x="410" y="682" width="20" height="12" rx="3" fill="rgba(255,109,0,0.04)" stroke="rgba(255,109,0,0.15)" stroke-width="0.8"/>
<text x="420" y="691" text-anchor="middle" font-size="5" fill="rgba(255,109,0,0.3)" font-family="Share Tech Mono">VALVE</text>
<!-- Pump light -->
<circle id="pumpLight" cx="420" cy="650" r="3" fill="#00E5FF" opacity="0"/>
<!-- Valve light -->
<circle id="valveLight" cx="420" cy="696" r="2.5" fill="#FF6D00" opacity="0"/>
</g>
</g>
<!-- Air particles along shaft and ribs -->
<g id="particlesG"></g>
<!-- ===== CROSS SECTION ===== -->
<g id="crossSectionG">
<!-- Panel background -->
<rect x="840" y="100" width="310" height="310" rx="8" fill="rgba(6,10,18,0.92)" stroke="rgba(0,229,255,0.15)" stroke-width="1"/>
<!-- Panel title -->
<text x="995" y="128" text-anchor="middle" font-family="Rajdhani,sans-serif" font-size="13" font-weight="700" fill="#3E5060" letter-spacing="3">CROSS SECTION</text>
<text x="995" y="143" text-anchor="middle" font-family="Share Tech Mono,monospace" font-size="9" fill="#1A2535" letter-spacing="1">截面剖面</text>
<!-- Cross section circles -->
<circle id="csKevlar" cx="995" cy="265" r="50" fill="none" stroke="#FF6D00" stroke-width="5" opacity="0.7" stroke-dasharray="3,2"/>
<circle id="csKevlarFill" cx="995" cy="265" r="50" fill="url(#kevlarP)" opacity="0.3"/>
<circle id="csSilicone" cx="995" cy="265" r="44" fill="none" stroke="#8B9AAF" stroke-width="10" opacity="0.4"/>
<circle id="csSiliconeFill" cx="995" cy="265" r="44" fill="url(#siliconeP)" opacity="0.2"/>
<circle id="csAir" cx="995" cy="265" r="34" fill="rgba(0,229,255,0.06)" stroke="rgba(0,229,255,0.25)" stroke-width="1"/>
<!-- CS air particles -->
<g id="csParticlesG"></g>
<!-- CS dimension lines -->
<line id="csDimWall" x1="995" y1="265" x2="995" y2="221" stroke="#5C6B7A" stroke-width="0.5" stroke-dasharray="2,2" opacity="0"/>
<text id="csDimWallT" x="1010" y="245" font-size="8" fill="#5C6B7A" font-family="Share Tech Mono" opacity="0">1.5mm</text>
<!-- CS Labels -->
<g id="csLabels" opacity="0">
<line x1="1045" y1="265" x2="1075" y2="265" stroke="#FF6D00" stroke-width="0.5" opacity="0.6"/>
<text x="1080" y="268" font-size="8" fill="#FF6D00" font-family="Share Tech Mono" opacity="0.8">凯夫拉约束层</text>
<line x1="1039" y1="248" x2="1075" y2="238" stroke="#8B9AAF" stroke-width="0.5" opacity="0.6"/>
<text x="1080" y="241" font-size="8" fill="#8B9AAF" font-family="Share Tech Mono" opacity="0.8">硅胶管壁</text>
<line x1="1029" y1="265" x2="1075" y2="280" stroke="rgba(0,229,255,0.5)" stroke-width="0.5"/>
<text x="1080" y="283" font-size="8" fill="#00E5FF" font-family="Share Tech Mono" opacity="0.8">气动内腔</text>
</g>
<!-- CS Pressure readout -->
<text id="csPressureT" x="995" y="370" text-anchor="middle" font-family="Rajdhani,sans-serif" font-size="18" font-weight="700" fill="#00E5FF" opacity="0">0.20 MPa</text>
<!-- CS state label -->
<text id="csStateT" x="995" y="390" text-anchor="middle" font-family="Share Tech Mono" font-size="9" fill="#3E5060" letter-spacing="2" opacity="0">刚化状态</text>
</g>
<!-- Connector line: cross section to rib -->
<line id="csConnector" x1="840" y1="265" x2="680" y2="280" stroke="rgba(255,109,0,0.1)" stroke-width="1" stroke-dasharray="4,4"/>
<!-- ===== PRESSURE GAUGE ===== -->
<g id="gaugeG" transform="translate(140,620)">
<circle r="52" fill="rgba(6,10,18,0.92)" stroke="rgba(0,229,255,0.15)" stroke-width="1"/>
<circle r="42" fill="none" stroke="rgba(0,229,255,0.08)" stroke-width="0.5"/>
<!-- Gauge tick marks -->
<g id="gaugeTicks"></g>
<!-- Gauge arc bg -->
<path id="gaugeArcBg" d="" fill="none" stroke="rgba(0,229,255,0.08)" stroke-width="5" stroke-linecap="round"/>
<!-- Gauge arc fill -->
<path id="gaugeArcFill" d="" fill="none" stroke="#00E5FF" stroke-width="5" stroke-linecap="round" filter="url(#gCyanS)"/>
<!-- Needle -->
<line id="gaugeNeedle" x1="0" y1="0" x2="0" y2="-32" stroke="#C8D6E5" stroke-width="1.5" stroke-linecap="round"/>
<circle r="4" fill="#C8D6E5"/>
<!-- Labels -->
<text x="0" y="-60" text-anchor="middle" font-family="Rajdhani,sans-serif" font-size="10" font-weight="700" fill="#3E5060" letter-spacing="2">PRESSURE</text>
<text id="gaugeVal" x="0" y="22" text-anchor="middle" font-family="Rajdhani,sans-serif" font-size="16" font-weight="700" fill="#00E5FF">0.00</text>
<text x="0" y="35" text-anchor="middle" font-size="7" fill="#1A2535" font-family="Share Tech Mono">MPa</text>
</g>
<!-- Connector: gauge to handle -->
<line x1="192" y1="620" x2="400" y2="670" stroke="rgba(0,229,255,0.06)" stroke-width="1" stroke-dasharray="4,4"/>
<!-- ===== IFR PRINCIPLE ANNOTATION ===== -->
<g id="ifrG" transform="translate(840,480)">
<rect x="0" y="0" width="310" height="170" rx="6" fill="rgba(6,10,18,0.88)" stroke="rgba(0,229,255,0.1)" stroke-width="1"/>
<text x="155" y="24" text-anchor="middle" font-family="Rajdhani,sans-serif" font-size="12" font-weight="700" fill="#3E5060" letter-spacing="3">IFR PRINCIPLE</text>
<line x1="20" y1="32" x2="290" y2="32" stroke="rgba(0,229,255,0.08)" stroke-width="0.5"/>
<text x="20" y="52" font-size="9" fill="#5C6B7A" font-family="Share Tech Mono">
<tspan x="20" dy="0">▸ 物理矛盾: 刚度需求冲突</tspan>
<tspan x="28" dy="16" fill="#3E5060">展开时需高刚度 vs 收纳时需高柔性</tspan>
<tspan x="20" dy="22" fill="#5C6B7A">▸ IFR 分离: 时间分离 + 气动场</tspan>
<tspan x="28" dy="16" fill="#3E5060">同一结构,不同时刻呈现截然不同属性</tspan>
<tspan x="20" dy="22" fill="#5C6B7A">▸ 资源利用: 气压 → 刚化媒介</tspan>
<tspan x="28" dy="16" fill="#00E5FF" opacity="0.7">充气=刚性杆 / 泄气=柔性条</tspan>
<tspan x="20" dy="22" fill="#5C6B7A">▸ 理想度: 铰链→0,柔性自坍缩</tspan>
</text>
</g>
<!-- ===== ANNOTATIONS (dynamic) ===== -->
<g id="annG"></g>
<!-- ===== NO HINGE MARKER ===== -->
<g id="noHingeG" opacity="0">
<circle cx="580" cy="240" r="18" fill="none" stroke="#76FF03" stroke-width="1.5" stroke-dasharray="3,2"/>
<line x1="598" y1="228" x2="640" y2="210" stroke="#76FF03" stroke-width="0.8"/>
<text x="645" y="207" font-size="10" fill="#76FF03" font-family="Rajdhani,sans-serif" font-weight="700" letter-spacing="1">NO HINGE</text>
<text x="645" y="219" font-size="8" fill="#3E6B3E" font-family="Share Tech Mono">无铰链 · 连续柔性体</text>
</g>
</svg>
</div>
<div class="controls">
<button class="btn btn-inflate" id="btnInflate">充气展开</button>
<div class="slider-grp">
<label>内压控制</label>
<input type="range" id="pSlider" min="0" max="100" value="0">
</div>
<button class="btn btn-deflate" id="btnDeflate">泄压收纳</button>
<div class="pressure-val" id="pDisplay">0.00 MPa</div>
</div>
<div class="timeline" id="timeline">
<span class="tl-step" data-idx="0">气泵加压</span>
<span class="tl-arrow">▸</span>
<span class="tl-step" data-idx="1">管体刚化</span>
<span class="tl-arrow">▸</span>
<span class="tl-step" data-idx="2">伞面展开</span>
<span class="tl-arrow">▸</span>
<span class="tl-step" data-idx="3">承力稳定</span>
</div>
<div class="status-bar" id="statusBar">
系统就绪 · 硅胶管柔化状态 · 内压 <span class="wr">0 MPa</span>
</div>
</div>
<script>
// ====== CONFIG ======
const HUB = {x:420, y:195};
const SHAFT_BTM = 655;
// Rib definitions: 6 ribs (3 per side)
// Each has open/closed endpoints and control points
const RIBS = [
// Right side (far to near)
{open:{ex:730,ey:380,cx:630,cy:185}, closed:{ex:432,ey:555,cx:428,cy:380}, op:0.35},
{open:{ex:710,ey:310,cx:610,cy:168}, closed:{ex:428,ey:540,cx:425,cy:370}, op:0.65},
{open:{ex:660,ey:250,cx:565,cy:162}, closed:{ex:424,ey:525,cx:422,cy:360}, op:1.0},
// Left side (near to far)
{open:{ex:180,ey:250,cx:275,cy:162}, closed:{ex:416,ey:525,cx:418,cy:360}, op:1.0},
{open:{ex:130,ey:310,cx:230,cy:168}, closed:{ex:412,ey:540,cx:415,cy:370}, op:0.65},
{open:{ex:110,ey:380,cx:210,cy:185}, closed:{ex:408,ey:555,cx:412,cy:380}, op:0.35},
];
// ====== STATE ======
let progress = 0; // 0=closed, 1=open
let target = 0;
let manualMode = false;
let particles = [];
let csParticles = [];
let frameCount = 0;
// ====== HELPERS ======
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,mn,mx){return Math.max(mn,Math.min(mx,v))}
function easeOut(t){return 1-Math.pow(1-t,3)}
function easeIn(t){return t*t*t}
// ====== SVG REFS ======
const svg = document.getElementById('mainSvg');
const ns = 'http://www.w3.org/2000/svg';
// Create rib SVG elements
const mainRibEls = [];
const kevlarRibEls = [];
const ribGroup = document.getElementById('mainRibs');
const kevGroup = document.getElementById('kevlarRibs');
const innerRibGroup = document.getElementById('canopyInnerRibs');
RIBS.forEach((rib,i)=>{
// Kevlar layer (wider, behind)
const kp = document.createElementNS(ns,'path');
kp.setAttribute('fill','none');
kp.setAttribute('stroke','#FF6D00');
kp.setAttribute('stroke-width','7');
kp.setAttribute('stroke-dasharray','4,3');
kp.setAttribute('opacity','0');
kp.setAttribute('stroke-linecap','round');
kevGroup.appendChild(kp);
kevlarRibEls.push(kp);
// Main rib (silicone core)
const mp = document.createElementNS(ns,'path');
mp.setAttribute('fill','none');
mp.setAttribute('stroke','#5C6B7A');
mp.setAttribute('stroke-width','4');
mp.setAttribute('stroke-linecap','round');
ribGroup.appendChild(mp);
mainRibEls.push(mp);
});
// Inner canopy ribs (thin decorative lines between main ribs)
const innerRibEls = [];
for(let i=0;i<4;i++){
const ip = document.createElementNS(ns,'path');
ip.setAttribute('fill','none');
ip.setAttribute('stroke','rgba(0,229,255,0.08)');
ip.setAttribute('stroke-width','0.8');
innerRibGroup.appendChild(ip);
innerRibEls.push(ip);
}
// Create shaft air particles
const partGroup = document.getElementById('particlesG');
for(let i=0;i<25;i++){
const c = document.createElementNS(ns,'circle');
c.setAttribute('r','2');
c.setAttribute('fill','#00E5FF');
c.setAttribute('opacity','0');
partGroup.appendChild(c);
particles.push({el:c, t:Math.random(), speed:0.003+Math.random()*0.004, rib:Math.floor(Math.random()*RIBS.length), phase:Math.random()*Math.PI*2});
}
// Create cross-section particles
const csPartGroup = document.getElementById('csParticlesG');
for(let i=0;i<12;i++){
const c = document.createElementNS(ns,'circle');
c.setAttribute('r','1.5');
c.setAttribute('fill','#00E5FF');
c.setAttribute('opacity','0');
csPartGroup.appendChild(c);
csParticles.push({el:c, angle:Math.random()*Math.PI*2, radius:5+Math.random()*25, speed:0.01+Math.random()*0.02});
}
// Gauge ticks
const gaugeTicksG = document.getElementById('gaugeTicks');
for(let i=0;i<=10;i++){
const angle = -225 + i * 27;
const rad = angle * Math.PI / 180;
const x1 = Math.cos(rad)*38, y1 = Math.sin(rad)*38;
const x2 = Math.cos(rad)*42, y2 = Math.sin(rad)*42;
const ln = document.createElementNS(ns,'line');
ln.setAttribute('x1',x1);ln.setAttribute('y1',y1);
ln.setAttribute('x2',x2);ln.setAttribute('y2',y2);
ln.setAttribute('stroke','rgba(0,229,255,0.2)');
ln.setAttribute('stroke-width',i%5===0?'1.5':'0.5');
gaugeTicksG.appendChild(ln);
}
// ====== GAUGE ARC PATH ======
function arcPath(cx,cy,r,startDeg,endDeg){
const s = startDeg*Math.PI/180, e = endDeg*Math.PI/180;
const x1=cx+r*Math.cos(s), y1=cy+r*Math.sin(s);
const x2=cx+r*Math.cos(e), y2=cy+r*Math.sin(e);
const large = (endDeg-startDeg)>180?1:0;
return `M${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2}`;
}
// ====== UPDATE FUNCTIONS ======
function updateRibs(p){
RIBS.forEach((rib,i)=>{
const ep = easeOut(p);
const ex = lerp(rib.closed.ex, rib.open.ex, ep);
const ey = lerp(rib.closed.ey, rib.open.ey, ep);
const cx = lerp(rib.closed.cx, rib.open.cx, ep);
const cy = lerp(rib.closed.cy, rib.open.cy, ep);
const d = `M${HUB.x},${HUB.y} Q${cx},${cy} ${ex},${ey}`;
// Main rib
mainRibEls[i].setAttribute('d', d);
// Color interpolation: gray → cyan
const r = Math.round(lerp(92,0,p));
const g = Math.round(lerp(107,229,p));
const b = Math.round(lerp(122,255,p));
mainRibEls[i].setAttribute('stroke', `rgb(${r},${g},${b})`);
mainRibEls[i].setAttribute('stroke-width', lerp(3, 4.5, p));
mainRibEls[i].setAttribute('opacity', rib.op);
if(p > 0.6) mainRibEls[i].setAttribute('filter','url(#gCyanS)');
else mainRibEls[i].removeAttribute('filter');
// Kevlar layer
kevlarRibEls[i].setAttribute('d', d);
kevlarRibEls[i].setAttribute('opacity', lerp(0, 0.45, p) * rib.op);
});
}
function updateInnerRibs(p){
const ep = easeOut(p);
// Create intermediate ribs between main ribs for canopy detail
const angles = [-60,-25,25,60];
innerRibEls.forEach((el,i)=>{
const a = angles[i]*Math.PI/180;
const len = lerp(15, 240, ep);
const droop = lerp(0.9, 0.15, ep);
const ex = HUB.x + Math.sin(a)*len;
const ey = HUB.y + Math.cos(a)*len*droop + lerp(200, 50, ep);
const cx = HUB.x + Math.sin(a)*len*0.5;
const cy = HUB.y + lerp(50, -20, ep) + Math.cos(a)*len*0.3*droop;
el.setAttribute('d', `M${HUB.x},${HUB.y} Q${cx},${cy} ${ex},${ey}`);
});
}
function updateCanopy(p){
const ep = easeOut(p);
const endpoints = RIBS.map((rib,i)=>{
return {
x: lerp(rib.closed.ex, rib.open.ex, ep),
y: lerp(rib.closed.ey, rib.open.ey, ep),
};
}).sort((a,b)=>a.x-b.x);
const left = endpoints[0];
const right = endpoints[endpoints.length-1];
// Dome shape
const domeH = lerp(10, 90, ep);
const cpLx = lerp(HUB.x-5, HUB.x-180, ep);
const cpLy = lerp(HUB.y-3, HUB.y-domeH, ep);
const cpRx = lerp(HUB.x+5, HUB.x+180, ep);
const cpRy = lerp(HUB.y-3, HUB.y-domeH, ep);
// Top dome
const topD = `M${left.x},${left.y} C${cpLx},${cpLy} ${cpRx},${cpRy} ${right.x},${right.y}`;
// Bottom scallop
const btmDip = lerp(3, 30, ep);
const midX = (left.x+right.x)/2;
const midY = Math.max(left.y, right.y) + btmDip;
const btmD = ` Q${midX},${midY} ${left.x},${left.y}`;
const fullPath = topD + btmD;
document.getElementById('canopyFill').setAttribute('d', fullPath);
document.getElementById('canopyOutline').setAttribute('d', topD);
}
function updateSlider(p){
const sy = lerp(520, 310, easeOut(p));
const sliderG = document.getElementById('sliderG');
sliderG.setAttribute('transform', `translate(0,${sy-400})`);
const dotOp = lerp(0.2, 0.8, p);
document.getElementById('sliderDot').setAttribute('fill', `rgba(0,229,255,${dotOp})`);
}
function updateHandleLights(p){
const pumpOp = p > 0.1 && p < 0.95 ? lerp(0, 0.9, Math.sin(frameCount*0.1)*0.5+0.5) : 0;
const valveOp = p < 0.9 && p > 0.05 ? lerp(0, 0.8, Math.sin(frameCount*0.15)*0.5+0.5) : 0;
document.getElementById('pumpLight').setAttribute('opacity', target>0.5 ? pumpOp*0.8 : 0);
document.getElementById('valveLight').setAttribute('opacity', target<0.5 ? valveOp*0.7 : 0);
}
function updateAirChannel(p){
const op = lerp(0.03, 0.25, p);
const col = p > 0.5 ? `rgba(0,229,255,${op})` : `rgba(0,229,255,${op*0.3})`;
document.getElementById('airChannel').setAttribute('fill', col);
document.getElementById('hubCore').setAttribute('opacity', lerp(0.1, 0.8, p));
}
function updateParticles(p){
particles.forEach(pt=>{
if(p > 0.15){
pt.t += pt.speed * (0.5 + p);
if(pt.t > 1) pt.t -= 1;
const rib = RIBS[pt.rib];
const ep = easeOut(p);
const ex = lerp(rib.closed.ex, rib.open.ex, ep);
const ey = lerp(rib.closed.ey, rib.open.ey, ep);
const cx = lerp(rib.closed.cx, rib.open.cx, ep);
const cy = lerp(rib.closed.cy, rib.open.cy, ep);
// Point on quadratic bezier at t
const t = pt.t;
const px = (1-t)*(1-t)*HUB.x + 2*(1-t)*t*cx + t*t*ex;
const py = (1-t)*(1-t)*HUB.y + 2*(1-t)*t*cy + t*t*ey;
pt.el.setAttribute('cx', px);
pt.el.setAttribute('cy', py);
pt.el.setAttribute('opacity', lerp(0, 0.6, p) * rib.op * (0.5+0.5*Math.sin(frameCount*0.08+pt.phase)));
pt.el.setAttribute('r', lerp(1, 2.2, p));
} else {
pt.el.setAttribute('opacity', 0);
}
});
}
function updateCrossSection(p){
const kevlarR = lerp(38, 50, p);
const siliconeR = lerp(33, 44, p);
const airR = lerp(18, 34, p);
document.getElementById('csKevlar').setAttribute('r', kevlarR);
document.getElementById('csKevlarFill').setAttribute('r', kevlarR);
document.getElementById('csSilicone').setAttribute('r', siliconeR);
document.getElementById('csSiliconeFill').setAttribute('r', siliconeR);
document.getElementById('csAir').setAttribute('r', airR);
// Air cavity color intensity
const airOp = lerp(0.03, 0.2, p);
document.getElementById('csAir').setAttribute('fill', `rgba(0,229,255,${airOp})`);
document.getElementById('csAir').setAttribute('stroke', `rgba(0,229,255,${lerp(0.1,0.5,p)})`);
// Kevlar opacity
document.getElementById('csKevlar').setAttribute('opacity', lerp(0.3, 0.8, p));
document.getElementById('csKevlarFill').setAttribute('opacity', lerp(0.1, 0.4, p));
// CS particles
csParticles.forEach(cp=>{
if(p > 0.2){
cp.angle += cp.speed * p;
const r = cp.radius * lerp(0.4, 1, p);
const px = 995 + Math.cos(cp.angle)*r;
const py = 265 + Math.sin(cp.angle)*r;
cp.el.setAttribute('cx', px);
cp.el.setAttribute('cy', py);
cp.el.setAttribute('opacity', lerp(0, 0.5, p));
} else {
cp.el.setAttribute('opacity', 0);
}
});
// Labels
const labelOp = lerp(0, 1, clamp((p-0.3)/0.4, 0, 1));
document.getElementById('csLabels').setAttribute('opacity', labelOp);
document.getElementById('csDimWall').setAttribute('opacity', labelOp*0.6);
document.getElementById('csDimWallT').setAttribute('opacity', labelOp*0.8);
// Pressure text
const pressure = (p * 0.2).toFixed(2);
const csPT = document.getElementById('csPressureT');
csPT.textContent = pressure + ' MPa';
csPT.setAttribute('opacity', labelOp);
const csST = document.getElementById('csStateT');
csST.textContent = p > 0.7 ? '刚化状态 · RIGID' : p > 0.3 ? '过渡态 · TRANSITION' : '柔化状态 · FLEXIBLE';
csST.setAttribute('opacity', labelOp * 0.8);
csST.setAttribute('fill', p > 0.7 ? '#00E5FF' : p > 0.3 ? '#5C6B7A' : '#FF6D00');
}
function updateGauge(p){
const startDeg = -225;
const endDeg = 45;
const fillEnd = startDeg + (endDeg - startDeg) * p;
document.getElementById('gaugeArcBg').setAttribute('d', arcPath(0,0,35,startDeg,endDeg));
document.getElementById('gaugeArcFill').setAttribute('d', arcPath(0,0,35,startDeg,fillEnd));
// Needle
const needleAngle = startDeg + (endDeg - startDeg) * p;
const rad = needleAngle * Math.PI / 180;
const nx = Math.cos(rad)*-32;
const ny = Math.sin(rad)*32; // Wait, the needle should point in the direction
// Actually let me recalculate. The needle rotates from startDeg to endDeg
const needleRad = needleAngle * Math.PI / 180;
const nxe = Math.cos(needleRad)*30;
const nye = Math.sin(needleRad)*30;
document.getElementById('gaugeNeedle').setAttribute('x2', nxe);
document.getElementById('gaugeNeedle').setAttribute('y2', nye);
document.getElementById('gaugeVal').textContent = (p*0.2).toFixed(2);
// Color
if(p > 0.7) document.getElementById('gaugeArcFill').setAttribute('stroke','#00E5FF');
else if(p > 0.3) document.getElementById('gaugeArcFill').setAttribute('stroke','#4DB6AC');
else document.getElementById('gaugeArcFill').setAttribute('stroke','#FF6D00');
}
function updateNoHinge(p){
const op = clamp((p-0.5)/0.3, 0, 1);
document.getElementById('noHingeG').setAttribute('opacity', op);
}
function updateTimeline(p){
const steps = document.querySelectorAll('.tl-step');
let activeIdx = 0;
if(p < 0.15) activeIdx = -1;
else if(p < 0.4) activeIdx = 0;
else if(p < 0.7) activeIdx = 1;
else if(p < 0.9) activeIdx = 2;
else activeIdx = 3;
// If deflating (target < 0.5), show reverse labels
const isDeflating = target < 0.3 && progress > 0.3;
if(isDeflating){
const defLabels = ['泄压阀开启','内压释放','刚度下降','柔性坍缩'];
steps.forEach((s,i)=>{
s.textContent = defLabels[i];
s.className = 'tl-step';
if(i <= (3-activeIdx+1)) s.classList.add('active-warn');
});
} else {
const infLabels = ['气泵加压','管体刚化','伞面展开','承力稳定'];
steps.forEach((s,i)=>{
s.textContent = infLabels[i];
s.className = 'tl-step';
if(i <= activeIdx) s.classList.add('active');
});
}
}
function updateStatusBar(p){
const pressure = (p*0.2).toFixed(2);
const sb = document.getElementById('statusBar');
if(p < 0.1){
sb.innerHTML = `系统就绪 · 硅胶管柔化状态 · 内压 <span class="wr">${pressure} MPa</span> · 曲率半径 < 5cm`;
} else if(p < 0.5){
sb.innerHTML = `加压过渡中 · 刚度渐增 · 内压 <span class="hl">${pressure} MPa</span>`;
} else if(p < 0.9){
sb.innerHTML = `刚化成形中 · 管体绷直 · 内压 <span class="hl">${pressure} MPa</span>`;
} else {
sb.innerHTML = `完全刚化 · 承力稳定 · 内压 <span class="hl">${pressure} MPa</span> · 凯夫拉层约束生效`;
}
}
// ====== ANNOTATION POPUPS ======
function updateAnnotations(p){
const annG = document.getElementById('annG');
annG.innerHTML = '';
if(p > 0.6){
// Annotation: pneumatic stiffening
addAnnotation(annG, 640, 220, 680, 190,
'气动刚化', '内压使硅胶管膨胀\n凯夫拉层约束→刚化挺直', '#00E5FF');
}
if(p < 0.3 && p > 0.01){
// Annotation: flexible collapse
addAnnotation(annG, 435, 480, 500, 460,
'柔性坍缩', '泄压后刚度急剧下降\n重力驱动自然弯曲', '#FF6D00');
}
}
function addAnnotation(parent, tx, ty, ax, ay, title, desc, color){
const g = document.createElementNS(ns,'g');
// Connection line
const ln = document.createElementNS(ns,'line');
ln.setAttribute('x1',tx);ln.setAttribute('y1',ty);
ln.setAttribute('x2',ax);ln.setAttribute('y2',ay);
ln.setAttribute('stroke',color);ln.setAttribute('stroke-width','0.8');
ln.setAttribute('stroke-dasharray','3,2');ln.setAttribute('opacity','0.5');
g.appendChild(ln);
// Dot at target
const dot = document.createElementNS(ns,'circle');
dot.setAttribute('cx',tx);dot.setAttribute('cy',ty);
dot.setAttribute('r','3');dot.setAttribute('fill',color);dot.setAttribute('opacity','0.6');
g.appendChild(dot);
// Title
const tt = document.createElementNS(ns,'text');
tt.setAttribute('x',ax);tt.setAttribute('y',ay-4);
tt.setAttribute('font-size','11');tt.setAttribute('fill',color);
tt.setAttribute('font-family','Rajdhani,sans-serif');tt.setAttribute('font-weight','700');
tt.setAttribute('letter-spacing','2');
tt.textContent = title;
g.appendChild(tt);
// Desc lines
const lines = desc.split('\n');
lines.forEach((line,i)=>{
const dt = document.createElementNS(ns,'text');
dt.setAttribute('x',ax);dt.setAttribute('y',ay+12+i*13);
dt.setAttribute('font-size','8');dt.setAttribute('fill','#5C6B7A');
dt.setAttribute('font-family','Share Tech Mono');
dt.textContent = line;
g.appendChild(dt);
});
parent.appendChild(g);
}
// ====== CS CONNECTOR UPDATE ======
function updateCSConnector(p){
const ep = easeOut(p);
// Connect cross-section panel to the main hero rib (RIB index 2)
const rib = RIBS[2];
const ribMidX = lerp(rib.closed.cx, rib.open.cx, ep);
const ribMidY = lerp(rib.closed.cy, rib.open.cy, ep);
document.getElementById('csConnector').setAttribute('x2', ribMidX);
document.getElementById('csConnector').setAttribute('y2', ribMidY);
document.getElementById('csConnector').setAttribute('opacity', lerp(0.05, 0.2, p));
}
// ====== MAIN LOOP ======
function tick(){
frameCount++;
// Smooth interpolation toward target
const speed = target > progress ? 0.018 : 0.035; // deflate faster
progress += (target - progress) * speed;
// Snap if close
if(Math.abs(progress - target) < 0.001) progress = target;
const p = clamp(progress, 0, 1);
updateRibs(p);
updateInnerRibs(p);
updateCanopy(p);
updateSlider(p);
updateHandleLights(p);
updateAirChannel(p);
updateParticles(p);
updateCrossSection(p);
updateGauge(p);
updateNoHinge(p);
updateTimeline(p);
updateStatusBar(p);
updateAnnotations(p);
updateCSConnector(p);
// Update pressure display
document.getElementById('pDisplay').textContent = (p*0.2).toFixed(2) + ' MPa';
// Update slider if not manual
if(!manualMode){
document.getElementById('pSlider').value = p * 100;
}
requestAnimationFrame(tick);
}
// ====== CONTROLS ======
document.getElementById('btnInflate').addEventListener('click', function(){
target = 1;
manualMode = false;
this.classList.add('active');
document.getElementById('btnDeflate').classList.remove('active');
});
document.getElementById('btnDeflate').addEventListener('click', function(){
target = 0;
manualMode = false;
this.classList.add('active');
document.getElementById('btnInflate').classList.remove('active');
});
document.getElementById('pSlider').addEventListener('input', function(){
target = this.value / 100;
manualMode = true;
document.getElementById('btnInflate').classList.remove('active');
document.getElementById('btnDeflate').classList.remove('active');
});
// ====== INIT ======
tick();
</script>
</body>
</html>
实现说明:
这个高保真 SVG 原理动画完整呈现了气动柔性伞骨的 IFR 方案,核心设计如下:
核心动画——刚度相变:6 根伞骨在充气(progress→1)时从灰色柔曲态渐变为青色刚直态,泄压时反向坍缩。颜色、粗细、发光效果同步联动,直观传达"同一结构、不同时刻截然不同属性"的时间分离原理。
截面剖析视图(右上):实时展示硅胶管壁、凯夫拉约束层、气动内腔三层结构随内压变化的膨胀/收缩,内腔粒子活跃度随压力变化,配合尺寸标注与层名标签。
压力仪表盘(左下):弧形指针式仪表实时反映 0~0.2MPa 内压,颜色随压力区间变化(橙→青),增强工程感。
粒子流可视化:充气时青色粒子沿伞骨路径流动,直观展示气压从手柄气泵经轴杆充入各管腔的传力路径。
交互控制:「充气展开」/「泄压收纳」按钮 + 手动压力滑块,用户可自由控制关键变量,体验刚化—柔化的完整动态过程。
IFR 原理面板:右下角明确标注物理矛盾、时间分离策略、资源利用方式与理想度提升,将 TRIZ 思维可视化。
过程时间线:底部四阶段指示器根据当前状态动态切换(充气流程 / 泄气流程),引导用户理解动作时序。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
