分享图
A
动画渲染工坊
就绪

这是一个极具创新性的 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 方案,核心设计如下:

  1. 核心动画——刚度相变:6 根伞骨在充气(progress→1)时从灰色柔曲态渐变为青色刚直态,泄压时反向坍缩。颜色、粗细、发光效果同步联动,直观传达"同一结构、不同时刻截然不同属性"的时间分离原理。

  2. 截面剖析视图(右上):实时展示硅胶管壁、凯夫拉约束层、气动内腔三层结构随内压变化的膨胀/收缩,内腔粒子活跃度随压力变化,配合尺寸标注与层名标签。

  3. 压力仪表盘(左下):弧形指针式仪表实时反映 0~0.2MPa 内压,颜色随压力区间变化(橙→青),增强工程感。

  4. 粒子流可视化:充气时青色粒子沿伞骨路径流动,直观展示气压从手柄气泵经轴杆充入各管腔的传力路径。

  5. 交互控制:「充气展开」/「泄压收纳」按钮 + 手动压力滑块,用户可自由控制关键变量,体验刚化—柔化的完整动态过程。

  6. IFR 原理面板:右下角明确标注物理矛盾、时间分离策略、资源利用方式与理想度提升,将 TRIZ 思维可视化。

  7. 过程时间线:底部四阶段指示器根据当前状态动态切换(充气流程 / 泄气流程),引导用户理解动作时序。

积分规则:第一轮对话扣减6分,后续每轮扣4分