<!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分
等待动画代码生成...
