分享图
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 rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=IBM+Plex+Mono:wght@300;400;500&family=Noto+Serif+SC:wght@600;900&display=swap" rel="stylesheet">
<style>
:root{--bg:#060810;--fg:#e0e4ec;--muted:#5a6272;--accent:#00e5ff;--accent2:#ff9f43;--danger:#ff4757;--card:rgba(12,14,22,0.92);--border:rgba(255,255,255,0.07);--glass:rgba(255,255,255,0.03)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;height:100vh;display:flex;flex-direction:column;overflow:hidden;position:relative}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 35% 25%,rgba(0,229,255,0.025) 0%,transparent 55%),radial-gradient(ellipse at 65% 75%,rgba(255,159,67,0.015) 0%,transparent 50%);z-index:0;pointer-events:none}
body::after{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(255,255,255,0.012) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.012) 1px,transparent 1px);background-size:50px 50px;z-index:0;pointer-events:none}
header{position:relative;z-index:10;padding:16px 28px 0;display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}
header h1{font-family:'Noto Serif SC',serif;font-weight:900;font-size:20px;letter-spacing:2px;background:linear-gradient(135deg,#e0e4ec 40%,var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
header .sub{font-size:10px;color:var(--muted);letter-spacing:1px;font-weight:300}
.main-wrap{flex:1;display:flex;align-items:center;justify-content:center;position:relative;z-index:5;padding:8px;min-height:0}
.svg-box{width:min(62vh,62vw);height:min(62vh,62vw);position:relative;flex-shrink:0}
.svg-box svg{width:100%;height:100%;display:block}
.panel{position:absolute;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;backdrop-filter:blur(16px);z-index:20}
.panel-t{font-size:9px;text-transform:uppercase;letter-spacing:2px;color:var(--accent);margin-bottom:8px;font-weight:500}
.ifr-panel{top:16px;left:20px;max-width:230px}
.ifr-panel .panel-t{color:var(--accent2)}
.ifr-panel p{font-size:10px;line-height:1.7;color:var(--muted)}
.ifr-panel .hl{color:var(--accent);font-weight:500}
.phase-panel{top:16px;right:20px;width:170px}
.phase-s{display:flex;align-items:center;gap:7px;padding:4px 0;font-size:10px;color:var(--muted);transition:color .3s,transform .3s}
.phase-s.on{color:var(--fg);transform:translateX(3px)}
.phase-d{width:7px;height:7px;border-radius:50%;border:1.5px solid var(--muted);transition:all .3s;flex-shrink:0}
.phase-s.on .phase-d{border-color:var(--accent);background:var(--accent);box-shadow:0 0 8px rgba(0,229,255,0.5)}
.xsec-panel{bottom:80px;left:20px;width:310px}
.xsec-panel svg{width:100%;height:110px;display:block}
.stat-panel{bottom:80px;right:20px;width:210px}
.stat-r{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid var(--border);font-size:10px}
.stat-r:last-child{border:none}
.stat-l{color:var(--muted)}
.stat-v{font-weight:500}
.stat-v.c1{color:var(--accent)}.stat-v.c2{color:var(--accent2)}.stat-v.cd{color:var(--danger)}
.ctrl{position:relative;z-index:20;padding:12px 28px 16px;display:flex;align-items:center;gap:18px;background:linear-gradient(transparent,rgba(6,8,16,0.85))}
.sl-grp{flex:1;display:flex;align-items:center;gap:10px}
.sl-lbl{font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);white-space:nowrap;min-width:60px}
input[type=range]{flex:1;-webkit-appearance:none;height:3px;background:var(--border);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 10px rgba(0,229,255,0.35);transition:box-shadow .2s}
input[type=range]::-webkit-slider-thumb:hover{box-shadow:0 0 16px rgba(0,229,255,0.6)}
.sl-val{font-size:11px;color:var(--accent);min-width:36px;text-align:right;font-weight:500}
.btn{padding:7px 16px;border:1px solid var(--border);border-radius:5px;background:var(--glass);color:var(--fg);font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:1px;cursor:pointer;transition:all .2s;text-transform:uppercase;white-space:nowrap}
.btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(0,229,255,0.04)}
.btn.on{border-color:var(--accent);color:var(--accent);background:rgba(0,229,255,0.08);box-shadow:0 0 10px rgba(0,229,255,0.12)}
.btn.wb.on{border-color:var(--accent2);color:var(--accent2);background:rgba(255,159,67,0.08);box-shadow:0 0 10px rgba(255,159,67,0.12)}
.toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.92);background:var(--card);border:1px solid var(--accent);border-radius:10px;padding:14px 24px;font-size:12px;color:var(--accent);letter-spacing:1px;z-index:100;opacity:0;transition:all .35s cubic-bezier(.34,1.56,.64,1);pointer-events:none;box-shadow:0 0 30px rgba(0,229,255,0.12);white-space:nowrap}
.toast.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
@media(max-width:900px){.xsec-panel,.stat-panel,.ifr-panel{display:none}.phase-panel{width:140px}.svg-box{width:min(70vh,80vw);height:min(70vh,80vw)}header h1{font-size:15px}}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<header>
<h1>IFR 自适应扩展伞</h1>
<span class="sub">遮挡不足 vs 进出门不便 — 矛盾消除原理动画</span>
</header>
<div class="main-wrap">
<div class="svg-box">
<svg id="msvg" viewBox="-300 -300 600 600" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="gc" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="5" result="b"/><feFlood flood-color="#00e5ff" flood-opacity="0.7"/><feComposite in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="ga" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="4" result="b"/><feFlood flood-color="#ff9f43" flood-opacity="0.55"/><feComposite in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="ss" x="-15%" y="-15%" width="130%" height="130%"><feGaussianBlur stdDeviation="2.5" result="b"/><feFlood flood-color="#000" flood-opacity="0.25"/><feComposite in2="b" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<radialGradient id="mcg" cx="50%" cy="50%" r="55%"><stop offset="0%" stop-color="#0a3d4a"/><stop offset="65%" stop-color="#0f4c5c"/><stop offset="100%" stop-color="#1a6a7a"/></radialGradient>
<radialGradient id="acg" cx="35%" cy="35%" r="70%"><stop offset="0%" stop-color="#1a7a8a"/><stop offset="100%" stop-color="#28a0b0"/></radialGradient>
</defs>
<g id="g-guide" opacity="0.12"></g>
<g id="g-rain"></g>
<g id="g-wind"></g>
<g id="g-lrib"></g>
<g id="g-mcan" filter="url(#ss)"></g>
<g id="g-sring"></g>
<g id="g-acan" filter="url(#ss)"></g>
<g id="g-srib"></g>
<g id="g-mag"></g>
<g id="g-flash"></g>
<g id="g-hub"></g>
<g id="g-anno"></g>
</svg>
</div>
</div>
<div class="panel ifr-panel">
<div class="panel-t">IFR 资源利用</div>
<p>利用已有<span class="hl">长骨作为导轨</span>,副骨空心套接滑动;<span class="hl">磁力被动锁合</span>无需额外卡扣。新增部件仅副骨架与磁吸条,<span class="hl">零操作负担</span>扩展遮挡面积。</p>
</div>
<div class="panel phase-panel">
<div class="panel-t">动作时序</div>
<div class="phase-s on" data-p="0"><span class="phase-d"></span><span>收拢状态</span></div>
<div class="phase-s" data-p="1"><span class="phase-d"></span><span>推出副骨</span></div>
<div class="phase-s" data-p="2"><span class="phase-d"></span><span>磁吸锁定</span></div>
<div class="phase-s" data-p="3"><span class="phase-d"></span><span>大面积遮挡</span></div>
</div>
<div class="panel xsec-panel">
<div class="panel-t">截面机理</div>
<svg id="xsvg" viewBox="0 0 380 130" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div class="panel stat-panel">
<div class="panel-t">实时参数</div>
<div class="stat-r"><span class="stat-l">遮挡半径</span><span class="stat-v c1" id="sv-r">50 cm</span></div>
<div class="stat-r"><span class="stat-l">遮挡面积</span><span class="stat-v" id="sv-a">0.79 m²</span></div>
<div class="stat-r"><span class="stat-l">磁吸状态</span><span class="stat-v" id="sv-m">未吸附</span></div>
<div class="stat-r"><span class="stat-l">副骨行程</span><span class="stat-v c2" id="sv-t">0 / 20 cm</span></div>
<div class="stat-r"><span class="stat-l">抗风等级</span><span class="stat-v" id="sv-w">强</span></div>
</div>
<div class="ctrl">
<div class="sl-grp">
<span class="sl-lbl">扩展程度</span>
<input type="range" id="sl" min="0" max="100" value="0" step="1">
<span class="sl-val" id="sv">0%</span>
</div>
<button class="btn" id="abtn">自动演示</button>
<button class="btn wb" id="wbtn">风力模拟</button>
</div>
<div class="toast" id="toast"></div>

<script>
/* ====== 常量 ====== */
const N=8, MAIN_R=150, LONG_RIB=268, SHORT_RIB=118;
const RET=35, EXT=150, GAP=0.025;
const SCALE=50/150; // cm / svg单位

/* ====== 状态 ====== */
let exp=0, windOn=false, autoOn=false, autoT=0, t=0;
let wasAttached=false, flashT=0;
let toastTimer=null;

/* ====== DOM ====== */
const sl=document.getElementById('sl'), svEl=document.getElementById('sv');
const abtn=document.getElementById('abtn'), wbtn=document.getElementById('wbtn');
const toastEl=document.getElementById('toast');

/* ====== 动态SVG元素引用 ====== */
let auxPaths=[], sRibs=[], mStrips=[], rainEls=[], windEls=[];
/* 截面元素 */
let xsLong,xsSTop,xsSBot,xsMainC,xsAuxC,xsRing,xsMag,xsArrow,xsLabelS,xsLabelM;

/* ====== 工具函数 ====== */
const NS='http://www.w3.org/2000/svg';
function mk(tag,attrs,parent){
  const el=document.createElementNS(NS,tag);
  for(const[k,v] of Object.entries(attrs)) el.setAttribute(k,v);
  if(parent) parent.appendChild(el);
  return el;
}
function g(id){return document.getElementById(id);}
function pol(r,a){return{x:r*Math.cos(a),y:r*Math.sin(a)};}
function ribA(){return Array.from({length:N},(_,i)=>i*2*Math.PI/N+Math.PI/N);}
function segA(angles,i){
  const a1=angles[i]+GAP, a2=angles[(i+1)%N]-GAP+((i+1)%N===0?2*Math.PI:0);
  return[a1,a2];
}

/* ====== 构建主场景 ====== */
function buildMain(){
  const angles=ribA();
  /* 参考圈 */
  const gg=g('g-guide');
  mk('circle',{cx:0,cy:0,r:MAIN_R,fill:'none',stroke:'#00e5ff','stroke-width':.5,'stroke-dasharray':'3 5'},gg);
  mk('circle',{cx:0,cy:0,r:EXT+SHORT_RIB,fill:'none',stroke:'#ff9f43','stroke-width':.5,'stroke-dasharray':'3 5'},gg);
  /* 参考标注 */
  mk('text',{x:MAIN_R+6,y:-6,fill:'#00e5ff','font-size':'7',opacity:.5,'font-family':'IBM Plex Mono,monospace'},gg).textContent='R=50cm';
  mk('text',{x:EXT+SHORT_RIB+6,y:-6,fill:'#ff9f43','font-size':'7',opacity:.5,'font-family':'IBM Plex Mono,monospace'},gg).textContent='R≈88cm';

  /* 长骨 */
  const gl=g('g-lrib');
  angles.forEach(a=>{
    const p=pol(LONG_RIB,a);
    mk('line',{x1:0,y1:0,x2:p.x,y2:p.y,stroke:'#6a7a8a','stroke-width':2,'stroke-linecap':'round',opacity:.55},gl);
  });

  /* 主伞面 */
  const gm=g('g-mcan');
  for(let i=0;i<N;i++){
    const[a1,a2]=segA(angles,i);
    const p1=pol(MAIN_R,a1),p2=pol(MAIN_R,a2);
    mk('path',{
      d:`M0 0L${p1.x.toFixed(1)} ${p1.y.toFixed(1)}A${MAIN_R} ${MAIN_R} 0 0 1 ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}Z`,
      fill:'url(#mcg)',stroke:'#0a3d4a','stroke-width':.4,opacity:.92
    },gm);
  }

  /* 钢圈 */
  const gs=g('g-sring');
  mk('circle',{cx:0,cy:0,r:MAIN_R,fill:'none',stroke:'#b0bcc8','stroke-width':2.2,opacity:.5},gs);

  /* 副伞面(动态) */
  const ga=g('g-acan');
  for(let i=0;i<N;i++){
    auxPaths.push(mk('path',{fill:'url(#acg)',stroke:'#1a6a7a','stroke-width':.4,opacity:0},ga));
  }

  /* 副骨架(动态) */
  const gsr=g('g-srib');
  angles.forEach(a=>{
    const o1=mk('line',{stroke:'#a0b8c8','stroke-width':3.5,'stroke-linecap':'round',opacity:.8},gsr);
    const o2=mk('line',{stroke:'#5a7080','stroke-width':1.2,'stroke-linecap':'round',opacity:.4},gsr);
    /* 管端装饰 */
    const cap=mk('circle',{r:2.5,fill:'#8aa0b0',opacity:.7},gsr);
    sRibs.push({o1,o2,cap,angle:a});
  });

  /* 磁吸条(动态) */
  const gms=g('g-mag');
  angles.forEach(a=>{
    const r=mk('rect',{width:16,height:4.5,rx:2,fill:'#00e5ff',opacity:0},gms);
    mStrips.push({el:r,angle:a});
  });

  /* 中心枢纽 */
  const gh=g('g-hub');
  mk('circle',{cx:0,cy:0,r:13,fill:'#1e2228',stroke:'#4a5568','stroke-width':1.8},gh);
  mk('circle',{cx:0,cy:0,r:5.5,fill:'#3a4555',stroke:'#5a6a7a','stroke-width':1},gh);
  mk('circle',{cx:0,cy:0,r:2,fill:'#7a8a9a'},gh);

  /* 雨 */
  const gr=g('g-rain');
  for(let i=0;i<70;i++){
    const x=-280+Math.random()*560, y=-280+Math.random()*560;
    const ln=mk('line',{x1:x,y1:y,x2:x-.5,y2:y+6,stroke:'rgba(100,150,200,0.25)','stroke-width':.7,'stroke-linecap':'round'},gr);
    rainEls.push({el:ln,x,y,sp:1.8+Math.random()*2.5,len:4+Math.random()*5});
  }

  /* 风(初始隐藏) */
  const gw=g('g-wind');
  for(let i=0;i<12;i++){
    const y=-200+i*35;
    const ln=mk('line',{x1:-300,y1:y,x2:-260,y2:y,stroke:'rgba(255,159,67,0.4)','stroke-width':1.5,'stroke-linecap':'round',opacity:0},gw);
    const ar=mk('polygon',{points:'-260,y -268,y-3 -268,y+3',fill:'rgba(255,159,67,0.4)',opacity:0},gw);
    windEls.push({ln,ar,baseY:y,off:Math.random()*300});
  }
}

/* ====== 构建截面图 ====== */
function buildXsec(){
  const svg=document.getElementById('xsvg');
  /* 长骨 */
  xsLong=mk('line',{x1:15,y1:72,x2:365,y2:72,stroke:'#6a7a8a','stroke-width':2.5,'stroke-linecap':'round'},svg);
  /* 主伞面 */
  xsMainC=mk('path',{d:'M15 72 Q90 22 175 70',fill:'#0f4c5c',stroke:'#0a3d4a','stroke-width':.8,opacity:.85},svg);
  /* 副骨架(管状) */
  xsSTop=mk('line',{x1:35,y1:65,x2:165,y2:65,stroke:'#a0b8c8','stroke-width':2.5,'stroke-linecap':'round'},svg);
  xsSBot=mk('line',{x1:35,y1:79,x2:165,y2:79,stroke:'#7a8a9a','stroke-width':2.5,'stroke-linecap':'round'},svg);
  /* 副伞面 */
  xsAuxC=mk('path',{d:'',fill:'#1a7a8a',stroke:'#1a6a7a','stroke-width':.8,opacity:0},svg);
  /* 钢圈 */
  xsRing=mk('circle',{cx:175,cy:72,r:4.5,fill:'#b0bcc8',stroke:'#8a9aaa','stroke-width':1},svg);
  /* 磁吸条 */
  xsMag=mk('rect',{x:168,y:62,width:14,height:5,rx:2,fill:'#00e5ff',opacity:0},svg);
  /* 滑动方向箭头 */
  xsArrow=mk('line',{x1:80,y1:55,x2:140,y2:55,stroke:'#ff9f43','stroke-width':1.2,'stroke-linecap':'round',opacity:0,'stroke-dasharray':'4 3'},svg);
  const arHead=mk('polygon',{points:'140,55 134,52 134,58',fill:'#ff9f43',opacity:0},svg);
  xsArrow._head=arHead;
  /* 标注 */
  const ft={'font-size':'7.5','font-family':'IBM Plex Mono,monospace',fill:'#5a6272'};
  mk('text',{x:8,y:92,...ft},svg).textContent='中心';
  xsLabelM=mk('text',{x:158,y:92,...ft,fill:'#b0bcc8'},svg);xsLabelM.textContent='钢圈';
  xsLabelS=mk('text',{x:40,y:55,...ft,fill:'#a0b8c8'},svg);xsLabelS.textContent='副骨';
  mk('text',{x:330,y:92,...ft},svg).textContent='伞尖';
  /* 行程标注 */
  xsTravelLine=mk('line',{x1:35,y1:100,x2:35,y2:105,stroke:'#ff9f43','stroke-width':.6,opacity:0},svg);
  xsTravelLine2=mk('line',{x1:175,y1:100,x2:175,y2:105,stroke:'#ff9f43','stroke-width':.6,opacity:0},svg);
  xsTravelH=mk('line',{x1:35,y1:103,x2:175,y2:103,stroke:'#ff9f43','stroke-width':.6,opacity:0,'stroke-dasharray':'2 2'},svg);
  xsTravelLbl=mk('text',{x:85,y:115,fill:'#ff9f43','font-size':'7','font-family':'IBM Plex Mono,monospace',opacity:0},svg);
  xsTravelLbl.textContent='20cm行程';
}

/* ====== 更新主场景 ====== */
function updateMain(){
  const e=exp;
  const innerR=RET+e*(EXT-RET);
  const outerR=innerR+SHORT_RIB;
  const angles=ribA();
  const attached=e>=0.97;
  const nearAttach=e>0.85&&e<0.97;

  /* 副伞面路径 */
  for(let i=0;i<N;i++){
    const[a1,a2]=segA(angles,i);
    const ip1=pol(innerR,a1),ip2=pol(innerR,a2);
    const op1=pol(outerR,a1),op2=pol(outerR,a2);
    auxPaths[i].setAttribute('d',
      `M${ip1.x.toFixed(1)} ${ip1.y.toFixed(1)}L${op1.x.toFixed(1)} ${op1.y.toFixed(1)}A${outerR} ${outerR} 0 0 1 ${op2.x.toFixed(1)} ${op2.y.toFixed(1)}L${ip2.x.toFixed(1)} ${ip2.y.toFixed(1)}A${innerR} ${innerR} 0 0 0 ${ip1.x.toFixed(1)} ${ip1.y.toFixed(1)}Z`
    );
    let op=e<0.04?0:Math.min(.88,e*1.15);
    /* 风力下副伞面微抬 */
    if(windOn&&e>0.5) op*=0.7+0.3*Math.sin(t*4+i);
    auxPaths[i].setAttribute('opacity',op.toFixed(2));
  }

  /* 副骨架 */
  sRibs.forEach((sr,idx)=>{
    const p1=pol(innerR,sr.angle),p2=pol(outerR,sr.angle);
    sr.o1.setAttribute('x1',p1.x.toFixed(1));sr.o1.setAttribute('y1',p1.y.toFixed(1));
    sr.o1.setAttribute('x2',p2.x.toFixed(1));sr.o1.setAttribute('y2',p2.y.toFixed(1));
    sr.o2.setAttribute('x1',p1.x.toFixed(1));sr.o2.setAttribute('y1',p1.y.toFixed(1));
    sr.o2.setAttribute('x2',p2.x.toFixed(1));sr.o2.setAttribute('y2',p2.y.toFixed(1));
    sr.cap.setAttribute('cx',p2.x.toFixed(1));sr.cap.setAttribute('cy',p2.y.toFixed(1));
  });

  /* 磁吸条 */
  mStrips.forEach((ms,idx)=>{
    const p=pol(innerR,ms.angle);
    const deg=ms.angle*180/Math.PI;
    ms.el.setAttribute('x',(p.x-8).toFixed(1));
    ms.el.setAttribute('y',(p.y-2.2).toFixed(1));
    ms.el.setAttribute('transform',`rotate(${deg.toFixed(1)},${p.x.toFixed(1)},${p.y.toFixed(1)})`);
    if(attached){
      ms.el.setAttribute('opacity','0.9');
      ms.el.setAttribute('fill','#00e5ff');
    }else if(nearAttach){
      ms.el.setAttribute('opacity',(0.4+0.35*Math.sin(t*8)).toFixed(2));
      ms.el.setAttribute('fill','#00c8dd');
    }else{
      ms.el.setAttribute('opacity',e>0.08?'0.25':'0');
    }
  });

  /* 磁吸锁定闪光 */
  if(attached&&!wasAttached){
    flashT=1;showToast('磁吸锁定 — 无缝拼接扩大遮挡');
  }
  wasAttached=attached;
  const gf=g('g-flash');
  if(flashT>0){
    flashT-=0.018;
    gf.innerHTML='';
    angles.forEach(a=>{
      const p=pol(MAIN_R,a);
      const r=6+flashT*18;
      const op=flashT*0.7;
      mk('circle',{cx:p.x.toFixed(1),cy:p.y.toFixed(1),r:r.toFixed(1),fill:'none',stroke:'#00e5ff','stroke-width':1.5,opacity:op.toFixed(2)},gf);
    });
  }else if(gf.innerHTML)gf.innerHTML='';

  /* 钢圈亮度 */
  const ringEl=g('g-sring').firstChild;
  if(attached){
    ringEl.setAttribute('stroke','#00e5ff');ringEl.setAttribute('opacity','0.7');
  }else if(nearAttach){
    ringEl.setAttribute('stroke','#88aabb');ringEl.setAttribute('opacity','0.5');
  }else{
    ringEl.setAttribute('stroke','#b0bcc8');ringEl.setAttribute('opacity','0.5');
  }
}

/* ====== 更新截面图 ====== */
function updateXsec(){
  const e=exp;
  const innerX=35+e*140;
  const outerX=innerX+135;
  const attached=e>=0.97;

  xsSTop.setAttribute('x1',innerX);xsSTop.setAttribute('x2',outerX);
  xsSBot.setAttribute('x1',innerX);xsSBot.setAttribute('x2',outerX);

  const auxOp=e<0.04?0:Math.min(.78,e*1.1);
  const pkY=28+(1-e)*18;
  xsAuxC.setAttribute('d',`M${innerX} 70 Q${(innerX+outerX)/2} ${pkY.toFixed(0)} ${outerX} 72`);
  xsAuxC.setAttribute('opacity',auxOp.toFixed(2));

  xsMag.setAttribute('x',innerX-7);
  xsMag.setAttribute('opacity',e>0.08?0.55:0);
  if(attached){xsMag.setAttribute('opacity','0.95');xsRing.setAttribute('fill','#00e5ff');}
  else{xsRing.setAttribute('fill','#b0bcc8');}

  /* 滑动箭头 */
  const arrowOp=e>0.05&&e<0.95?0.7:0;
  xsArrow.setAttribute('opacity',arrowOp);
  xsArrow._head.setAttribute('opacity',arrowOp);
  const ax1=innerX+10,ax2=innerX+50;
  xsArrow.setAttribute('x1',ax1);xsArrow.setAttribute('x2',ax2);
  const hy=55;
  xsArrow._head.setAttribute('points',`${ax2},${hy} ${ax2-6},${hy-3} ${ax2-6},${hy+3}`);

  /* 行程标注 */
  const top=e>0.05?0.6:0;
  xsTravelLine.setAttribute('x1',innerX);xsTravelLine.setAttribute('x2',innerX);
  xsTravelLine2.setAttribute('x1',175);xsTravelLine2.setAttribute('x2',175);
  xsTravelH.setAttribute('x1',innerX);xsTravelH.setAttribute('x2',175);
  xsTravelLbl.setAttribute('x',(innerX+175)/2-18);
  [xsTravelLine,xsTravelLine2,xsTravelH,xsTravelLbl].forEach(el=>el.setAttribute('opacity',top));
}

/* ====== 更新雨 ====== */
function updateRain(){
  const outerR=RET+exp*(EXT-RET)+SHORT_RIB;
  rainEls.forEach(r=>{
    r.y+=r.sp;
    if(r.y>285){r.y=-285;r.x=-280+Math.random()*560;}
    /* 伞面遮挡区域 */
    const dist=Math.sqrt(r.x*r.x+r.y*r.y);
    const underUmbrella=dist<outerR&&r.y>-20;
    r.el.setAttribute('x1',r.x);r.el.setAttribute('y1',r.y);
    r.el.setAttribute('x2',r.x-.3);r.el.setAttribute('y2',r.y+r.len);
    r.el.setAttribute('opacity',underUmbrella?'0':'0.25');
  });
}

/* ====== 更新风 ====== */
function updateWind(){
  windEls.forEach((w,idx)=>{
    if(!windOn){w.ln.setAttribute('opacity','0');w.ar.setAttribute('opacity','0');return;}
    w.off+=3;
    if(w.off>620)w.off=-60;
    const x=-300+w.off;
    w.ln.setAttribute('x1',x);w.ln.setAttribute('x2',x+35);
    w.ln.setAttribute('y1',w.baseY);w.ln.setAttribute('y2',w.baseY);
    w.ar.setAttribute('points',`${x+35},${w.baseY} ${x+29},${w.baseY-3} ${x+29},${w.baseY+3}`);
    const op=exp>0.7?0.5:0.3;
    w.ln.setAttribute('opacity',op);w.ar.setAttribute('opacity',op);
  });
}

/* ====== 更新状态面板 ====== */
function updateStat(){
  const innerR=RET+exp*(EXT-RET);
  const outerR=innerR+SHORT_RIB;
  const radiusCm=(outerR*SCALE).toFixed(0);
  const areaM2=(Math.PI*Math.pow(outerR*SCALE/100,2)).toFixed(2);
  const travelCm=((innerR-RET)/(EXT-RET)*20).toFixed(1);
  const attached=exp>=0.97;

  document.getElementById('sv-r').textContent=radiusCm+' cm';
  document.getElementById('sv-a').textContent=areaM2+' m²';
  document.getElementById('sv-t').textContent=travelCm+' / 20 cm';

  const mEl=document.getElementById('sv-m');
  if(attached){mEl.textContent='已吸附';mEl.className='stat-v c1';}
  else if(exp>0.85){mEl.textContent='接近中';mEl.className='stat-v c2';}
  else{mEl.textContent='未吸附';mEl.className='stat-v';}

  const wEl=document.getElementById('sv-w');
  if(exp>0.8){wEl.textContent='较弱';wEl.className='stat-v cd';}
  else if(exp>0.4){wEl.textContent='中等';wEl.className='stat-v c2';}
  else{wEl.textContent='强';wEl.className='stat-v c1';}
}

/* ====== 更新相位指示 ====== */
function updatePhase(){
  let p=0;
  if(exp>=0.97)p=3;
  else if(exp>=0.85)p=2;
  else if(exp>=0.05)p=1;
  document.querySelectorAll('.phase-s').forEach(el=>{
    el.classList.toggle('on',parseInt(el.dataset.p)===p);
  });
}

/* ====== Toast ====== */
function showToast(msg){
  toastEl.textContent=msg;toastEl.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer=setTimeout(()=>toastEl.classList.remove('show'),1800);
}

/* ====== 自动演示 ====== */
let autoPhase=0,autoDir=1;
function toggleAuto(){
  autoOn=!autoOn;
  abtn.classList.toggle('on',autoOn);
  abtn.textContent=autoOn?'停止演示':'自动演示';
  if(autoOn){autoPhase=0;autoT=0;autoDir=1;}
}
function tickAuto(){
  if(!autoOn)return;
  autoT+=0.016;
  /* 阶段:0=收拢等待, 1=展开, 2=锁定闪烁, 3=全展开等待, 4=风力测试, 5=收回 */
  if(autoPhase===0){
    exp=Math.max(0,exp-0.01);
    if(autoT>1){autoPhase=1;autoT=0;}
  }else if(autoPhase===1){
    exp=Math.min(0.97,exp+0.008);
    if(exp>=0.97){autoPhase=2;autoT=0;}
  }else if(autoPhase===2){
    if(autoT>0.6){autoPhase=3;autoT=0;exp=1;}
  }else if(autoPhase===3){
    exp=1;
    if(autoT>1.2){autoPhase=4;autoT=0;windOn=true;wbtn.classList.add('on');}
  }else if(autoPhase===4){
    exp=1;
    if(autoT>2){autoPhase=5;autoT=0;windOn=false;wbtn.classList.remove('on');}
  }else if(autoPhase===5){
    exp=Math.max(0,exp-0.006);
    if(exp<=0){autoPhase=0;autoT=0;}
  }
  sl.value=Math.round(exp*100);
}

/* ====== 主循环 ====== */
function loop(){
  t+=0.016;
  tickAuto();
  updateMain();
  updateXsec();
  updateRain();
  updateWind();
  updateStat();
  updatePhase();
  svEl.textContent=Math.round(exp*100)+'%';
  requestAnimationFrame(loop);
}

/* ====== 事件 ====== */
sl.addEventListener('input',()=>{
  if(autoOn){autoOn=false;abtn.classList.remove('on');abtn.textContent='自动演示';}
  exp=sl.value/100;
});
wbtn.addEventListener('click',()=>{
  windOn=!windOn;
  wbtn.classList.toggle('on',windOn);
  if(windOn&&exp>0.8) showToast('风力加载 — 副伞面自适应卸载');
});

/* ====== 初始化 ====== */
buildMain();
buildXsec();
requestAnimationFrame(loop);
</script>
</body>
</html>

实现说明:

  1. 核心动画原理:顶视视角的伞面采用 8 片扇形段表示主伞面与副伞面,副骨架(空心管状)沿长骨滑动的位移由 expansion 变量统一驱动,所有关联元素(副伞面路径、副骨架位置、磁吸条位置)实时联动更新。

  2. IFR 理想解视觉引导

    • 磁吸锁定是核心创新时刻——当副骨内端抵达钢圈半径时,8 个磁吸点同时触发青色脉冲光环(flashT 控制),配合 Toast 提示"磁吸锁定—无缝拼接";
    • 副骨架的滑动方向在截面图中用琥珀色箭头标注,行程用虚线标注"20cm";
    • 雨粒子在伞面覆盖区域内自动隐藏,直观展示遮挡效果。
  3. 交互控制

    • 扩展滑块:0→100% 连续控制副骨行程;
    • 自动演示:依次演示收拢→展开→磁吸锁定→大风测试→收回的完整时序;
    • 风力模拟:开启后显示风向箭头,副伞面透明度随风力波动(模拟卸载风压),状态面板抗风等级实时变化。
  4. 截面机理图:同步展示单根长骨/副骨套接滑动关系、主/副伞面布料曲线、钢圈与磁吸条的对接过程,所有元素与顶视图联动。

  5. 失效边界提示:展开面积增大后,状态面板"抗风等级"由"强"变为"较弱"并以红色标注,风力模拟时副伞面抖动,直观传达设计边界。

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