分享图
A
动画渲染工坊
就绪
<!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=Syne:wght@400;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400&display=swap" rel="stylesheet">
<style>
:root {
  --bg-deep:#060b14;--bg-mid:#0d1525;--text-pri:#dfe9f5;--text-sec:#6b7f99;
  --accent:#00ffc8;--accent-dim:rgba(0,255,200,.12);--stress:#ff6b35;
  --danger:#ff3355;--canopy:#00d4be;--struct:#8a9db5;--spring-norm:#44ddaa;
  --spring-stress:#ff5533;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg-deep);color:var(--text-pri);font-family:'DM Mono',monospace;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
header{text-align:center;padding:28px 20px 8px;position:relative;z-index:2}
header h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(22px,3.6vw,40px);
  letter-spacing:-.02em;background:linear-gradient(135deg,var(--accent),#00b8ff);
  -webkit-background-clip:text;-webkit-text-fill-color:transparent}
header p{font-size:13px;color:var(--text-sec);margin-top:4px;letter-spacing:.04em}
.wrap{width:100%;max-width:1100px;padding:0 16px;flex:1;display:flex;flex-direction:column}
.svg-box{width:100%;aspect-ratio:11/6.5;margin:10px auto 0;position:relative;
  border-radius:18px;overflow:hidden;
  background:radial-gradient(ellipse 70% 60% at 50% 40%,#0e1a30,var(--bg-deep));
  border:1px solid rgba(100,140,180,.1);box-shadow:0 0 60px rgba(0,255,200,.04)}
.svg-box svg{width:100%;height:100%;display:block}
.panel-row{display:flex;gap:14px;margin:14px 0 20px;flex-wrap:wrap;align-items:stretch}
.card{background:rgba(14,22,40,.85);border:1px solid rgba(100,150,200,.12);
  border-radius:14px;padding:16px 20px;backdrop-filter:blur(10px)}
.card-data{flex:0 0 220px;display:flex;flex-direction:column;gap:8px}
.card-ctrl{flex:1;min-width:280px;display:flex;flex-direction:column;gap:12px}
.card-principle{flex:0 0 260px;font-size:12px;line-height:1.65;color:var(--text-sec)}
.card-principle strong{color:var(--accent);font-weight:600}
.dlabel{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-sec);margin-bottom:2px}
.dval{font-size:22px;font-weight:500;font-family:'Syne',sans-serif;letter-spacing:-.01em}
.dval.accent{color:var(--accent)}.dval.stress{color:var(--stress)}.dval.danger{color:var(--danger)}
.bar-wrap{height:6px;border-radius:3px;background:rgba(255,255,255,.06);overflow:hidden;margin-top:4px}
.bar-fill{height:100%;border-radius:3px;transition:width .15s,background .3s}
.slider-row{display:flex;align-items:center;gap:12px}
.slider-row label{font-size:12px;color:var(--text-sec);white-space:nowrap;min-width:80px}
.slider-row input[type=range]{flex:1;-webkit-appearance:none;height:6px;border-radius:3px;
  background:rgba(255,255,255,.08);outline:none}
.slider-row input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;
  border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 10px rgba(0,255,200,.4)}
.slider-row .sval{font-size:15px;font-weight:600;min-width:32px;text-align:right;font-family:'Syne',sans-serif}
.btn-row{display:flex;gap:10px;flex-wrap:wrap}
.btn{padding:8px 18px;border-radius:8px;border:1px solid rgba(100,180,220,.2);
  background:rgba(0,255,200,.06);color:var(--accent);cursor:pointer;font-size:12px;
  font-family:'DM Mono',monospace;transition:all .2s;letter-spacing:.02em}
.btn:hover{background:rgba(0,255,200,.14);border-color:rgba(0,255,200,.35)}
.btn.active{background:rgba(0,255,200,.18);border-color:var(--accent);box-shadow:0 0 12px rgba(0,255,200,.15)}
.state-badge{display:inline-block;padding:3px 10px;border-radius:6px;font-size:11px;font-weight:500;letter-spacing:.04em}
.state-normal{background:rgba(0,255,200,.1);color:var(--accent)}
.state-stressed{background:rgba(255,170,0,.12);color:#ffaa00}
.state-releasing{background:rgba(255,107,53,.14);color:var(--stress)}
.state-reset{background:rgba(100,180,255,.1);color:#66bbff}
@keyframes pulseGlow{0%,100%{opacity:.5}50%{opacity:1}}
@media(max-width:700px){.card-data,.card-principle{flex:1 1 100%}.card-ctrl{min-width:100%}}
</style>
</head>
<body>
<header>
  <h1>抗风自卸载伞</h1>
  <p>TRIZ 最终理想解 (IFR) · 原理动态演示</p>
</header>
<div class="wrap">
  <div class="svg-box">
    <svg id="mainSvg" viewBox="0 0 1100 650" preserveAspectRatio="xMidYMid meet"></svg>
  </div>
  <div class="panel-row">
    <div class="card card-data">
      <div><div class="dlabel">风力等级</div><div class="dval accent" id="dWind">0.0</div></div>
      <div><div class="dlabel">风速 (m/s)</div><div class="dval" id="dSpeed">0.0</div></div>
      <div><div class="dlabel">铰接扭矩 / 临界值</div>
        <div class="dval" id="dTorque">0.00 <span style="font-size:13px;color:var(--text-sec)">/ 3.50 N·m</span></div>
        <div class="bar-wrap"><div class="bar-fill" id="torqueBar" style="width:0%;background:var(--accent)"></div></div>
      </div>
      <div><div class="dlabel">伞骨状态</div><div id="dState"><span class="state-badge state-normal">标准遮挡</span></div></div>
    </div>
    <div class="card card-ctrl">
      <div class="slider-row">
        <label>风力控制</label>
        <input type="range" id="windSlider" min="0" max="10" step="0.1" value="0">
        <span class="sval" id="sliderVal">0</span>
      </div>
      <div class="btn-row">
        <button class="btn" id="btnAuto">自动演示</button>
        <button class="btn" id="btnGust">阵风模式</button>
        <button class="btn" id="btnReset">复位归零</button>
      </div>
      <div style="font-size:11px;color:var(--text-sec);line-height:1.55;margin-top:2px">
        拖动滑块控制风力,或点击「自动演示」观看完整卸载-复位周期。<br>
        当扭矩超过 <span style="color:var(--stress)">3.5 N·m</span> 临界值时,外侧伞骨自动上翻卸载风压。
      </div>
    </div>
    <div class="card card-principle">
      <strong>IFR 核心思路</strong><br>
      伞在强风时<strong>自行卸载</strong>风压免于翻折,风停后<strong>自行复位</strong>,无需用户任何额外操作。<br><br>
      <strong>关键资源巧用</strong><br>
      ① 分段铰接伞骨 + 扭簧 → 利用风力的<strong>升力分量</strong>触发翻转,以风治风<br>
      ② 顶部释风孔 + 导流罩 → 利用<strong>文丘里效应</strong>加速气流抽离,同时阻挡雨水<br><br>
      <strong>不增加</strong>额外操作负担和显著重量,系统<strong>自组织</strong>适应环境。
    </div>
  </div>
</div>

<script>
/* ===== 配置常量 ===== */
const C = {
  cx: 550, hubY: 235, shaftBottom: 590,
  innerLen: 175, outerLen: 135,
  innerAng: 28, outerNormAng: 36, outerFlipAng: -42,
  holeHW: 26, deflectorHW: 42, deflectorH: 36,
  criticalTorque: 3.5, torquePerWind: 0.72,
  maxParticles: 75
};

/* ===== 状态 ===== */
const S = {
  wind: 0, targetWind: 0,
  flip: 0, targetFlip: 0,
  torque: 0, time: 0,
  autoPlay: false, autoPhase: 0, autoTimer: 0,
  gustMode: false, gustTimer: 0
};

/* ===== SVG 工具 ===== */
const NS = 'http://www.w3.org/2000/svg';
function el(tag, attrs, parent) {
  const e = document.createElementNS(NS, tag);
  if (attrs) Object.entries(attrs).forEach(([k,v]) => e.setAttribute(k,v));
  if (parent) parent.appendChild(e);
  return e;
}
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}

/* ===== 初始化 SVG ===== */
const svg = document.getElementById('mainSvg');
const layers = {};
['bg','grid','particles','canopyFill','canopyStroke','ribs','hinges','springs',
 'shaft','deflector','holeGlow','forces','venturi','annotations'].forEach(name => {
  layers[name] = el('g',{id:'layer-'+name}, svg);
});

/* 滤镜 */
const defs = el('defs',null,svg);
const fGlow = el('filter',{id:'glow',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},fGlow);
const fMerge = el('feMerge',null,fGlow);
el('feMergeNode',{in:'b'},fMerge); el('feMergeNode',{in:'SourceGraphic'},fMerge);

const fGlowStrong = el('filter',{id:'glowStrong',x:'-80%',y:'-80%',width:'260%',height:'260%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'8',result:'b2'},fGlowStrong);
const fMerge2 = el('feMerge',null,fGlowStrong);
el('feMergeNode',{in:'b2'},fMerge2); el('feMergeNode',{in:'SourceGraphic'},fMerge2);

/* 背景网格 */
(function buildGrid(){
  const g = layers.grid;
  for(let x=0;x<=1100;x+=50){
    el('line',{x1:x,y1:0,x2:x,y2:650,stroke:'rgba(60,90,130,0.07)','stroke-width':1},g);
  }
  for(let y=0;y<=650;y+=50){
    el('line',{x1:0,y1:y,x2:1100,y2:y,stroke:'rgba(60,90,130,0.07)','stroke-width':1},g);
  }
})();

/* ===== 创建伞骨结构元素 ===== */
const shaftEl = el('line',{stroke:'#6b7f99','stroke-width':3.5,'stroke-linecap':'round'},layers.shaft);
const hubEl = el('circle',{r:6,fill:'#8a9db5',stroke:'#a0b8d0','stroke-width':1.5},layers.shaft);

/* 伞骨 - 左右各两段 */
const ribInnerR = el('line',{stroke:'#8a9db5','stroke-width':3,'stroke-linecap':'round'},layers.ribs);
const ribInnerL = el('line',{stroke:'#8a9db5','stroke-width':3,'stroke-linecap':'round'},layers.ribs);
const ribOuterR = el('line',{stroke:'#8a9db5','stroke-width':2.8,'stroke-linecap':'round'},layers.ribs);
const ribOuterL = el('line',{stroke:'#8a9db5','stroke-width':2.8,'stroke-linecap':'round'},layers.ribs);

/* 深度暗示 - 后排伞骨(半透明) */
const ribBack1 = el('line',{stroke:'rgba(138,157,181,0.18)','stroke-width':2,'stroke-linecap':'round'},layers.ribs);
const ribBack2 = el('line',{stroke:'rgba(138,157,181,0.18)','stroke-width':2,'stroke-linecap':'round'},layers.ribs);
const ribBack3 = el('line',{stroke:'rgba(138,157,181,0.18)','stroke-width':2,'stroke-linecap':'round'},layers.ribs);
const ribBack4 = el('line',{stroke:'rgba(138,157,181,0.18)','stroke-width':2,'stroke-linecap':'round'},layers.ribs);

/* 铰接点 */
const hingeR = el('circle',{r:5.5,fill:'#0d1525',stroke:'#00ffc8','stroke-width':2},layers.hinges);
const hingeL = el('circle',{r:5.5,fill:'#0d1525',stroke:'#00ffc8','stroke-width':2},layers.hinges);
const hingeGlowR = el('circle',{r:12,fill:'none',stroke:'rgba(0,255,200,0.15)','stroke-width':1,filter:'url(#glow)'},layers.hinges);
const hingeGlowL = el('circle',{r:12,fill:'none',stroke:'rgba(0,255,200,0.15)','stroke-width':1,filter:'url(#glow)'},layers.hinges);

/* 扭簧 */
const springR = el('path',{fill:'none','stroke-width':1.8,'stroke-linecap':'round','stroke-linejoin':'round'},layers.springs);
const springL = el('path',{fill:'none','stroke-width':1.8,'stroke-linecap':'round','stroke-linejoin':'round'},layers.springs);

/* 伞面填充 */
const canopyFillR = el('path',{fill:'rgba(0,212,190,0.06)',stroke:'none'},layers.canopyFill);
const canopyFillL = el('path',{fill:'rgba(0,212,190,0.06)',stroke:'none'},layers.canopyFill);

/* 伞面描边 */
const canopyR = el('path',{fill:'none',stroke:'rgba(0,212,190,0.55)','stroke-width':2.8,'stroke-linecap':'round'},layers.canopyStroke);
const canopyL = el('path',{fill:'none',stroke:'rgba(0,212,190,0.55)','stroke-width':2.8,'stroke-linecap':'round'},layers.canopyStroke);

/* 释风孔高亮 */
const holeGlowEl = el('ellipse',{fill:'none',stroke:'rgba(0,255,200,0.2)','stroke-width':1.5,filter:'url(#glow)',rx:C.holeHW,ry:6},layers.holeGlow);

/* 导流罩 */
const deflectorPole = el('line',{stroke:'#6b7f99','stroke-width':2,'stroke-dasharray':'4,3'},layers.deflector);
const deflectorCover = el('path',{fill:'rgba(0,180,160,0.12)',stroke:'rgba(0,212,190,0.4)','stroke-width':1.8},layers.deflector);
const deflectorConnectR = el('line',{stroke:'rgba(0,212,190,0.25)','stroke-width':1,'stroke-dasharray':'2,2'},layers.deflector);
const deflectorConnectL = el('line',{stroke:'rgba(0,212,190,0.25)','stroke-width':1,'stroke-dasharray':'2,2'},layers.deflector);

/* 力箭头 */
const forceArrows = [];
for(let i=0;i<6;i++){
  const arr = el('path',{fill:'none','stroke-width':1.5,'stroke-linecap':'round',opacity:0},layers.forces);
  forceArrows.push(arr);
}

/* 文丘里粒子 */
const venturiParticles = [];
for(let i=0;i<12;i++){
  const vp = el('circle',{r:2,fill:'rgba(0,255,200,0.6)',opacity:0},layers.venturi);
  venturiParticles.push({el:vp, t:i/12, speed:0.008+Math.random()*0.006});
}

/* 标注 */
const annData = [
  {id:'annSpring', text:'扭簧铰接', sub:'临界 3.5 N·m'},
  {id:'annHole', text:'释风孔', sub:'Ø 12 cm'},
  {id:'annDeflector', text:'柔性导流罩', sub:'悬空 5 cm'},
  {id:'annVenturi', text:'文丘里加速', sub:'气流抽离'},
  {id:'annReset', text:'扭簧复位', sub:'自动回弹'}
];
const annEls = {};
annData.forEach(a=>{
  const g = el('g',{opacity:0,transition:'opacity 0.5s'},layers.annotations);
  const line = el('line',{stroke:'rgba(0,255,200,0.3)','stroke-width':1,'stroke-dasharray':'3,2'},g);
  const t1 = el('text',{fill:'#00ffc8','font-size':'11','font-family':'Syne, sans-serif','font-weight':'600'},g);
  const t2 = el('text',{fill:'rgba(0,255,200,0.5)','font-size':'9','font-family':'DM Mono, monospace'},g);
  t1.textContent = a.text;
  t2.textContent = a.sub;
  annEls[a.id] = {g, line, t1, t2};
});

/* ===== 粒子系统 ===== */
const particles = [];
function createParticle(){
  const circle = el('circle',{r:1.5+Math.random()*1.5},layers.particles);
  const p = {
    el: circle, x:0, y:0, vx:0, vy:0,
    baseR: 1.5+Math.random()*1.5, life:1
  };
  resetParticle(p, true);
  return p;
}
function resetParticle(p, initial){
  p.x = initial ? Math.random()*1100 : -10 - Math.random()*80;
  p.y = 120 + Math.random()*400;
  p.vx = 0; p.vy = 0; p.life = 1;
}
for(let i=0;i<C.maxParticles;i++) particles.push(createParticle());

/* ===== 几何计算 ===== */
function computeGeo(flip){
  const {cx,hubY,innerLen,outerLen,innerAng,outerNormAng,outerFlipAng} = C;
  const iRad = innerAng * Math.PI/180;
  const oAng = lerp(outerNormAng, outerFlipAng, flip);
  const oRad = oAng * Math.PI/180;

  const rHx = cx + innerLen*Math.cos(iRad), rHy = hubY + innerLen*Math.sin(iRad);
  const rTx = rHx + outerLen*Math.cos(oRad), rTy = rHy + outerLen*Math.sin(oRad);
  const lHx = cx - innerLen*Math.cos(iRad), lHy = hubY + innerLen*Math.sin(iRad);
  const lTx = lHx - outerLen*Math.cos(oRad), lTy = lHy + outerLen*Math.sin(oRad);

  return {hub:{x:cx,y:hubY}, rH:{x:rHx,y:rHy}, rT:{x:rTx,y:rTy},
    lH:{x:lHx,y:lHy}, lT:{x:lTx,y:lTy}, oAng};
}

function canopyPath(geo, side){
  const {hub,rH,rT,lH,lT} = geo;
  const holeY = hub.y + 16;
  if(side==='right'){
    const p0x=C.cx+C.holeHW, p0y=holeY;
    const p1x=rH.x, p1y=rH.y-7;
    const p2x=rT.x, p2y=rT.y-4;
    const sag1=14, sag2=10;
    return `M${p0x} ${p0y} C${p0x+(p1x-p0x)*.45} ${p0y+sag1}, ${p1x-(p1x-p0x)*.25} ${p1y+sag1*.3}, ${p1x} ${p1y} C${p1x+(p2x-p1x)*.5} ${p1y+sag2}, ${p2x-(p2x-p1x)*.25} ${p2y+sag2*.3}, ${p2x} ${p2y}`;
  } else {
    const p0x=C.cx-C.holeHW, p0y=holeY;
    const p1x=lH.x, p1y=lH.y-7;
    const p2x=lT.x, p2y=lT.y-4;
    const sag1=14, sag2=10;
    return `M${p0x} ${p0y} C${p0x+(p1x-p0x)*.55} ${p0y+sag1}, ${p1x-(p1x-p0x)*.75} ${p1y+sag1*.3}, ${p1x} ${p1y} C${p1x+(p2x-p1x)*.5} ${p1y+sag2}, ${p2x-(p2x-p1x)*.75} ${p2y+sag2*.3}, ${p2x} ${p2y}`;
  }
}

function canopyFillPath(geo, side){
  const {hub} = geo;
  const holeY = hub.y + 16;
  const botY = hub.y + 80;
  if(side==='right'){
    const surface = canopyPath(geo,'right');
    return surface + ` L${geo.rT.x} ${botY} L${C.cx+C.holeHW} ${botY} Z`;
  } else {
    const surface = canopyPath(geo,'left');
    return surface + ` L${geo.lT.x} ${botY} L${C.cx-C.holeHW} ${botY} Z`;
  }
}

function springPath(cx, cy, angle, stress){
  const len=18, amp=4+stress*4, segs=5;
  const dx=Math.cos(angle), dy=Math.sin(angle);
  const nx=-dy, ny=dx;
  let d = `M${cx-dx*len/2} ${cy-dy*len/2}`;
  for(let i=1;i<=segs*2;i++){
    const t=i/(segs*2);
    const px=cx-dx*len/2+dx*len*t;
    const py=cy-dy*len/2+dy*len*t;
    const side=(i%2===0)?1:-1;
    d+=` L${px+nx*amp*side} ${py+ny*amp*side}`;
  }
  d+=` L${cx+dx*len/2} ${cy+dy*len/2}`;
  return d;
}

/* ===== 更新伞体 ===== */
function updateUmbrella(geo){
  const {hub,rH,rT,lH,lT,oAng} = geo;

  shaftEl.setAttribute('x1',C.cx); shaftEl.setAttribute('y1',hub.y-2);
  shaftEl.setAttribute('x2',C.cx); shaftEl.setAttribute('y2',C.shaftBottom);
  hubEl.setAttribute('cx',hub.x); hubEl.setAttribute('cy',hub.y);

  /* 伞骨 */
  ribInnerR.setAttribute('x1',hub.x); ribInnerR.setAttribute('y1',hub.y);
  ribInnerR.setAttribute('x2',rH.x); ribInnerR.setAttribute('y2',rH.y);
  ribOuterR.setAttribute('x1',rH.x); ribOuterR.setAttribute('y1',rH.y);
  ribOuterR.setAttribute('x2',rT.x); ribOuterR.setAttribute('y2',rT.y);
  ribInnerL.setAttribute('x1',hub.x); ribInnerL.setAttribute('y1',hub.y);
  ribInnerL.setAttribute('x2',lH.x); ribInnerL.setAttribute('y2',lH.y);
  ribOuterL.setAttribute('x1',lH.x); ribOuterL.setAttribute('y1',lH.y);
  ribOuterL.setAttribute('x2',lT.x); ribOuterL.setAttribute('y2',lT.y);

  /* 后排伞骨 - 透视缩短 */
  const backScale = 0.55;
  const bHubY = hub.y;
  const bRHx = C.cx + (rH.x-C.cx)*backScale, bRHy = hub.y + (rH.y-hub.y)*backScale;
  const bRTx = bRHx + (rT.x-rH.x)*backScale, bRTy = bRHy + (rT.y-rH.y)*backScale;
  const bLHx = C.cx - (rH.x-C.cx)*backScale, bLHy = bRHy;
  const bLtx = bLHx - (rT.x-rH.x)*backScale, bLty = bLHy - (rT.y-rH.y)*backScale;
  ribBack1.setAttribute('x1',C.cx);ribBack1.setAttribute('y1',bHubY);
  ribBack1.setAttribute('x2',bRHx);ribBack1.setAttribute('y2',bRHy);
  ribBack2.setAttribute('x1',bRHx);ribBack2.setAttribute('y1',bRHy);
  ribBack2.setAttribute('x2',bRTx);ribBack2.setAttribute('y2',bRTy);
  ribBack3.setAttribute('x1',C.cx);ribBack3.setAttribute('y1',bHubY);
  ribBack3.setAttribute('x2',bLHx);ribBack3.setAttribute('y2',bLHy);
  ribBack4.setAttribute('x1',bLHx);ribBack4.setAttribute('y1',bLHy);
  ribBack4.setAttribute('x2',bLtx);ribBack4.setAttribute('y2',bLty);

  /* 铰接点 */
  hingeR.setAttribute('cx',rH.x); hingeR.setAttribute('cy',rH.y);
  hingeL.setAttribute('cx',lH.x); hingeL.setAttribute('cy',lH.y);
  hingeGlowR.setAttribute('cx',rH.x); hingeGlowR.setAttribute('cy',rH.y);
  hingeGlowL.setAttribute('cx',lH.x); hingeGlowL.setAttribute('cy',lH.y);

  /* 铰接点颜色 - 根据应力变化 */
  const stressLevel = clamp((S.torque - C.criticalTorque*0.6)/(C.criticalTorque*0.8), 0, 1);
  const hColor = stressLevel < 0.5
    ? `rgba(0,255,200,${0.6+stressLevel*0.8})`
    : `rgba(${Math.round(lerp(255,255,stressLevel))},${Math.round(lerp(255,80,stressLevel))},${Math.round(lerp(200,50,stressLevel))},${0.7+stressLevel*0.3})`;
  hingeR.setAttribute('stroke', hColor);
  hingeL.setAttribute('stroke', hColor);
  hingeGlowR.setAttribute('stroke', hColor.replace(/[\d.]+\)$/,'0.15)'));
  hingeGlowL.setAttribute('stroke', hColor.replace(/[\d.]+\)$/,'0.15)'));

  /* 扭簧 */
  const springAngR = Math.atan2(rH.y-hub.y, rH.x-hub.x);
  const springAngL = Math.atan2(lH.y-hub.y, lH.x-hub.x);
  springR.setAttribute('d', springPath(rH.x, rH.y, springAngR+Math.PI/2, stressLevel));
  springL.setAttribute('d', springPath(lH.x, lH.y, springAngL-Math.PI/2, stressLevel));
  const springColor = stressLevel < 0.5 ? C.springNorm : 
    `rgb(${Math.round(lerp(68,255,stressLevel*2))},${Math.round(lerp(221,85,stressLevel*2))},${Math.round(lerp(170,51,stressLevel*2))})`;
  springR.setAttribute('stroke', springColor);
  springL.setAttribute('stroke', springColor);

  /* 伞面 */
  canopyR.setAttribute('d', canopyPath(geo,'right'));
  canopyL.setAttribute('d', canopyPath(geo,'left'));
  canopyFillR.setAttribute('d', canopyFillPath(geo,'right'));
  canopyFillL.setAttribute('d', canopyFillPath(geo,'left'));

  /* 伞面应力色变 */
  const canopyStroke = S.flip > 0.1
    ? `rgba(${Math.round(lerp(0,255,S.flip))},${Math.round(lerp(212,140,S.flip))},${Math.round(lerp(190,60,S.flip))},${lerp(0.55,0.7,S.flip)})`
    : 'rgba(0,212,190,0.55)';
  canopyR.setAttribute('stroke', canopyStroke);
  canopyL.setAttribute('stroke', canopyStroke);
  canopyFillR.setAttribute('fill', `rgba(0,212,190,${lerp(0.06,0.03,S.flip)})`);
  canopyFillL.setAttribute('fill', `rgba(0,212,190,${lerp(0.06,0.03,S.flip)})`);

  /* 释风孔 */
  const holeY = hub.y + 16;
  holeGlowEl.setAttribute('cx', C.cx);
  holeGlowEl.setAttribute('cy', holeY);
  const holeOpacity = S.wind > 1 ? clamp(S.wind/5, 0.1, 0.7) : 0;
  holeGlowEl.setAttribute('stroke', `rgba(0,255,200,${holeOpacity})`);

  /* 导流罩 */
  const defBaseY = holeY - 2;
  const defTopY = defBaseY - C.deflectorH;
  deflectorPole.setAttribute('x1',C.cx); deflectorPole.setAttribute('y1',hub.y+2);
  deflectorPole.setAttribute('x2',C.cx); deflectorPole.setAttribute('y2',defTopY+4);
  const defPath = `M${C.cx-C.deflectorHW} ${defBaseY} Q${C.cx-C.deflectorHW*0.3} ${defTopY-4} ${C.cx} ${defTopY} Q${C.cx+C.deflectorHW*0.3} ${defTopY-4} ${C.cx+C.deflectorHW} ${defBaseY}`;
  deflectorCover.setAttribute('d', defPath);
  /* 导流罩与伞面连接线 */
  deflectorConnectR.setAttribute('x1',C.cx+C.deflectorHW); deflectorConnectR.setAttribute('y1',defBaseY);
  deflectorConnectR.setAttribute('x2',C.cx+C.holeHW+2); deflectorConnectR.setAttribute('y2',holeY);
  deflectorConnectL.setAttribute('x1',C.cx-C.deflectorHW); deflectorConnectL.setAttribute('y1',defBaseY);
  deflectorConnectL.setAttribute('x2',C.cx-C.holeHW-2); deflectorConnectL.setAttribute('y2',holeY);
}

/* ===== 更新力箭头 ===== */
function updateForces(geo){
  const {hub,rH,rT,lH,lT} = geo;
  const windIntensity = clamp(S.wind/10, 0, 1);
  /* 右侧风向箭头(3个) */
  const arrowYs = [hub.y+30, rH.y-10, rT.y-20];
  const arrowXs = [C.cx+80, rH.x+20, rT.x-30];
  for(let i=0;i<3;i++){
    if(windIntensity < 0.05){forceArrows[i].setAttribute('opacity',0);continue;}
    const ax = arrowXs[i] + 40;
    const ay = arrowYs[i];
    const aLen = 25 + windIntensity*30;
    /* 升力分量 - 上翻时减小 */
    const liftY = -8*windIntensity*(1-S.flip*0.7);
    const d = `M${ax} ${ay} L${ax-aLen} ${ay} M${ax-aLen} ${ay} L${ax-aLen+6} ${ay-4} M${ax-aLen} ${ay} L${ax-aLen+6} ${ay+4}`;
    forceArrows[i].setAttribute('d', d);
    const arrowColor = S.flip > 0.3 ? `rgba(255,${Math.round(lerp(180,107,S.flip))},${Math.round(lerp(80,53,S.flip))},${windIntensity*0.6})` : `rgba(100,170,255,${windIntensity*0.5})`;
    forceArrows[i].setAttribute('stroke', arrowColor);
    forceArrows[i].setAttribute('opacity', windIntensity);
  }
  /* 左侧镜像 */
  const arrowYsL = [hub.y+30, lH.y-10, lT.y-20];
  const arrowXsL = [C.cx-80, lH.x-20, lT.x+30];
  for(let i=3;i<6;i++){
    const idx=i-3;
    if(windIntensity < 0.05){forceArrows[i].setAttribute('opacity',0);continue;}
    const ax = arrowXsL[idx] - 40;
    const ay = arrowYsL[idx];
    const aLen = 25 + windIntensity*30;
    const d = `M${ax} ${ay} L${ax+aLen} ${ay} M${ax+aLen} ${ay} L${ax+aLen-6} ${ay-4} M${ax+aLen} ${ay} L${ax+aLen-6} ${ay+4}`;
    forceArrows[i].setAttribute('d', d);
    const arrowColor = S.flip > 0.3 ? `rgba(255,${Math.round(lerp(180,107,S.flip))},${Math.round(lerp(80,53,S.flip))},${windIntensity*0.6})` : `rgba(100,170,255,${windIntensity*0.5})`;
    forceArrows[i].setAttribute('stroke', arrowColor);
    forceArrows[i].setAttribute('opacity', windIntensity);
  }
}

/* ===== 更新粒子 ===== */
function updateParticles(geo, dt){
  const windMag = Math.max(0.2, S.wind);
  const {hub,rH,rT,lH,lT} = geo;

  particles.forEach(p => {
    /* 基础风力 */
    p.vx = 1.2 + windMag * 0.55;
    p.vy = (Math.sin(S.time*2 + p.x*0.01))*0.3;

    /* 伞面区域检测 */
    const inCanopyXRange = p.x > lT.x-20 && p.x < rT.x+20;
    const nearCanopyY = p.y > hub.y - 10 && p.y < Math.max(rT.y, lT.y) + 30;

    if(inCanopyXRange && nearCanopyY){
      /* 计算当前x位置的伞面y值(简化) */
      const relX = p.x - C.cx;
      const absRelX = Math.abs(relX);
      const canopySpan = Math.abs(rT.x - C.cx);
      const t = clamp(absRelX / canopySpan, 0, 1);

      /* 伞面y - 用线性插值近似 */
      let canopyY;
      if(t < 0.52){ /* 内段 */
        const lt = t / 0.52;
        canopyY = lerp(hub.y + 16, (relX > 0 ? rH.y : lH.y) - 7, lt) + 10*lt*(1-lt)*4;
      } else { /* 外段 */
        const lt = (t - 0.52) / 0.48;
        const hY = (relX > 0 ? rH.y : lH.y) - 7;
        const tY = (relX > 0 ? rT.y : lT.y) - 4;
        canopyY = lerp(hY, tY, lt) + 8*lt*(1-lt)*4;
      }

      /* 释风孔区域 - 粒子可通过 */
      const inHoleZone = absRelX < C.holeHW + 5;

      if(p.y > canopyY - 8 && !inHoleZone){
        /* 偏转 - 沿伞面上方流过 */
        p.vy = -2.5 - windMag * 0.4;
        p.vx *= 0.7;
      }

      /* 释风孔 - 向上抽离 */
      if(inHoleZone && p.y > hub.y - 10 && p.y < hub.y + 40){
        p.vy -= 1.5 + windMag * 0.3 * Math.max(0, S.flip);
      }

      /* 上翻后的边缘缝隙 - 粒子穿过 */
      if(S.flip > 0.3 && absRelX > canopySpan * 0.7){
        p.vy -= 0.5;
      }
    }

    /* 导流罩区域 - 文丘里加速 */
    const inDeflectorZone = Math.abs(p.x - C.cx) < C.deflectorHW + 5 && p.y < hub.y + 20 && p.y > hub.y - 50;
    if(inDeflectorZone && S.wind > 3){
      p.vy -= 2.5;
      p.vx *= 0.4;
    }

    p.x += p.vx;
    p.y += p.vy;

    /* 边界重置 */
    if(p.x > 1120 || p.y < -20 || p.y > 660) resetParticle(p, false);

    /* 更新SVG */
    const particleOpacity = clamp(windMag * 0.12, 0.05, 0.7);
    const particleColor = S.flip > 0.3
      ? `rgba(${Math.round(lerp(100,255,S.flip))},${Math.round(lerp(170,140,S.flip))},${Math.round(lerp(255,80,S.flip))},${particleOpacity})`
      : `rgba(100,170,255,${particleOpacity})`;
    p.el.setAttribute('cx', p.x);
    p.el.setAttribute('cy', p.y);
    p.el.setAttribute('fill', particleColor);
    p.el.setAttribute('r', p.baseR * (0.7 + windMag*0.06));
  });
}

/* ===== 文丘里粒子 ===== */
function updateVenturi(geo){
  const {hub} = geo;
  const active = S.wind > 3 && S.flip > 0.15;
  venturiParticles.forEach(vp => {
    if(!active){vp.el.setAttribute('opacity',0);return;}
    vp.t += vp.speed * (1 + S.wind * 0.2);
    if(vp.t > 1) vp.t -= 1;
    /* 从导流罩底部向上加速 */
    const defBaseY = hub.y + 14;
    const defTopY = defBaseY - C.deflectorH - 10;
    const vy = lerp(defBaseY, defTopY - 30, vp.t);
    const vx = C.cx + (Math.sin(vp.t * 6 + S.time) * 8);
    const opacity = clamp(S.wind/6, 0, 0.8) * (vp.t < 0.1 ? vp.t/0.1 : vp.t > 0.85 ? (1-vp.t)/0.15 : 1);
    vp.el.setAttribute('cx', vx);
    vp.el.setAttribute('cy', vy);
    vp.el.setAttribute('opacity', opacity);
    vp.el.setAttribute('r', 1.5 + vp.t * 2);
    vp.el.setAttribute('fill', `rgba(0,255,200,${0.4+vp.t*0.4})`);
  });
}

/* ===== 标注更新 ===== */
function updateAnnotations(geo){
  const {hub,rH,rT,lH,lT} = geo;
  const w = S.wind;

  /* 扭簧铰接 - 始终可见,高亮时更亮 */
  const springVis = w > 0.5 ? 1 : 0.3;
  setAnn('annSpring', rH.x+18, rH.y-28, rH.x+5, rH.y-8, springVis);

  /* 释风孔 */
  const holeVis = w > 1 ? clamp((w-1)/4, 0, 1) : 0;
  setAnn('annHole', C.cx + C.holeHW + 15, hub.y+5, C.cx + C.holeHW + 3, hub.y+14, holeVis);

  /* 导流罩 */
  const defVis = w > 1.5 ? clamp((w-1.5)/3, 0, 1) : 0;
  setAnn('annDeflector', C.cx + C.deflectorHW + 15, hub.y - C.deflectorH + 5, C.cx + C.deflectorHW + 2, hub.y - C.deflectorH/2 + 8, defVis);

  /* 文丘里 */
  const ventVis = S.flip > 0.15 ? clamp(S.flip * 2, 0, 1) : 0;
  setAnn('annVenturi', C.cx - C.deflectorHW - 85, hub.y - C.deflectorH - 10, C.cx - C.deflectorHW - 5, hub.y - C.deflectorH/2, ventVis);

  /* 复位 */
  const resetVis = (S.targetWind < S.wind - 0.5 && S.flip > 0.05) ? clamp(S.flip, 0, 0.9) : 0;
  setAnn('annReset', lH.x - 65, lH.y + 20, lH.x - 5, lH.y + 5, resetVis);
}

function setAnn(id, tx, ty, lx, ly, opacity){
  const a = annEls[id];
  a.g.setAttribute('opacity', opacity);
  a.line.setAttribute('x1',lx); a.line.setAttribute('y1',ly);
  a.line.setAttribute('x2',tx); a.line.setAttribute('y2',ty);
  a.t1.setAttribute('x',tx+4); a.t1.setAttribute('y',ty);
  a.t2.setAttribute('x',tx+4); a.t2.setAttribute('y',ty+12);
}

/* ===== 数据面板更新 ===== */
function updateUI(){
  document.getElementById('dWind').textContent = S.wind.toFixed(1);
  const speed = (S.wind * 3.1).toFixed(1);
  document.getElementById('dSpeed').textContent = speed;

  const torqueStr = S.torque.toFixed(2);
  const torqueEl = document.getElementById('dTorque');
  const torquePct = clamp(S.torque / (C.criticalTorque * 2) * 100, 0, 100);
  torqueEl.innerHTML = `${torqueStr} <span style="font-size:13px;color:var(--text-sec)">/ 3.50 N·m</span>`;
  if(S.torque > C.criticalTorque) torqueEl.className = 'dval stress';
  else if(S.torque > C.criticalTorque * 0.7) torqueEl.className = 'dval';
  else torqueEl.className = 'dval accent';

  const bar = document.getElementById('torqueBar');
  bar.style.width = torquePct + '%';
  bar.style.background = S.torque > C.criticalTorque ? 'var(--stress)' : S.torque > C.criticalTorque*0.7 ? '#ffaa00' : 'var(--accent)';

  /* 状态 */
  const stateEl = document.getElementById('dState');
  if(S.flip > 0.3){
    stateEl.innerHTML = '<span class="state-badge state-releasing">风压卸载中</span>';
  } else if(S.torque > C.criticalTorque * 0.7 && S.flip <= 0.3){
    stateEl.innerHTML = '<span class="state-badge state-stressed">承压临界</span>';
  } else if(S.targetWind < S.wind - 0.5 && S.flip > 0.02 && S.flip <= 0.3){
    stateEl.innerHTML = '<span class="state-badge state-reset">扭簧复位中</span>';
  } else {
    stateEl.innerHTML = '<span class="state-badge state-normal">标准遮挡</span>';
  }

  document.getElementById('sliderVal').textContent = Math.round(S.targetWind * 10) / 10;
}

/* ===== 主动画循环 ===== */
let lastTime = 0;
function animate(time){
  const dt = Math.min((time - lastTime) / 1000, 0.05);
  lastTime = time;
  S.time = time / 1000;

  /* 自动演示 */
  if(S.autoPlay){
    S.autoTimer += dt;
    const cycle = 16;
    const phase = S.autoTimer % cycle;
    if(phase < 3) S.targetWind = lerp(0, 3, phase/3);
    else if(phase < 5) S.targetWind = lerp(3, 7, (phase-3)/2);
    else if(phase < 8) S.targetWind = 7 + Math.sin((phase-5)*1.5)*1.5;
    else if(phase < 10) S.targetWind = lerp(7, 9.5, (phase-8)/2);
    else if(phase < 12) S.targetWind = lerp(9.5, 4, (phase-10)/2);
    else if(phase < 14) S.targetWind = lerp(4, 1, (phase-12)/2);
    else S.targetWind = lerp(1, 0, (phase-14)/2);
    document.getElementById('windSlider').value = S.targetWind;
  }

  /* 阵风模式 */
  if(S.gustMode && !S.autoPlay){
    S.gustTimer += dt;
    S.targetWind = 3 + Math.sin(S.gustTimer * 0.8) * 2 + Math.sin(S.gustTimer * 2.3) * 1.5 + Math.sin(S.gustTimer * 5.1) * 0.8;
    S.targetWind = clamp(S.targetWind, 0, 10);
    document.getElementById('windSlider').value = S.targetWind;
  }

  /* 平滑风力插值 */
  S.wind += (S.targetWind - S.wind) * 0.04;

  /* 扭矩计算 */
  S.torque = S.wind * C.torquePerWind;

  /* 翻转计算 */
  if(S.torque > C.criticalTorque){
    S.targetFlip = clamp((S.torque - C.criticalTorque) / (C.criticalTorque * 0.9), 0, 1);
  } else {
    S.targetFlip = 0;
  }
  S.flip += (S.targetFlip - S.flip) * 0.055;

  /* 计算几何 */
  const geo = computeGeo(S.flip);

  /* 更新各层 */
  updateUmbrella(geo);
  updateForces(geo);
  updateParticles(geo, dt);
  updateVenturi(geo);
  updateAnnotations(geo);
  updateUI();

  requestAnimationFrame(animate);
}

/* ===== 交互控制 ===== */
const slider = document.getElementById('windSlider');
slider.addEventListener('input', e => {
  if(!S.autoPlay && !S.gustMode) S.targetWind = parseFloat(e.target.value);
});

document.getElementById('btnAuto').addEventListener('click', function(){
  S.autoPlay = !S.autoPlay;
  S.gustMode = false;
  this.classList.toggle('active', S.autoPlay);
  document.getElementById('btnGust').classList.remove('active');
  if(S.autoPlay) S.autoTimer = 0;
});

document.getElementById('btnGust').addEventListener('click', function(){
  S.gustMode = !S.gustMode;
  S.autoPlay = false;
  this.classList.toggle('active', S.gustMode);
  document.getElementById('btnAuto').classList.remove('active');
  if(S.gustMode) S.gustTimer = 0;
});

document.getElementById('btnReset').addEventListener('click', function(){
  S.targetWind = 0; S.autoPlay = false; S.gustMode = false;
  document.getElementById('btnAuto').classList.remove('active');
  document.getElementById('btnGust').classList.remove('active');
  slider.value = 0;
});

/* 启动 */
requestAnimationFrame(animate);
</script>
</body>
</html>

实现说明

这是一个完整的抗风自卸载伞 IFR 原理动画,所有代码在单个 HTML 文件中运行,核心设计思路如下:

视觉与交互设计

  • 采用深色技术蓝图风格,背景叠加微妙网格,营造工程制图氛围
  • 伞体使用 SVG 侧面剖视图,清晰展示分段铰接结构、释风孔和导流罩的空间关系
  • 伞骨后排以透视缩短的半透明线条暗示三维深度
  • 铰接点颜色随扭矩应力从青绿渐变至橙红,扭簧符号同步变色——视觉引导直接锁定核心创新点

动态原理展示

  • 风力粒子系统(75 个粒子)实时响应风力滑块,遇伞面偏转、穿释风孔上行、经导流罩加速
  • 扭矩超过 3.5 N·m 临界值时,外侧伞骨平滑上翻形成"倒碗"导流形态,伞面颜色同步偏暖表示应力释放
  • 导流罩下方专设文丘里加速粒子组,强风时可见气流被抽离加速
  • 风力减弱后扭簧自动释放势能驱动伞骨复位,全程无需操作——直接体现 IFR 的"自行"特征

交互方式

  • 风力滑块:0-10 级连续控制,实时观察各阶段机理
  • 自动演示:16 秒完整周期(微风→强风→阵风→衰减→复位)
  • 阵风模式:叠加多频率正弦波模拟真实阵风,观察动态翻转-复位过程
  • 右侧数据面板实时显示风速、扭矩(含进度条)、伞骨状态标签
积分规则:第一轮对话扣减6分,后续每轮扣4分