分享图
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=Outfit:wght@300;500;700;900&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{
  --bg:#060a14;--surface:#0b1120;--card:#10182d;--border:#172040;
  --text:#cdd8ea;--dim:#4e6389;--teal:#00e8a2;--amber:#f5a623;
  --pink:#ec4899;--pink-dk:#7e1a4a;--steel:#3b4e6e;--steel-lt:#5a7699;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:18px 12px}
header{text-align:center;margin-bottom:14px}
header h1{font-size:26px;font-weight:900;letter-spacing:-.5px;
  background:linear-gradient(120deg,var(--teal),var(--amber));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
header p{font-size:12px;color:var(--dim);font-family:'IBM Plex Mono',monospace;margin-top:3px}
.wrap{width:100%;max-width:1100px;position:relative}
.svg-box{width:100%;aspect-ratio:11/7;background:var(--surface);
  border:1px solid var(--border);border-radius:14px;overflow:hidden;position:relative}
.svg-box svg{width:100%;height:100%;display:block}
.phase-tag{position:absolute;top:10px;left:10px;background:rgba(6,10,20,.78);
  padding:6px 14px;border-radius:8px;font-family:'IBM Plex Mono',monospace;
  font-size:12px;border:1px solid var(--border);display:flex;align-items:center;gap:7px;pointer-events:none}
.phase-dot{width:8px;height:8px;border-radius:50%;animation:pdot 1.4s infinite}
@keyframes pdot{0%,100%{opacity:1}50%{opacity:.3}}
.ifr-tag{position:absolute;top:10px;right:10px;background:rgba(6,10,20,.78);
  padding:6px 14px;border-radius:8px;font-size:11px;border:1px solid var(--border);
  max-width:240px;pointer-events:none;line-height:1.5;color:var(--dim);transition:opacity .4s}
.ifr-tag b{color:var(--teal)}
.legend{position:absolute;bottom:10px;right:10px;background:rgba(6,10,20,.78);
  padding:8px 14px;border-radius:8px;font-size:11px;border:1px solid var(--border);pointer-events:none}
.legend-row{display:flex;align-items:center;gap:7px;margin:3px 0}
.legend-bar{width:14px;height:4px;border-radius:2px}
.ctrls{width:100%;max-width:1100px;margin-top:12px;display:grid;
  grid-template-columns:auto 1fr 1fr 1fr;gap:10px;align-items:end}
@media(max-width:700px){.ctrls{grid-template-columns:1fr 1fr}}
.btn-row{display:flex;gap:6px;flex-wrap:wrap}
.btn{background:var(--card);border:1px solid var(--border);color:var(--text);
  padding:7px 14px;border-radius:7px;cursor:pointer;font-family:'Outfit',sans-serif;
  font-size:12px;transition:all .18s;display:flex;align-items:center;gap:5px}
.btn:hover{border-color:var(--teal);background:rgba(0,232,162,.07)}
.btn.on{border-color:var(--teal);color:var(--teal);background:rgba(0,232,162,.12)}
.sld-grp{display:flex;flex-direction:column;gap:3px}
.sld-lbl{font-size:10px;color:var(--dim);font-family:'IBM Plex Mono',monospace;
  display:flex;justify-content:space-between}
.sld-lbl span{color:var(--amber)}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:4px;
  background:var(--border);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;
  background:var(--amber);border-radius:50%;cursor:pointer}
</style>
</head>
<body>
<header>
  <h1>随动压实刮板机构 · IFR 原理演示</h1>
  <p>利用气缸往返动作 · 零新增动力源 · 破除蓬松拱桥</p>
</header>

<div class="wrap">
  <div class="svg-box">
    <svg id="svg" viewBox="0 0 1100 700" xmlns="http://www.w3.org/2000/svg"></svg>
    <div class="phase-tag"><span class="phase-dot" id="pDot" style="background:var(--teal)"></span><span id="pTxt">初始化</span></div>
    <div class="ifr-tag" id="ifrTag"><b>IFR 理想解:</b>利用气缸回程的"免费"往复动作,顺势压实,无需新增动力源</div>
    <div class="legend">
      <div class="legend-row"><div class="legend-bar" style="background:var(--teal)"></div>创新点1 · 随动刮板+扭簧</div>
      <div class="legend-row"><div class="legend-bar" style="background:var(--amber)"></div>创新点2 · 固定倾斜压板</div>
      <div class="legend-row"><div class="legend-bar" style="background:var(--pink)"></div>膜材料</div>
    </div>
  </div>
</div>

<div class="ctrls">
  <div class="btn-row">
    <button class="btn on" id="bPlay" onclick="togglePlay()"><i class="fa-solid fa-pause"></i>暂停</button>
    <button class="btn" onclick="resetAnim()"><i class="fa-solid fa-rotate-left"></i>重置</button>
    <button class="btn" onclick="stepPhase()"><i class="fa-solid fa-forward-step"></i>单步</button>
  </div>
  <div class="sld-grp">
    <div class="sld-lbl">扭簧扭矩 <span id="vT">2.0</span> N·cm</div>
    <input type="range" min="0.5" max="5" step="0.1" value="2" id="sT" oninput="onParam()">
  </div>
  <div class="sld-grp">
    <div class="sld-lbl">压板角度 <span id="vA">30</span>°</div>
    <input type="range" min="10" max="50" step="1" value="30" id="sA" oninput="onParam()">
  </div>
  <div class="sld-grp">
    <div class="sld-lbl">压板高度 <span id="vH">100</span> mm</div>
    <input type="range" min="40" max="160" step="5" value="100" id="sH" oninput="onParam()">
  </div>
</div>

<script>
/* ===== 常量 ===== */
const NS='http://www.w3.org/2000/svg';
const BX={l:450,r:850,t:230,b:585}; // 收膜框
const SL_Y=230, SL_L=140;            // 滑轨
const PL_MIN=370, PL_MAX=510;        // 推膜钣金行程
const CYL={x:40,y:198,w:105,h:54};   // 气缸
const SC_LEN=135, SC_HINGE_OFF=-8;   // 刮板

/* ===== 工具函数 ===== */
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}
function easeIO(t){return t<.5?2*t*t:-1+(4-2*t)*t}
function easeO(t){return 1-Math.pow(1-t,3)}
function easeI(t){return t*t*t}
function el(tag,attrs,parent){
  const e=document.createElementNS(NS,tag);
  for(const[k,v]of Object.entries(attrs))e.setAttribute(k,v);
  if(parent)parent.appendChild(e);
  return e;
}

/* ===== 全局状态 ===== */
let S={time:0,playing:true,speed:1,torque:2,angle:30,height:100};
let cur={px:PL_MIN,sa:0,fl:0,fc:0,slide:true,phase:'就绪',cyc:0,sub:0};
let lastTS=0;

/* ===== SVG 初始化 ===== */
const svg=document.getElementById('svg');

// 背景网格
const defs=el('defs',{},svg);
const pat=el('pattern',{id:'grd',width:25,height:25,patternUnits:'userSpaceOnUse'},defs);
el('path',{d:'M 25 0 L 0 0 0 25',fill:'none',stroke:'#131d34','stroke-width':.5},pat);
el('rect',{width:1100,height:700,fill:'#070b16'},svg);
el('rect',{width:1100,height:700,fill:'url(#grd)',opacity:.6},svg);

// 发光滤镜
const gf=el('filter',{id:'glow',x:-50%,y:-50%,width:200%,height:200%},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:4,result:'b'},gf);
const fm=el('feMerge',{},gf);el('feMergeNode',{in:'b'},fm);el('feMergeNode',{in:'SourceGraphic'},fm);

const gf2=el('filter',{id:'glow2',x:-50%,y=-50%,width:200%,height:200%},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:6,result:'b'},gf2);
const fm2=el('feMerge',{},gf2);el('feMergeNode',{in:'b'},fm2);el('feMergeNode',{in:'SourceGraphic'},fm2);

// 箭头标记
const mk=el('marker',{id:'arrT',markerWidth:8,markerHeight:6,refX:8,refY:3,orient:'auto'},defs);
el('path',{d:'M0,0 L8,3 L0,6',fill:'#00e8a2'},mk);
const mk2=el('marker',{id:'arrA',markerWidth:8,markerHeight:6,refX:8,refY:3,orient:'auto'},defs);
el('path',{d:'M0,0 L8,3 L0,6',fill:'#f5a623'},mk2);
const mk3=el('marker',{id:'arrP',markerWidth:8,markerHeight:6,refX:8,refY:3,orient:'auto'},defs);
el('path',{d:'M0,0 L8,3 L0,6',fill:'#ec4899'},mk3);

// 动态图层(按绘制顺序)
const gBox=el('g',{id:'gBox'},svg);
const gFilm=el('g',{id:'gFilm'},svg);
const gSlide=el('g',{id:'gSlide'},svg);
const gCyl=el('g',{id:'gCyl'},svg);
const gPlate=el('g',{id:'gPlate'},svg);
const gScraper=el('g',{id:'gScraper'},svg);
const gIncline=el('g',{id:'gIncline'},svg);
const gArrows=el('g',{id:'gArrows'},svg);
const gAnnot=el('g',{id:'gAnnot'},svg);
const gFall=el('g',{id:'gFall'},svg);

/* ===== 绘制静态元素 ===== */
function drawStatics(){
  // 收膜框
  const bw=3;
  el('rect',{x:BX.l,y:BX.t,width:BX.r-BX.l,height:BX.b-BX.t,
    fill:'none',stroke:'#2a3a5a','stroke-width':bw,rx:2},gBox);
  // 框底
  el('line',{x1:BX.l,y1:BX.b,x2:BX.r,y2:BX.b,stroke:'#3b5070','stroke-width':bw+2},gBox);
  // 框内底纹
  for(let i=0;i<8;i++){
    const x=BX.l+20+i*45;
    el('line',{x1:x,y1:BX.b-6,x2:x+10,y2:BX.b+2,stroke:'#1e2e48','stroke-width':1},gBox);
  }
  // 框标签
  el('text',{x:BX.l+(BX.r-BX.l)/2,y:BX.b+28,fill:'#4e6389',
    'font-size':12,'font-family':'IBM Plex Mono,monospace','text-anchor':'middle'},gBox).textContent='收膜框';

  // 滑轨
  el('rect',{x:SL_L,y:SL_Y-3,width:BX.l-SL_L+25,height:6,fill:'#2a3a5a',rx:2},gSlide);
  el('line',{x1:SL_L+10,y1:SL_Y-8,x2:SL_L+10,y2:SL_Y+8,stroke:'#2a3a5a','stroke-width':2},gSlide);
  // 滑轨标签
  el('text',{x:SL_L+60,y:SL_Y-16,fill:'#4e6389','font-size':10,
    'font-family':'IBM Plex Mono,monospace'},gSlide).textContent='滑轨';

  // 气缸主体
  el('rect',{x:CYL.x,y:CYL.y,width:CYL.w,height:CYL.h,fill:'#1e2e48',
    stroke:'#3b5070','stroke-width':2,rx:6},gCyl);
  // 气缸端盖
  el('rect',{x:CYL.x-4,y:CYL.y+8,width:8,height:CYL.h-16,fill:'#2a3a5a',rx:2},gCyl);
  // 气缸标签
  el('text',{x:CYL.x+CYL.w/2,y:CYL.y+CYL.h+20,fill:'#4e6389','font-size':10,
    'font-family':'IBM Plex Mono,monospace','text-anchor':'middle'},gCyl).textContent='气缸';
}

/* ===== 绘制动态元素 ===== */
function drawFrame(){
  const st=cur;
  // 清空动态图层
  [gFilm,gPlate,gScraper,gIncline,gArrows,gAnnot,gFall].forEach(g=>{while(g.firstChild)g.removeChild(g.firstChild)});

  // ---- 膜(框内)----
  if(st.fl>0.01){
    const maxH=BX.b-BX.t-30;
    const rawH=st.fl*maxH;
    const compH=rawH*(1-st.fc*0.25); // 压实后高度降低
    const filmTop=BX.b-compH;
    const waveAmp=(1-st.fc)*14; // 蓬松时波幅大

    // 膜主体路径
    let d=`M${BX.l+4} ${BX.b-3} L${BX.l+4} ${filmTop}`;
    const steps=24;
    for(let i=0;i<=steps;i++){
      const t=i/steps;
      const x=BX.l+4+t*(BX.r-BX.l-8);
      const wave=Math.sin(t*Math.PI*5+S.time*0.002)*waveAmp*(1-t*0.3);
      const y=filmTop+wave;
      d+=` L${x.toFixed(1)} ${y.toFixed(1)}`;
    }
    d+=` L${BX.r-4} ${BX.b-3} Z`;

    const pinkOp=0.55+st.fc*0.35;
    const r=Math.round(lerp(236,158,st.fc));
    const g=Math.round(lerp(72,24,st.fc));
    const b_=Math.round(lerp(153,78,st.fc));
    el('path',{d:d,fill:`rgba(${r},${g},${b_},${pinkOp})`,stroke:`rgba(${r},${g},${b_},0.7)`,'stroke-width':1},gFilm);

    // 气泡(蓬松时可见)
    if(st.fc<0.8){
      const bubbleOp=(1-st.fc)*0.5;
      const seed=[0.2,0.45,0.65,0.8,0.35,0.55,0.75,0.15,0.6,0.9];
      for(let i=0;i<seed.length;i++){
        const bx=BX.l+20+seed[i]*(BX.r-BX.l-40);
        const by=filmTop+10+((i*37)%Math.max(1,Math.floor(compH-20)));
        if(by<BX.b-8 && by>filmTop+5){
          el('circle',{cx:bx,cy:by,r:3+((i*7)%4),fill:'none',
            stroke:`rgba(255,255,255,${bubbleOp})`,'stroke-width':.7},gFilm);
        }
      }
    }

    // 压实度指示
    const pct=Math.round(st.fc*100);
    el('text',{x:BX.r+18,y:filmTop+compH/2+4,fill:st.fc>0.7?'#00e8a2':'#4e6389',
      'font-size':11,'font-family':'IBM Plex Mono,monospace'},gFilm).textContent=`压实 ${pct}%`;
  }

  // ---- 活塞杆 ----
  const rodEndX=st.px;
  el('rect',{x:CYL.x+CYL.w,y:CYL.y+CYL.h/2-5,width:rodEndX-CYL.x-CYL.w,height:10,
    fill:'#3b5070',stroke:'#4e6389','stroke-width':1},gPlate);

  // ---- 推膜钣金 ----
  const pTop=140, pBot=SL_Y;
  el('rect',{x:st.px,y:pTop,width:14,height:pBot-pTop,fill:'#3b5070',
    stroke:'#5a7699','stroke-width':1.5,rx:2},gPlate);
  // 钣金标签
  el('text',{x:st.px+7,y:pTop-8,fill:'#5a7699','font-size':9,
    'font-family':'IBM Plex Mono,monospace','text-anchor':'middle'},gPlate).textContent='推膜钣金';

  // ---- 滑轨上的膜 ----
  if(st.slide && st.phase!=='膜落入框' && st.phase!=='刮板下压压实' && st.phase!=='刮板+压板压实'){
    const fx=st.px+16, fw=90, fh=28;
    el('rect',{x:fx,y:SL_Y-fh,width:fw,height:fh,fill:'rgba(236,72,153,0.45)',
      stroke:'rgba(236,72,153,0.6)','stroke-width':1,rx:4},gPlate);
    // 膜纹理
    for(let i=0;i<3;i++){
      el('line',{x1:fx+15+i*25,y1:SL_Y-fh+6,x2:fx+25+i*25,y2:SL_Y-6,
        stroke:'rgba(236,72,153,0.3)','stroke-width':1},gPlate);
    }
  }

  // ---- 落膜动画 ----
  if(st.phase==='膜落入框'){
    const p=clamp(st.sub,0,1);
    const startFX=PL_MAX+16;
    const startFY=SL_Y-28;
    const targetFY=BX.b-20-cur.fl*(BX.b-BX.t-30)*(1-cur.fc*0.25);
    const fx=startFX+p*30;
    const fy=lerp(startFY,targetFY,easeI(p));
    const rot=p*45;
    const op=1-p*0.3;
    el('rect',{x:fx,y:fy,width:70*(1-p*0.3),height:28*(1-p*0.2),
      fill:`rgba(236,72,153,${op})`,stroke:`rgba(236,72,153,${op*0.7})`,
      'stroke-width':1,rx:3,transform:`rotate(${rot},${fx+35},${fy+14})`},gFall);
  }

  // ---- 刮板 + 扭簧 ----
  const hingeX=st.px+7;
  const hingeY=pTop+SC_HINGE_OFF;
  const sAngRad=st.sa*Math.PI/180;
  const sEndX=hingeX+Math.cos(sAngRad)*SC_LEN;
  const sEndY=hingeY+Math.sin(sAngRad)*SC_LEN;
  const isActive=Math.abs(st.sa)>15;
  const sColor=isActive?'#00e8a2':'#5a7699';
  const sWidth=isActive?5:3;

  // 刮板主体
  el('line',{x1:hingeX,y1:hingeY,x2:sEndX,y2:sEndY,stroke:sColor,
    'stroke-width':sWidth,'stroke-linecap':'round',
    filter:isActive?'url(#glow)':'none'},gScraper);
  // 刮板末端小横档
  const perpAng=sAngRad+Math.PI/2;
  const ex1=sEndX+Math.cos(perpAng)*10;
  const ey1=sEndY+Math.sin(perpAng)*10;
  const ex2=sEndX-Math.cos(perpAng)*10;
  const ey2=sEndY-Math.sin(perpAng)*10;
  el('line',{x1:ex1,y1:ey1,x2:ex2,y2:ey2,stroke:sColor,
    'stroke-width':sWidth-1,'stroke-linecap':'round'},gScraper);

  // 铰接点
  el('circle',{cx:hingeX,cy:hingeY,r:5,fill:'#0b1120',stroke:sColor,'stroke-width':2},gScraper);

  // 扭簧示意
  const springCoils=4;
  const springR=12;
  let spD=`M${hingeX} ${hingeY-springR}`;
  for(let i=0;i<springCoils;i++){
    const a1=Math.PI*1.5+i*Math.PI*2/springCoils*0.8;
    const a2=a1+Math.PI*2/springCoils*0.8;
    spD+=` A${springR} ${springR} 0 0 1 ${hingeX+Math.cos(a2)*springR} ${hingeY+Math.sin(a2)*springR}`;
  }
  el('path',{d:spD,fill:'none',stroke:isActive?'#00e8a2':'#3b5070',
    'stroke-width':1.5,opacity:isActive?0.9:0.5},gScraper);
  // 扭簧标签
  if(isActive){
    el('text',{x:hingeX-20,y:hingeY-22,fill:'#00e8a2','font-size':9,
      'font-family':'IBM Plex Mono,monospace','text-anchor':'middle',
      filter:'url(#glow)'},gScraper).textContent=`扭簧 ${S.torque.toFixed(1)}N·cm`;
  }

  // 刮板运动轨迹(刮压时显示)
  if(isActive && st.sa<-20){
    const trailArc=`M${hingeX} ${hingeY} A${SC_LEN} ${SC_LEN} 0 0 1 ${sEndX} ${sEndY}`;
    el('path',{d:trailArc,fill:'none',stroke:'rgba(0,232,162,0.15)',
      'stroke-width':20,'stroke-linecap':'round'},gScraper);
  }

  // ---- 固定倾斜压板 ----
  const angRad=S.angle*Math.PI/180;
  const incW=220; // 水平投影宽度
  const heightFactor=1-(S.height-40)/120; // 高度参数影响压板位置
  const incBaseY=BX.t+60+heightFactor*180; // 压板下沿Y
  const incLX=BX.r-incW-10;
  const incLY=incBaseY;
  const incRX=BX.r-10;
  const incRY=incBaseY-incW*Math.tan(angRad);
  const incTouching=st.fl>0.42+heightFactor*0.15;
  const incColor=incTouching?'#f5a623':'#3b5070';
  const incOp=incTouching?1:0.5;

  // 压板主体
  el('line',{x1:incLX,y1:incLY,x2:incRX,y2:incRY,stroke:incColor,
    'stroke-width':6,'stroke-linecap':'round',opacity:incOp,
    filter:incTouching?'url(#glow)':'none'},gIncline);
  // 压板厚度效果
  const nx=Math.sin(angRad)*4, ny=-Math.cos(angRad)*4;
  el('line',{x1:incLX+nx,y1:incLY+ny,x2:incRX+nx,y2:incRY+ny,stroke:incColor,
    'stroke-width':2,'stroke-linecap':'round',opacity:incOp*0.5},gIncline);
  // 固定支架
  el('line',{x1:incRX,y1:incRY,x2:incRX+5,y2:BX.t-10,stroke:'#2a3a5a','stroke-width':3},gIncline);
  el('line',{x1:incLX,y1:incLY,x2:incLX-5,y2:BX.t-10,stroke:'#2a3a5a','stroke-width':2},gIncline);
  // 角度标注
  const arcR=40;
  const arcEndX=incLX+arcR;
  const arcEndY=incLY;
  const arcStartX=incLX+arcR*Math.cos(-angRad);
  const arcStartY=incLY+arcR*Math.sin(-angRad);
  el('path',{d:`M${arcEndX} ${arcEndY} A${arcR} ${arcR} 0 0 0 ${arcStartX} ${arcStartY}`,
    fill:'none',stroke:incColor,'stroke-width':1.5,opacity:incOp,
    'stroke-dasharray':'3,3'},gIncline);
  el('text',{x:incLX+arcR+8,y:incLY-8,fill:incColor,'font-size':10,
    'font-family':'IBM Plex Mono,monospace',opacity:incOp},gIncline).textContent=`${S.angle}°`;
  // 压板标签
  el('text',{x:incLX+incW/2-20,y:incRY-14,fill:incColor,'font-size':10,
    'font-family':'IBM Plex Mono,monospace','text-anchor':'middle',opacity:incOp},gIncline).textContent='固定倾斜压板';

  // 压板高度标注线
  const hLineX=BX.r+8;
  el('line',{x1:hLineX,y1:incLY,x2:hLineX,y2:BX.b,stroke:'#2a3a5a',
    'stroke-width':1,'stroke-dasharray':'4,3'},gIncline);
  el('text',{x:hLineX+4,y:(incLY+BX.b)/2,fill:'#4e6389','font-size':9,
    'font-family':'IBM Plex Mono,monospace',transform:`rotate(90,${hLineX+4},${(incLY+BX.b)/2})`},gIncline).textContent=`H=${S.height}mm`;

  // ---- 力箭头 ----
  // 推力箭头
  if(st.phase==='推膜入框'){
    const arrowY=SL_Y-14;
    el('line',{x1:st.px-30,y1:arrowY,x2:st.px+5,y2:arrowY,
      stroke:'#5a7699','stroke-width':2,'marker-end':'url(#arrP)'},gArrows);
    el('text',{x:st.px-15,y:arrowY-8,fill:'#5a7699','font-size':9,
      'font-family':'IBM Plex Mono,monospace','text-anchor':'middle'},gArrows).textContent='推力';
  }
  // 刮板压力箭头
  if(isActive && st.sa<-25){
    const midFrac=0.6;
    const mx=hingeX+Math.cos(sAngRad)*SC_LEN*midFrac;
    const my=hingeY+Math.sin(sAngRad)*SC_LEN*midFrac;
    const downX=mx;
    const downY=my+30;
    el('line',{x1:mx,y1:my,x2:downX,y2:downY,stroke:'#00e8a2',
      'stroke-width':2.5,'marker-end':'url(#arrT)',filter:'url(#glow)'},gArrows);
    el('text',{x:downX+12,y:downY,fill:'#00e8a2','font-size':9,
      'font-family':'IBM Plex Mono,monospace'},gArrows).textContent='刮压力';
  }
  // 倾斜压板反力箭头
  if(incTouching && (st.phase==='刮板+压板压实'||st.phase==='强制压扁')){
    const arrX=incLX+incW*0.4;
    const arrY1=incLY-incW*0.4*Math.tan(angRad)-10;
    const arrY2=arrY1+35;
    el('line',{x1:arrX,y1:arrY1,x2:arrX,y2:arrY2,stroke:'#f5a623',
      'stroke-width':2.5,'marker-end':'url(#arrA)',filter:'url(#glow)'},gArrows);
    el('text',{x:arrX+14,y:arrY2-5,fill:'#f5a623','font-size':9,
      'font-family':'IBM Plex Mono,monospace'},gArrows).textContent='强制压扁';
  }

  // ---- 资源利用标注 ----
  if(st.phase==='刮板下压压实'||st.phase==='刮板+压板压实'){
    // 气缸回程→压实 能量流
    const efx=CYL.x+CYL.w+15;
    const efy=CYL.y-20;
    el('text',{x:efx,y:efy,fill:'#00e8a2','font-size':10,
      'font-family':'IBM Plex Mono,monospace',opacity:0.85,filter:'url(#glow)'},gAnnot).textContent='↩ 气缸回程能量 → 压实功';
    // 连接虚线
    el('line',{x1:efx+50,y1:efy+4,x2:hingeX,y2:hingeY,
      stroke:'rgba(0,232,162,0.2)','stroke-width':1,'stroke-dasharray':'4,4'},gAnnot);
  }

  // ---- IFR 核心标注 ----
  if(st.phase==='刮板+压板压实'||st.phase==='强制压扁'||st.phase==='完全压实'){
    const cx=BX.l-10, cy=BX.t+40;
    el('rect',{x:cx-90,y:cy-14,width:175,height:50,fill:'rgba(0,232,162,0.06)',
      stroke:'rgba(0,232,162,0.2)','stroke-width':1,rx:6},gAnnot);
    el('text',{x:cx-2,y:cy+2,fill:'#00e8a2','font-size':10,
      'font-family':'IBM Plex Mono,monospace','text-anchor':'middle',fontWeight:600},gAnnot).textContent='IFR: 零新增动力';
    el('text',{x:cx-2,y:cy+18,fill:'#4e6389','font-size':9,
      'font-family':'IBM Plex Mono,monospace','text-anchor':'middle'},gAnnot).textContent='往复动作→顺势压实';
  }
}

/* ===== 动画状态计算 ===== */
const CYC_MS=4800;
const NUM_CYC=4;

function calcState(){
  const totalMs=CYC_MS*NUM_CYC;
  const lt=((S.time%totalMs)+totalMs)%totalMs;
  const cyc=Math.floor(lt/CYC_MS);
  const cp=(lt%CYC_MS)/CYC_MS;

  // 压板接触膜位级
  const hFac=1-(S.height-40)/120;
  const contactLvl=0.42+hFac*0.15;
  const maxScrAng=-25-S.torque*8;

  let px,sa,fl,fc,slide,phase,sub=0;
  const dropsSoFar=cyc+(cp>=0.38?1:0);
  const baseFl=dropsSoFar*0.19;
  const baseFc=Math.min(1,dropsSoFar*0.22);

  if(cp<0.36){
    // 推膜
    const p=easeIO(cp/0.36);
    px=lerp(PL_MIN,PL_MAX,p);
    sa=lerp(0,-10,p);
    slide=true;
    phase='推膜入框';
    fl=baseFl-(cp>=0.38?0.19:0);
    fc=baseFc-(cp>=0.38?0.22:0);
  }else if(cp<0.46){
    // 落膜
    const p=(cp-0.36)/0.1;
    px=PL_MAX;
    sa=-10;
    slide=false;
    phase='膜落入框';
    sub=easeI(p);
    fl=baseFl-0.19+0.19*easeO(p);
    fc=baseFc-0.22;
  }else if(cp<0.82){
    // 回程+刮压
    const p=easeIO((cp-0.46)/0.36);
    px=lerp(PL_MAX,PL_MIN,p);
    sa=lerp(-10,maxScrAng,easeO(p));
    slide=false;
    const touching=fl>contactLvl;
    phase=touching?'刮板+压板压实':'刮板下压压实';
    fl=baseFl;
    fc=Math.min(1,baseFc-0.22+0.22*easeO(p)+(touching?0.15*easeO(p):0));
  }else{
    // 沉稳
    const p=easeIO((cp-0.82)/0.18);
    px=PL_MIN;
    sa=lerp(maxScrAng,0,p);
    slide=true;
    phase=cyc>=NUM_CYC-1?'完全压实':'准备推膜';
    fl=baseFl;
    fc=Math.min(1,baseFc);
  }

  // 最终周期强制高压实
  if(cyc>=NUM_CYC-1 && cp>0.5){
    fc=Math.min(1,fc+0.2);
    phase=cp>0.8?'完全压实':'强制压扁';
  }

  cur={px,sa,fl:clamp(fl,0,0.92),fc:clamp(fc,0,1),slide,phase,cyc,sub:clamp(sub,0,1)};
}

/* ===== 主循环 ===== */
function loop(ts){
  if(!lastTS)lastTS=ts;
  const dt=ts-lastTS;
  lastTS=ts;

  if(S.playing){
    S.time+=dt*S.speed;
  }

  calcState();
  drawFrame();

  // 更新UI
  document.getElementById('pTxt').textContent=cur.phase;
  const dotColor=cur.phase.includes('刮板')||cur.phase.includes('压实')?'var(--teal)':
    cur.phase.includes('压板')||cur.phase.includes('强制')?'var(--amber)':'var(--dim)';
  document.getElementById('pDot').style.background=dotColor;

  // IFR标签
  const ifrEl=document.getElementById('ifrTag');
  if(cur.phase==='刮板下压压实'||cur.phase==='刮板+压板压实'){
    ifrEl.style.opacity='1';
    ifrEl.innerHTML='<b>IFR 理想解:</b>气缸回程的"免费"往复动作 → 扭簧驱动刮板下压 → 顺势压实,零新增动力源';
  }else if(cur.phase==='强制压扁'||cur.phase==='完全压实'){
    ifrEl.style.opacity='1';
    ifrEl.innerHTML='<b>IFR 理想解:</b>固定压板提供刚性高度限制 → 膜冲击压板强制排挤空气 → 破除拱桥蓬松';
  }else{
    ifrEl.style.opacity='0.5';
    ifrEl.innerHTML='<b>IFR 理想解:</b>利用气缸回程的"免费"往复动作,顺势压实,无需新增动力源';
  }

  requestAnimationFrame(loop);
}

/* ===== 交互控制 ===== */
function togglePlay(){
  S.playing=!S.playing;
  const btn=document.getElementById('bPlay');
  btn.innerHTML=S.playing?'<i class="fa-solid fa-pause"></i>暂停':'<i class="fa-solid fa-play"></i>播放';
  btn.classList.toggle('on',S.playing);
}
function resetAnim(){
  S.time=0;lastTS=0;cur={px:PL_MIN,sa:0,fl:0,fc:0,slide:true,phase:'就绪',cyc:0,sub:0};
}
function stepPhase(){
  S.time+=CYC_MS*0.25; // 推进1/4周期
}
function onParam(){
  S.torque=parseFloat(document.getElementById('sT').value);
  S.angle=parseInt(document.getElementById('sA').value);
  S.height=parseInt(document.getElementById('sH').value);
  document.getElementById('vT').textContent=S.torque.toFixed(1);
  document.getElementById('vA').textContent=S.angle;
  document.getElementById('vH').textContent=S.height;
}

/* ===== 启动 ===== */
drawStatics();
requestAnimationFrame(loop);
</script>
</body>
</html>

实现说明:

本动画聚焦展示 IFR(最终理想解)的核心思想——利用气缸必有的往返动作,顺手完成压实工序,零新增动力源

视觉设计:

  • 深色工业蓝图风格背景,配合微妙网格营造工程制图氛围
  • 创新点1(随动刮板+扭簧)以 青绿色 高亮,激活时附带发光效果
  • 创新点2(固定倾斜压板)以 琥珀色 高亮,膜触及压板时自动点亮
  • 膜材料用 粉红色 渲染,蓬松时表面波浪起伏+气泡可见,压实时表面平滑+气泡消失+颜色加深

动画机制:

  • 4个完整推膜循环,膜逐层堆积,压实度递增
  • 第3周期起膜触及倾斜压板,展示"强制破拱"效果
  • 刮板下压时显示扫掠轨迹弧和力箭头
  • 压板接触时显示反力箭头
  • 气缸回程阶段显示"能量流向"标注,强调资源复用

交互控制:

  • 扭簧扭矩滑块:影响刮板下压角度(扭矩越大,刮压越深)
  • 压板角度滑块:实时改变倾斜压板倾角
  • 压板高度滑块:调整压板下沿位置,改变膜触发强制压实的时机
  • 播放/暂停、重置、单步推进按钮
积分规则:第一轮对话扣减6分,后续每轮扣4分