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