独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>指纹捕捉载玻片 - 理想解(IFR)分析系统</title>
<style>
:root {
--bg-base: #050914;
--bg-panel: rgba(12, 18, 34, 0.7);
--border-color: #1f2d4a;
--text-main: #e0e6ed;
--text-muted: #6b7c9e;
--color-normal: #00f0ff;
--color-normal-glow: rgba(0, 240, 255, 0.4);
--color-degraded: #ff0055;
--color-degraded-glow: rgba(255, 0, 85, 0.4);
--color-warn: #ffaa00;
--font-mono: 'Consolas', 'Courier New', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg-base);
color: var(--text-main);
font-family: var(--font-sans);
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-image:
radial-gradient(circle at 50% 50%, rgba(10, 20, 40, 1) 0%, rgba(5, 9, 20, 1) 100%),
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 100% 100%, 30px 30px, 30px 30px;
background-position: center center;
}
.dashboard {
width: 96vw;
height: 92vh;
max-width: 1600px;
display: grid;
grid-template-columns: 320px 1fr 320px;
gap: 20px;
padding: 20px;
}
/* Panels */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border-color);
border-radius: 12px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
position: relative;
overflow: hidden;
}
.panel::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, transparent, var(--color-normal), transparent);
opacity: 0.5;
transition: var(--transition);
}
.panel.degraded-mode::before {
background: linear-gradient(90deg, transparent, var(--color-degraded), transparent);
}
.panel-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--text-muted);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.panel-title::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--color-normal);
border-radius: 50%;
box-shadow: 0 0 8px var(--color-normal);
transition: var(--transition);
}
.degraded-mode .panel-title::before {
background-color: var(--color-degraded);
box-shadow: 0 0 8px var(--color-degraded);
}
/* Controls */
.control-group { margin-bottom: 30px; }
.control-label {
font-size: 13px;
color: var(--text-muted);
margin-bottom: 12px;
display: block;
}
.radio-group {
display: flex;
gap: 10px;
}
.radio-btn {
flex: 1;
padding: 10px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
text-align: center;
font-size: 13px;
transition: var(--transition);
user-select: none;
}
.radio-btn:hover { background: rgba(255,255,255,0.08); }
.radio-btn.active[data-type="normal"] {
border-color: var(--color-normal);
color: var(--color-normal);
background: rgba(0, 240, 255, 0.1);
box-shadow: 0 0 15px var(--color-normal-glow);
}
.radio-btn.active[data-type="degraded"] {
border-color: var(--color-degraded);
color: var(--color-degraded);
background: rgba(255, 0, 85, 0.1);
box-shadow: 0 0 15px var(--color-degraded-glow);
}
/* Slider */
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
height: 4px;
background: var(--border-color);
border-radius: 2px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--text-main);
cursor: pointer;
box-shadow: 0 0 10px rgba(255,255,255,0.5);
transition: var(--transition);
}
.slider:active::-webkit-slider-thumb { transform: scale(1.2); }
.val-display { font-family: var(--font-mono); font-size: 14px; width: 45px; text-align: right; }
/* Data display */
.data-list { display: flex; flex-direction: column; gap: 15px; }
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px dashed rgba(255,255,255,0.1);
}
.data-label { font-size: 12px; color: var(--text-muted); }
.data-value { font-family: var(--font-mono); font-size: 14px; color: var(--color-normal); transition: var(--transition); }
.degraded-mode .data-value.dynamic-color { color: var(--color-degraded); }
/* Status Timeline */
.timeline { margin-top: auto; display: flex; flex-direction: column; gap: 10px; }
.timeline-step { display: flex; align-items: center; gap: 10px; opacity: 0.3; transition: var(--transition); }
.timeline-step.active { opacity: 1; }
.timeline-step.active .step-dot { background: var(--color-normal); box-shadow: 0 0 10px var(--color-normal); }
.degraded-mode .timeline-step.active .step-dot { background: var(--color-degraded); box-shadow: 0 0 10px var(--color-degraded); }
.step-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); }
.step-text { font-size: 12px; font-family: var(--font-mono); }
/* Center Stage */
.center-stage {
position: relative;
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
background: radial-gradient(circle at center, rgba(15, 22, 38, 0.8) 0%, rgba(5, 9, 20, 0.4) 100%);
border: 1px solid var(--border-color);
box-shadow: inset 0 0 50px rgba(0,0,0,0.8);
overflow: hidden;
}
.corner-bracket {
position: absolute;
width: 30px;
height: 30px;
border: 2px solid var(--color-normal);
opacity: 0.5;
transition: var(--transition);
}
.degraded-mode .corner-bracket { border-color: var(--color-degraded); }
.tl { top: 20px; left: 20px; border-right: none; border-bottom: none; }
.tr { top: 20px; right: 20px; border-left: none; border-bottom: none; }
.bl { bottom: 20px; left: 20px; border-right: none; border-top: none; }
.br { bottom: 20px; right: 20px; border-left: none; border-top: none; }
/* Main SVG Animation container */
#sim-svg {
width: 100%;
height: 100%;
max-width: 800px;
max-height: 800px;
}
/* Overlay text */
.status-overlay {
position: absolute;
top: 40px;
text-align: center;
width: 100%;
pointer-events: none;
}
.status-text {
font-size: 24px;
font-weight: 300;
letter-spacing: 4px;
color: var(--color-normal);
text-shadow: 0 0 20px var(--color-normal-glow);
transition: var(--transition);
}
.status-sub {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
margin-top: 5px;
}
.degraded-mode .status-text {
color: var(--color-degraded);
text-shadow: 0 0 20px var(--color-degraded-glow);
}
/* AI Analysis Panel Right */
.ai-graph {
height: 120px;
width: 100%;
border-bottom: 1px solid var(--border-color);
position: relative;
margin-bottom: 20px;
display: flex;
align-items: flex-end;
}
.ai-bar {
flex: 1;
background: var(--color-normal);
opacity: 0.2;
margin: 0 2px;
transition: height 0.5s ease;
}
.match-score {
font-size: 48px;
font-weight: 200;
font-family: var(--font-mono);
color: var(--color-normal);
margin: 20px 0;
text-shadow: 0 0 20px var(--color-normal-glow);
transition: var(--transition);
}
.degraded-mode .match-score {
color: var(--color-degraded);
text-shadow: 0 0 20px var(--color-degraded-glow);
}
.alert-box {
padding: 15px;
background: rgba(0, 240, 255, 0.05);
border-left: 3px solid var(--color-normal);
font-size: 13px;
line-height: 1.6;
margin-top: auto;
transition: var(--transition);
}
.degraded-mode .alert-box {
background: rgba(255, 0, 85, 0.05);
border-left-color: var(--color-degraded);
}
.humidity-warning {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 170, 0, 0.1);
border: 1px solid var(--color-warn);
color: var(--color-warn);
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
letter-spacing: 2px;
display: none;
box-shadow: 0 0 20px rgba(255, 170, 0, 0.3);
backdrop-filter: blur(4px);
z-index: 100;
}
/* IFR Focus Tooltip */
.ifr-tooltip {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(12, 18, 34, 0.85);
border: 1px solid var(--color-normal);
padding: 12px 20px;
border-radius: 6px;
font-size: 13px;
color: var(--text-main);
text-align: center;
max-width: 80%;
box-shadow: 0 0 30px rgba(0,0,0,0.8);
opacity: 0;
transition: opacity 0.5s;
pointer-events: none;
}
.ifr-tooltip span { color: var(--color-normal); font-weight: bold; }
.degraded-mode .ifr-tooltip { border-color: var(--color-degraded); }
.degraded-mode .ifr-tooltip span { color: var(--color-degraded); }
</style>
</head>
<body>
<div class="dashboard" id="dashboard">
<!-- Left Panel: Controls -->
<div class="panel">
<div class="panel-title">系统控制 & 参数 (Controls)</div>
<div class="control-group">
<span class="control-label">注入标本类型 (Drug Sample)</span>
<div class="radio-group">
<div class="radio-btn active" data-type="normal" onclick="setDrugType('normal')">合格品 (Normal)</div>
<div class="radio-btn" data-type="degraded" onclick="setDrugType('degraded')">劣质品 (Degraded)</div>
</div>
</div>
<div class="control-group">
<span class="control-label">环境相对湿度干扰 (Humidity)</span>
<div class="slider-container">
<input type="range" id="humidity-slider" class="slider" min="20" max="95" value="40" oninput="updateHumidity()">
<div class="val-display" id="humidity-val">40%</div>
</div>
</div>
<div class="panel-title" style="margin-top: 20px;">物理场参数 (Parameters)</div>
<div class="data-list">
<div class="data-item">
<span class="data-label">液滴体积 (Volume)</span>
<span class="data-value">2.0 μL</span>
</div>
<div class="data-item">
<span class="data-label">亲水区接触角 (Hydrophilic)</span>
<span class="data-value">45°</span>
</div>
<div class="data-item">
<span class="data-label">疏水区接触角 (Hydrophobic)</span>
<span class="data-value">120°</span>
</div>
<div class="data-item">
<span class="data-label">当前蒸发速率 (Evap. Rate)</span>
<span class="data-value dynamic-color" id="evap-rate">0.015 μL/s</span>
</div>
</div>
<div class="timeline">
<div class="timeline-step" id="step-1"><div class="step-dot"></div><span class="step-text">01. 载片准备 (Preparation)</span></div>
<div class="timeline-step" id="step-2"><div class="step-dot"></div><span class="step-text">02. 药液滴加 (Deposition)</span></div>
<div class="timeline-step" id="step-3"><div class="step-dot"></div><span class="step-text">03. 强制相变流场 (Evaporation)</span></div>
<div class="timeline-step" id="step-4"><div class="step-dot"></div><span class="step-text">04. 多光谱扫描 (Scanning)</span></div>
<div class="timeline-step" id="step-5"><div class="step-dot"></div><span class="step-text">05. AI云端比对 (Analysis)</span></div>
</div>
</div>
<!-- Center Stage: SVG Animation -->
<div class="center-stage">
<div class="corner-bracket tl"></div>
<div class="corner-bracket tr"></div>
<div class="corner-bracket bl"></div>
<div class="corner-bracket br"></div>
<div class="status-overlay">
<div class="status-text" id="main-status">系统待机</div>
<div class="status-sub" id="sub-status">AWAITING INITIALIZATION</div>
</div>
<div class="humidity-warning" id="humidity-warning">
【边界失效】湿度接近饱和,毛细流无法有效建立
</div>
<!-- SVG Container -->
<svg id="sim-svg" viewBox="0 0 800 800" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- Base Grid Pattern -->
<pattern id="hex-grid" width="40" height="69.282" patternUnits="userSpaceOnUse">
<path d="M 40 17.32 L 20 5.77 L 0 17.32 L 0 40.41 L 20 51.96 L 40 40.41 Z" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<path d="M 20 40.41 L 0 51.96 L 0 75.05 L 20 86.6 L 40 75.05 L 40 51.96 Z" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
</pattern>
<!-- Glow Filters -->
<filter id="glow-normal" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="glow-degraded" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="10" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<!-- Scanner Gradient -->
<linearGradient id="scan-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--color-normal)" stop-opacity="0" class="scan-stop-1" />
<stop offset="50%" stop-color="var(--color-normal)" stop-opacity="0.1" class="scan-stop-2" />
<stop offset="100%" stop-color="var(--color-normal)" stop-opacity="0.8" class="scan-stop-3" />
</linearGradient>
<!-- Base Slide Masking (Hydrophilic 45° vs Hydrophobic 120°) -->
<radialGradient id="hydro-mask" cx="50%" cy="50%" r="50%">
<stop offset="60%" stop-color="#fff" />
<stop offset="100%" stop-color="#000" />
</radialGradient>
</defs>
<!-- Background Elements -->
<rect width="800" height="800" fill="url(#hex-grid)" />
<!-- The Slide Base (Micro-nano patterned array) -->
<g id="slide-base" transform="translate(400, 400)">
<!-- Outer limit (Hydrophobic 120°) -->
<circle cx="0" cy="0" r="250" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="2" stroke-dasharray="10 5"/>
<text x="0" y="-260" fill="rgba(255,255,255,0.3)" font-size="12" font-family="monospace" text-anchor="middle">SUPERHYDROPHOBIC BOUNDARY (120°)</text>
<!-- Inner rings (Hydrophilic array 45°) -->
<g id="hydrophilic-rings" opacity="0.3">
<circle cx="0" cy="0" r="50" fill="none" stroke="var(--color-normal)" stroke-width="1" />
<circle cx="0" cy="0" r="100" fill="none" stroke="var(--color-normal)" stroke-width="1" />
<circle cx="0" cy="0" r="150" fill="none" stroke="var(--color-normal)" stroke-width="1" />
<circle cx="0" cy="0" r="200" fill="none" stroke="var(--color-normal)" stroke-width="1" />
<line x1="-250" y1="0" x2="250" y2="0" stroke="var(--color-normal)" stroke-width="0.5" opacity="0.5"/>
<line x1="0" y1="-250" x2="0" y2="250" stroke="var(--color-normal)" stroke-width="0.5" opacity="0.5"/>
</g>
</g>
<!-- Droplet Element -->
<g id="droplet-group" transform="translate(400, 400)">
<circle id="droplet" cx="0" cy="0" r="0" fill="rgba(0, 240, 255, 0.15)" stroke="var(--color-normal)" stroke-width="2" filter="url(#glow-normal)"/>
<circle id="droplet-ripple" cx="0" cy="0" r="0" fill="none" stroke="var(--color-normal)" stroke-width="1" opacity="0"/>
</g>
<!-- Evaporation Particles (Generated by JS) -->
<g id="particle-group" transform="translate(400, 400)"></g>
<!-- Residue "Coffee Ring" Fingerprint -->
<g id="residue-group" transform="translate(400, 400)" opacity="0">
<!-- Normal Pattern (Perfect symmetrical rings) -->
<path id="pattern-normal" d="" fill="none" stroke="var(--color-normal)" stroke-width="3" filter="url(#glow-normal)"/>
<!-- Degraded Pattern (Broken, multiple centers, erratic) -->
<path id="pattern-degraded" d="" fill="none" stroke="var(--color-degraded)" stroke-width="2" filter="url(#glow-degraded)"/>
<g id="degraded-blobs"></g>
</g>
<!-- Scanner Element -->
<g id="scanner-group" opacity="0" transform="translate(0, -100)">
<rect x="100" y="0" width="600" height="150" fill="url(#scan-grad)" />
<line x1="100" y1="150" x2="700" y2="150" stroke="var(--color-normal)" stroke-width="3" filter="url(#glow-normal)" class="scan-line" />
<!-- Crosshair -->
<path d="M 400 130 L 400 170 M 380 150 L 420 150" stroke="#fff" stroke-width="1"/>
</g>
</svg>
<div class="ifr-tooltip" id="ifr-tooltip">
TRIZ IFR 提示: 巧妙利用 <span>特制亲疏水基底</span> 强制重塑蒸发流场,<br>将微观理化差异自动放大为肉眼/机器可辨的 <span>宏观结晶指纹</span>。
</div>
</div>
<!-- Right Panel: AI Analysis -->
<div class="panel">
<div class="panel-title">AI云端比对图谱 (AI MATCHING)</div>
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 10px;">实时形貌特征频率分析 (Morphological Frequency)</div>
<div class="ai-graph" id="ai-graph">
<!-- Bars generated by JS -->
</div>
<div style="font-size: 12px; color: var(--text-muted);">图谱匹配度 (Standard Match %)</div>
<div class="match-score" id="match-score">--%</div>
<div class="alert-box" id="result-alert">
等待扫描数据接入...
</div>
</div>
</div>
<script>
/** STATE MANAGEMENT **/
let state = {
drugType: 'normal', // 'normal' | 'degraded'
humidity: 40, // 20 - 95
phase: 0, // 0: Init, 1: Drop, 2: Evap, 3: Scan, 4: Result
isPlaying: false,
animationTimer: null,
particles: []
};
const colors = {
normal: '#00f0ff',
degraded: '#ff0055'
};
/** DOM ELEMENTS **/
const dashboard = document.getElementById('dashboard');
const humiditySlider = document.getElementById('humidity-slider');
const humidityVal = document.getElementById('humidity-val');
const evapRateText = document.getElementById('evap-rate');
const mainStatus = document.getElementById('main-status');
const subStatus = document.getElementById('sub-status');
const aiGraph = document.getElementById('ai-graph');
const matchScore = document.getElementById('match-score');
const resultAlert = document.getElementById('result-alert');
const humidityWarning = document.getElementById('humidity-warning');
const ifrTooltip = document.getElementById('ifr-tooltip');
// SVG Elements
const droplet = document.getElementById('droplet');
const dropletRipple = document.getElementById('droplet-ripple');
const particleGroup = document.getElementById('particle-group');
const residueGroup = document.getElementById('residue-group');
const patternNormal = document.getElementById('pattern-normal');
const patternDegraded = document.getElementById('pattern-degraded');
const degradedBlobs = document.getElementById('degraded-blobs');
const scannerGroup = document.getElementById('scanner-group');
const hydroRings = document.getElementById('hydrophilic-rings');
/** INITIALIZATION **/
function init() {
generateAIGraph();
generateResiduePaths();
startAnimationLoop();
}
/** INTERACTION HANDLERS **/
function setDrugType(type) {
state.drugType = type;
document.querySelectorAll('.radio-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.type === type);
});
if (type === 'degraded') {
dashboard.classList.add('degraded-mode');
// Update SVG gradients/colors for scanner based on mode
document.querySelector('.scan-stop-1').setAttribute('stop-color', colors.degraded);
document.querySelector('.scan-stop-2').setAttribute('stop-color', colors.degraded);
document.querySelector('.scan-stop-3').setAttribute('stop-color', colors.degraded);
document.querySelector('.scan-line').setAttribute('stroke', colors.degraded);
document.querySelector('.scan-line').setAttribute('filter', 'url(#glow-degraded)');
hydroRings.querySelectorAll('circle, line').forEach(el => el.setAttribute('stroke', colors.degraded));
} else {
dashboard.classList.remove('degraded-mode');
document.querySelector('.scan-stop-1').setAttribute('stop-color', colors.normal);
document.querySelector('.scan-stop-2').setAttribute('stop-color', colors.normal);
document.querySelector('.scan-stop-3').setAttribute('stop-color', colors.normal);
document.querySelector('.scan-line').setAttribute('stroke', colors.normal);
document.querySelector('.scan-line').setAttribute('filter', 'url(#glow-normal)');
hydroRings.querySelectorAll('circle, line').forEach(el => el.setAttribute('stroke', colors.normal));
}
// If animation is in result phase, quickly reset and replay
if (state.phase === 4) {
clearTimeout(state.animationTimer);
resetAnimationState();
runSequence();
}
}
function updateHumidity() {
state.humidity = parseInt(humiditySlider.value);
humidityVal.innerText = state.humidity + '%';
// Calculate mock evap rate
let baseRate = 0.025;
let currentRate = Math.max(0, baseRate * (1 - (state.humidity / 100)));
evapRateText.innerText = currentRate.toFixed(3) + ' μL/s';
if (state.humidity > 80 && state.phase === 2) {
humidityWarning.style.display = 'block';
} else {
humidityWarning.style.display = 'none';
}
}
function updateStep(stepNum) {
document.querySelectorAll('.timeline-step').forEach((el, idx) => {
if (idx + 1 === stepNum) el.classList.add('active');
else el.classList.remove('active');
});
}
/** SVG GENERATION **/
function generateAIGraph() {
aiGraph.innerHTML = '';
for (let i = 0; i < 30; i++) {
const bar = document.createElement('div');
bar.className = 'ai-bar';
bar.style.height = '10%';
aiGraph.appendChild(bar);
}
}
function animateAIGraph(type) {
const bars = document.querySelectorAll('.ai-bar');
bars.forEach(bar => {
let height = 10;
if (type === 'normal') {
// Clean, predictable peaks
height = 20 + Math.random() * 20;
if (Math.random() > 0.8) height = 80 + Math.random() * 20;
} else if (type === 'degraded') {
// Erratic, noisy
height = 10 + Math.random() * 80;
} else {
// Reset
height = 10;
}
bar.style.height = height + '%';
bar.style.backgroundColor = type === 'degraded' ? colors.degraded : colors.normal;
});
}
function generateResiduePaths() {
// Normal: Perfect fractal/mandala ring
let pathNormal = "";
const numPoints = 60;
for (let i = 0; i <= numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
// Outer clean ring
const r1 = 200 + Math.sin(angle * 12) * 5;
const x1 = Math.cos(angle) * r1;
const y1 = Math.sin(angle) * r1;
pathNormal += (i === 0 ? "M " : "L ") + `${x1} ${y1} `;
}
// Inner crisp ring
for (let i = 0; i <= numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const r2 = 150 + Math.cos(angle * 6) * 3;
const x2 = Math.cos(angle) * r2;
const y2 = Math.sin(angle) * r2;
pathNormal += (i === 0 ? " M " : "L ") + `${x2} ${y2} `;
}
patternNormal.setAttribute('d', pathNormal);
// Degraded: Broken ring, irregular
let pathDegraded = "";
for (let i = 0; i <= numPoints; i++) {
if (Math.random() > 0.8) continue; // Break the path
const angle = (i / numPoints) * Math.PI * 2;
const r = 180 + Math.random() * 40 - 20; // Erratic radius
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r;
pathDegraded += (i === 0 || Math.random() > 0.9 ? "M " : "L ") + `${x} ${y} `;
}
patternDegraded.setAttribute('d', pathDegraded);
// Center blobs for degraded
degradedBlobs.innerHTML = '';
for(let i=0; i<15; i++) {
const blob = document.createElementNS("http://www.w3.org/2000/svg", "circle");
const angle = Math.random() * Math.PI * 2;
const r = Math.random() * 80;
blob.setAttribute("cx", Math.cos(angle) * r);
blob.setAttribute("cy", Math.sin(angle) * r);
blob.setAttribute("r", 2 + Math.random() * 6);
blob.setAttribute("fill", colors.degraded);
blob.setAttribute("filter", "url(#glow-degraded)");
degradedBlobs.appendChild(blob);
}
}
/** ANIMATION SEQUENCE ENGINE **/
const delay = ms => new Promise(res => { state.animationTimer = setTimeout(res, ms); });
async function startAnimationLoop() {
state.isPlaying = true;
while(state.isPlaying) {
await runSequence();
await delay(3000); // Hold result before restart
resetAnimationState();
}
}
function resetAnimationState() {
droplet.setAttribute('r', '0');
droplet.setAttribute('opacity', '1');
droplet.setAttribute('fill', state.drugType === 'normal' ? 'rgba(0, 240, 255, 0.15)' : 'rgba(255, 0, 85, 0.15)');
droplet.setAttribute('stroke', state.drugType === 'normal' ? colors.normal : colors.degraded);
droplet.setAttribute('filter', state.drugType === 'normal' ? 'url(#glow-normal)' : 'url(#glow-degraded)');
dropletRipple.setAttribute('r', '0');
dropletRipple.setAttribute('opacity', '0');
residueGroup.setAttribute('opacity', '0');
scannerGroup.setAttribute('opacity', '0');
scannerGroup.setAttribute('transform', 'translate(0, -100)');
particleGroup.innerHTML = '';
mainStatus.innerText = "系统待机";
subStatus.innerText = "AWAITING INITIALIZATION";
matchScore.innerText = "--%";
resultAlert.innerText = "等待扫描数据接入...";
animateAIGraph('reset');
humidityWarning.style.display = 'none';
ifrTooltip.style.opacity = '0';
updateStep(0);
}
async function runSequence() {
const isDegraded = state.drugType === 'degraded';
const color = isDegraded ? colors.degraded : colors.normal;
// --- PHASE 1: PREP ---
state.phase = 1;
updateStep(1);
mainStatus.innerText = "载玻片阵列激活";
subStatus.innerText = "SURFACE ENERGY CALIBRATION: 45°/120°";
hydroRings.style.opacity = '0.8';
hydroRings.style.transition = 'opacity 1s';
await delay(1000);
hydroRings.style.opacity = '0.3';
// --- PHASE 2: DROP ---
state.phase = 2;
updateStep(2);
mainStatus.innerText = "精密滴注";
subStatus.innerText = "DEPOSITING 2.0 μL SAMPLE";
// Droplet lands and expands
droplet.style.transition = "r 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
droplet.setAttribute('r', '250');
// Ripple effect
dropletRipple.setAttribute('stroke', color);
dropletRipple.style.transition = "r 1s ease-out, opacity 1s ease-out";
setTimeout(() => {
dropletRipple.setAttribute('opacity', '0.5');
dropletRipple.setAttribute('r', '280');
setTimeout(() => dropletRipple.setAttribute('opacity', '0'), 500);
}, 200);
await delay(1500);
// --- PHASE 3: EVAPORATION & MARANGONI FLOW ---
state.phase = 3;
updateStep(3);
mainStatus.innerText = "恒温强制蒸发场";
subStatus.innerText = "CAPILLARY FLOW REMAPPING IN PROGRESS";
// Show TRIZ IFR Tip
ifrTooltip.style.opacity = '1';
if (state.humidity > 80) {
// FAILURE CONDITION
humidityWarning.style.display = 'block';
mainStatus.innerText = "蒸发阻滞";
subStatus.innerText = "ERROR: HIGH HUMIDITY PREVENTS CAPILLARY FLOW";
droplet.style.transition = "r 4s linear";
droplet.setAttribute('r', '240'); // Barely shrinks
await delay(4000);
return; // Abort this loop iteration
}
// Normal evaporation process
const evapDuration = 3000;
droplet.style.transition = `r ${evapDuration}ms ease-in, opacity ${evapDuration}ms ease-in`;
droplet.setAttribute('r', '50'); // Shrinks to center
droplet.setAttribute('opacity', '0');
// Generate Capillary particles moving outward
createParticles(isDegraded, evapDuration);
// Fade in residue based on specific patterned interaction
patternNormal.style.display = isDegraded ? 'none' : 'block';
patternDegraded.style.display = isDegraded ? 'block' : 'none';
degradedBlobs.style.display = isDegraded ? 'block' : 'none';
residueGroup.style.transition = `opacity ${evapDuration/2}ms ease-in`;
setTimeout(() => {
residueGroup.setAttribute('opacity', '1');
}, evapDuration/2);
await delay(evapDuration + 500);
ifrTooltip.style.opacity = '0';
// --- PHASE 4: SCAN ---
state.phase = 4;
updateStep(4);
mainStatus.innerText = "多光谱形态扫描";
subStatus.innerText = "CAPTURING MICRO-CRYSTALLIZATION FINGERPRINT";
scannerGroup.setAttribute('opacity', '1');
scannerGroup.style.transition = "transform 2s ease-in-out";
scannerGroup.setAttribute('transform', 'translate(0, 700)'); // Sweep down
// Animate AI Graph processing
let graphInterval = setInterval(() => animateAIGraph(state.drugType), 150);
await delay(2000);
clearInterval(graphInterval);
scannerGroup.style.transition = "opacity 0.5s";
scannerGroup.setAttribute('opacity', '0');
// --- PHASE 5: RESULT ---
state.phase = 5;
updateStep(5);
if (!isDegraded) {
mainStatus.innerText = "分析完成:合格";
subStatus.innerText = "STANDARD MATCH: OPTIMAL CONCENTRIC PATTERN";
animateAIGraph('normal');
animateScore(95, 99.9);
resultAlert.innerHTML = "<strong>检测通过:</strong>流场形成完美的双重环状沉淀,符合标准正品【指纹图谱】特征。未发现大分子杂质聚集。";
} else {
mainStatus.innerText = "分析完成:劣质/变质";
subStatus.innerText = "WARNING: ANOMALOUS DEPOSITION PATTERN DETECTED";
animateAIGraph('degraded');
animateScore(20, 45.3);
resultAlert.innerHTML = "<strong>异常警报:</strong>蒸发结晶环破裂,中心存在大量不规则降解产物沉积。粘度或表面活性剂变异!";
}
}
function createParticles(isDegraded, duration) {
particleGroup.innerHTML = '';
const numParticles = isDegraded ? 80 : 150;
const color = isDegraded ? colors.degraded : colors.normal;
for(let i=0; i<numParticles; i++) {
const p = document.createElementNS("http://www.w3.org/2000/svg", "circle");
p.setAttribute("r", 1.5 + Math.random() * 2);
p.setAttribute("fill", color);
// Start near center
const startR = Math.random() * 50;
const angle = Math.random() * Math.PI * 2;
p.setAttribute("cx", Math.cos(angle) * startR);
p.setAttribute("cy", Math.sin(angle) * startR);
particleGroup.appendChild(p);
// Animate outward to ring boundary
setTimeout(() => {
p.style.transition = `all ${duration * (0.5 + Math.random()*0.5)}ms ease-out`;
let endR;
if(isDegraded) {
endR = Math.random() > 0.5 ? (Math.random() * 80) : (180 + Math.random() * 40 - 20);
} else {
endR = Math.random() > 0.5 ? 200 : 150; // Drive to the specific hydrophilic rings
}
p.setAttribute("cx", Math.cos(angle) * endR);
p.setAttribute("cy", Math.sin(angle) * endR);
p.setAttribute("opacity", "0");
}, 50);
}
}
function animateScore(min, max) {
let current = 0;
const steps = 20;
const inc = max / steps;
let step = 0;
const timer = setInterval(() => {
current += inc;
step++;
matchScore.innerText = current.toFixed(1) + '%';
if(step >= steps) {
clearInterval(timer);
matchScore.innerText = max.toFixed(1) + '%';
}
}, 50);
}
// Boot
window.onload = () => {
updateHumidity();
init();
};
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
