分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柔性颈缩式硅胶漏斗 · 动静界面过渡原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#06090f;--fg:#c0cfe0;--muted:#3a4a62;--accent:#00e5bf;--accent2:#00ffd5;--bullet:#ff7043;--bullet2:#ff9a76;--steel:#4a6080;--steel2:#2a3a52;--gold:#ffd54f;--card:#0c1220;--border:#162030}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Noto Sans SC','Chakra Petch',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 50% 20%,#0a1828 0%,transparent 70%),radial-gradient(ellipse at 30% 80%,#08101a 0%,transparent 60%);pointer-events:none;z-index:0}
header{text-align:center;padding:28px 20px 8px;position:relative;z-index:1}
header h1{font-family:'Chakra Petch','Noto Sans SC',sans-serif;font-weight:700;font-size:clamp(20px,3.2vw,32px);letter-spacing:0.06em;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
header p{font-size:13px;color:var(--muted);margin-top:4px;letter-spacing:0.1em;font-weight:300}
.svg-wrap{position:relative;z-index:1;width:96%;max-width:780px;margin:10px auto 0;aspect-ratio:800/1020;border-radius:12px;background:var(--card);border:1px solid var(--border);box-shadow:0 0 60px rgba(0,229,191,0.04),inset 0 0 40px rgba(0,0,0,0.4);overflow:hidden}
.svg-wrap svg{width:100%;height:100%;display:block}
.controls{position:relative;z-index:1;width:96%;max-width:780px;margin:14px auto 30px;display:flex;flex-wrap:wrap;gap:14px 28px;padding:18px 24px;border-radius:10px;background:var(--card);border:1px solid var(--border)}
.ctrl-group{display:flex;align-items:center;gap:10px;flex:1 1 220px;min-width:200px}
.ctrl-group label{font-size:12px;color:var(--muted);white-space:nowrap;min-width:80px;font-family:'Chakra Petch','Noto Sans SC',sans-serif;letter-spacing:0.04em}
.ctrl-group input[type=range]{flex:1;-webkit-appearance:none;appearance:none;height:4px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px rgba(0,229,191,0.5);cursor:pointer}
.ctrl-group .val{font-size:12px;color:var(--accent);min-width:36px;text-align:right;font-family:'Chakra Petch',monospace}
.phase-indicator{width:96%;max-width:780px;margin:0 auto 10px;padding:8px 20px;border-radius:8px;background:rgba(0,229,191,0.06);border:1px solid rgba(0,229,191,0.12);text-align:center;font-size:13px;color:var(--accent2);font-family:'Chakra Petch','Noto Sans SC',sans-serif;letter-spacing:0.06em;min-height:36px;display:flex;align-items:center;justify-content:center;transition:all .3s}
@media(max-width:600px){.ctrl-group{flex:1 1 100%}}
</style>
</head>
<body>
<header>
<h1>柔性颈缩式硅胶漏斗</h1>
<p>动静界面过渡 · IFR 最终理想解原理动画</p>
</header>
<div class="phase-indicator" id="phaseText">初始化中...</div>
<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 800 1020" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div class="controls">
<div class="ctrl-group">
<label>动画速度</label>
<input type="range" id="speedSlider" min="0.2" max="2.5" step="0.1" value="1.0">
<span class="val" id="speedVal">1.0x</span>
</div>
<div class="ctrl-group">
<label>漏斗弹性</label>
<input type="range" id="elastSlider" min="0.3" max="1.8" step="0.1" value="1.0">
<span class="val" id="elastVal">1.0</span>
</div>
<div class="ctrl-group">
<label>软弹可压缩性</label>
<input type="range" id="softSlider" min="0.2" max="1.5" step="0.1" value="1.0">
<span class="val" id="softVal">1.0</span>
</div>
</div>
<script>
(function(){
/* ===== 常量与配置 ===== */
const NS='http://www.w3.org/2000/svg';
const CX=400;
const svg=document.getElementById('mainSvg');

/* 关键几何参数 */
const BULLET_D=56;           // 软弹直径(SVG单位)
const BULLET_L=66;           // 软弹长度
const NECK_D=BULLET_D*0.85;  // 颈缩内径≈47.6
const GUIDE_W=130;            // 导链盒内宽
const GUIDE_TOP=55;
const GUIDE_BOT=245;
const FUNNEL_TOP=245;
const FUNNEL_BOT=600;
const ROTOR_CY=740;
const ROTOR_R=110;
const CHAMBER_D=BULLET_D+8;
const NUM_CH=6;

/* 动画配置 */
let cfg={speed:1.0,elasticity:1.0,softness:1.0};

/* ===== 工具函数 ===== */
function el(tag,attrs,parent){
  const e=document.createElementNS(NS,tag);
  for(const k in attrs) e.setAttribute(k,attrs[k]);
  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))}
function easeInOut(t){return t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
function easeOut(t){return 1-Math.pow(1-t,3)}
function easeIn(t){return t*t*t}

/* ===== 创建SVG结构 ===== */
const defs=el('defs',{},svg);

/* 网格图案 */
const gridPat=el('pattern',{id:'grid',width:40,height:40,patternUnits:'userSpaceOnUse'},defs);
el('line',{x1:0,y1:0,x2:0,y2:40,stroke:'#0e1828',strokeWidth:0.5},gridPat);
el('line',{x1:0,y1:0,x2:40,y2:0,stroke:'#0e1828',strokeWidth:0.5},gridPat);

/* 发光滤镜 */
function makeGlow(id,color,dev){
  const f=el('filter',{id,x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
  el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:dev,result:'b'},f);
  const m=el('feMerge',{},f);
  el('feMergeNode',{in:'b'},m);
  el('feMergeNode',{in:'SourceGraphic'},m);
  return f;
}
makeGlow('glowAcc','',6);
makeGlow('glowBullet','',4);
makeGlow('glowBig','',14);

/* 漏斗渐变 */
const funGrad=el('linearGradient',{id:'funnelGrad',x1:0,y1:0,x2:0,y2:1},defs);
el('stop',{offset:'0%','stop-color':'#00e5bf','stop-opacity':0.25},funGrad);
el('stop',{offset:'60%','stop-color':'#00c9a7','stop-opacity':0.35},funGrad);
el('stop',{offset:'100%','stop-color':'#00ffd5','stop-opacity':0.5},funGrad);

const funGradActive=el('linearGradient',{id:'funnelGradA',x1:0,y1:0,x2:0,y2:1},defs);
el('stop',{offset:'0%','stop-color':'#00e5bf','stop-opacity':0.35},funGradActive);
el('stop',{offset:'50%','stop-color':'#00ffd5','stop-opacity':0.55},funGradActive);
el('stop',{offset:'100%','stop-color':'#80ffee','stop-opacity':0.7},funGradActive);

/* 金属渐变 */
const metalGrad=el('linearGradient',{id:'metalGrad',x1:0,y1:0,x2:1,y2:0},defs);
el('stop',{offset:'0%','stop-color':'#1e2e44'},metalGrad);
el('stop',{offset:'30%','stop-color':'#2a3e58'},metalGrad);
el('stop',{offset:'70%','stop-color':'#2a3e58'},metalGrad);
el('stop',{offset:'100%','stop-color':'#1e2e44'},metalGrad);

/* 背景层 */
el('rect',{width:800,height:1020,fill:'url(#grid)'},svg);
/* 微光氛围 */
el('ellipse',{cx:CX,cy:420,rx:300,ry:350,fill:'rgba(0,229,191,0.015)'},svg);

/* ===== 静态部件:导链盒 ===== */
const guideG=el('g',{id:'guideBox'},svg);
// 外壳
el('rect',{x:CX-GUIDE_W/2-16,y:GUIDE_TOP-10,width:GUIDE_W+32,height:GUIDE_BOT-GUIDE_TOP+20,rx:6,fill:'#0e1828',stroke:'#1e3050',strokeWidth:1.5},guideG);
// 内壁左
el('rect',{x:CX-GUIDE_W/2,y:GUIDE_TOP,width:8,height:GUIDE_BOT-GUIDE_TOP,fill:'url(#metalGrad)'},guideG);
// 内壁右
el('rect',{x:CX+GUIDE_W/2-8,y:GUIDE_TOP,width:8,height:GUIDE_BOT-GUIDE_TOP,fill:'url(#metalGrad)'},guideG);
// 标签
el('text',{x:CX,y:GUIDE_TOP-18,fill:'#4a6a8a','font-size':11,'text-anchor':'middle','font-family':'Chakra Petch,Noto Sans SC,sans-serif','letter-spacing':'0.08em'},guideG).textContent='导链盒出口';

/* ===== 队列子弹(静态装饰) ===== */
const queueG=el('g',{id:'queueBullets'},svg);
function drawQueueBullet(y,opacity){
  const b=el('g',{opacity},queueG);
  el('rect',{x:CX-BULLET_D/2,y:y-BULLET_L/2,width:BULLET_D,height:BULLET_L,rx:BULLET_D/2,fill:'#cc5a36',stroke:'#ff7043',strokeWidth:0.8},b);
  return b;
}
drawQueueBullet(GUIDE_TOP+40,0.35);
drawQueueBullet(GUIDE_TOP+100,0.5);

/* ===== 漏斗组(动态) ===== */
const funnelG=el('g',{id:'funnelGroup'},svg);

/* 漏斗外发光层 */
const funnelGlow=el('path',{id:'funnelGlowPath',fill:'none',stroke:'#00ffd5',strokeWidth:3,opacity:0,filter:'url(#glowBig)'},funnelG);

/* 漏斗填充 */
const funnelFill=el('path',{id:'funnelFill',fill:'url(#funnelGrad)',stroke:'none'},funnelG);

/* 螺旋凸起 */
const ridgeG=el('g',{id:'ridgeGroup'},funnelG);
const RIDGE_COUNT=7;
const ridgeEls=[];
for(let i=0;i<RIDGE_COUNT;i++){
  const r=el('line',{stroke:'#00c9a7','stroke-width':2.5,'stroke-linecap':'round',opacity:0.5},ridgeG);
  ridgeEls.push(r);
}

/* 漏斗壁 */
const funnelWallL=el('path',{id:'funnelWL',fill:'none',stroke:'#00e5bf','stroke-width':2.5},funnelG);
const funnelWallR=el('path',{id:'funnelWR',fill:'none',stroke:'#00e5bf','stroke-width':2.5},funnelG);

/* 颈缩标注线 */
const neckAnnotG=el('g',{id:'neckAnnot',opacity:0},funnelG);

/* ===== 主子弹(动态) ===== */
const bulletG=el('g',{id:'bulletGroup'},svg);
const bulletBody=el('rect',{x:-BULLET_D/2,y:-BULLET_L/2,width:BULLET_D,height:BULLET_L,rx:BULLET_D/2,fill:'#ff7043',stroke:'#ff9a76',strokeWidth:1.2,filter:'url(#glowBullet)'},bulletG);
/* 子弹内部纹理 */
el('ellipse',{cx:0,cy:0,rx:BULLET_D*0.28,ry:BULLET_L*0.32,fill:'rgba(255,154,118,0.25)'},bulletG);

/* ===== 旋转组件 ===== */
const rotorG=el('g',{id:'rotorGroup'},svg);

/* 旋转组件底座 */
el('rect',{x:CX-ROTOR_R-20,y:ROTOR_CY-30,width:(ROTOR_R+20)*2,height:ROTOR_R*2+40,rx:10,fill:'#0a1020',stroke:'#1a2a40',strokeWidth:1.2},rotorG);

/* 受弹口标注 */
el('text',{x:CX,y:ROTOR_CY-ROTOR_R-38,fill:'#4a6a8a','font-size':11,'text-anchor':'middle','font-family':'Chakra Petch,Noto Sans SC,sans-serif','letter-spacing':'0.08em'},rotorG).textContent='旋转组件受弹口';

const rotorSpin=el('g',{id:'rotorSpin'},rotorG);

/* 转子主体圆 */
el('circle',{cx:CX,cy:ROTOR_CY,r:ROTOR_R,fill:'#0e1828',stroke:'#2a3e58',strokeWidth:2},rotorSpin);

/* 枪膛 */
const chambers=[];
for(let i=0;i<NUM_CH;i++){
  const ang=i*(360/NUM_CH);
  const rad=ang*Math.PI/180;
  const chx=CX+ROTOR_R*0.55*Math.cos(rad);
  const chy=ROTOR_CY+ROTOR_R*0.55*Math.sin(rad);
  const ch=el('circle',{cx:chx,cy:chy,r:CHAMBER_D/2,fill:'#060a14',stroke:'#1e3050',strokeWidth:1.5},rotorSpin);
  chambers.push(ch);
}

/* 拨弹齿 */
const teethG=el('g',{id:'teethGroup'},rotorSpin);
const NUM_TEETH=12;
for(let i=0;i<NUM_TEETH;i++){
  const ang=i*(360/NUM_TEETH);
  const rad=ang*Math.PI/180;
  const ix=CX+(ROTOR_R-8)*Math.cos(rad);
  const iy=ROTOR_CY+(ROTOR_R-8)*Math.sin(rad);
  const ox=CX+(ROTOR_R+10)*Math.cos(rad);
  const oy=ROTOR_CY+(ROTOR_R+10)*Math.sin(rad);
  el('line',{x1:ix,y1:iy,x2:ox,y2:oy,stroke:'#4a6a8a','stroke-width':2.5,'stroke-linecap':'round'},teethG);
}

/* 转子中心 */
el('circle',{cx:CX,cy:ROTOR_CY,r:14,fill:'#1a2a40',stroke:'#2a3e58',strokeWidth:1.5},rotorSpin);

/* 旋转方向箭头 */
const arrowG=el('g',{opacity:0.5},rotorG);

/* ===== 标注层 ===== */
const annotG=el('g',{id:'annotGroup'},svg);

/* 颈缩尺寸标注 */
const neckDimG=el('g',{id:'neckDim',opacity:0},annotG);

/* 扩张提示 */
const expandAnnotG=el('g',{id:'expandAnnot',opacity:0},annotG);

/* IFR提示 */
const ifrG=el('g',{id:'ifrHint',opacity:0},annotG);

/* ===== 构建漏斗路径 ===== */
function calcFunnel(exp){
  const topHL=GUIDE_W/2;
  const neckHL=NECK_D/2;
  const expAmt=exp*16*cfg.elasticity;
  const nL=CX-neckHL-expAmt;
  const nR=CX+neckHL+expAmt;
  const tL=CX-topHL;
  const tR=CX+topHL;
  const tY=FUNNEL_TOP;
  const nY=FUNNEL_BOT;

  /* 漏斗轮廓用三段贝塞尔让曲线更自然 */
  const lPath=`M ${tL},${tY} C ${tL},${tY+80} ${nL+20},${nY-140} ${nL+10},${nY-60} Q ${nL},${nY-20} ${nL},${nY}`;
  const rPath=`M ${tR},${tY} C ${tR},${tY+80} ${nR-20},${nY-140} ${nR-10},${nY-60} Q ${nR},${nY-20} ${nR},${nY}`;
  const fillPath=lPath+` L ${nR},${nY} Q ${nR},${nY-20} ${nR-10},${nY-60} C ${nR-20},${nY-140} ${tR},${tY+80} ${tR},${tY} Z`;
  const glowPath=lPath+` L ${nR},${nY} Q ${nR},${nY-20} ${nR-10},${nY-60} C ${nR-20},${nY-140} ${tR},${tY+80} ${tR},${tY} Z`;

  return{lPath,rPath,fillPath,glowPath,nL,nR,tL,tR,tY,nY,neckHL,expAmt};
}

/* ===== 更新螺旋凸起位置 ===== */
function updateRidges(fd){
  const step=20; // 导程20mm,映射到SVG
  const totalH=fd.nY-fd.tY;
  for(let i=0;i<RIDGE_COUNT;i++){
    const t=(i+0.5)/RIDGE_COUNT;
    const y=lerp(fd.tY+20,fd.nY-20,t);
    /* 沿漏斗壁插值得到x */
    const xL=lerp(fd.tL,fd.nL,t);
    const xR=lerp(fd.tR,fd.nR,t);
    const side=i%2===0?-1:1;
    const bx=side<0?xL+4:xR-4;
    const ex=side<0?xL+18:xR-18;
    const angle=side<0?-30:30;
    ridgeEls[i].setAttribute('x1',bx);
    ridgeEls[i].setAttribute('y1',y-8);
    ridgeEls[i].setAttribute('x2',ex);
    ridgeEls[i].setAttribute('y2',y+8);
  }
}

/* ===== 动画状态 ===== */
let progress=0;
let lastTS=0;
let rotorAngle=0;

/* 动画阶段定义 */
const PHASES=[
  {name:'子弹受重力/弹簧下压至漏斗',start:0,end:0.15},
  {name:'沿螺旋凸起滑入颈缩段',start:0.15,end:0.35},
  {name:'拨弹齿抓取 → 硅胶漏斗弹性扩张',start:0.35,end:0.58},
  {name:'软弹滑入旋转枪膛',start:0.58,end:0.75},
  {name:'漏斗回弹复位 · 等待下一发',start:0.75,end:0.92},
  {name:'循环重置',start:0.92,end:1.0}
];

function getPhase(p){
  for(const ph of PHASES){if(p>=ph.start&&p<ph.end)return ph}
  return PHASES[PHASES.length-1];
}
function phaseLocal(p,ph){return clamp((p-ph.start)/(ph.end-ph.start),0,1)}

/* ===== 主动画循环 ===== */
function animate(ts){
  if(!lastTS)lastTS=ts;
  const dt=Math.min((ts-lastTS)/1000,0.05);
  lastTS=ts;

  progress+=dt*cfg.speed*0.28;
  if(progress>=1)progress-=1;

  /* 转子持续旋转 */
  rotorAngle+=dt*cfg.speed*30;
  document.getElementById('rotorSpin').setAttribute('transform',`rotate(${rotorAngle},${CX},${ROTOR_CY})`);

  /* ---- 各阶段计算 ---- */
  const ph=getPhase(progress);
  const lt=phaseLocal(progress,ph);

  /* 子弹位置与形变 */
  let bY,bScaleX,bScaleY,bOpacity;
  /* 漏斗扩张量 */
  let expansion=0;
  /* 子弹Y在各阶段的位置 */
  const bulletStartY=GUIDE_BOT-BULLET_L/2-5;
  const funnelMidY=(FUNNEL_TOP+FUNNEL_BOT)/2;
  const neckY=FUNNEL_BOT-BULLET_L/2-10;
  const chamberY=ROTOR_CY-ROTOR_R*0.45;

  if(progress<0.15){
    /* 阶段1:子弹下落进入漏斗 */
    const t=easeInOut(lt);
    bY=lerp(bulletStartY,FUNNEL_TOP+30,t);
    bScaleX=1;bScaleY=1;bOpacity=1;
  }else if(progress<0.35){
    /* 阶段2:沿漏斗滑向颈缩 */
    const t=easeInOut(lt);
    bY=lerp(FUNNEL_TOP+30,neckY-20,t);
    /* 子弹轻微压缩 */
    const compT=lt*cfg.softness*0.12;
    bScaleX=1-compT;bScaleY=1+compT*0.5;bOpacity=1;
    expansion=lt*0.15;
  }else if(progress<0.58){
    /* 阶段3:拨弹齿抓取,漏斗扩张——核心! */
    const t=easeInOut(lt);
    bY=lerp(neckY-20,neckY+30,t);
    /* 子弹被压缩 */
    const compMax=0.18*cfg.softness;
    const compT=lt<0.5?lt*2:(1-lt)*2;
    bScaleX=1-compMax*compT;bScaleY=1+compMax*compT*0.6;bOpacity=1;
    /* 漏斗扩张:先增后减 */
    expansion=lt<0.6?easeOut(lt/0.6)*cfg.elasticity:cfg.elasticity*(1-easeIn((lt-0.6)/0.4)*0.3);
  }else if(progress<0.75){
    /* 阶段4:子弹进入枪膛 */
    const t=easeOut(lt);
    bY=lerp(neckY+30,chamberY,t);
    bScaleX=lerp(0.88,1,easeOut(lt));bScaleY=lerp(1.08,1,easeOut(lt));bOpacity=1;
    expansion=cfg.elasticity*0.7*(1-easeOut(lt));
  }else if(progress<0.92){
    /* 阶段5:漏斗回弹 */
    const t=easeOut(lt);
    bY=chamberY;bScaleX=1;bScaleY=1;
    bOpacity=1-lt*0.8;
    expansion=cfg.elasticity*0.15*(1-t);
  }else{
    /* 阶段6:重置 */
    bY=bulletStartY;bScaleX=1;bScaleY=1;bOpacity=lt;
    expansion=0;
  }

  /* ---- 更新漏斗 ---- */
  const fd=calcFunnel(expansion);
  document.getElementById('funnelWL').setAttribute('d',fd.lPath);
  document.getElementById('funnerWR')&&0; // skip
  document.getElementById('funnelWR').setAttribute('d',fd.rPath);
  document.getElementById('funnelFill').setAttribute('d',fd.fillPath);
  document.getElementById('funnelGlowPath').setAttribute('d',fd.glowPath);

  /* 漏斗发光强度 */
  const glowIntensity=clamp(expansion/cfg.elasticity,0,1);
  document.getElementById('funnelGlowPath').setAttribute('opacity',glowIntensity*0.6);
  document.getElementById('funnelFill').setAttribute('fill',glowIntensity>0.3?'url(#funnelGradA)':'url(#funnelGrad)');
  document.getElementById('funnelWL').setAttribute('stroke',glowIntensity>0.3?'#00ffd5':'#00e5bf');
  document.getElementById('funnelWR').setAttribute('stroke',glowIntensity>0.3?'#00ffd5':'#00e5bf');
  document.getElementById('funnelWL').setAttribute('stroke-width',2.5+glowIntensity*2);
  document.getElementById('funnelWR').setAttribute('stroke-width',2.5+glowIntensity*2);

  /* 螺旋凸起 */
  updateRidges(fd);
  for(let i=0;i<RIDGE_COUNT;i++){
    ridgeEls[i].setAttribute('opacity',0.4+glowIntensity*0.4);
    ridgeEls[i].setAttribute('stroke-width',2.5+glowIntensity*1.5);
  }

  /* ---- 更新子弹 ---- */
  const bx=CX;
  bulletG.setAttribute('transform',`translate(${bx},${bY}) scale(${bScaleX},${bScaleY})`);
  bulletG.setAttribute('opacity',bOpacity);

  /* ---- 标注更新 ---- */
  // 颈缩尺寸标注(始终显示,扩张时高亮)
  const neckDimEl=document.getElementById('neckDim');
  neckDimEl.setAttribute('opacity',0.5+glowIntensity*0.5);
  updateNeckDim(fd,glowIntensity);

  // 扩张提示(核心阶段高亮)
  const expandEl=document.getElementById('expandAnnot');
  const showExpand=progress>=0.35&&progress<0.65;
  expandEl.setAttribute('opacity',showExpand?0.9:0);
  if(showExpand)updateExpandAnnot(fd);

  // IFR提示
  const ifrEl=document.getElementById('ifrHint');
  const showIFR=progress>=0.4&&progress<0.7;
  ifrEl.setAttribute('opacity',showIFR?0.85:0);

  /* 阶段文字 */
  document.getElementById('phaseText').textContent=ph.name;

  requestAnimationFrame(animate);
}

/* ===== 标注绘制 ===== */
function updateNeckDim(fd,glow){
  const g=document.getElementById('neckDim');
  g.innerHTML='';
  const y=fd.nY+18;
  /* 左尺寸线 */
  el('line',{x1:fd.nL,y1:y,x2:fd.nR,y2:y,stroke:'#4a6a8a','stroke-width':0.8,'stroke-dasharray':'3,3'},g);
  el('line',{x1:fd.nL,y1:y-6,x2:fd.nL,y2:y+6,stroke:'#4a6a8a','stroke-width':0.8},g);
  el('line',{x1:fd.nR,y1:y-6,x2:fd.nR,y2:y+6,stroke:'#4a6a8a','stroke-width':0.8},g);
  const realD=(NECK_D+fd.expAmt*2).toFixed(1);
  el('text',{x:CX,y:y+16,fill:glow>0.3?'#00ffd5':'#4a6a8a','font-size':10,'text-anchor':'middle','font-family':'Chakra Petch,monospace'},g).textContent=`颈缩内径 ≈ ${realD} (${(NECK_D/BULLET_D*100).toFixed(0)}%D)`;
}

function updateExpandAnnot(fd){
  const g=document.getElementById('expandAnnot');
  g.innerHTML='';
  /* 径向扩张箭头 */
  const arrY=fd.nY-30;
  // 左箭头
  el('line',{x1:fd.nL+5,y1:arrY,x2:fd.nL-18,y2:arrY,stroke:'#ffd54f','stroke-width':1.5,'marker-end':'none'},g);
  drawArrowHead(g,fd.nL-18,arrY,-1);
  el('text',{x:fd.nL-22,y:arrY-6,fill:'#ffd54f','font-size':9,'text-anchor':'end','font-family':'Chakra Petch,Noto Sans SC,sans-serif'},g).textContent='径向扩张';
  // 右箭头
  el('line',{x1:fd.nR-5,y1:arrY,x2:fd.nR+18,y2:arrY,stroke:'#ffd54f','stroke-width':1.5},g);
  drawArrowHead(g,fd.nR+18,arrY,1);
  el('text',{x:fd.nR+22,y:arrY-6,fill:'#ffd54f','font-size':9,'text-anchor':'start','font-family':'Chakra Petch,Noto Sans SC,sans-serif'},g).textContent='径向扩张';
}

function drawArrowHead(g,x,y,dir){
  el('polygon',{points:`${x},${y} ${x-dir*7},${y-4} ${x-dir*7},${y+4}`,fill:'#ffd54f'},g);
}

/* IFR提示 */
(function buildIFR(){
  const g=document.getElementById('ifrHint');
  const bx=620,by=480;
  el('rect',{x:bx-10,y:by-16,width:175,height:56,rx:6,fill:'rgba(0,229,191,0.08)',stroke:'rgba(0,229,191,0.25)','stroke-width':1},g);
  el('text',{x:bx,y,fill:'#00e5bf','font-size':11,'font-family':'Chakra Petch,Noto Sans SC,sans-serif','font-weight':600},g).textContent='IFR · 零额外运动件';
  el('text',{x:bx,y:by+18,fill:'#7aaa9a','font-size':10,'font-family':'Noto Sans SC,sans-serif'},g).textContent='仅利用材料弹性消除矛盾';
})();

/* ===== 控件绑定 ===== */
document.getElementById('speedSlider').addEventListener('input',function(){
  cfg.speed=parseFloat(this.value);
  document.getElementById('speedVal').textContent=cfg.speed.toFixed(1)+'x';
});
document.getElementById('elastSlider').addEventListener('input',function(){
  cfg.elasticity=parseFloat(this.value);
  document.getElementById('elastVal').textContent=cfg.elasticity.toFixed(1);
});
document.getElementById('softSlider').addEventListener('input',function(){
  cfg.softness=parseFloat(this.value);
  document.getElementById('softVal').textContent=cfg.softness.toFixed(1);
});

/* ===== 初始化漏斗路径(防止闪烁) ===== */
const initFd=calcFunnel(0);
document.getElementById('funnelWL').setAttribute('d',initFd.lPath);
document.getElementById('funnelWR').setAttribute('d',initFd.rPath);
document.getElementById('funnelFill').setAttribute('d',initFd.fillPath);
document.getElementById('funnelGlowPath').setAttribute('d',initFd.glowPath);
updateRidges(initFd);
updateNeckDim(initFd,0);

/* 启动动画 */
requestAnimationFrame(animate);

})();
</script>
</body>
</html>

实现说明

这是一个完整的单文件 SVG 原理动画,围绕 TRIZ 最终理想解(IFR)思想设计,展示了柔性颈缩式硅胶漏斗如何以零额外运动件消除动静界面的卡弹矛盾。

核心动画逻辑

动画按六个阶段循环运行,每个阶段对应真实的动作时序:

  1. 子弹下压入漏斗 — 子弹靠重力/弹簧滑入漏斗顶部
  2. 沿螺旋凸起滑入颈缩段 — 内壁螺旋导程 20mm 的凸起引导子弹走向
  3. 拨弹齿抓取 → 漏斗弹性扩张核心创新点,颈缩段径向扩张顺应子弹轨迹,子弹同步被压缩
  4. 子弹滑入旋转枪膛 — 扩张消退,子弹恢复原形进入枪膛
  5. 漏斗回弹复位 — 硅胶弹性恢复,等待下一发
  6. 循环重置 — 新子弹就位

视觉引导策略

  • 漏斗扩张时:壁面从青绿变为亮青,线宽加粗,外发光激活,标注变为高亮色
  • 子弹压缩时:水平缩窄、纵向微伸,直观展示软弹可压缩性
  • 黄色箭头标注:在扩张阶段动态出现"径向扩张"方向提示
  • IFR 提示框:核心阶段浮现"零额外运动件 · 仅利用材料弹性消除矛盾"

交互控制

三个滑块分别控制:

  • 动画速度(0.2x ~ 2.5x)
  • 漏斗弹性(影响扩张幅度)
  • 软弹可压缩性(影响子弹形变程度)

用户可以实时调整参数,观察不同弹性/压缩性组合下的过渡行为。

我来重写一个更稳健的版本,修复之前的渲染问题。

<!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=Chakra+Petch:wght@300;400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#060a10;--card:#0a1220;--border:#14202e;--fg:#b0c4de;--muted:#3a506a;--accent:#00e8c0;--accent2:#00ffd5;--bullet:#ff6b35;--gold:#ffd54f}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Noto Sans SC','Chakra Petch',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:12px 8px;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 50% 35%,rgba(0,232,192,0.025) 0%,transparent 65%);pointer-events:none}
header{text-align:center;margin-bottom:6px;position:relative;z-index:1}
header h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:clamp(17px,2.8vw,26px);background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:.06em}
header p{font-size:11px;color:var(--muted);letter-spacing:.12em;margin-top:2px}
.phase-bar{width:95%;max-width:840px;padding:7px 16px;margin-bottom:6px;border-radius:6px;background:rgba(0,232,192,.06);border:1px solid rgba(0,232,192,.1);text-align:center;font-size:12.5px;color:var(--accent2);font-family:'Chakra Petch','Noto Sans SC',sans-serif;letter-spacing:.05em;min-height:32px;display:flex;align-items:center;justify-content:center;position:relative;z-index:1;transition:background .3s}
.svg-box{width:95%;max-width:840px;position:relative;z-index:1;border-radius:10px;background:var(--card);border:1px solid var(--border);box-shadow:0 0 50px rgba(0,232,192,.025),inset 0 0 30px rgba(0,0,0,.4);overflow:hidden}
.svg-box svg{display:block;width:100%;height:auto}
.ctrls{width:95%;max-width:840px;margin-top:10px;margin-bottom:18px;padding:14px 18px;border-radius:8px;background:var(--card);border:1px solid var(--border);display:flex;flex-wrap:wrap;gap:10px 22px;position:relative;z-index:1}
.cg{display:flex;align-items:center;gap:8px;flex:1 1 200px;min-width:175px}
.cg label{font-size:10.5px;color:var(--muted);white-space:nowrap;font-family:'Chakra Petch',sans-serif;letter-spacing:.04em;min-width:72px}
.cg input[type=range]{flex:1;-webkit-appearance:none;appearance:none;height:3px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
.cg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--accent);box-shadow:0 0 6px rgba(0,232,192,.5);cursor:pointer}
.cg .v{font-size:10.5px;color:var(--accent);min-width:28px;text-align:right;font-family:'Chakra Petch',monospace}
@media(max-width:600px){.cg{flex:1 1 100%}}
</style>
</head>
<body>
<header>
<h1>柔性颈缩式硅胶漏斗</h1>
<p>动静界面过渡 · IFR 最终理想解</p>
</header>
<div class="phase-bar" id="phaseBar">准备就绪</div>
<div class="svg-box">
<svg id="svg" viewBox="0 0 900 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="gd" width="40" height="40" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="40" stroke="#0c1520" stroke-width=".5"/>
<line x1="0" y1="0" x2="40" y2="0" stroke="#0c1520" stroke-width=".5"/>
</pattern>
<linearGradient id="fGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00e8c0" stop-opacity=".1"/>
<stop offset="60%" stop-color="#00c9a7" stop-opacity=".18"/>
<stop offset="100%" stop-color="#00ffd5" stop-opacity=".28"/>
</linearGradient>
<linearGradient id="fGradA" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00e8c0" stop-opacity=".18"/>
<stop offset="50%" stop-color="#00ffd5" stop-opacity=".38"/>
<stop offset="100%" stop-color="#80ffe8" stop-opacity=".52"/>
</linearGradient>
<linearGradient id="mGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a2840"/><stop offset="50%" stop-color="#2a3e58"/><stop offset="100%" stop-color="#1a2840"/>
</linearGradient>
<radialGradient id="bGrad" cx=".4" cy=".35">
<stop offset="0%" stop-color="#ff9a6c"/><stop offset="100%" stop-color="#e05520"/>
</radialGradient>
<filter id="gS" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gL" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>

<!-- 背景 -->
<rect width="900" height="1080" fill="#070b12"/>
<rect width="900" height="1080" fill="url(#gd)" opacity=".55"/>
<ellipse cx="450" cy="440" rx="260" ry="300" fill="rgba(0,200,170,.01)"/>

<!-- 导链盒 -->
<rect x="365" y="48" width="170" height="234" rx="5" fill="#0b1525" stroke="#1a2a42" stroke-width="1.5"/>
<rect x="373" y="55" width="7" height="220" rx="1" fill="url(#mGrad)"/>
<rect x="520" y="55" width="7" height="220" rx="1" fill="url(#mGrad)"/>
<text x="450" y="40" fill="#4a6a8a" font-size="11" text-anchor="middle" font-family="Chakra Petch,sans-serif" letter-spacing=".1em">导链盒出口</text>

<!-- 队列子弹 -->
<g opacity=".3">
<rect x="422" y="72" width="56" height="60" rx="28" fill="#a84520" stroke="#c45830" stroke-width=".8"/>
<rect x="422" y="140" width="56" height="60" rx="28" fill="#a84520" stroke="#c45830" stroke-width=".8"/>
</g>

<!-- 漏斗 -->
<path id="fFill" d="" fill="url(#fGrad)"/>
<path id="fGlow" d="" fill="none" stroke="#00ffd5" stroke-width="5" opacity="0" filter="url(#gL)"/>
<line id="r0" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<line id="r1" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<line id="r2" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<line id="r3" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<line id="r4" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<line id="r5" stroke="#00b89c" stroke-width="2" stroke-linecap="round" opacity=".4"/>
<path id="fWL" d="" fill="none" stroke="#00e8c0" stroke-width="2.5" stroke-linecap="round"/>
<path id="fWR" d="" fill="none" stroke="#00e8c0" stroke-width="2.5" stroke-linecap="round"/>

<!-- 连接标记 -->
<text x="450" y="670" fill="#2a4060" font-size="9" text-anchor="middle" font-family="Chakra Petch,sans-serif" letter-spacing=".08em">▼ 受弹口对接 ▼</text>

<!-- 子弹 -->
<g id="bG" filter="url(#gS)">
<rect id="bR" x="-28" y="-33" width="56" height="66" rx="28" fill="url(#bGrad)" stroke="#ff9a6c" stroke-width="1"/>
<ellipse cx="0" cy="-2" rx="12" ry="17" fill="rgba(255,180,130,.22)"/>
</g>

<!-- 旋转组件外壳 -->
<rect x="300" y="690" width="300" height="270" rx="8" fill="#090e18" stroke="#162035" stroke-width="1.2"/>
<text x="450" y="682" fill="#4a6a8a" font-size="11" text-anchor="middle" font-family="Chakra Petch,sans-serif" letter-spacing=".1em">旋转组件</text>

<!-- 旋转转子 -->
<g id="rSpin">
<circle cx="450" cy="815" r="110" fill="#0c1525" stroke="#243650" stroke-width="2"/>
</g>

<!-- 标注:颈缩 -->
<g id="aNeck" opacity="0">
<line id="nL" stroke="#4a6a8a" stroke-width=".8" stroke-dasharray="3,3"/>
<line id="nTL" stroke="#4a6a8a" stroke-width=".8"/>
<line id="nTR" stroke="#4a6a8a" stroke-width=".8"/>
<text id="nTx" fill="#4a6a8a" font-size="10" text-anchor="middle" font-family="Chakra Petch,monospace"/>
</g>

<!-- 标注:扩张箭头 -->
<g id="aExp" opacity="0">
<line id="eaL" stroke="#ffd54f" stroke-width="1.5"/>
<polygon id="ehL" fill="#ffd54f"/>
<text id="etL" fill="#ffd54f" font-size="9" text-anchor="end" font-family="Noto Sans SC,sans-serif"/>
<line id="eaR" stroke="#ffd54f" stroke-width="1.5"/>
<polygon id="ehR" fill="#ffd54f"/>
<text id="etR" fill="#ffd54f" font-size="9" text-anchor="start" font-family="Noto Sans SC,sans-serif"/>
</g>

<!-- 标注:IFR -->
<g id="aIFR" opacity="0">
<rect x="618" y="440" width="180" height="54" rx="6" fill="rgba(0,232,192,.07)" stroke="rgba(0,232,192,.2)" stroke-width="1"/>
<text x="630" y="460" fill="#00e8c0" font-size="11.5" font-weight="600" font-family="Chakra Petch,sans-serif">IFR · 零额外运动件</text>
<text x="630" y="478" fill="#6aaa98" font-size="9.5" font-family="Noto Sans SC,sans-serif">仅利用材料弹性消除动静矛盾</text>
</g>

<!-- 标注:运动轨迹 -->
<path id="trail" fill="none" stroke="rgba(255,107,53,.25)" stroke-width="2" stroke-dasharray="6,4" opacity="0"/>

</svg>
</div>
<div class="ctrls">
<div class="cg"><label>动画速度</label><input type="range" id="sSpd" min="0.2" max="2.5" step="0.1" value="1.0"><span class="v" id="vSpd">1.0x</span></div>
<div class="cg"><label>漏斗弹性</label><input type="range" id="sEl" min="0.3" max="2.0" step="0.1" value="1.0"><span class="v" id="vEl">1.0</span></div>
<div class="cg"><label>软弹可压缩性</label><input type="range" id="sSf" min="0.2" max="1.5" step="0.1" value="1.0"><span class="v" id="vSf">1.0</span></div>
</div>

<script>
(function(){
'use strict';
var NS='http://www.w3.org/2000/svg';
var CX=450;

/* 关键几何 */
var BD=56,BL=66,ND=BD*0.85,NHW=ND/2,GHW=70;
var FT=275,FB=650;
var RCX=450,RCY=815,RR=110,NC=6,CR=28;

/* 动画参数 */
var spd=1,elast=1,soft=1;

/* 工具 */
function lerp(a,b,t){return a+(b-a)*t}
function cl(v,lo,hi){return Math.max(lo,Math.min(hi,v))}
function eio(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
function eo(t){return 1-Math.pow(1-t,3)}
function ei(t){return t*t*t}

/* 创建转子枪膛和拨弹齿 */
var rSpin=document.getElementById('rSpin');
var hub=rSpin.lastElementChild; // 中心圆(如果有的话),但当前SVG里没有

// 先添加拨弹齿(底层)
for(var i=0;i<12;i++){
  var ang=i*30*Math.PI/180;
  var ix=RCX+(RR-6)*Math.cos(ang),iy=RCY+(RR-6)*Math.sin(ang);
  var ox=RCX+(RR+9)*Math.cos(ang),oy=RCY+(RR+9)*Math.sin(ang);
  var ln=document.createElementNS(NS,'line');
  ln.setAttribute('x1',ix);ln.setAttribute('y1',iy);
  ln.setAttribute('x2',ox);ln.setAttribute('y2',oy);
  ln.setAttribute('stroke','#3a5068');ln.setAttribute('stroke-width','2.5');
  ln.setAttribute('stroke-linecap','round');
  rSpin.appendChild(ln);
}

// 枪膛
for(var i=0;i<NC;i++){
  var ang=i*(360/NC)*Math.PI/180;
  var cx=RCX+RR*0.55*Math.cos(ang),cy=RCY+RR*0.55*Math.sin(ang);
  var c=document.createElementNS(NS,'circle');
  c.setAttribute('cx',cx);c.setAttribute('cy',cy);c.setAttribute('r',CR);
  c.setAttribute('fill','#060a14');c.setAttribute('stroke','#1a2a42');c.setAttribute('stroke-width','1.2');
  rSpin.appendChild(c);
}

// 中心轮毂
var hubC=document.createElementNS(NS,'circle');
hubC.setAttribute('cx',RCX);hubC.setAttribute('cy',RCY);hubC.setAttribute('r','13');
hubC.setAttribute('fill','#162035');hubC.setAttribute('stroke','#243650');hubC.setAttribute('stroke-width','1.5');
rSpin.appendChild(hubC);

/* 漏斗路径计算 */
function funPath(exp){
  var nhw=NHW+exp*14*elast;
  var tL=CX-GHW,tR=CX+GHW;
  var nL=CX-nhw,nR=CX+nhw;
  var lw='M'+tL+','+FT+' C'+tL+','+(FT+110)+' '+(nL+12)+','+(FB-130)+' '+nL+','+FB;
  var rw='M'+tR+','+FT+' C'+tR+','+(FT+110)+' '+(nR-12)+','+(FB-130)+' '+nR+','+FB;
  var fl='M'+tL+','+FT+' C'+tL+','+(FT+110)+' '+(nL+12)+','+(FB-130)+' '+nL+','+FB
        +' L'+nR+','+FB+' C'+(nR-12)+','+(FB-130)+' '+tR+','+(FT+110)+' '+tR+','+FT+' Z';
  return{lw:lw,rw:rw,fl:fl,nL:nL,nR:nR,nhw:nhw};
}

/* 更新漏斗 */
function updFun(exp){
  var p=funPath(exp);
  document.getElementById('fFill').setAttribute('d',p.fl);
  document.getElementById('fWL').setAttribute('d',p.lw);
  document.getElementById('fWR').setAttribute('d',p.rw);
  document.getElementById('fGlow').setAttribute('d',p.fl);
  // 螺旋凸起
  for(var i=0;i<6;i++){
    var t=(i+.5)/6;
    var y=lerp(FT+28,FB-18,t);
    var xL=lerp(CX-GHW,p.nL,t);
    var xR=lerp(CX+GHW,p.nR,t);
    var sd=i%2===0?-1:1;
    var rid=document.getElementById('r'+i);
    if(sd<0){
      rid.setAttribute('x1',xL+4);rid.setAttribute('y1',y-7);
      rid.setAttribute('x2',xL+22);rid.setAttribute('y2',y+7);
    }else{
      rid.setAttribute('x1',xR-4);rid.setAttribute('y1',y-7);
      rid.setAttribute('x2',xR-22);rid.setAttribute('y2',y+7);
    }
  }
  return p;
}

/* 阶段定义 */
var PH=[
  {n:'软弹受重力/弹簧下压至漏斗',s:0,e:.13},
  {n:'沿螺旋凸起滑入颈缩段',s:.13,e:.34},
  {n:'拨弹齿抓取 → 硅胶漏斗弹性扩张',s:.34,e:.58},
  {n:'软弹滑入旋转枪膛',s:.58,e:.76},
  {n:'漏斗回弹复位 · 等待下一发',s:.76,e:.90},
  {n:'循环重置',s:.90,e:1}
];
function gPh(p){for(var i=0;i<PH.length;i++)if(p>=PH[i].s&&p<PH[i].e)return PH[i];return PH[PH.length-1]}
function pht(p,ph){return cl((p-ph.s)/(ph.e-ph.s),0,1)}

/* 动画状态 */
var prog=0,lt=0,rAng=0;

/* 关键Y坐标 */
var Y0=260,Y1=FT+30,Y2=(FT+FB)/2,Y3=FB-30,Y4=FB+15,Y5=RCY-RR*0.55;

/* 主循环 */
function tick(ts){
  if(!lt)lt=ts;
  var dt=Math.min((ts-lt)/1000,.05);
  lt=ts;
  prog+=dt*spd*.28;
  if(prog>=1)prog-=1;

  /* 转子旋转 */
  rAng+=dt*spd*32;
  document.getElementById('rSpin').setAttribute('transform','rotate('+rAng+','+RCX+','+RCY+')');

  var ph=gPh(prog),t=pht(prog,ph);
  var bY,bSX,bSY,bOp,exp=0;

  if(prog<.13){
    /* 下落 */
    var u=eio(t);
    bY=lerp(Y0,Y1,u);bSX=1;bSY=1;bOp=1;
  }else if(prog<.34){
    /* 滑向颈缩 */
    var u=eio(t);
    bY=lerp(Y1,Y3-30,u);
    var cp=t*soft*.1;
    bSX=1-cp;bSY=1+cp*.5;bOp=1;
    exp=t*.2;
  }else if(prog<.58){
    /* 核心:扩张 */
    var u=eio(t);
    bY=lerp(Y3-30,Y4+10,u);
    var cm=.2*soft;
    var ct=t<.5?t*2:(1-t)*2;
    bSX=1-cm*ct;bSY=1+cm*ct*.6;bOp=1;
    exp=t<.6?eo(t/.6)*elast:elast*(1-ei((t-.6)/.4)*.35);
  }else if(prog<.76){
    /* 进入枪膛 */
    var u=eo(t);
    bY=lerp(Y4+10,Y5,u);
    bSX=lerp(1-.08*soft,1,t);bSY=lerp(1+.05*soft,1,t);bOp=1;
    exp=elast*.6*(1-t);
  }else if(prog<.90){
    /* 回弹 */
    var u=eo(t);
    bY=Y5;bSX=1;bSY=1;bOp=1-t;
    exp=elast*.1*(1-u);
    // 微呼吸
    exp+=Math.sin(t*Math.PI*3)*.02;
  }else{
    /* 重置 */
    var u=eio(t);
    bY=Y0;bSX=1;bSY=1;bOp=u;exp=0;
  }

  /* 更新漏斗 */
  var fp=updFun(exp);

  /* 漏斗视觉反馈 */
  var gi=cl(exp/Math.max(elast,.01),0,1);
  document.getElementById('fGlow').setAttribute('opacity',gi*.5);
  document.getElementById('fFill').setAttribute('fill',gi>.25?'url(#fGradA)':'url(#fGrad)');
  var wc=gi>.25?'#00ffd5':'#00e8c0',ww=2.5+gi*2;
  document.getElementById('fWL').setAttribute('stroke',wc);
  document.getElementById('fWR').setAttribute('stroke',wc);
  document.getElementById('fWL').setAttribute('stroke-width',ww);
  document.getElementById('fWR').setAttribute('stroke-width',ww);
  for(var i=0;i<6;i++){
    var rid=document.getElementById('r'+i);
    rid.setAttribute('opacity',.3+gi*.5);
    rid.setAttribute('stroke-width',2+gi*1.5);
  }

  /* 子弹 */
  document.getElementById('bG').setAttribute('transform','translate('+CX+','+bY+') scale('+bSX+','+bSY+')');
  document.getElementById('bG').setAttribute('opacity',bOp);

  /* === 标注 === */
  // 颈缩尺寸
  var an=document.getElementById('aNeck');
  an.setAttribute('opacity',.35+gi*.65);
  var dy=fp.nL?FB+18:FB+18;
  document.getElementById('nL').setAttribute('x1',fp.nL);
  document.getElementById('nL').setAttribute('y1',dy);
  document.getElementById('nL').setAttribute('x2',fp.nR);
  document.getElementById('nL').setAttribute('y2',dy);
  document.getElementById('nTL').setAttribute('x1',fp.nL);document.getElementById('nTL').setAttribute('y1',dy-5);
  document.getElementById('nTL').setAttribute('x2',fp.nL);document.getElementById('nTL').setAttribute('y2',dy+5);
  document.getElementById('nTR').setAttribute('x1',fp.nR);document.getElementById('nTR').setAttribute('y1',dy-5);
  document.getElementById('nTR').setAttribute('x2',fp.nR);document.getElementById('nTR').setAttribute('y2',dy+5);
  var rd=(fp.nhw*2).toFixed(1);
  var nt=document.getElementById('nTx');
  nt.setAttribute('x',CX);nt.setAttribute('y',dy+14);
  nt.setAttribute('fill',gi>.25?'#00ffd5':'#4a6a8a');
  nt.textContent='颈缩内径 ≈ '+rd+' ('+(ND/BD*100).toFixed(0)+'%D)';

  // 扩张箭头
  var se=prog>=.34&&prog<.62;
  document.getElementById('aExp').setAttribute('opacity',se?.88:0);
  if(se){
    var ay=FB-45;
    document.getElementById('eaL').setAttribute('x1',fp.nL+5);document.getElementById('eaL').setAttribute('y1',ay);
    document.getElementById('eaL').setAttribute('x2',fp.nL-22);document.getElementById('eaL').setAttribute('y2',ay);
    document.getElementById('ehL').setAttribute('points',(fp.nL-22)+','+ay+' '+(fp.nL-15)+','+(ay-4)+' '+(fp.nL-15)+','+(ay+4));
    document.getElementById('etL').setAttribute('x',fp.nL-26);document.getElementById('etL').setAttribute('y',ay-6);
    document.getElementById('etL').textContent='径向扩张';
    document.getElementById('eaR').setAttribute('x1',fp.nR-5);document.getElementById('eaR').setAttribute('y1',ay);
    document.getElementById('eaR').setAttribute('x2',fp.nR+22);document.getElementById('eaR').setAttribute('y2',ay);
    document.getElementById('ehR').setAttribute('points',(fp.nR+22)+','+ay+' '+(fp.nR+15)+','+(ay-4)+' '+(fp.nR+15)+','+(ay+4));
    document.getElementById('etR').setAttribute('x',fp.nR+26);document.getElementById('etR').setAttribute('y',ay-6);
    document.getElementById('etR').textContent='径向扩张';
  }

  // IFR
  document.getElementById('aIFR').setAttribute('opacity',(prog>=.38&&prog<.68)?.85:0);

  // 轨迹
  var st=prog>=.13&&prog<.76;
  var tr=document.getElementById('trail');
  tr.setAttribute('opacity',st?.55:0);
  if(st)tr.setAttribute('d','M'+CX+','+Y1+' L'+CX+','+bY);

  // 阶段文字
  document.getElementById('phaseBar').textContent=ph.n;

  requestAnimationFrame(tick);
}

/* 初始化漏斗 */
updFun(0);

/* 滑块 */
document.getElementById('sSpd').addEventListener('input',function(){
  spd=parseFloat(this.value);document.getElementById('vSpd').textContent=spd.toFixed(1)+'x';
});
document.getElementById('sEl').addEventListener('input',function(){
  elast=parseFloat(this.value);document.getElementById('vEl').textContent=elast.toFixed(1);
});
document.getElementById('sSf').addEventListener('input',function(){
  soft=parseFloat(this.value);document.getElementById('vSf').textContent=soft.toFixed(1);
});

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

修复说明

之前版本可能因以下原因无法渲染:

  1. SVG 元素全部由 JS 动态创建——任何一处 JS 报错都会导致后续元素全部丢失。新版本将所有静态 SVG 结构直接写在 HTML 中,JS 仅负责更新动态属性(dtransformopacity),即使脚本出问题也能看到基础画面。

  2. innerHTML 操作 SVG——旧版用 innerHTML='' 清空标注组再重建,这在部分浏览器的 SVG 命名空间下会失败。新版所有标注元素预建在 DOM 中,只切换 opacity 和属性值。

  3. 路径计算容错——新增 Math.max(elast, 0.01) 防止除零,所有坐标用字符串拼接而非模板字面量,兼容性更好。

核心动画逻辑(不变)

六个阶段循环:子弹下落 → 沿螺旋凸起滑入颈缩 → 拨弹齿抓取 + 漏斗弹性扩张(核心) → 子弹入膛 → 漏斗回弹 → 重置。扩张阶段漏斗壁发光变亮、颈缩标注高亮、黄色径向扩张箭头出现、IFR 提示浮现,三层视觉引导同时指向创新点。

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