<!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 rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#070B12;--surface:#0E1520;--card:#141D2B;
--border:#1E2A3A;--text:#D4DAE3;--muted:#6B7A8D;
--accent:#00E5FF;--accent2:#00BFA5;--warn:#FF6D00;
--pcb:#1B5E20;--membrane:#FFB74D;--stress-ok:#00E676;--stress-bad:#FF1744;
}
body{
background:var(--bg);color:var(--text);
font-family:'IBM Plex Mono',monospace;
min-height:100vh;display:flex;flex-direction:column;align-items:center;
padding:16px;overflow-x:hidden;
}
.container{max-width:1500px;width:100%}
header{text-align:center;margin-bottom:12px}
header h1{
font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(22px,3.5vw,38px);
letter-spacing:-0.02em;color:#fff;
background:linear-gradient(135deg,#fff 30%,var(--accent));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
header p{font-size:13px;color:var(--muted);margin-top:4px;letter-spacing:0.05em}
.svg-wrap{
background:var(--surface);border:1px solid var(--border);border-radius:12px;
overflow:hidden;position:relative;
box-shadow:0 0 60px rgba(0,229,255,0.04),0 4px 30px rgba(0,0,0,0.5);
}
.svg-wrap svg{display:block;width:100%;height:auto}
.controls{
display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:center;
margin-top:14px;padding:14px 20px;
background:var(--card);border:1px solid var(--border);border-radius:10px;
}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{font-size:12px;color:var(--muted);white-space:nowrap}
.ctrl-group span.val{font-size:12px;color:var(--accent);min-width:42px;text-align:right}
button{
background:var(--surface);border:1px solid var(--border);color:var(--text);
padding:7px 16px;border-radius:6px;cursor:pointer;font-family:inherit;font-size:12px;
transition:all .2s;display:flex;align-items:center;gap:6px;
}
button:hover{border-color:var(--accent);color:#fff;background:rgba(0,229,255,0.08)}
button:active{transform:scale(0.96)}
button.primary{background:rgba(0,229,255,0.12);border-color:var(--accent);color:var(--accent)}
input[type=range]{
-webkit-appearance:none;width:100px;height:4px;border-radius:2px;
background:var(--border);outline:none;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;border-radius:50%;
background:var(--accent);cursor:pointer;border:2px solid var(--bg);
}
.toggle-btn{position:relative;padding-left:32px}
.toggle-btn::before{
content:'';position:absolute;left:8px;top:50%;transform:translateY(-50%);
width:16px;height:16px;border-radius:3px;border:1.5px solid var(--muted);
transition:all .2s;
}
.toggle-btn.active::before{background:var(--accent);border-color:var(--accent)}
.legend{
display:flex;flex-wrap:wrap;gap:16px;justify-content:center;
margin-top:10px;font-size:11px;color:var(--muted);
}
.legend-item{display:flex;align-items:center;gap:5px}
.legend-dot{width:10px;height:10px;border-radius:2px}
.phase-bar{
margin-top:10px;display:flex;gap:2px;height:4px;border-radius:2px;overflow:hidden;
}
.phase-seg{flex:1;background:var(--border);border-radius:1px;transition:background .3s}
.phase-seg.active{background:var(--accent)}
.phase-seg.done{background:var(--accent2)}
.info-row{
display:flex;gap:20px;justify-content:center;flex-wrap:wrap;
margin-top:10px;font-size:11px;
}
.info-card{
background:var(--card);border:1px solid var(--border);border-radius:8px;
padding:10px 16px;text-align:center;min-width:140px;
}
.info-card .label{color:var(--muted);font-size:10px;margin-bottom:3px}
.info-card .value{color:var(--accent);font-size:16px;font-weight:600;font-family:'Syne',sans-serif}
@media(max-width:700px){
.controls{gap:8px;padding:10px 12px}
input[type=range]{width:70px}
button{padding:6px 10px;font-size:11px}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>柔性剥离排架 — 线剥离原理</h1>
<p>IFR: 将点拉扯转化为线剥离,应力均匀分散,高速不断裂</p>
</header>
<div class="svg-wrap">
<svg id="scene" viewBox="0 0 1400 720" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 背景网格 -->
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1A2332" stroke-width="0.5"/>
</pattern>
<!-- PCB 纹理 -->
<pattern id="pcbTex" width="20" height="20" patternUnits="userSpaceOnUse">
<rect width="20" height="20" fill="#1B5E20"/>
<circle cx="10" cy="10" r="1.2" fill="#2E7D32" opacity="0.5"/>
<path d="M0 10h20M10 0v20" stroke="#2E7D32" stroke-width="0.3" opacity="0.3"/>
</pattern>
<!-- 吸盘金属渐变 -->
<linearGradient id="cupGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#90A4AE"/>
<stop offset="50%" stop-color="#607D8B"/>
<stop offset="100%" stop-color="#455A64"/>
</linearGradient>
<!-- 膜渐变 -->
<linearGradient id="memGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FFB74D" stop-opacity="0.7"/>
<stop offset="100%" stop-color="#FF9800" stop-opacity="0.5"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowStrong">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowSoft">
<feGaussianBlur stdDeviation="12" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- 剥离前线渐变 -->
<linearGradient id="peelLineGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00E5FF" stop-opacity="0"/>
<stop offset="30%" stop-color="#00E5FF" stop-opacity="1"/>
<stop offset="70%" stop-color="#00E5FF" stop-opacity="1"/>
<stop offset="100%" stop-color="#00E5FF" stop-opacity="0"/>
</linearGradient>
<!-- 应力色谱 -->
<linearGradient id="stressSpectrum" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#00E676"/>
<stop offset="50%" stop-color="#FFEB3B"/>
<stop offset="100%" stop-color="#FF1744"/>
</linearGradient>
<!-- 卷膜辊渐变 -->
<linearGradient id="rollerGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#78909C"/>
<stop offset="30%" stop-color="#B0BEC5"/>
<stop offset="70%" stop-color="#B0BEC5"/>
<stop offset="100%" stop-color="#546E7A"/>
</linearGradient>
</defs>
<!-- 背景 -->
<rect width="1400" height="720" fill="#0A0F18"/>
<rect width="1400" height="720" fill="url(#grid)" opacity="0.5"/>
<!-- 标注层(最后渲染,确保在最上方) -->
<g id="annotationsLayer"></g>
<!-- PCB -->
<g id="pcbGroup">
<rect id="pcb" x="200" y="430" width="1000" height="45" rx="2" fill="url(#pcbTex)" stroke="#2E7D32" stroke-width="1"/>
<text x="700" y="458" text-anchor="middle" fill="#4CAF50" font-size="11" font-family="'IBM Plex Mono',monospace" opacity="0.6">PCB 基板</text>
</g>
<!-- 膜 -->
<path id="membrane" fill="url(#memGrad)" stroke="#FF9800" stroke-width="0.8"/>
<!-- 剥离前线发光 -->
<g id="peelFrontGroup" opacity="0">
<line id="peelFrontLine" x1="0" y1="390" x2="0" y2="475" stroke="url(#peelLineGrad)" stroke-width="3" filter="url(#glowStrong)"/>
<line id="peelFrontCore" x1="0" y1="400" x2="0" y2="465" stroke="#fff" stroke-width="1" opacity="0.8"/>
</g>
<!-- 排架组 -->
<g id="rackGroup">
<rect id="rackBar" x="215" y="330" width="260" height="14" rx="4" fill="#37474F" stroke="#546E7A" stroke-width="1"/>
<text id="rackLabel" x="345" y="342" text-anchor="middle" fill="#90A4AE" font-size="9" font-family="'IBM Plex Mono',monospace">柔性剥离排架</text>
<!-- 吸盘将由 JS 生成 -->
<g id="cupsGroup"></g>
</g>
<!-- 卷膜辊 -->
<g id="rollerGroup" opacity="0">
<rect x="1080" y="380" width="60" height="80" rx="6" fill="#263238" stroke="#455A64" stroke-width="1"/>
<circle cx="1110" cy="400" r="16" fill="url(#rollerGrad)" stroke="#78909C" stroke-width="1"/>
<circle cx="1110" cy="400" r="5" fill="#455A64"/>
<line id="rollerIndicator" x1="1110" y1="400" x2="1123" y2="396" stroke="#90A4AE" stroke-width="1.5" stroke-linecap="round"/>
<text x="1110" y="445" text-anchor="middle" fill="#78909C" font-size="9" font-family="'IBM Plex Mono',monospace">卷膜辊</text>
</g>
<!-- 应力可视化条 -->
<g id="stressGroup" opacity="0">
<text id="stressTitle" x="200" y="520" fill="#90A4AE" font-size="11" font-family="'IBM Plex Mono',monospace">应力分布</text>
<g id="stressBars"></g>
<text id="stressLabelOk" x="700" y="560" text-anchor="middle" fill="#00E676" font-size="12" font-family="'Syne',sans-serif" font-weight="700" opacity="0">应力均匀 — 线剥离</text>
</g>
<!-- 剥离角度标注 -->
<g id="angleAnnotation" opacity="0">
<path id="angleArc" d="" fill="none" stroke="#00E5FF" stroke-width="1" stroke-dasharray="3,2"/>
<text id="angleText" x="0" y="0" fill="#00E5FF" font-size="11" font-family="'IBM Plex Mono',monospace"></text>
</g>
<!-- 阶梯高度标注 -->
<g id="stepAnnotation" opacity="0">
<line id="stepLine1" x1="0" y1="0" x2="0" y2="0" stroke="#FF6D00" stroke-width="0.8" stroke-dasharray="4,2"/>
<line id="stepLine2" x1="0" y1="0" x2="0" y2="0" stroke="#FF6D00" stroke-width="0.8" stroke-dasharray="4,2"/>
<line id="stepLineH" x1="0" y1="0" x2="0" y2="0" stroke="#FF6D00" stroke-width="0.8"/>
<text id="stepText" x="0" y="0" fill="#FF6D00" font-size="10" font-family="'IBM Plex Mono',monospace"></text>
</g>
<!-- 间距标注 -->
<g id="spacingAnnotation" opacity="0">
<line id="spLine1" x1="0" y1="0" x2="0" y2="0" stroke="#CE93D8" stroke-width="0.8"/>
<line id="spLine2" x1="0" y1="0" x2="0" y2="0" stroke="#CE93D8" stroke-width="0.8"/>
<line id="spLineH" x1="0" y1="0" x2="0" y2="0" stroke="#CE93D8" stroke-width="0.8" stroke-dasharray="3,2"/>
<text id="spText" x="0" y="0" fill="#CE93D8" font-size="10" font-family="'IBM Plex Mono',monospace"></text>
</g>
<!-- 顶部视图插图 -->
<g id="insetGroup" transform="translate(1050,30)">
<rect x="0" y="0" width="300" height="160" rx="8" fill="#0E1520" stroke="#1E2A3A" stroke-width="1"/>
<text x="150" y="20" text-anchor="middle" fill="#6B7A8D" font-size="10" font-family="'IBM Plex Mono',monospace">俯视图 — 吸盘排布</text>
<g id="insetContent" transform="translate(30,30)"></g>
</g>
<!-- 阶段指示 -->
<g id="phaseIndicator" transform="translate(30,30)">
<text id="phaseText" x="0" y="0" fill="#00E5FF" font-size="14" font-family="'Syne',sans-serif" font-weight="700"></text>
<text id="phaseDesc" x="0" y="20" fill="#6B7A8D" font-size="11" font-family="'IBM Plex Mono',monospace"></text>
</g>
<!-- 运动轨迹 -->
<g id="motionTrail" opacity="0"></g>
</svg>
</div>
<!-- 进度条 -->
<div class="phase-bar" id="phaseBar">
<div class="phase-seg" data-phase="descend"></div>
<div class="phase-seg" data-phase="engage"></div>
<div class="phase-seg" data-phase="peel"></div>
<div class="phase-seg" data-phase="handoff"></div>
</div>
<!-- 控制面板 -->
<div class="controls">
<div class="ctrl-group">
<button id="btnPlay" class="primary"><i class="fas fa-play"></i> 播放</button>
<button id="btnRestart"><i class="fas fa-redo"></i> 重置</button>
</div>
<div class="ctrl-group">
<label>速度</label>
<input type="range" id="speedSlider" min="0.3" max="3" step="0.1" value="1">
<span class="val" id="speedVal">1.0x</span>
</div>
<div class="ctrl-group">
<label>阶梯高度</label>
<input type="range" id="stepSlider" min="0.5" max="3" step="0.1" value="1.5">
<span class="val" id="stepVal">1.5mm</span>
</div>
<div class="ctrl-group">
<label>吸盘数</label>
<input type="range" id="cupCountSlider" min="3" max="9" step="1" value="6">
<span class="val" id="cupCountVal">6</span>
</div>
<div class="ctrl-group">
<button id="btnStress" class="toggle-btn active">应力分布</button>
</div>
</div>
<!-- 参数信息 -->
<div class="info-row">
<div class="info-card"><div class="label">吸盘间距</div><div class="value" id="infoSpacing">10mm</div></div>
<div class="info-card"><div class="label">阶梯高度差</div><div class="value" id="infoStep">1.5mm</div></div>
<div class="info-card"><div class="label">起撕角度</div><div class="value" id="infoAngle">15°</div></div>
<div class="info-card"><div class="label">当前阶段</div><div class="value" id="infoPhase">待命</div></div>
</div>
<!-- 图例 -->
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#FFB74D"></div>保护膜</div>
<div class="legend-item"><div class="legend-dot" style="background:#1B5E20"></div>PCB基板</div>
<div class="legend-item"><div class="legend-dot" style="background:#607D8B"></div>真空吸盘</div>
<div class="legend-item"><div class="legend-dot" style="background:#00E5FF"></div>剥离前线</div>
<div class="legend-item"><div class="legend-dot" style="background:#00E676"></div>均匀应力</div>
</div>
</div>
<script>
// ===================== 配置 =====================
const C = {
pcbLeft: 200, pcbRight: 1200, pcbTop: 430, pcbH: 45,
memH: 8,
numCups: 6,
cupSpacing: 40, // SVG像素(代表10mm)
stepH: 8, // SVG像素(代表1.5mm,已放大以便可见)
stepHmm: 1.5, // 真实mm值
cupW: 22, cupH: 28,
rackStartY: 310,
peelAngle: 15,
maxPullX: 420,
maxLiftY: 100,
maxPeelDist: 750,
};
// 阶段时长(ms)
const PHASES = [
{ name: 'descend', label: '排架下降', desc: '排架下降贴合膜边缘', duration: 1800 },
{ name: 'engage', label: '依次吸附', desc: '阶梯吸盘依次真空吸附', duration: 2400 },
{ name: 'peel', label: '高速起撕', desc: '整体后拉,线剥离推进', duration: 5500 },
{ name: 'handoff', label: '卷膜接手', desc: '卷膜辊接管剩余剥离', duration: 2000 },
];
// ===================== 状态 =====================
const S = {
playing: false,
time: 0,
speed: 1,
stepHmm: 1.5,
numCups: 6,
showStress: true,
phase: 'idle',
progress: 0,
phaseIndex: -1,
};
let lastTs = null;
let cupEls = [];
let stressBarEls = [];
let insetCupEls = [];
// ===================== DOM 引用 =====================
const svg = document.getElementById('scene');
const membraneEl = document.getElementById('membrane');
const rackGroup = document.getElementById('rackGroup');
const rackBar = document.getElementById('rackBar');
const rackLabel = document.getElementById('rackLabel');
const cupsGroup = document.getElementById('cupsGroup');
const peelFrontGroup = document.getElementById('peelFrontGroup');
const peelFrontLine = document.getElementById('peelFrontLine');
const peelFrontCore = document.getElementById('peelFrontCore');
const rollerGroup = document.getElementById('rollerGroup');
const rollerIndicator = document.getElementById('rollerIndicator');
const stressGroup = document.getElementById('stressGroup');
const stressBarsG = document.getElementById('stressBars');
const stressLabelOk = document.getElementById('stressLabelOk');
const angleAnnotation = document.getElementById('angleAnnotation');
const stepAnnotation = document.getElementById('stepAnnotation');
const spacingAnnotation = document.getElementById('spacingAnnotation');
const phaseText = document.getElementById('phaseText');
const phaseDesc = document.getElementById('phaseDesc');
const insetContent = document.getElementById('insetContent');
// ===================== 初始化吸盘 =====================
function createCups() {
cupsGroup.innerHTML = '';
cupEls = [];
const n = S.numCups;
const totalW = (n - 1) * C.cupSpacing;
const startX = 345 - totalW / 2;
for (let i = 0; i < n; i++) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const x = startX + i * C.cupSpacing;
const stepOffset = i * C.stepH;
// 连接管
const tube = document.createElementNS('http://www.w3.org/2000/svg', 'line');
tube.setAttribute('x1', x); tube.setAttribute('y1', 344);
tube.setAttribute('x2', x); tube.setAttribute('y2', 430 - stepOffset - C.cupH);
tube.setAttribute('stroke', '#546E7A'); tube.setAttribute('stroke-width', '3');
tube.setAttribute('stroke-linecap', 'round');
g.appendChild(tube);
// 吸盘体
const body = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const bx = x - C.cupW / 2;
const by = 430 - stepOffset - C.cupH;
body.setAttribute('d', `M${bx + 3},${by} L${bx + C.cupW - 3},${by} Q${bx + C.cupW},${by} ${bx + C.cupW},${by + 3} L${bx + C.cupW + 2},${by + C.cupH - 4} Q${bx + C.cupW + 2},${by + C.cupH} ${bx + C.cupW - 2},${by + C.cupH} L${bx + 2},${by + C.cupH} Q${bx - 2},${by + C.cupH} ${bx - 2},${by + C.cupH - 4} L${bx},${by + 3} Q${bx},${by} ${bx + 3},${by} Z`);
body.setAttribute('fill', 'url(#cupGrad)'); body.setAttribute('stroke', '#78909C'); body.setAttribute('stroke-width', '0.5');
g.appendChild(body);
// 真空口指示
const vac = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
vac.setAttribute('cx', x); vac.setAttribute('cy', by + C.cupH - 1);
vac.setAttribute('rx', C.cupW / 2 - 3); vac.setAttribute('ry', '2');
vac.setAttribute('fill', '#37474F'); vac.setAttribute('stroke', '#546E7A'); vac.setAttribute('stroke-width', '0.5');
g.appendChild(vac);
// 发光圈(激活时显示)
const glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
glow.setAttribute('cx', x); glow.setAttribute('cy', by + C.cupH / 2);
glow.setAttribute('r', '18');
glow.setAttribute('fill', 'none'); glow.setAttribute('stroke', '#00E5FF');
glow.setAttribute('stroke-width', '2'); glow.setAttribute('opacity', '0');
glow.setAttribute('filter', 'url(#glow)');
g.appendChild(glow);
// 真空激活小圆点
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', by + C.cupH - 1);
dot.setAttribute('r', '3');
dot.setAttribute('fill', '#00E5FF'); dot.setAttribute('opacity', '0');
g.appendChild(dot);
cupsGroup.appendChild(g);
cupEls.push({ g, tube, body, vac, glow, dot, x, baseY: by + C.cupH, stepOffset });
}
// 更新排架条
const leftX = startX - C.cupW / 2 - 15;
const rightX = startX + (n - 1) * C.cupSpacing + C.cupW / 2 + 15;
rackBar.setAttribute('x', leftX);
rackBar.setAttribute('width', rightX - leftX);
rackLabel.setAttribute('x', (leftX + rightX) / 2);
}
// ===================== 初始化应力条 =====================
function createStressBars() {
stressBarsG.innerHTML = '';
stressBarEls = [];
const n = S.numCups;
const barW = 600 / n - 4;
const startX = 200;
for (let i = 0; i < n; i++) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', startX + i * (barW + 4));
rect.setAttribute('y', 535);
rect.setAttribute('width', barW);
rect.setAttribute('height', 0);
rect.setAttribute('rx', 2);
rect.setAttribute('fill', '#00E676');
rect.setAttribute('opacity', '0.8');
stressBarsG.appendChild(rect);
stressBarEls.push(rect);
}
}
// ===================== 初始化俯视图插图 =====================
function createInset() {
insetContent.innerHTML = '';
insetCupEls = [];
const n = S.numCups;
const spacing = 240 / (n + 1);
// PCB 俯视
const pcbRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
pcbRect.setAttribute('x', '0'); pcbRect.setAttribute('y', '40');
pcbRect.setAttribute('width', '240'); pcbRect.setAttribute('height', '70');
pcbRect.setAttribute('rx', '3'); pcbRect.setAttribute('fill', '#1B5E20'); pcbRect.setAttribute('opacity', '0.5');
insetContent.appendChild(pcbRect);
const pcbLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
pcbLabel.setAttribute('x', '120'); pcbLabel.setAttribute('y', '80');
pcbLabel.setAttribute('text-anchor', 'middle'); pcbLabel.setAttribute('fill', '#4CAF50');
pcbLabel.setAttribute('font-size', '9'); pcbLabel.setAttribute('font-family', "'IBM Plex Mono',monospace");
pcbLabel.textContent = 'PCB';
insetContent.appendChild(pcbLabel);
// 膜俯视
const memRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
memRect.setAttribute('x', '0'); memRect.setAttribute('y', '38');
memRect.setAttribute('width', '240'); memRect.setAttribute('height', '6');
memRect.setAttribute('rx', '1'); memRect.setAttribute('fill', '#FFB74D'); memRect.setAttribute('opacity', '0.4');
insetContent.appendChild(memRect);
// 吸盘
for (let i = 0; i < n; i++) {
const cx = spacing * (i + 1);
const cy = 38;
const cup = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
cup.setAttribute('cx', cx); cup.setAttribute('cy', cy);
cup.setAttribute('r', '5');
cup.setAttribute('fill', '#607D8B'); cup.setAttribute('stroke', '#90A4AE');
cup.setAttribute('stroke-width', '0.8');
insetContent.appendChild(cup);
const glowC = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
glowC.setAttribute('cx', cx); glowC.setAttribute('cy', cy);
glowC.setAttribute('r', '8');
glowC.setAttribute('fill', 'none'); glowC.setAttribute('stroke', '#00E5FF');
glowC.setAttribute('stroke-width', '1.5'); glowC.setAttribute('opacity', '0');
insetContent.appendChild(glowC);
insetCupEls.push({ cup, glowC });
}
// 拉伸方向箭头
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
arrow.setAttribute('d', 'M120,100 L120,118 M114,112 L120,118 L126,112');
arrow.setAttribute('stroke', '#00E5FF'); arrow.setAttribute('stroke-width', '1.5');
arrow.setAttribute('fill', 'none'); arrow.setAttribute('opacity', '0.5');
arrow.setAttribute('stroke-linecap', 'round');
insetContent.appendChild(arrow);
const arrowLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
arrowLabel.setAttribute('x', '120'); arrowLabel.setAttribute('y', '128');
arrowLabel.setAttribute('text-anchor', 'middle'); arrowLabel.setAttribute('fill', '#6B7A8D');
arrowLabel.setAttribute('font-size', '8'); arrowLabel.setAttribute('font-family', "'IBM Plex Mono',monospace");
arrowLabel.textContent = '剥离方向';
insetContent.appendChild(arrowLabel);
}
// ===================== 膜路径计算 =====================
function getMembraneD(peelProgress) {
const memY = C.pcbTop;
const n = S.numCups;
if (peelProgress <= 0) {
// 完全平铺在PCB上
return `M${C.pcbLeft},${memY} L${C.pcbRight},${memY} L${C.pcbRight},${memY + C.memH} L${C.pcbLeft},${memY + C.memH} Z`;
}
const p = Math.min(peelProgress, 1);
const peelX = C.pcbLeft + p * C.maxPeelDist;
// 排架位移
const pullX = p * C.maxPullX;
const liftY = p * C.maxLiftY;
// 排架中心原始X
const totalW = (n - 1) * C.cupSpacing;
const rackCenterX = 345;
const rackCurX = rackCenterX - pullX;
const rackCurY = C.rackStartY - liftY;
// 最低吸盘位置(膜被提升到的最高点)
const firstCupBottomY = rackCurY + 14 + C.cupH; // rackBar下沿 + cup高度 - step0
const lastCupBottomY = firstCupBottomY - (n - 1) * C.stepH;
// 膜连接到排架的X位置
const memEndX = rackCurX - totalW / 2 - 10;
const memEndY = firstCupBottomY;
if (peelX >= C.pcbRight) {
// 完全剥离
return `M${C.pcbRight},${memY} L${memEndX},${memEndY} L${memEndX},${memEndY + C.memH} L${C.pcbRight},${memY + C.memH} Z`;
}
// 上边缘:平铺段 + 过渡曲线 + 悬空段
const angle = C.peelAngle * Math.PI / 180;
const dx = peelX - memEndX;
const dy = memY - memEndY;
const curveLen = Math.abs(dx) * 0.4;
let d = `M${C.pcbRight},${memY} `;
d += `L${peelX},${memY} `;
// 贝塞尔曲线:从剥离前线到排架
const cp1x = peelX - curveLen * 0.6;
const cp1y = memY;
const cp2x = memEndX + curveLen * 0.3;
const cp2y = memEndY + (memY - memEndY) * 0.2;
d += `C${cp1x},${cp1y} ${cp2x},${cp2y} ${memEndX},${memEndY} `;
// 下边缘返回
const memEndYb = memEndY + C.memH;
d += `L${memEndX},${memEndYb} `;
d += `C${cp2x},${memEndYb + (memY - memEndY) * 0.15} ${cp1x},${memY + C.memH} ${peelX},${memY + C.memH} `;
d += `L${C.pcbRight},${memY + C.memH} Z`;
return d;
}
// ===================== 更新函数 =====================
function updateDescend(p) {
// 排架从上方下降到接触膜
const startY = C.rackStartY - 80;
const endY = C.rackStartY;
const curY = startY + (endY - startY) * easeOutCubic(p);
rackGroup.setAttribute('transform', `translate(0, ${curY - C.rackStartY})`);
// 膜保持平铺
membraneEl.setAttribute('d', getMembraneD(0));
// 更新吸盘位置(管子长度)
cupEls.forEach((cup, i) => {
const stepOffset = i * C.stepH;
const newBottom = C.pcbTop - stepOffset + (curY - C.rackStartY);
const newTop = newBottom - C.cupH;
const bx = cup.x - C.cupW / 2;
cup.body.setAttribute('d', `M${bx + 3},${newTop} L${bx + C.cupW - 3},${newTop} Q${bx + C.cupW},${newTop} ${bx + C.cupW},${newTop + 3} L${bx + C.cupW + 2},${newBottom - 4} Q${bx + C.cupW + 2},${newBottom} ${bx + C.cupW - 2},${newBottom} L${bx + 2},${newBottom} Q${bx - 2},${newBottom} ${bx - 2},${newBottom - 4} L${bx},${newTop + 3} Q${bx},${newTop} ${bx + 3},${newTop} Z`);
cup.vac.setAttribute('cy', newBottom - 1);
cup.glow.setAttribute('cy', newTop + C.cupH / 2);
cup.dot.setAttribute('cy', newBottom - 1);
cup.tube.setAttribute('y2', newTop);
cup.baseY = newBottom;
});
setPhaseUI('排架下降', '排架下降贴合膜边缘');
}
function updateEngage(p) {
rackGroup.setAttribute('transform', 'translate(0, 0)');
// 重置吸盘形状到接触位置
cupEls.forEach((cup, i) => {
const stepOffset = i * C.stepH;
const bottom = C.pcbTop - stepOffset;
const top = bottom - C.cupH;
const bx = cup.x - C.cupW / 2;
cup.body.setAttribute('d', `M${bx + 3},${top} L${bx + C.cupW - 3},${top} Q${bx + C.cupW},${top} ${bx + C.cupW},${top + 3} L${bx + C.cupW + 2},${bottom - 4} Q${bx + C.cupW + 2},${bottom} ${bx + C.cupW - 2},${bottom} L${bx + 2},${bottom} Q${bx - 2},${bottom} ${bx - 2},${bottom - 4} L${bx},${top + 3} Q${bx},${top} ${bx + 3},${top} Z`);
cup.vac.setAttribute('cy', bottom - 1);
cup.glow.setAttribute('cy', top + C.cupH / 2);
cup.dot.setAttribute('cy', bottom - 1);
cup.tube.setAttribute('y2', top);
cup.baseY = bottom;
});
// 依次激活吸盘
const n = S.numCups;
cupEls.forEach((cup, i) => {
const activateAt = i / n;
if (p >= activateAt) {
const subP = Math.min(1, (p - activateAt) * n * 0.6);
cup.glow.setAttribute('opacity', subP * 0.7);
cup.dot.setAttribute('opacity', subP * 0.9);
// 吸盘轻微收缩效果
const scale = 1 - subP * 0.03;
cup.body.setAttribute('transform', `translate(${cup.x}, ${cup.baseY - C.cupH / 2}) scale(1, ${scale}) translate(${-cup.x}, ${-(cup.baseY - C.cupH / 2)})`);
} else {
cup.glow.setAttribute('opacity', '0');
cup.dot.setAttribute('opacity', '0');
}
});
// 俯视图吸盘激活
insetCupEls.forEach((el, i) => {
const activateAt = i / n;
el.glowC.setAttribute('opacity', p >= activateAt ? '0.7' : '0');
});
membraneEl.setAttribute('d', getMembraneD(0));
setPhaseUI('依次吸附', '阶梯吸盘依次真空吸附');
}
function updatePeel(p) {
// 排架后拉
const pullX = p * C.maxPullX;
const liftY = p * C.maxLiftY;
const n = S.numCups;
const totalW = (n - 1) * C.cupSpacing;
// 排架向左上方移动
const rackDx = -pullX;
const rackDy = -liftY;
rackGroup.setAttribute('transform', `translate(${rackDx}, ${rackDy})`);
// 更新膜
membraneEl.setAttribute('d', getMembraneD(p));
// 吸盘保持激活状态,微脉动
cupEls.forEach((cup, i) => {
const pulse = 0.5 + 0.2 * Math.sin(S.time * 0.005 + i * 0.8);
cup.glow.setAttribute('opacity', pulse);
cup.dot.setAttribute('opacity', '0.9');
cup.body.setAttribute('transform', '');
});
// 剥离前线
const peelX = C.pcbLeft + p * C.maxPeelDist;
peelFrontGroup.setAttribute('opacity', '1');
peelFrontLine.setAttribute('x1', peelX);
peelFrontLine.setAttribute('x2', peelX);
peelFrontCore.setAttribute('x1', peelX);
peelFrontCore.setAttribute('x2', peelX);
// 剥离前线脉动
const pfPulse = 0.7 + 0.3 * Math.sin(S.time * 0.008);
peelFrontGroup.setAttribute('opacity', pfPulse);
// 应力可视化
if (S.showStress) {
stressGroup.setAttribute('opacity', '1');
const barN = stressBarEls.length;
stressBarEls.forEach((bar, i) => {
const h = 15 + 3 * Math.sin(S.time * 0.003 + i * 1.2); // 均匀低应力
bar.setAttribute('height', h);
bar.setAttribute('y', 555 - h);
bar.setAttribute('fill', '#00E676');
});
stressLabelOk.setAttribute('opacity', '1');
}
// 角度标注
if (p > 0.05 && p < 0.9) {
angleAnnotation.setAttribute('opacity', Math.min(1, (p - 0.05) * 5).toString());
const arcR = 50;
const angleRad = C.peelAngle * Math.PI / 180;
const arcX = peelX;
const arcY = C.pcbTop;
const ax = arcX - arcR * Math.cos(angleRad);
const ay = arcY - arcR * Math.sin(angleRad);
document.getElementById('angleArc').setAttribute('d',
`M${arcX - arcR},${arcY} A${arcR},${arcR} 0 0,1 ${ax},${ay}`);
document.getElementById('angleText').setAttribute('x', arcX - arcR - 30);
document.getElementById('angleText').setAttribute('y', arcY - 15);
document.getElementById('angleText').textContent = `${C.peelAngle}°`;
}
// 阶梯高度标注
if (p > 0.03 && cupEls.length >= 2) {
stepAnnotation.setAttribute('opacity', Math.min(1, (p - 0.03) * 4).toString());
const c0 = cupEls[0];
const c1 = cupEls[1];
const rx = rackDx;
const ry = rackDy;
const x0 = c0.x + rx;
const y0 = c0.baseY + ry;
const x1 = c1.x + rx;
const y1 = c1.baseY + ry;
const annX = x1 + C.cupW / 2 + 12;
document.getElementById('stepLine1').setAttribute('x1', x0 + C.cupW / 2 + 5);
document.getElementById('stepLine1').setAttribute('y1', y0);
document.getElementById('stepLine1').setAttribute('x2', annX + 5);
document.getElementById('stepLine1').setAttribute('y2', y0);
document.getElementById('stepLine2').setAttribute('x1', x1 + C.cupW / 2 + 5);
document.getElementById('stepLine2').setAttribute('y1', y1);
document.getElementById('stepLine2').setAttribute('x2', annX + 5);
document.getElementById('stepLine2').setAttribute('y2', y1);
document.getElementById('stepLineH').setAttribute('x1', annX);
document.getElementById('stepLineH').setAttribute('y1', y0);
document.getElementById('stepLineH').setAttribute('x2', annX);
document.getElementById('stepLineH').setAttribute('y2', y1);
document.getElementById('stepText').setAttribute('x', annX + 8);
document.getElementById('stepText').setAttribute('y', (y0 + y1) / 2 + 4);
document.getElementById('stepText').textContent = `${S.stepHmm}mm`;
}
// 间距标注
if (p > 0.06 && cupEls.length >= 2) {
spacingAnnotation.setAttribute('opacity', Math.min(1, (p - 0.06) * 4).toString());
const c0 = cupEls[0];
const c1 = cupEls[1];
const rx = rackDx;
const ry = rackDy;
const x0 = c0.x + rx;
const y0 = c0.baseY + ry;
const x1 = c1.x + rx;
const y1b = c1.baseY + ry;
const annY = y0 + 18;
document.getElementById('spLine1').setAttribute('x1', x0);
document.getElementById('spLine1').setAttribute('y1', y0 + 3);
document.getElementById('spLine1').setAttribute('x2', x0);
document.getElementById('spLine1').setAttribute('y2', annY + 3);
document.getElementById('spLine2').setAttribute('x1', x1);
document.getElementById('spLine2').setAttribute('y1', y1b + 3);
document.getElementById('spLine2').setAttribute('x2', x1);
document.getElementById('spLine2').setAttribute('y2', annY + 3);
document.getElementById('spLineH').setAttribute('x1', x0);
document.getElementById('spLineH').setAttribute('y1', annY);
document.getElementById('spLineH').setAttribute('x2', x1);
document.getElementById('spLineH').setAttribute('y2', annY);
document.getElementById('spText').setAttribute('x', (x0 + x1) / 2);
document.getElementById('spText').setAttribute('y', annY + 14);
document.getElementById('spText').textContent = '10mm';
}
// 运动轨迹
updateMotionTrail(p, rackDx, rackDy);
setPhaseUI('高速起撕', '整体后拉,线剥离推进');
}
function updateHandoff(p) {
// 卷膜辊逐渐接管
const rollerOpacity = easeOutCubic(p);
rollerGroup.setAttribute('opacity', rollerOpacity);
// 卷膜辊旋转
const angle = p * 720;
const rad = angle * Math.PI / 180;
const ix = 1110 + 13 * Math.cos(rad);
const iy = 400 + 13 * Math.sin(rad);
rollerIndicator.setAttribute('x2', ix);
rollerIndicator.setAttribute('y2', iy);
// 膜继续剥离到完成
const peelP = 0.75 + p * 0.25; // 从75%到100%
membraneEl.setAttribute('d', getMembraneD(peelP));
// 排架继续移动
const pullX = peelP * C.maxPullX;
const liftY = peelP * C.maxLiftY;
rackGroup.setAttribute('transform', `translate(${-pullX}, ${-liftY})`);
// 吸盘逐渐释放
cupEls.forEach((cup, i) => {
const releaseAt = 0.3 + i * 0.1;
if (p > releaseAt) {
cup.glow.setAttribute('opacity', Math.max(0, 0.5 * (1 - (p - releaseAt) * 3)));
cup.dot.setAttribute('opacity', Math.max(0, 0.9 * (1 - (p - releaseAt) * 3)));
}
});
// 剥离前线继续前进
const peelX = C.pcbLeft + peelP * C.maxPeelDist;
peelFrontLine.setAttribute('x1', peelX);
peelFrontLine.setAttribute('x2', peelX);
peelFrontCore.setAttribute('x1', peelX);
peelFrontCore.setAttribute('x2', peelX);
peelFrontGroup.setAttribute('opacity', Math.max(0, 1 - p * 1.5));
// 应力可视化保持
if (S.showStress) {
stressGroup.setAttribute('opacity', Math.max(0, 1 - p));
}
// 标注淡出
angleAnnotation.setAttribute('opacity', Math.max(0, 1 - p * 2));
stepAnnotation.setAttribute('opacity', Math.max(0, 1 - p * 2));
spacingAnnotation.setAttribute('opacity', Math.max(0, 1 - p * 2));
setPhaseUI('卷膜接手', '卷膜辊接管剩余剥离');
}
function updateMotionTrail(p, rackDx, rackDy) {
const trailG = document.getElementById('motionTrail');
trailG.setAttribute('opacity', '0.3');
trailG.innerHTML = '';
// 绘制排架运动轨迹箭头
const n = S.numCups;
const totalW = (n - 1) * C.cupSpacing;
const rackCenterX = 345;
const startX = rackCenterX;
const startY = C.rackStartY + 7;
const endX = startX + rackDx;
const endY = startY + rackDy;
if (Math.abs(rackDx) > 5) {
const trail = document.createElementNS('http://www.w3.org/2000/svg', 'line');
trail.setAttribute('x1', startX); trail.setAttribute('y1', startY);
trail.setAttribute('x2', endX); trail.setAttribute('y2', endY);
trail.setAttribute('stroke', '#00E5FF'); trail.setAttribute('stroke-width', '1.5');
trail.setAttribute('stroke-dasharray', '6,4'); trail.setAttribute('opacity', '0.4');
trailG.appendChild(trail);
// 箭头
const arrowSize = 8;
const angle = Math.atan2(rackDy, rackDx);
const ax1 = endX - arrowSize * Math.cos(angle - 0.4);
const ay1 = endY - arrowSize * Math.sin(angle - 0.4);
const ax2 = endX - arrowSize * Math.cos(angle + 0.4);
const ay2 = endY - arrowSize * Math.sin(angle + 0.4);
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
arrow.setAttribute('d', `M${ax1},${ay1} L${endX},${endY} L${ax2},${ay2}`);
arrow.setAttribute('stroke', '#00E5FF'); arrow.setAttribute('stroke-width', '1.5');
arrow.setAttribute('fill', 'none'); arrow.setAttribute('opacity', '0.5');
trailG.appendChild(arrow);
}
}
// ===================== 缓动函数 =====================
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
// ===================== 阶段UI =====================
function setPhaseUI(label, desc) {
phaseText.textContent = label;
phaseDesc.textContent = desc;
document.getElementById('infoPhase').textContent = label;
}
function updatePhaseBar() {
const segs = document.querySelectorAll('.phase-seg');
segs.forEach((seg, i) => {
seg.classList.remove('active', 'done');
if (i < S.phaseIndex) seg.classList.add('done');
else if (i === S.phaseIndex) seg.classList.add('active');
});
}
// ===================== 主动画循环 =====================
function animate(ts) {
if (lastTs === null) lastTs = ts;
const dt = (ts - lastTs) * S.speed;
lastTs = ts;
if (S.playing) {
S.time += dt;
// 计算当前阶段
let elapsed = 0;
let found = false;
for (let i = 0; i < PHASES.length; i++) {
if (S.time < elapsed + PHASES[i].duration) {
S.phaseIndex = i;
S.phase = PHASES[i].name;
S.progress = (S.time - elapsed) / PHASES[i].duration;
found = true;
break;
}
elapsed += PHASES[i].duration;
}
if (!found) {
// 动画完成
S.playing = false;
S.phaseIndex = PHASES.length;
S.progress = 1;
document.getElementById('btnPlay').innerHTML = '<i class="fas fa-play"></i> 播放';
setPhaseUI('完成', '剥离完成,可重置回放');
}
updatePhaseBar();
}
// 根据阶段更新
const p = Math.max(0, Math.min(1, S.progress));
switch (S.phase) {
case 'descend': updateDescend(p); break;
case 'engage': updateEngage(p); break;
case 'peel': updatePeel(p); break;
case 'handoff': updateHandoff(p); break;
}
requestAnimationFrame(animate);
}
// ===================== 重置 =====================
function resetAnimation() {
S.time = 0;
S.progress = 0;
S.phase = 'idle';
S.phaseIndex = -1;
S.playing = false;
lastTs = null;
rackGroup.setAttribute('transform', 'translate(0, -80)');
membraneEl.setAttribute('d', getMembraneD(0));
peelFrontGroup.setAttribute('opacity', '0');
rollerGroup.setAttribute('opacity', '0');
stressGroup.setAttribute('opacity', '0');
stressLabelOk.setAttribute('opacity', '0');
angleAnnotation.setAttribute('opacity', '0');
stepAnnotation.setAttribute('opacity', '0');
spacingAnnotation.setAttribute('opacity', '0');
document.getElementById('motionTrail').setAttribute('opacity', '0');
cupEls.forEach(cup => {
cup.glow.setAttribute('opacity', '0');
cup.dot.setAttribute('opacity', '0');
cup.body.setAttribute('transform', '');
});
insetCupEls.forEach(el => {
el.glowC.setAttribute('opacity', '0');
});
document.getElementById('btnPlay').innerHTML = '<i class="fas fa-play"></i> 播放';
setPhaseUI('待命', '点击播放启动动画');
updatePhaseBar();
}
// ===================== 控件事件 =====================
document.getElementById('btnPlay').addEventListener('click', () => {
if (S.phase === 'idle' || S.phaseIndex === -1) {
S.time = 0;
S.phase = 'descend';
S.phaseIndex = 0;
S.progress = 0;
}
if (S.phaseIndex >= PHASES.length) {
resetAnimation();
S.time = 0;
S.phase = 'descend';
S.phaseIndex = 0;
}
S.playing = !S.playing;
lastTs = null;
document.getElementById('btnPlay').innerHTML = S.playing
? '<i class="fas fa-pause"></i> 暂停'
: '<i class="fas fa-play"></i> 继续';
});
document.getElementById('btnRestart').addEventListener('click', () => {
resetAnimation();
});
document.getElementById('speedSlider').addEventListener('input', (e) => {
S.speed = parseFloat(e.target.value);
document.getElementById('speedVal').textContent = S.speed.toFixed(1) + 'x';
});
document.getElementById('stepSlider').addEventListener('input', (e) => {
S.stepHmm = parseFloat(e.target.value);
C.stepH = S.stepHmm / 1.5 * 8; // 按比例缩放
document.getElementById('stepVal').textContent = S.stepHmm.toFixed(1) + 'mm';
document.getElementById('infoStep').textContent = S.stepHmm.toFixed(1) + 'mm';
// 重新创建吸盘以反映新阶梯高度
createCups();
createInset();
if (!S.playing) resetAnimation();
});
document.getElementById('cupCountSlider').addEventListener('input', (e) => {
S.numCups = parseInt(e.target.value);
C.numCups = S.numCups;
document.getElementById('cupCountVal').textContent = S.numCups;
createCups();
createStressBars();
createInset();
if (!S.playing) resetAnimation();
});
document.getElementById('btnStress').addEventListener('click', (e) => {
S.showStress = !S.showStress;
e.currentTarget.classList.toggle('active', S.showStress);
if (!S.showStress) {
stressGroup.setAttribute('opacity', '0');
}
});
// ===================== 初始化 =====================
function init() {
createCups();
createStressBars();
createInset();
resetAnimation();
requestAnimationFrame(animate);
}
init();
</script>
</body>
</html>
实现说明
这个动画完整展示了柔性剥离排架将"点拉扯"转化为"线剥离"的 IFR 理想解原理:
动画四阶段时序
- 排架下降 — 柔性剥离排架从上方下降,阶梯排列的真空吸盘对准膜边缘
- 依次吸附 — 吸盘按阶梯顺序逐个激活(青色发光),从最低位到最高位,体现阶梯排列自然形成的时序接触
- 高速起撕 — 排架整体后拉,剥离前线(青色脉冲竖线)沿边缘推进;应力可视化条显示均匀低应力分布(全绿),角度/间距/阶梯标注同步显现
- 卷膜接手 — 卷膜辊接管剩余剥离,吸盘逐步释放
IFR 核心视觉引导
- 剥离前线:脉动发光的青色竖线,直观展示"线剥离"替代"点拉扯"
- 应力分布条:全部等高绿色条,应力均匀分散,无需对比即传达理想状态
- 阶梯标注:橙色标注 1.5mm 阶梯高度差,紫色标注 10mm 吸盘间距,15° 角度弧线——三参数联动阐明机理
- 俯视插图:右上角显示吸盘沿 PCB 边缘的线状排布,补充侧视图无法展示的宽度覆盖
交互控制
- 阶梯高度滑块:实时调整阶梯参数,吸盘排布即刻重建
- 吸盘数滑块:3~9 只吸盘可调,观察应力分散程度变化
- 速度控制:0.3x~3x 变速,便于细看关键动作
- 应力分布开关:可关闭应力条专注观察机械运动
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
