<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>面阵式伯努利吸盘 · 极速装盘原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=Noto+Sans+SC:wght@300;400;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#070b14;--surface:#0d1424;--border:#1a2540;--text:#d8e2f0;--muted:#6b7fa0;--cyan:#00d4e8;--amber:#f5a623;--emerald:#00c9a7;--rose:#ff4f6e;--purple:#a87bff;--conv:#141e35;--lane:#1c2d4a}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans SC',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 25% 30%,rgba(0,212,232,.04),transparent 55%),radial-gradient(ellipse at 75% 70%,rgba(0,201,167,.04),transparent 55%);pointer-events:none}
.wrap{position:relative;z-index:1;width:100%;max-width:1520px;padding:16px 20px}
.hdr{text-align:center;margin-bottom:12px}
.hdr h1{font-family:'Rajdhani',sans-serif;font-size:1.85rem;font-weight:700;letter-spacing:3px;background:linear-gradient(120deg,var(--cyan),var(--emerald));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hdr .sub{font-size:.82rem;color:var(--muted);margin-top:2px;font-weight:300;letter-spacing:1px}
.svg-box{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;box-shadow:0 0 60px rgba(0,212,232,.04),inset 0 0 80px rgba(0,0,0,.3)}
.svg-box svg{display:block;width:100%;height:auto}
.panel{display:flex;gap:14px;margin-top:14px;flex-wrap:wrap}
.ctrl{display:flex;align-items:center;gap:12px;padding:12px 20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;flex:1;min-width:300px;flex-wrap:wrap}
.ctrl button{font-family:'Rajdhani',sans-serif;font-size:.88rem;font-weight:600;padding:7px 18px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);cursor:pointer;transition:all .2s;letter-spacing:1px}
.ctrl button:hover{border-color:var(--cyan);color:var(--cyan)}
.ctrl button.on{background:var(--cyan);color:var(--bg);border-color:var(--cyan)}
.ctrl label{font-size:.78rem;color:var(--muted);display:flex;align-items:center;gap:6px;white-space:nowrap}
.ctrl input[type=range]{width:100px;accent-color:var(--cyan);cursor:pointer}
.sval{font-family:'Rajdhani',sans-serif;font-weight:700;color:var(--cyan);min-width:28px;text-align:center}
.stats{display:flex;gap:18px;padding:10px 20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;flex:2;min-width:400px;flex-wrap:wrap;align-items:center}
.si{display:flex;align-items:center;gap:6px}
.sl{font-size:.72rem;color:var(--muted)}
.sv{font-family:'Rajdhani',sans-serif;font-size:1.05rem;font-weight:700}
.sv.c1{color:var(--cyan)}.sv.c2{color:var(--amber)}.sv.c3{color:var(--emerald)}.sv.c4{color:var(--rose)}
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.dot.on{background:var(--emerald);box-shadow:0 0 8px var(--emerald)}
.dot.off{background:var(--muted)}
@keyframes scanPulse{0%,100%{opacity:.3}50%{opacity:.9}}
</style>
</head>
<body>
<div class="wrap">
<div class="hdr">
<h1>BERNOULLI ARRAY PICKER</h1>
<div class="sub">IFR 理想解 — 面阵式非接触吸附 · 8列并行 · 零停顿装盘</div>
</div>
<div class="svg-box"><svg id="cv" viewBox="0 0 1440 880" xmlns="http://www.w3.org/2000/svg"></svg></div>
<div class="panel">
<div class="ctrl">
<button id="btnPlay" class="on">RUN</button>
<button id="btnStep">STEP</button>
<button id="btnReset">RESET</button>
<label>Speed <input type="range" id="spd" min="0.2" max="3" step="0.1" value="1"><span class="sval" id="spdV">1.0x</span></label>
<label>Gap <input type="range" id="gapR" min="1" max="3" step="0.5" value="1.5"><span class="sval" id="gapV">1.5mm</span></label>
</div>
<div class="stats">
<div class="si"><div class="dot on" id="runDot"></div><span class="sl">状态</span><span class="sv c1" id="stPhase">IDLE</span></div>
<div class="si"><span class="sl">行进度</span><span class="sv c2" id="stRow">0/8</span></div>
<div class="si"><span class="sl">累计</span><span class="sv c3" id="stTotal">0 pcs</span></div>
<div class="si"><span class="sl">等效节拍</span><span class="sv c4" id="stRate">—</span></div>
<div class="si"><span class="sl">停顿</span><span class="sv c3" id="stStop">0s</span></div>
</div>
</div>
</div>
<script>
/* ============================================
面阵式伯努利吸盘极速装盘 — 原理动画引擎
============================================ */
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('cv');
/* ---- 工具函数 ---- */
function el(tag, attrs, parent) {
const e = document.createElementNS(SVG_NS, tag);
if (attrs) Object.entries(attrs).forEach(([k,v]) => e.setAttribute(k, v));
if (parent) parent.appendChild(e);
return e;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function easeInOut(t) { return t < .5 ? 2*t*t : 1 - Math.pow(-2*t+2,2)/2; }
function easeOut(t) { return 1 - Math.pow(1-t,3); }
/* ---- 布局常量 ---- */
const L = {
// 输送带
convX: 70, convY: 100, convW: 560, convH: 224,
laneH: 24, laneGap: 4, numLanes: 8,
// 视觉/翻转区
visX: 240, visW: 28,
flipX: 380, flipW: 28,
// 吸盘模组 (8x8)
modCellSize: 24, modCellGap: 4,
// 模组在输送带终端的位置
modConvX: 680, modConvY: 100,
// 模组在Tray盘上方位置
modTrayX: 1020, modTrayY: 100,
// Tray盘
trayX: 980, trayY: 380, trayW: 240, trayH: 240,
trayCellSize: 26, trayCellGap: 4,
// 时间线
tlY: 700, tlH: 24, tlX: 70, tlW: 1300,
};
const modGridSize = L.numLanes * (L.modCellSize + L.modCellGap) - L.modCellGap;
const trayGridCols = 8, trayGridRows = 8;
/* ---- 颜色 ---- */
const C = {
bg: '#080c18', grid: '#0d1525', surface: '#111b30',
convBg: '#0e1728', lane: '#182844', laneLine: '#1e3458',
mat: '#f5a623', matGlow: 'rgba(245,166,35,.5)',
visZone: '#00d4e8', visBg: 'rgba(0,212,232,.08)',
flipZone: '#a87bff', flipBg: 'rgba(168,123,255,.08)',
modFrame: '#00c9a7', modCell: '#1a2e48', modActive: '#00e6be',
modHead: '#2a4a6a', modHeadActive: '#00ffd0',
trayBg: '#0c1520', trayCell: '#152238', trayFilled: '#f5a623',
text: '#c8d8ec', muted: '#5a7090', accent: '#00d4e8',
rose: '#ff4f6e', emerald: '#00c9a7',
arrowFlow: 'rgba(0,212,232,.35)',
};
/* ---- 构建SVG ---- */
// defs
const defs = el('defs', null, svg);
// 网格背景图案
const pat = el('pattern', {id:'grid',width:40,height:40,patternUnits:'userSpaceOnUse'}, defs);
el('rect', {width:40,height:40,fill:C.bg}, pat);
el('line', {x1:0,y1:0,x2:0,y2:40,stroke:'#0e1830',strokeWidth:.5}, pat);
el('line', {x1:0,y1:0,x2:40,y2:0,stroke:'#0e1830',strokeWidth:.5}, pat);
// 发光滤镜
function makeGlow(id, color, std) {
const f = el('filter', {id, x:'-50%',y:'-50%',width:'200%',height:'200%'}, defs);
const gb = el('feGaussianBlur', {in:'SourceGraphic',stdDeviation:String(std),result:'blur'}, f);
const cm = el('feColorMatrix', {in:'blur',type:'matrix',values:'1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0',result:'glow'}, f);
const merge = el('feMerge', null, f);
el('feMergeNode', {in:'glow'}, merge);
el('feMergeNode', {in:'SourceGraphic'}, merge);
}
makeGlow('glowCyan', C.visZone, 4);
makeGlow('glowAmber', C.mat, 3);
makeGlow('glowEmerald', C.modActive, 4);
makeGlow('glowRose', C.rose, 3);
// 背景层
const bgLayer = el('g', {id:'bg'}, svg);
el('rect', {width:1440,height:880,fill:'url(#grid)'}, bgLayer);
// 静态标注层
const labelLayer = el('g', {id:'labels'}, svg);
// 输送带
const convGroup = el('g', {id:'conv'}, svg);
el('rect', {x:L.convX, y:L.convY, width:L.convW, height:L.convH, rx:6, fill:C.convBg, stroke:C.laneLine,strokeWidth:1}, convGroup);
// 8条流道
for (let i = 0; i < L.numLanes; i++) {
const ly = L.convY + 8 + i * (L.laneH + L.laneGap);
el('rect', {x:L.convX+4, y:ly, width:L.convW-8, height:L.laneH, rx:3, fill:C.lane, opacity:.7}, convGroup);
}
// 视觉区
el('rect', {x:L.convX+L.visX-L.convX, y:L.convY-4, width:L.visW, height:L.convH+8, rx:3, fill:C.visBg, stroke:C.visZone,strokeWidth:1,strokeDasharray:'3,3',opacity:.7}, convGroup);
// 翻转区
el('rect', {x:L.convX+L.flipX-L.convX, y:L.convY-4, width:L.flipW, height:L.convH+8, rx:3, fill:C.flipBg, stroke:C.flipZone,strokeWidth:1,strokeDasharray:'3,3',opacity:.7}, convGroup);
// 流道箭头
for (let i = 0; i < L.numLanes; i++) {
const ly = L.convY + 8 + i * (L.laneH + L.laneGap) + L.laneH/2;
for (let ax = 100; ax < L.convW-40; ax += 80) {
el('polygon', {points:`${L.convX+ax},${ly-3} ${L.convX+ax+8},${ly} ${L.convX+ax},${ly+3}`,fill:C.arrowFlow}, convGroup);
}
}
// 标签
function addLabel(x, y, text, color, size, anchor) {
const t = el('text', {x,y,fill:color||C.text,'font-family':"'Rajdhani',sans-serif",'font-size':size||13,'font-weight':600,'text-anchor':anchor||'start'}, labelLayer);
t.textContent = text;
return t;
}
function addLabelCN(x, y, text, color, size, anchor) {
const t = el('text', {x,y,fill:color||C.muted,'font-family':"'Noto Sans SC',sans-serif",'font-size':size||11,'text-anchor':anchor||'start'}, labelLayer);
t.textContent = text;
return t;
}
addLabel(L.convX+10, L.convY-14, 'WIDE CONVEYOR', C.visZone, 13);
addLabelCN(L.convX+10, L.convY-2, '8列宽幅输送带 · 线扫视觉+瞬时翻板', C.muted, 10);
addLabel(L.convX+L.visX-L.convX+L.visW/2, L.convY+L.convH+20, 'VISION', C.visZone, 11, 'middle');
addLabelCN(L.convX+L.visX-L.convX+L.visW/2, L.convY+L.convH+34, '线扫识别', C.muted, 9, 'middle');
addLabel(L.convX+L.flipX-L.convX+L.flipW/2, L.convY+L.convH+20, 'FLIP', C.flipZone, 11, 'middle');
addLabelCN(L.convX+L.flipX-L.convX+L.flipW/2, L.convY+L.convH+34, '瞬时翻板', C.muted, 9, 'middle');
// 伯努利吸盘模组框架
const modGroup = el('g', {id:'module'}, svg);
const modFrameRect = el('rect', {x:0,y:0,width:modGridSize+20,height:modGridSize+20,rx:6,fill:'rgba(0,201,167,.06)',stroke:C.modFrame,strokeWidth:1.5,strokeDasharray:'6,3'}, modGroup);
// 模组标题
const modTitle = el('text', {x:modGridSize/2+10,y:-12,fill:C.modFrame,'font-family':"'Rajdhani',sans-serif",'font-size':12,'font-weight':600,'text-anchor':'middle'}, modGroup);
modTitle.textContent = '8×8 BERNOULLI ARRAY';
// 64个吸盘头
const modCells = [];
for (let r = 0; r < 8; r++) {
modCells[r] = [];
for (let c = 0; c < 8; c++) {
const cx = 10 + c * (L.modCellSize + L.modCellGap) + L.modCellSize/2;
const cy = 10 + r * (L.modCellSize + L.modCellGap) + L.modCellSize/2;
// 伯努利吸盘外圈
const outer = el('circle', {cx,cy,r:L.modCellSize/2,fill:C.modCell,stroke:C.modHead,strokeWidth:1}, modGroup);
// 内圈(吸附孔)
const inner = el('circle', {cx,cy,r:L.modCellSize/5,fill:C.modHead}, modGroup);
// 物料占位(初始隐藏)
const mat = el('rect', {x:cx-L.modCellSize/2+2,y:cy-L.modCellSize/2+2,width:L.modCellSize-4,height:L.modCellSize-4,rx:3,fill:C.mat,opacity:0}, modGroup);
modCells[r][c] = {outer, inner, mat, cx, cy, filled: false};
}
}
// Tray盘区域
const trayGroup = el('g', {id:'tray'}, svg);
el('rect', {x:L.trayX-10,y:L.trayY-10,width:L.trayW+20,height:L.trayH+50,rx:8,fill:C.trayBg,stroke:'#1e3458',strokeWidth:1}, trayGroup);
addLabel(L.trayX+L.trayW/2, L.trayY-18, 'TRAY STATION', C.muted, 12, 'middle');
addLabelCN(L.trayX+L.trayW/2, L.trayY+L.trayH+35, '双工位切换 · 满盘/空盘无缝衔接', C.muted, 10, 'middle');
const trayCells = [];
for (let r = 0; r < trayGridRows; r++) {
trayCells[r] = [];
for (let c = 0; c < trayGridCols; c++) {
const tx = L.trayX + c * (L.trayCellSize + L.trayCellGap);
const ty = L.trayY + r * (L.trayCellSize + L.trayCellGap);
const cell = el('rect', {x:tx,y:ty,width:L.trayCellSize,height:L.trayCellSize,rx:3,fill:C.trayCell,stroke:'#1a2e48',strokeWidth:.5}, trayGroup);
trayCells[r][c] = {el: cell, filled: false};
}
}
// 物料在输送带上的动画元素
const matGroup = el('g', {id:'mats'}, svg);
const matPool = [];
const MAT_POOL_SIZE = 48;
for (let i = 0; i < MAT_POOL_SIZE; i++) {
const m = el('rect', {width:L.laneH-4,height:L.laneH-4,rx:3,fill:C.mat,opacity:0}, matGroup);
matPool.push({el:m, active:false, lane:0, x:0, flipped:false});
}
// 视觉扫描线
const scanLine = el('line', {x1:0,y1:0,x2:0,y2:L.convH+8,stroke:C.visZone,strokeWidth:2,opacity:0}, svg);
// 翻转指示器
const flipIndicators = [];
for (let i = 0; i < L.numLanes; i++) {
const fy = L.convY + 8 + i * (L.laneH + L.laneGap) + L.laneH/2;
const fx = L.convX + L.flipX - L.convX + L.flipW/2;
const ind = el('text', {x:fx,y:fy+4,fill:C.flipZone,'font-size':14,'text-anchor':'middle',opacity:0}, svg);
ind.textContent = '↻';
flipIndicators.push(ind);
}
// 高亮脉冲(吸取时)
const pickPulse = el('rect', {x:0,y:0,width:20,height:L.convH,rx:4,fill:'none',stroke:C.emerald,strokeWidth:2,opacity:0}, svg);
// 释放脉冲
const releasePulse = el('rect', {x:L.trayX-5,y:L.trayY-5,width:L.trayW+10,height:L.trayH+10,rx:10,fill:'none',stroke:C.amber,strokeWidth:2,opacity:0}, svg);
// 动态标注
const dynLabel = el('text', {x:720,y:660,fill:C.emerald,'font-family':"'Rajdhani',sans-serif",'font-size':16,'font-weight':700,'text-anchor':'middle',opacity:0}, svg);
// 连接箭头(模组移动路径)
const pathLine = el('line', {x1:L.modConvX+modGridSize/2+10,y1:L.modConvY+modGridSize+30,x2:L.modTrayX+modGridSize/2+10,y2:L.modConvY+modGridSize+30,stroke:'#1a2e48',strokeWidth:1.5,strokeDasharray:'8,6'}, svg);
addLabel((L.modConvX+L.modTrayX)/2+modGridSize/2+10, L.modConvY+modGridSize+48, 'TRANSFER PATH', '#1a2e48', 10, 'middle');
// 下方时间线
const tlGroup = el('g', {id:'timeline'}, svg);
el('rect', {x:L.tlX,y:L.tlY,width:L.tlW,height:L.tlH,rx:4,fill:'#0a1020',stroke:C.laneLine,strokeWidth:1}, tlGroup);
const tlProgress = el('rect', {x:L.tlX,y:L.tlY,width:0,height:L.tlH,rx:4,fill:'rgba(0,212,232,.25)'}, tlGroup);
const tlMarker = el('rect', {x:L.tlX,y:L.tlY-2,width:3,height:L.tlH+4,rx:1,fill:C.visZone}, tlGroup);
addLabelCN(L.tlX, L.tlY+L.tlH+18, '时间线 · 一个完整周期', C.muted, 10);
// IFR说明
const ifrGroup = el('g', {id:'ifr'}, svg);
addLabel(L.tlX, L.tlY+60, 'IFR IDEAL FINAL RESULT', C.emerald, 14);
addLabelCN(L.tlX, L.tlY+80, '核心矛盾: 1s/pcs极速节拍 vs 换盘停顿', C.muted, 11);
addLabelCN(L.tlX, L.tlY+98, '理想解: 8列并吸→8次步进→整体放置 → 等效0.03s/pcs, 零停顿', C.visZone, 11);
addLabelCN(L.tlX, L.tlY+116, '关键资源复用: 模组自身8×8框架作为缓存空间, 省去独立缓存机构', C.amber, 11);
// 节拍对比图
const cmpGroup = el('g', {id:'compare'}, svg);
const cmpX = 820, cmpY = L.tlY + 55;
addLabel(cmpX, cmpY, 'BEAT COMPARISON', C.muted, 12);
// 传统方式
addLabelCN(cmpX, cmpY+20, '传统串行: 1s/pcs + 换盘停顿', C.rose, 10);
el('rect', {x:cmpX,y:cmpY+28,width:300,height:14,rx:3,fill:'#1a1020'}, cmpGroup);
el('rect', {x:cmpX,y:cmpY+28,width:250,height:14,rx:3,fill:'rgba(255,79,110,.3)'}, cmpGroup);
el('rect', {x:cmpX+250,y:cmpY+28,width:50,height:14,rx:0,fill:C.rose,opacity:.6}, cmpGroup);
addLabelCN(cmpX+260, cmpY+39, '停顿', '#fff', 8);
// 并行方式
addLabelCN(cmpX, cmpY+56, '8列并行: 等效0.03s/pcs + 零停顿', C.emerald, 10);
el('rect', {x:cmpX,y:cmpY+64,width:300,height:14,rx:3,fill:'rgba(0,201,167,.3)'}, cmpGroup);
el('rect', {x:cmpX,y:cmpY+64,width:300,height:14,rx:3,fill:'rgba(0,201,167,.5)'}, cmpGroup);
addLabelCN(cmpX+200, cmpY+75, '连续无中断', '#fff', 8);
/* ---- 动画状态机 ---- */
const PHASES = [
{name:'CONVEYOR',dur:.5}, // 输送带送料
{name:'SCAN',dur:.3}, // 视觉扫描
{name:'FLIP',dur:.2}, // 翻转
{name:'DESCEND',dur:.25}, // 模组下降
{name:'PICK',dur:.15}, // 吸取
{name:'ASCEND',dur:.25}, // 模组上升
{name:'SHIFT',dur:.25}, // 模组内部平移
];
const POST_PHASES = [
{name:'TRANSFER',dur:.6}, // 模组移至Tray
{name:'RELEASE_DESC',dur:.25}, // 下降释放
{name:'RELEASE',dur:.2}, // 释放64个
{name:'RELEASE_ASC',dur:.25}, // 上升
{name:'RETURN',dur:.5}, // 返回输送带
{name:'SWITCH',dur:.3}, // 切换Tray
];
let state = {
playing: true,
speed: 1.0,
gap: 1.5,
rowIdx: 0, // 当前吸取行 0-7
phaseIdx: 0, // 当前阶段
phaseTime: 0, // 当前阶段已过时间
inPost: false, // 是否在后期阶段(吸取8行后)
moduleX: L.modConvX,
moduleY: L.modConvY,
moduleH: 0, // 0=上方, 1=下降到位
moduleGrid: Array.from({length:8}, ()=>Array(8).fill(false)), // 吸取状态
trayGrid: Array.from({length:8}, ()=>Array(8).fill(false)),
totalPlaced: 0,
cyclesDone: 0,
conveyorOffset: 0,
scanActive: false,
flipLanes: [], // 需要翻转的流道
};
let lastTime = 0;
/* ---- 物料在输送带上的管理 ---- */
function spawnMaterials() {
// 在输送带终端放置8个物料(每条流道一个)
for (let i = 0; i < L.numLanes; i++) {
const m = getFreeMat();
if (m) {
m.active = true;
m.lane = i;
m.x = L.convX + L.convW - L.laneH - 10;
m.flipped = Math.random() > 0.7; // 30%需要翻转
m.el.setAttribute('opacity', '0.85');
}
}
}
function getFreeMat() {
for (const m of matPool) {
if (!m.active) return m;
}
return null;
}
function updateMaterials(dt) {
state.conveyorOffset += dt * 120; // 输送带速度
for (const m of matPool) {
if (m.active) {
const ly = L.convY + 8 + m.lane * (L.laneH + L.laneGap) + 2;
m.el.setAttribute('x', m.x);
m.el.setAttribute('y', ly);
m.el.setAttribute('opacity', '0.85');
}
}
}
function hideMat(lane) {
for (const m of matPool) {
if (m.active && m.lane === lane) {
m.active = false;
m.el.setAttribute('opacity', '0');
return;
}
}
}
function clearAllMats() {
for (const m of matPool) {
m.active = false;
m.el.setAttribute('opacity', '0');
}
}
/* ---- 模组渲染 ---- */
function renderModule() {
modGroup.setAttribute('transform', `translate(${state.moduleX},${state.moduleY})`);
// 高度指示: 通过缩放和透明度
const hScale = lerp(1, 0.92, state.moduleH);
const hOpacity = lerp(1, 0.7, state.moduleH);
// 模组整体缩放表示高度变化
const cx = modGridSize/2+10, cy = modGridSize/2+10;
const subGroup = modGroup;
// 更简洁: 用translate模拟下降
const yOff = state.moduleH * 20; // 下降20px
subGroup.setAttribute('transform', `translate(${state.moduleX},${state.moduleY + yOff})`);
// 更新每个格子
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const cell = modCells[r][c];
if (state.moduleGrid[r][c]) {
cell.mat.setAttribute('opacity', '0.9');
cell.inner.setAttribute('fill', C.modHeadActive);
cell.inner.setAttribute('filter', 'url(#glowEmerald)');
cell.outer.setAttribute('stroke', C.modActive);
} else {
cell.mat.setAttribute('opacity', '0');
cell.inner.setAttribute('fill', C.modHead);
cell.inner.removeAttribute('filter');
cell.outer.setAttribute('stroke', C.modHead);
}
}
}
}
/* ---- Tray渲染 ---- */
function renderTray() {
for (let r = 0; r < trayGridRows; r++) {
for (let c = 0; c < trayGridCols; c++) {
const cell = trayCells[r][c];
if (state.trayGrid[r][c]) {
cell.el.setAttribute('fill', C.trayFilled);
cell.el.setAttribute('opacity', '0.9');
} else {
cell.el.setAttribute('fill', C.trayCell);
cell.el.setAttribute('opacity', '1');
}
}
}
}
/* ---- 扫描线动画 ---- */
function renderScanLine(t) {
if (state.scanActive) {
const sx = L.convX + L.visX - L.convX;
const animX = sx + Math.sin(t * 8) * L.visW * 0.4;
scanLine.setAttribute('x1', animX);
scanLine.setAttribute('x2', animX);
scanLine.setAttribute('y1', L.convY - 4);
scanLine.setAttribute('y2', L.convY + L.convH + 4);
scanLine.setAttribute('opacity', 0.7 + Math.sin(t * 12) * 0.3);
scanLine.setAttribute('filter', 'url(#glowCyan)');
} else {
scanLine.setAttribute('opacity', '0');
}
}
/* ---- 翻转指示 ---- */
function renderFlipIndicators() {
for (let i = 0; i < L.numLanes; i++) {
const isFlipping = state.flipLanes.includes(i);
flipIndicators[i].setAttribute('opacity', isFlipping ? '0.9' : '0');
}
}
/* ---- 选取脉冲 ---- */
function renderPickPulse(opacity) {
if (opacity > 0) {
const px = L.convX + L.convW - L.laneH - 10;
pickPulse.setAttribute('x', px - 4);
pickPulse.setAttribute('y', L.convY);
pickPulse.setAttribute('width', L.laneH + 8);
pickPulse.setAttribute('height', L.convH);
pickPulse.setAttribute('opacity', opacity);
pickPulse.setAttribute('filter', 'url(#glowEmerald)');
} else {
pickPulse.setAttribute('opacity', '0');
}
}
/* ---- 释放脉冲 ---- */
function renderReleasePulse(opacity) {
if (opacity > 0) {
releasePulse.setAttribute('opacity', opacity);
releasePulse.setAttribute('filter', 'url(#glowAmber)');
} else {
releasePulse.setAttribute('opacity', '0');
}
}
/* ---- 动态标注 ---- */
function renderDynLabel(text, opacity) {
dynLabel.textContent = text;
dynLabel.setAttribute('opacity', opacity);
}
/* ---- 时间线 ---- */
function renderTimeline() {
const totalPhases = PHASES.length * 8 + POST_PHASES.length;
let completedPhases = state.rowIdx * PHASES.length + state.phaseIdx;
if (state.inPost) {
completedPhases = 8 * PHASES.length + state.phaseIdx;
}
const progress = completedPhases / totalPhases;
const pw = Math.max(0, Math.min(L.tlW, progress * L.tlW));
tlProgress.setAttribute('width', pw);
const mx = L.tlX + pw;
tlMarker.setAttribute('x', mx - 1);
}
/* ---- UI更新 ---- */
function updateUI() {
const phases = state.inPost ? POST_PHASES : PHASES;
const phaseName = phases[state.phaseIdx]?.name || 'IDLE';
document.getElementById('stPhase').textContent = phaseName;
document.getElementById('stRow').textContent = `${state.rowIdx}/8`;
document.getElementById('stTotal').textContent = `${state.totalPlaced} pcs`;
document.getElementById('stRate').textContent = state.totalPlaced > 0 ? '0.03s/pcs' : '—';
document.getElementById('stStop').textContent = '0s';
const dot = document.getElementById('runDot');
dot.className = state.playing ? 'dot on' : 'dot off';
}
/* ---- 阶段逻辑 ---- */
function advancePhase() {
if (!state.inPost) {
state.phaseIdx++;
if (state.phaseIdx >= PHASES.length) {
state.phaseIdx = 0;
state.rowIdx++;
if (state.rowIdx >= 8) {
// 进入后期阶段
state.inPost = true;
state.phaseIdx = 0;
} else {
// 下一行, 重新生成物料
spawnMaterials();
}
}
} else {
state.phaseIdx++;
if (state.phaseIdx >= POST_PHASES.length) {
// 完成一个完整周期
resetCycle();
}
}
}
function resetCycle() {
state.rowIdx = 0;
state.phaseIdx = 0;
state.inPost = false;
state.moduleX = L.modConvX;
state.moduleY = L.modConvY;
state.moduleH = 0;
state.moduleGrid = Array.from({length:8}, ()=>Array(8).fill(false));
state.cyclesDone++;
clearAllMats();
spawnMaterials();
}
/* ---- 阶段更新 ---- */
let pickPulseOp = 0;
let releasePulseOp = 0;
let dynLabelOp = 0;
let dynLabelText = '';
function updatePhase(dt) {
const phases = state.inPost ? POST_PHASES : PHASES;
if (state.phaseIdx >= phases.length) return;
const phase = phases[state.phaseIdx];
const dur = phase.dur / state.speed;
state.phaseTime += dt;
const t = Math.min(1, state.phaseTime / dur);
switch(phase.name) {
case 'CONVEYOR':
// 物料流向终端
state.scanActive = false;
for (const m of matPool) {
if (m.active) {
m.x += dt * 200 * state.speed;
if (m.x > L.convX + L.convW - L.laneH - 10) {
m.x = L.convX + L.convW - L.laneH - 10;
}
}
}
renderDynLabel('', 0);
break;
case 'SCAN':
state.scanActive = true;
// 决定哪些流道需要翻转
state.flipLanes = [];
for (const m of matPool) {
if (m.active && m.flipped) {
state.flipLanes.push(m.lane);
}
}
renderDynLabel(`SCANNING · ${8-state.flipLanes.length} OK / ${state.flipLanes.length} FLIP`, t);
break;
case 'FLIP':
state.scanActive = false;
// 翻转动画(旋转指示器)
for (const m of matPool) {
if (m.active && m.flipped) {
// 视觉翻转效果 - 用颜色变化表示
m.el.setAttribute('fill', '#e88e0a');
setTimeout(() => { if(m.active) m.el.setAttribute('fill', C.mat); }, 200);
}
}
renderDynLabel(`FLIPPING ${state.flipLanes.length} PIECES`, t);
break;
case 'DESCEND':
state.moduleH = easeInOut(t);
state.scanActive = false;
renderDynLabel('DESCENDING · GAP ' + state.gap + 'mm', t);
break;
case 'PICK':
pickPulseOp = Math.sin(t * Math.PI);
if (t >= 0.5 && !state.moduleGrid[state.rowIdx].some(v=>v)) {
// 吸取! 将8个物料放入模组当前行
for (let c = 0; c < 8; c++) {
state.moduleGrid[state.rowIdx][c] = true;
hideMat(c);
}
// 伯努利效应高亮
for (let c = 0; c < 8; c++) {
const cell = modCells[state.rowIdx][c];
cell.inner.setAttribute('fill', C.modHeadActive);
}
}
renderDynLabel('PICK · BERNOULLI NON-CONTACT', t);
break;
case 'ASCEND':
pickPulseOp = Math.max(0, pickPulseOp - dt * 4);
state.moduleH = 1 - easeInOut(t);
renderDynLabel(`ROW ${state.rowIdx+1}/8 LOADED`, t);
break;
case 'SHIFT':
// 内部平移 - 视觉上微微晃动表示移位
pickPulseOp = 0;
const shiftX = Math.sin(t * Math.PI * 2) * 2;
state.moduleX = L.modConvX + shiftX;
renderDynLabel(`SHIFTING · ROW ${state.rowIdx+1}/8 STORED`, t);
break;
case 'TRANSFER':
state.moduleX = lerp(L.modConvX, L.modTrayX, easeInOut(t));
renderDynLabel('TRANSFERRING 64 PCS → TRAY', t);
break;
case 'RELEASE_DESC':
state.moduleH = easeInOut(t);
renderDynLabel('DESCENDING TO TRAY', t);
break;
case 'RELEASE':
releasePulseOp = Math.sin(t * Math.PI);
if (t >= 0.3) {
// 释放64个物料到Tray
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
state.trayGrid[r][c] = true;
}
}
state.totalPlaced += 64;
}
renderDynLabel('RELEASE · 64 PCS BATCH DROP', t);
break;
case 'RELEASE_ASC':
releasePulseOp = Math.max(0, releasePulseOp - dt * 3);
state.moduleH = 1 - easeInOut(t);
if (t > 0.5) {
// 清空模组
state.moduleGrid = Array.from({length:8}, ()=>Array(8).fill(false));
}
renderDynLabel('ASCENDING · TRAY FILLED', t);
break;
case 'RETURN':
state.moduleX = lerp(L.modTrayX, L.modConvX, easeInOut(t));
renderDynLabel('RETURNING · TRAY SWITCHING', t);
break;
case 'SWITCH':
// Tray切换 - 清空Tray准备下一轮
if (t > 0.5) {
state.trayGrid = Array.from({length:8}, ()=>Array(8).fill(false));
}
renderDynLabel('SWITCHING TRAY · ZERO PAUSE', t);
break;
}
if (state.phaseTime >= dur) {
state.phaseTime = 0;
pickPulseOp = 0;
advancePhase();
}
}
/* ---- 主循环 ---- */
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = Math.min(0.05, (timestamp - lastTime) / 1000);
lastTime = timestamp;
if (state.playing) {
updatePhase(dt);
}
updateMaterials(dt);
renderModule();
renderTray();
renderScanLine(timestamp / 1000);
renderFlipIndicators();
renderPickPulse(pickPulseOp);
renderReleasePulse(releasePulseOp);
renderDynLabel(dynLabelText, dynLabelOp);
renderTimeline();
updateUI();
requestAnimationFrame(animate);
}
// 覆盖renderDynLabel以保存状态
const origRenderDynLabel = renderDynLabel;
renderDynLabel = function(text, opacity) {
dynLabelText = text;
dynLabelOp = opacity;
};
/* ---- 初始化 ---- */
function init() {
spawnMaterials();
renderModule();
renderTray();
requestAnimationFrame(animate);
}
/* ---- 控件 ---- */
document.getElementById('btnPlay').addEventListener('click', function() {
state.playing = !state.playing;
this.textContent = state.playing ? 'RUN' : 'PAUSE';
this.classList.toggle('on', state.playing);
});
document.getElementById('btnStep').addEventListener('click', function() {
if (!state.playing) {
// 单步: 推进当前阶段到结束
state.phaseTime = 999;
}
});
document.getElementById('btnReset').addEventListener('click', function() {
state.rowIdx = 0;
state.phaseIdx = 0;
state.phaseTime = 0;
state.inPost = false;
state.moduleX = L.modConvX;
state.moduleY = L.modConvY;
state.moduleH = 0;
state.moduleGrid = Array.from({length:8}, ()=>Array(8).fill(false));
state.trayGrid = Array.from({length:8}, ()=>Array(8).fill(false));
state.totalPlaced = 0;
pickPulseOp = 0;
releasePulseOp = 0;
clearAllMats();
spawnMaterials();
});
document.getElementById('spd').addEventListener('input', function() {
state.speed = parseFloat(this.value);
document.getElementById('spdV').textContent = state.speed.toFixed(1) + 'x';
});
document.getElementById('gapR').addEventListener('input', function() {
state.gap = parseFloat(this.value);
document.getElementById('gapV').textContent = state.gap.toFixed(1) + 'mm';
});
init();
</script>
</body>
</html>
这个实现提供了一个完整的面阵式伯努利吸盘极速装盘原理动画,包含以下核心特性:
IFR 理想解视觉叙事:
- 聚焦展示"8列并吸 → 8次步进 → 整体放置"的完整工作循环,直接呈现消除停顿后的理想状态
- 底部节拍对比图直观呈现传统串行方案与并行方案的本质差异(停顿 vs 连续)
- 关键资源复用逻辑:模组自身 8×8 框架即缓存空间,无需额外机构
动画核心流程:
- 8条流道物料并行流向终端
- 线扫视觉区扫描识别(青色扫描线动画)
- 翻转区对需要翻面的物料执行 180° 翻转(紫色指示器)
- 8×8 伯努利阵列模组下降 → 非接触吸取 8 个 → 上升 → 内部平移蓄位
- 循环 8 次吸满 64 个后,整体平移至 Tray 盘上方
- 一次性释放 64 个物料 → 模组返回 → 双工位无缝切换
交互控件:
- RUN/PAUSE 播放控制、STEP 单步推进、RESET 重置
- Speed 滑块调节动画速率(0.2x–3x)
- Gap 滑块调节伯努利吸盘间隙参数(1–3mm),实时显示在下降阶段标注中
视觉设计:
- 深色工业主题,青色/琥珀/翠绿/紫色四色编码区分功能区域
- 吸取/释放瞬间的脉冲发光效果突出核心创新动作
- 模组网格逐行填充过程清晰可见,吸头激活态以发光标识
- 时间线进度条展示完整周期推进
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
