分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>8通道视觉整列流道 · IFR原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{
  --bg:#060a14;--fg:#c8d6e5;--muted:#4a5d78;--accent:#00d4ff;
  --accent2:#ff6d00;--accent3:#00e676;--card:#0c1524;--border:#162744;
  --glow-cyan:0 0 12px rgba(0,212,255,.45);--glow-orange:0 0 12px rgba(255,109,0,.5);
  --glow-green:0 0 14px rgba(0,230,118,.5);
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
.header{text-align:center;padding:24px 20px 8px;width:100%}
.header h1{font-family:'Rajdhani',sans-serif;font-weight:700;font-size:2rem;
  letter-spacing:2px;color:#e8f0f8;text-transform:uppercase}
.header p{font-size:.82rem;color:var(--muted);margin-top:4px;letter-spacing:1px}
.main-container{display:flex;gap:16px;width:100%;max-width:1400px;padding:10px 20px;flex:1;min-height:0}
.svg-wrap{flex:1;min-width:0;background:var(--card);border:1px solid var(--border);
  border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.svg-wrap svg{width:100%;height:100%;display:block}
.info-panel{width:290px;flex-shrink:0;display:flex;flex-direction:column;gap:10px;overflow-y:auto}
.panel-section{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px}
.panel-section h3{font-family:'Rajdhani',sans-serif;font-weight:500;font-size:.9rem;
  color:var(--accent);margin-bottom:10px;letter-spacing:1px;text-transform:uppercase}
.grid-view{display:grid;grid-template-columns:repeat(8,1fr);gap:4px}
.grid-cell{aspect-ratio:1;border-radius:50%;border:1px solid var(--border);background:transparent;
  transition:background .3s,box-shadow .3s}
.grid-cell.filled{background:var(--accent);box-shadow:var(--glow-cyan)}
.grid-cell.pressed{background:var(--accent3);box-shadow:var(--glow-green)}
.timing-row{display:flex;justify-content:space-between;align-items:center;
  padding:4px 0;border-bottom:1px solid var(--border);font-size:.75rem}
.timing-row:last-child{border:none}
.timing-label{color:var(--muted)}
.timing-val{color:var(--fg);font-weight:500}
.timing-val.highlight{color:var(--accent3);font-weight:700}
.phase-indicator{display:flex;align-items:center;gap:8px;font-size:.8rem}
.phase-dot{width:10px;height:10px;border-radius:50%;background:var(--muted);transition:all .3s}
.phase-dot.active{background:var(--accent);box-shadow:var(--glow-cyan)}
.phase-dot.active-press{background:var(--accent2);box-shadow:var(--glow-orange)}
.phase-dot.active-done{background:var(--accent3);box-shadow:var(--glow-green)}
.ifr-note{font-size:.72rem;line-height:1.6;color:var(--muted)}
.ifr-note strong{color:var(--accent)}
.throughput{font-size:1.6rem;font-family:'Rajdhani',sans-serif;font-weight:700;color:var(--accent3)}
.throughput-unit{font-size:.75rem;color:var(--muted);margin-left:4px}
.controls{display:flex;align-items:center;gap:16px;padding:14px 20px;width:100%;max-width:1400px;
  flex-wrap:wrap;background:var(--card);border:1px solid var(--border);border-radius:10px;margin:10px 20px 20px}
.controls button{background:var(--border);border:1px solid var(--muted);color:var(--fg);
  padding:6px 18px;border-radius:6px;cursor:pointer;font-family:'IBM Plex Mono',monospace;
  font-size:.8rem;transition:all .2s}
.controls button:hover{background:var(--accent);color:var(--bg);border-color:var(--accent)}
.controls label{font-size:.75rem;color:var(--muted);display:flex;align-items:center;gap:6px}
.controls input[type=range]{width:100px;accent-color:var(--accent)}
.controls input[type=checkbox]{accent-color:var(--accent)}
.speed-val{color:var(--accent);font-weight:500;min-width:32px}
@keyframes scanPulse{0%,100%{opacity:.3}50%{opacity:.8}}
@keyframes flipGlow{0%{filter:drop-shadow(0 0 2px var(--accent2))}
  50%{filter:drop-shadow(0 0 8px var(--accent2))}100%{filter:drop-shadow(0 0 2px var(--accent2))}}
@media(max-width:900px){.main-container{flex-direction:column}.info-panel{width:100%;flex-direction:row;flex-wrap:wrap}
  .panel-section{flex:1;min-width:200px}}
</style>
</head>
<body>
<header class="header">
  <h1>8通道视觉整列流道 · 并行入盘原理</h1>
  <p>基于 TRIZ 最终理想解 (IFR) — 串行抓取 → 并行流水线阵列入盘</p>
</header>
<div class="main-container">
  <div class="svg-wrap" id="svgWrap"></div>
  <aside class="info-panel" id="infoPanel">
    <div class="panel-section">
      <h3>8×8 阵列缓存状态</h3>
      <div class="grid-view" id="gridView"></div>
    </div>
    <div class="panel-section">
      <h3>节拍分析</h3>
      <div id="timingInfo">
        <div class="timing-row"><span class="timing-label">流道积累</span><span class="timing-val">3~5s / 64pcs</span></div>
        <div class="timing-row"><span class="timing-label">同步压入</span><span class="timing-val">0.5s</span></div>
        <div class="timing-row"><span class="timing-label">换盘时间</span><span class="timing-val">0.5s</span></div>
        <div class="timing-row"><span class="timing-label">单件折合</span><span class="timing-val highlight" id="tPerPcs">—</span></div>
      </div>
    </div>
    <div class="panel-section">
      <h3>当前阶段</h3>
      <div id="phaseInfo">
        <div class="phase-indicator"><div class="phase-dot" id="pdFlow"></div><span>流道供料</span></div>
        <div class="phase-indicator"><div class="phase-dot" id="pdPress"></div><span>阵列压入</span></div>
        <div class="phase-indicator"><div class="phase-dot" id="pdSwitch"></div><span>双工位换盘</span></div>
        <div class="phase-indicator"><div class="phase-dot" id="pdRise"></div><span>缓存板复位</span></div>
      </div>
    </div>
    <div class="panel-section">
      <h3>等效吞吐</h3>
      <div><span class="throughput" id="throughput">0</span><span class="throughput-unit">pcs/min</span></div>
    </div>
    <div class="panel-section ifr-note">
      <h3>IFR 核心解构</h3>
      <p><strong>理想状态:</strong>物料自行滑入定位孔,无需逐件抓取——消除了机械手这个"中介"。</p>
      <p style="margin-top:6px"><strong>资源利用:</strong>气浮翻转巧妙利用物料 13mm 内孔的空气动力学差,以高压气嘴替代翻转机械手,零额外占地。</p>
      <p style="margin-top:6px"><strong>矛盾消解:</strong>速度与精度的矛盾→并行流水线(8通道同时处理)+ 阵列同步入盘(64件/0.5s),精度由流道定位保证。</p>
    </div>
  </aside>
</div>
<div class="controls" id="controls">
  <button id="btnPlay">暂停</button>
  <label>速度 <input type="range" id="speedSlider" min="0.3" max="3" step="0.1" value="1"><span class="speed-val" id="speedVal">1.0x</span></label>
  <label><input type="checkbox" id="chkFlip" checked> 高亮翻转</label>
  <label><input type="checkbox" id="chkDetail" checked> 翻转详图</label>
  <button id="btnReset">重置</button>
</div>

<script>
(function(){
'use strict';

/* ====== 配置 ====== */
const C={
  chCount:8, gridSize:8, total:64,
  chY0:72, chDy:28, chH:16,
  chX0:60, chX1:620,
  visX:240, visX1:340,     // 视觉区
  flipX:380, flipX1:510,   // 翻转区
  dropX:620,               // 下落起点
  bufY0:320, bufDy:16,     // 缓存板网格行距
  bufDx:52, bufR:5,        // 缓存板网格列距、点半径
  bufCx0:230, bufCy0:328,  // 缓存板网格起点
  pressDist:90,            // 压入行程
  trayY0:475, trayDy:16, trayDx:52, trayR:5,
  trayCx0:230, trayCy0:483,
  spawnInterval:.55,       // 每批间隔(秒)
  pressDur:.5, switchDur:.5, riseDur:.5,
  particleSpeed:160,
};

/* ====== 状态 ====== */
let S={
  phase:'flow', phaseTime:0,
  particles:[], bufferGrid:new Array(64).fill(0),
  bufferCount:0, bufferOffsetY:0,
  trayOffsetX:0, trayFull:false,
  spawnTimer:0, batchCount:0,
  speed:1, paused:false,
  highlightFlip:true, showDetail:true,
  cycleCount:0, totalTime:0,
  scanAngle:0, flipEffects:[],
  detailChannel:-1, detailTimer:0,
};

/* ====== SVG辅助 ====== */
const NS='http://www.w3.org/2000/svg';
function el(tag,a={}){const e=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(a))e.setAttribute(k,String(v));return e}
function addTo(parent,tag,a={}){const e=el(tag,a);parent.appendChild(e);return e}

/* ====== 构建SVG ====== */
const wrap=document.getElementById('svgWrap');
const svg=el('svg',{viewBox:'0 0 920 600',preserveAspectRatio:'xMidYMid meet'});
svg.style.width='100%';svg.style.height='100%';

// 定义
const defs=addTo(svg,'defs');
// 辉光滤镜
const fGlow=addTo(defs,'filter',{id:'glow',x:'-50%',y:'-50%',width:'200%',height:'200%'});
addTo(fGlow,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:'3',result:'blur'});
const fm=addTo(fGlow,'feMerge');
addTo(fm,'feMergeNode',{in:'blur'});addTo(fm,'feMergeNode',{in:'SourceGraphic'});
// 橙色辉光
const fGlowO=addTo(defs,'filter',{id:'glowO',x:'-50%',y:'-50%',width:'200%',height:'200%'});
addTo(fGlowO,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'blur'});
const fm2=addTo(fGlowO,'feMerge');
addTo(fm2,'feMergeNode',{in:'blur'});addTo(fm2,'feMergeNode',{in:'SourceGraphic'});
// 渐变
const bgGrad=addTo(defs,'linearGradient',{id:'bgGrad',x1:0,y1:0,x2:0,y2:1});
addTo(bgGrad,'stop',{offset:'0%','stop-color':'#080e1e'});
addTo(bgGrad,'stop',{offset:'100%','stop-color':'#0a1220'});

/* --- 背景层 --- */
addTo(svg,'rect',{width:920,height:600,fill:'url(#bgGrad)'});
// 微弱网格
const gridG=addTo(svg,'g',{opacity:.06});
for(let x=0;x<920;x+=40) addTo(gridG,'line',{x1:x,y1:0,x2:x,y2:600,stroke:'#00d4ff','stroke-width':.5});
for(let y=0;y<600;y+=40) addTo(gridG,'line',{x1:0,y1:y,x2:920,y2:y,stroke:'#00d4ff','stroke-width':.5});

/* --- 标注层 --- */
const labelG=addTo(svg,'g',{class:'labels'});
// 层标签
addTo(labelG,'text',{x:22,y:55,fill:'#4a7da8','font-size':'11','font-family':'Rajdhani, sans-serif','font-weight':'500'}).textContent='上层 · 8通道并行流道';
addTo(labelG,'text',{x:22,y:310,fill:'#4a7da8','font-size':'11','font-family':'Rajdhani, sans-serif','font-weight':'500'}).textContent='中层 · 8×8阵列缓存中转板';
addTo(labelG,'text',{x:22,y:465,fill:'#4a7da8','font-size':'11','font-family':'Rajdhani, sans-serif','font-weight':'500'}).textContent='下层 · 双工位Tray盘切换台';

// 区域标注
const zoneG=addTo(svg,'g',{class:'zones'});
// 视觉区
addTo(zoneG,'rect',{x:C.visX,y:C.chY0-8,width:C.visX1-C.visX,height:C.chCount*C.chDy+8,rx:4,
  fill:'rgba(124,77,255,.06)',stroke:'rgba(124,77,255,.25)','stroke-width':1,'stroke-dasharray':'4 3'});
addTo(zoneG,'text',{x:(C.visX+C.visX1)/2,y:C.chY0-14,fill:'#7c4dff','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace'}).textContent='线扫视觉检测';
// 翻转区
addTo(zoneG,'rect',{x:C.flipX,y:C.chY0-8,width:C.flipX1-C.flipX,height:C.chCount*C.chDy+8,rx:4,
  fill:'rgba(255,109,0,.05)',stroke:'rgba(255,109,0,.25)','stroke-width':1,'stroke-dasharray':'4 3'});
addTo(zoneG,'text',{x:(C.flipX+C.flipX1)/2,y:C.chY0-14,fill:'#ff6d00','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace'}).textContent='气浮翻转区';

/* --- 通道层 --- */
const channelG=addTo(svg,'g',{id:'channels'});
for(let i=0;i<C.chCount;i++){
  const y=C.chY0+i*C.chDy;
  // 通道背景
  addTo(channelG,'rect',{x:C.chX0,y:y,width:C.chX1-C.chX0,height:C.chH,rx:3,
    fill:'#0a1628',stroke:'#1a3a5f','stroke-width':.8});
  // 流动虚线
  const flowLine=addTo(channelG,'line',{
    x1:C.chX0+4,y1:y+C.chH/2,x2:C.chX1-4,y2:y+C.chH/2,
    stroke:'#0e2a4a','stroke-width':1,'stroke-dasharray':'6 8',
    class:'flow-line','data-ch':i
  });
  // 通道编号
  addTo(channelG,'text',{x:C.chX0-4,y:y+C.chH/2+4,fill:'#2a5070','font-size':'9',
    'text-anchor':'end','font-family':'IBM Plex Mono, monospace'}).textContent='CH'+(i+1);
  // 气嘴标记(在翻转区上方)
  if(i%2===0){
    const nx=C.flipX+30+i*15;
    addTo(channelG,'polygon',{points:`${nx},${y-2} ${nx-3},${y-6} ${nx+3},${y-6}`,
      fill:'#ff6d00',opacity:.5,class:'nozzle','data-ch':i});
  }
}

/* --- 扫描线 --- */
const scanLine=addTo(svg,'line',{x1:C.visX,y1:C.chY0-4,x2:C.visX,y2:C.chY0+C.chCount*C.chDy+4,
  stroke:'#7c4dff','stroke-width':1.5,opacity:.6,filter:'url(#glow)'});

/* --- 下落路径(通道→缓存板) --- */
const dropG=addTo(svg,'g',{id:'dropPaths',opacity:.3});
for(let i=0;i<C.chCount;i++){
  const topX=C.chX1;
  const topY=C.chY0+i*C.chDy+C.chH/2;
  const botX=C.bufCx0+i*C.bufDx;
  const botY=C.bufCy0;
  const midY=(topY+botY)/2+20;
  addTo(dropG,'path',{d:`M${topX},${topY} C${topX+30},${midY} ${botX+10},${midY} ${botX},${botY}`,
    fill:'none',stroke:'#1a5070','stroke-width':1,'stroke-dasharray':'3 4'});
}

/* --- 缓存板 --- */
const bufferGroup=addTo(svg,'g',{id:'bufferBoard'});
const bufBg=addTo(bufferGroup,'rect',{x:C.bufCx0-20,y:C.bufCy0-16,
  width:C.bufDx*7+40,height:C.bufDy*7+32,rx:6,
  fill:'rgba(10,22,40,.85)',stroke:'#1a4a6f','stroke-width':1.2});
addTo(bufferGroup,'text',{x:C.bufCx0+C.bufDx*3.5,y:C.bufCy0-22,fill:'#3a8abf','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace'}).textContent='8×8 缓存板 (64真空微孔)';

const bufDots=[];
for(let r=0;r<C.gridSize;r++){
  for(let c=0;c<C.gridSize;c++){
    const cx=C.bufCx0+c*C.bufDx;
    const cy=C.bufCy0+r*C.bufDy;
    const d=addTo(bufferGroup,'circle',{cx,cy,r:C.bufR,fill:'transparent',
      stroke:'#1a3a5f','stroke-width':.8,class:'buf-dot','data-idx':r*8+c});
    bufDots.push(d);
  }
}

/* --- Tray盘 --- */
const trayGroupA=addTo(svg,'g',{id:'trayA'});
const trayGroupB=addTo(svg,'g',{id:'trayB',opacity:.4});
// Tray A 背景
addTo(trayGroupA,'rect',{x:C.trayCx0-24,y:C.trayCy0-18,
  width:C.trayDx*7+48,height:C.trayDy*7+36,rx:4,
  fill:'rgba(12,20,35,.9)',stroke:'#2a5a3f','stroke-width':1});
addTo(trayGroupA,'text',{x:C.trayCx0+C.trayDx*3.5,y:C.trayCy0-24,fill:'#3aaa6f','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace'}).textContent='Tray A (工位1)');
// Tray B 背景
addTo(trayGroupB,'rect',{x:C.trayCx0-24+400,y:C.trayCy0-18,
  width:C.trayDx*7+48,height:C.trayDy*7+36,rx:4,
  fill:'rgba(12,20,35,.6)',stroke:'#3a3a5f','stroke-width':1,'stroke-dasharray':'4 3'});
addTo(trayGroupB,'text',{x:C.trayCx0+C.trayDx*3.5+400,y:C.trayCy0-24,fill:'#5a5a8f','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace'}).textContent='Tray B (工位2 · 待机)');

const trayDotsA=[];
for(let r=0;r<C.gridSize;r++){
  for(let c=0;c<C.gridSize;c++){
    const cx=C.trayCx0+c*C.trayDx;
    const cy=C.trayCy0+r*C.trayDy;
    const d=addTo(trayGroupA,'rect',{x:cx-C.trayR,y:cy-C.trayR,width:C.trayR*2,height:C.trayR*2,
      rx:1,fill:'transparent',stroke:'#1a4a3f','stroke-width':.6,class:'tray-dot','data-idx':r*8+c});
    trayDotsA.push(d);
  }
}

/* --- 粒子组(最上层) --- */
const particleG=addTo(svg,'g',{id:'particles'});

/* --- 翻转详图(右上角) --- */
const detailG=addTo(svg,'g',{id:'flipDetail',transform:'translate(700,20)'});
const detailBg=addTo(detailG,'rect',{x:0,y:0,width:200,height:180,rx:8,
  fill:'rgba(8,14,26,.92)',stroke:'#ff6d00','stroke-width':1,'stroke-dasharray':'4 2'});
addTo(detailG,'text',{x:100,y:18,fill:'#ff6d00','font-size':'10','text-anchor':'middle',
  'font-family':'Rajdhani, sans-serif','font-weight':'500'}).textContent='气浮翻转详图';
// 物料示意
const detailItem=addTo(detailG,'g',{transform:'translate(100,90)'});
addTo(detailItem,'rect',{x:-16,y:-8,width:32,height:16,rx:3,fill:'#0e2040',stroke:'#00d4ff','stroke-width':1.2,id:'detailItemRect'});
addTo(detailItem,'circle',{cx:0,cy:0,r:4,fill:'none',stroke:'#ff6d00','stroke-width':1,id:'detailHole'});
// 气嘴
addTo(detailG,'polygon',{points:'100,30 95,38 105,38',fill:'#ff6d00',opacity:.7,id:'detailNozzle'});
// 气流线
const airLines=addTo(detailG,'g',{id:'airLines',opacity:0});
for(let i=-1;i<=1;i++){
  addTo(airLines,'line',{x1:100+i*5,y1:38,x2:100+i*8,y2:70,stroke:'#ff6d00',
    'stroke-width':1.5,opacity:.6});
}
addTo(airLines,'line',{x1:100,y1:38,x2:100,y2:72,stroke:'#ff9100',
  'stroke-width':2.5,opacity:.8});
// 标注
addTo(detailG,'text',{x:14,y:80,fill:'#7c8da8','font-size':'8',
  'font-family':'IBM Plex Mono, monospace'}).textContent='← 13mm内孔';
addTo(detailG,'text',{x:130,y:55,fill:'#7c8da8','font-size':'8',
  'font-family':'IBM Plex Mono, monospace'}).textContent='高压气↑';
// 翻转箭头
const flipArrow=addTo(detailG,'path',{d:'M120,95 A15,15 0 0 1 120,75',fill:'none',
  stroke:'#ff6d00','stroke-width':1.2,opacity:0,id:'flipArrow'});
addTo(detailG,'polygon',{points:'120,75 117,81 123,81',fill:'#ff6d00',opacity:0,id:'flipArrowHead'});
// 状态文字
const detailStatus=addTo(detailG,'text',{x:100,y:165,fill:'#ff6d00','font-size':'9',
  'text-anchor':'middle','font-family':'IBM Plex Mono, monospace',id:'detailStatus'}).textContent='待检测...';

/* --- 右侧时序图 --- */
const timingG=addTo(svg,'g',{transform:'translate(700,220)'});
addTo(timingG,'rect',{x:0,y:0,width:200,height:160,rx:8,
  fill:'rgba(8,14,26,.88)',stroke:'#1a3a5f','stroke-width':1});
addTo(timingG,'text',{x:100,y:16,fill:'#3a8abf','font-size':'10','text-anchor':'middle',
  'font-family':'Rajdhani, sans-serif','font-weight':'500'}).textContent='动作时序';

// 时序条
const seqData=[
  {label:'流道积累',color:'#00d4ff',y:30,w:140,h:14,dur:'3~5s'},
  {label:'阵列压入',color:'#ff6d00',y:54,w:35,h:14,dur:'0.5s'},
  {label:'双盘切换',color:'#ffc107',y:78,w:35,h:14,dur:'0.5s'},
  {label:'缓存复位',color:'#7c4dff',y:102,w:35,h:14,dur:'0.5s'},
];
seqData.forEach((d,i)=>{
  addTo(timingG,'rect',{x:60,y:d.y,width:d.w,height:d.h,rx:3,fill:d.color,opacity:.2,
    stroke:d.color,'stroke-width':.8});
  addTo(timingG,'text',{x:55,y:d.y+11,fill:d.color,'font-size':'9','text-anchor':'end',
    'font-family':'IBM Plex Mono, monospace'}).textContent=d.label;
  addTo(timingG,'text',{x:60+d.w+6,y:d.y+11,fill:'#5a7a9a','font-size':'8',
    'font-family':'IBM Plex Mono, monospace'}).textContent=d.dur;
  // 进度指示
  addTo(timingG,'rect',{x:60,y:d.y,width:0,height:d.h,rx:3,fill:d.color,opacity:.5,
    id:'seqBar'+i,class:'seq-bar'});
});
// 重叠标注
addTo(timingG,'text',{x:100,y:135,fill:'#3a8abf','font-size':'8','text-anchor':'middle',
  'font-family':'IBM Plex Mono, monospace'}).textContent='↑ 复位与下批积累可并行重叠';

wrap.appendChild(svg);

/* ====== 右侧面板 - 网格初始化 ====== */
const gridEl=document.getElementById('gridView');
const gridCells=[];
for(let i=0;i<64;i++){
  const d=document.createElement('div');
  d.className='grid-cell';
  gridEl.appendChild(d);
  gridCells.push(d);
}

/* ====== 动画引擎 ====== */
let lastTime=0;

function spawnBatch(){
  for(let i=0;i<C.chCount;i++){
    const needsFlip=Math.random()>.55;
    const p={
      ch:i,
      x:C.chX0,
      y:C.chY0+i*C.chDy+C.chH/2,
      needsFlip, flipped:false, flipping:false,
      flipAngle:0, flipProgress:0,
      state:'move', // move, flip, drop, done
      speed:C.particleSpeed+(Math.random()-.5)*20,
      visionDone:false,
      el:null, dropProgress:0,
      targetBufX:C.bufCx0+i*C.bufDx,
      targetBufY:0, // will be set when drop starts
    };
    S.particles.push(p);
  }
}

function update(dt){
  S.totalTime+=dt;
  S.scanAngle=(S.scanAngle+dt*120)%(C.visX1-C.visX);
  
  // 流动虚线动画
  document.querySelectorAll('.flow-line').forEach(l=>{
    const offset=(S.totalTime*60)%14;
    l.setAttribute('stroke-dashoffset',-offset);
  });

  switch(S.phase){
    case 'flow': updateFlow(dt); break;
    case 'press': updatePress(dt); break;
    case 'switch': updateSwitch(dt); break;
    case 'rise': updateRise(dt); break;
  }

  // 更新粒子
  for(let i=S.particles.length-1;i>=0;i--){
    const p=S.particles[i];
    if(p.state==='move'){
      p.x+=p.speed*dt;
      if(p.x>=C.visX&&!p.visionDone){
        p.visionDone=true;
        if(p.needsFlip&&S.highlightFlip) addFlipEffect(p.x,p.y);
      }
      if(p.needsFlip&&p.x>=C.flipX+20&&!p.flipped&&!p.flipping){
        p.flipping=true; p.state='flip'; p.flipProgress=0;
        S.detailChannel=p.ch; S.detailTimer=1;
      }
      if(p.x>=C.dropX){ p.state='drop'; p.dropProgress=0; }
    }
    if(p.state==='flip'){
      p.flipProgress+=dt*3.5;
      p.flipAngle=Math.min(p.flipProgress*180,180);
      p.x+=p.speed*dt*0.35;
      if(p.flipProgress>=1){ p.flipped=true; p.state='move'; p.flipAngle=180; }
      if(p.x>=C.dropX){ p.state='drop'; p.dropProgress=0; }
    }
    if(p.state==='drop'){
      p.dropProgress+=dt*2.5;
      if(p.dropProgress>=1){
        p.state='done';
        // 放入缓存板
        const col=p.ch;
        const row=S.batchCount-1;
        const idx=Math.min(row,7)*8+col;
        if(idx>=0&&idx<64&&S.bufferGrid[idx]===0){
          S.bufferGrid[idx]=1; S.bufferCount++;
        }
        if(p.el){p.el.remove(); p.el=null;}
        S.particles.splice(i,1);
      }
    }
  }

  // 翻转特效
  for(let i=S.flipEffects.length-1;i>=0;i--){
    S.flipEffects[i].life-=dt;
    if(S.flipEffects[i].life<=0){
      if(S.flipEffects[i].el) S.flipEffects[i].el.remove();
      S.flipEffects.splice(i,1);
    }
  }

  // 详图更新
  if(S.detailTimer>0){
    S.detailTimer-=dt;
    updateDetail(dt);
  }
  
  render();
}

function updateFlow(dt){
  S.spawnTimer+=dt;
  if(S.spawnTimer>=C.spawnInterval){
    S.spawnTimer-=C.spawnInterval;
    spawnBatch();
    S.batchCount++;
    if(S.batchCount>=8){
      S.phase='press'; S.phaseTime=0;
      // 确保所有粒子已入板
    }
  }
}

function updatePress(dt){
  S.phaseTime+=dt;
  const t=Math.min(S.phaseTime/C.pressDur,1);
  // 缓动
  const ease=t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
  S.bufferOffsetY=ease*C.pressDist;
  if(t>=1){
    // 压入完成 - 转移到Tray
    for(let i=0;i<64;i++){
      if(S.bufferGrid[i]===1){
        S.bufferGrid[i]=2; // 标记已压入
      }
    }
    S.phase='switch'; S.phaseTime=0; S.trayFull=true;
  }
}

function updateSwitch(dt){
  S.phaseTime+=dt;
  const t=Math.min(S.phaseTime/C.switchDur,1);
  const ease=t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
  S.trayOffsetX=ease*400;
  if(t>=1){
    S.phase='rise'; S.phaseTime=0;
  }
}

function updateRise(dt){
  S.phaseTime+=dt;
  const t=Math.min(S.phaseTime/C.riseDur,1);
  const ease=t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
  S.bufferOffsetY=C.pressDist*(1-ease);
  if(t>=1){
    // 复位完成,开始新周期
    S.phase='flow'; S.phaseTime=0; S.bufferOffsetY=0;
    S.bufferGrid=new Array(64).fill(0); S.bufferCount=0;
    S.batchCount=0; S.spawnTimer=0; S.trayOffsetX=0;
    S.trayFull=false; S.cycleCount++;
  }
}

function addFlipEffect(x,y){
  const g=addTo(particleG,'g',{class:'flip-fx'});
  // 气流线
  for(let i=-1;i<=1;i++){
    addTo(g,'line',{x1:x+i*3,y1:y-20,x2:x+i*5,y2:y-6,stroke:'#ff6d00',
      'stroke-width':1.5,opacity:.7});
  }
  addTo(g,'line',{x1:x,y1:y-22,x2:x,y2:y-4,stroke:'#ff9100','stroke-width':2,opacity:.9});
  S.flipEffects.push({el:g,life:.6,x,y});
}

function updateDetail(dt){
  const active=S.particles.find(p=>p.flipping);
  if(active){
    const prog=active.flipProgress;
    // 旋转物料
    const rect=document.getElementById('detailItemRect');
    const hole=document.getElementById('detailHole');
    const angle=prog*180;
    rect.setAttribute('transform',`rotate(${angle})`);
    hole.setAttribute('transform',`rotate(${angle})`);
    // 气流
    const airL=document.getElementById('airLines');
    airL.setAttribute('opacity',Math.min(prog*3,1));
    // 翻转箭头
    const fA=document.getElementById('flipArrow');
    const fAH=document.getElementById('flipArrowHead');
    fA.setAttribute('opacity',prog>.3?1:0);
    fAH.setAttribute('opacity',prog>.3?1:0);
    // 状态文字
    const st=document.getElementById('detailStatus');
    if(prog<.2) st.textContent='检测: 反面朝上';
    else if(prog<.8) st.textContent='气浮翻转中...';
    else st.textContent='翻转完成 ✓';
    // 颜色
    rect.setAttribute('fill',prog>.8?'#0a2818':'#0e2040');
    rect.setAttribute('stroke',prog>.8?'#00e676':'#00d4ff');
  } else {
    const st=document.getElementById('detailStatus');
    st.textContent='物料滑行中...';
    document.getElementById('airLines').setAttribute('opacity',0);
    document.getElementById('flipArrow').setAttribute('opacity',0);
    document.getElementById('flipArrowHead').setAttribute('opacity',0);
  }
}

/* ====== 渲染 ====== */
function render(){
  // 扫描线
  scanLine.setAttribute('x1',C.visX+S.scanAngle);
  scanLine.setAttribute('x2',C.visX+S.scanAngle);

  // 粒子
  for(const p of S.particles){
    if(!p.el){
      p.el=el('rect',{width:10,height:6,rx:2});
      particleG.appendChild(p.el);
    }
    let fill='#00d4ff';
    if(p.needsFlip&&!p.flipped) fill='#ff6d00';
    if(p.flipped&&p.flipProgress>=1) fill='#00e676';
    if(p.state==='flip') fill='#ff9100';
    
    let px=p.x, py=p.y;
    if(p.state==='drop'){
      const t=p.dropProgress;
      const ease=t*t;
      px=p.x+(p.targetBufX-p.x)*ease;
      py=p.y+(C.bufCy0+S.bufferOffsetY-p.y)*ease;
    }
    
    p.el.setAttribute('x',px-5);
    p.el.setAttribute('y',py-3);
    p.el.setAttribute('fill',fill);
    p.el.setAttribute('opacity',p.state==='drop'?(1-p.dropProgress*.3):1);
    p.el.setAttribute('filter',p.state==='flip'?'url(#glowO)':'');
    
    if(p.flipAngle>0){
      p.el.setAttribute('transform',`rotate(${p.flipAngle},${px},${py})`);
    }
  }

  // 翻转特效透明度
  for(const fx of S.flipEffects){
    if(fx.el) fx.el.setAttribute('opacity',Math.max(0,fx.life/.6));
  }

  // 缓存板位置
  bufferGroup.setAttribute('transform',`translate(0,${S.bufferOffsetY})`);
  
  // 缓存板网格状态
  for(let i=0;i<64;i++){
    const v=S.bufferGrid[i];
    if(v===0){
      bufDots[i].setAttribute('fill','transparent');
      bufDots[i].setAttribute('stroke','#1a3a5f');
    } else if(v===1){
      bufDots[i].setAttribute('fill','#00d4ff');
      bufDots[i].setAttribute('stroke','#00d4ff');
      bufDots[i].setAttribute('filter','url(#glow)');
    } else {
      bufDots[i].setAttribute('fill','#00e676');
      bufDots[i].setAttribute('stroke','#00e676');
      bufDots[i].setAttribute('filter','url(#glow)');
    }
  }

  // Tray A 位置
  trayGroupA.setAttribute('transform',`translate(${-S.trayOffsetX},0)`);
  // Tray B
  const bOff=Math.max(0,400-S.trayOffsetX);
  trayGroupB.setAttribute('transform',`translate(${-bOff+400},0)`);
  trayGroupB.setAttribute('opacity',.4+S.trayOffsetX/400*.5);

  // Tray A 网格
  for(let i=0;i<64;i++){
    if(S.bufferGrid[i]===2){
      trayDotsA[i].setAttribute('fill','#00e676');
      trayDotsA[i].setAttribute('stroke','#00e676');
    }
  }

  // 右侧面板网格
  for(let i=0;i<64;i++){
    const v=S.bufferGrid[i];
    gridCells[i].className='grid-cell'+(v===1?' filled':'')+(v===2?' pressed':'');
  }

  // 时序条
  const phaseIdx={flow:0,press:1,switch:2,rise:3};
  const phaseMaxW=[140,35,35,35];
  const phaseDur=[C.spawnInterval*8,C.pressDur,C.switchDur,C.riseDur];
  seqData.forEach((d,i)=>{
    const bar=document.getElementById('seqBar'+i);
    if(i===phaseIdx[S.phase]){
      const prog=Math.min(S.phaseTime/phaseDur[i],1);
      bar.setAttribute('width',phaseMaxW[i]*prog);
    } else if(i<phaseIdx[S.phase]){
      bar.setAttribute('width',phaseMaxW[i]);
    } else {
      bar.setAttribute('width',0);
    }
  });

  // 阶段指示
  const dots=['pdFlow','pdPress','pdSwitch','pdRise'];
  const activeClasses=['active','active-press','active-press','active-done'];
  dots.forEach((id,i)=>{
    const d=document.getElementById(id);
    d.className='phase-dot';
    if(i===phaseIdx[S.phase]) d.classList.add(activeClasses[i]);
  });

  // 吞吐计算
  const tPerPcs=(C.spawnInterval*8+C.pressDur+C.switchDur+C.riseDur)/64;
  const pcsMin=Math.round(60/tPerPcs);
  document.getElementById('throughput').textContent=pcsMin;
  document.getElementById('tPerPcs').textContent=tPerPcs.toFixed(2)+'s/pcs';

  // 详图显示
  detailG.setAttribute('opacity',S.showDetail&&S.detailTimer>0?1:
    (S.showDetail&&S.phase==='flow'?.4:0));
}

/* ====== 主循环 ====== */
function loop(ts){
  if(!lastTime) lastTime=ts;
  const rawDt=Math.min((ts-lastTime)/1000,.1);
  lastTime=ts;
  if(!S.paused){
    update(rawDt*S.speed);
  }
  requestAnimationFrame(loop);
}

/* ====== 控件绑定 ====== */
document.getElementById('btnPlay').addEventListener('click',function(){
  S.paused=!S.paused;
  this.textContent=S.paused?'播放':'暂停';
});
document.getElementById('speedSlider').addEventListener('input',function(){
  S.speed=parseFloat(this.value);
  document.getElementById('speedVal').textContent=S.speed.toFixed(1)+'x';
});
document.getElementById('chkFlip').addEventListener('change',function(){
  S.highlightFlip=this.checked;
});
document.getElementById('chkDetail').addEventListener('change',function(){
  S.showDetail=this.checked;
});
document.getElementById('btnReset').addEventListener('click',function(){
  // 清理粒子
  S.particles.forEach(p=>{if(p.el)p.el.remove()});
  S.flipEffects.forEach(f=>{if(f.el)f.el.remove()});
  S.particles=[]; S.flipEffects=[];
  S.bufferGrid=new Array(64).fill(0); S.bufferCount=0;
  S.bufferOffsetY=0; S.trayOffsetX=0; S.trayFull=false;
  S.phase='flow'; S.phaseTime=0; S.batchCount=0;
  S.spawnTimer=0; S.cycleCount=0; S.detailTimer=0;
  // 重置Tray网格
  trayDotsA.forEach(d=>{d.setAttribute('fill','transparent');d.setAttribute('stroke','#1a4a3f')});
  // 重置详图
  document.getElementById('detailItemRect').setAttribute('transform','');
  document.getElementById('detailHole').setAttribute('transform','');
  document.getElementById('airLines').setAttribute('opacity',0);
  document.getElementById('flipArrow').setAttribute('opacity',0);
  document.getElementById('flipArrowHead').setAttribute('opacity',0);
});

/* ====== 启动 ====== */
requestAnimationFrame(loop);

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

这段代码实现了一个完整的 8通道视觉整列流道 IFR 原理动画,核心设计思路如下:

视觉架构

  • 三层结构清晰分离:上层8通道并行流道、中层8×8阵列缓存中转板、下层双工位Tray盘切换台,通过深色背景和微弱网格营造技术蓝图氛围
  • 色域策略:青色(#00d4ff)表示正常流程物料,橙色(#ff6d00)标记翻转区/需翻转物料,绿色(#00e676)表示完成状态,紫色(#7c4dff)用于视觉扫描区

IFR 核心表达

  1. 并行替代串行:8条通道同时出料,粒子同步流动,视觉上一目了然
  2. 资源自利用:右上角翻转详图展示气浮翻转原理——高压气流穿过13mm内孔产生空气动力学差,使物料在半空翻转180°,无需翻转机械手
  3. 时间重叠:时序图展示缓存板复位与下批物料积累可并行进行

交互设计

  • 速度滑块:0.3x~3x 调节动画速率,深入观察关键动作
  • 高亮翻转:开关气浮翻转区的视觉强调
  • 翻转详图:右上角放大展示翻转物理过程
  • 右侧面板:实时显示8×8缓存状态、节拍分析、等效吞吐率和阶段指示

动画周期

流道积累(≈4.4s) → 阵列压入(0.5s) → 双盘切换(0.5s) → 缓存复位(0.5s),单件折合约 0.09s/pcs,等效吞吐约 640 pcs/min,远超1s/pcs目标。

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