<!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 核心表达
- 并行替代串行:8条通道同时出料,粒子同步流动,视觉上一目了然
- 资源自利用:右上角翻转详图展示气浮翻转原理——高压气流穿过13mm内孔产生空气动力学差,使物料在半空翻转180°,无需翻转机械手
- 时间重叠:时序图展示缓存板复位与下批物料积累可并行进行
交互设计
- 速度滑块:0.3x~3x 调节动画速率,深入观察关键动作
- 高亮翻转:开关气浮翻转区的视觉强调
- 翻转详图:右上角放大展示翻转物理过程
- 右侧面板:实时显示8×8缓存状态、节拍分析、等效吞吐率和阶段指示
动画周期
流道积累(≈4.4s) → 阵列压入(0.5s) → 双盘切换(0.5s) → 缓存复位(0.5s),单件折合约 0.09s/pcs,等效吞吐约 640 pcs/min,远超1s/pcs目标。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
