这是一个非常精彩的工程问题——用"时间换空间"的 TRIZ 思维,将 32 个静态传感器压缩为 1 对扫描激光。我将围绕旋转棱镜扫掠光幕的核心机理,打造一个深色光子学仪器风格的高保真交互动画。
<!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>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root{--bg:#060c1a;--fg:#b0c4de;--muted:#4a6080;--accent:#00ff88;--card:#0a1525;--border:#152540;--laser:#00ff88;--prism:#4488ff;--film:#ff8844;--detect:#ffffaa}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;overflow-x:hidden}
.title-font{font-family:'Orbitron',sans-serif}
#scene-wrap{width:100%;max-width:1100px;margin:0 auto;aspect-ratio:12/8;position:relative}
#scene{width:100%;height:100%;display:block}
.ctrl-panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 24px;display:flex;flex-wrap:wrap;gap:16px 32px;align-items:center;justify-content:center;max-width:1100px;margin:12px auto}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{font-size:12px;color:var(--muted);white-space:nowrap}
.ctrl-group .val{color:var(--accent);font-weight:600;min-width:48px;text-align:right;font-size:13px}
input[type=range]{-webkit-appearance:none;background:var(--border);height:4px;border-radius:2px;outline:none;width:120px;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--accent);border-radius:50%;cursor:pointer;box-shadow:0 0 6px var(--accent)}
button{font-family:'IBM Plex Mono',monospace;font-size:13px;font-weight:600;padding:8px 20px;border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .2s;background:var(--card);color:var(--fg)}
button:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 0 12px rgba(0,255,136,.15)}
button.active{background:rgba(0,255,136,.12);border-color:var(--accent);color:var(--accent);box-shadow:0 0 16px rgba(0,255,136,.2)}
.ifr-banner{max-width:1100px;margin:8px auto 0;text-align:center;padding:10px 16px;border:1px solid rgba(0,255,136,.15);border-radius:8px;background:rgba(0,255,136,.04);font-size:12px;color:var(--muted);line-height:1.7}
.ifr-banner strong{color:var(--accent);font-weight:600}
@keyframes pulse-glow{0%,100%{opacity:.6}50%{opacity:1}}
</style>
</head>
<body class="flex flex-col items-center px-4 py-6 min-h-screen">
<!-- 标题 -->
<header class="text-center mb-4">
<h1 class="title-font text-2xl md:text-3xl font-black tracking-wider" style="color:var(--accent)">LASER PRISM SCANNER</h1>
<p class="text-sm mt-1" style="color:var(--muted)">旋转棱镜扫描检测系统 — 最终理想解原理</p>
</header>
<!-- 主 SVG 动画 -->
<div id="scene-wrap">
<svg id="scene" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 激光光晕 -->
<filter id="fLaser" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="fLaserWide" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="10"/>
</filter>
<!-- 检测闪光 -->
<filter id="fFlash" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur in="SourceGraphic" stdDeviation="12"/>
</filter>
<!-- 棱镜渐变 -->
<linearGradient id="gPrism" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3366bb" stop-opacity="0.5"/>
<stop offset="50%" stop-color="#6699ee" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#4477cc" stop-opacity="0.5"/>
</linearGradient>
<!-- 网格 -->
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#0d1830" stroke-width="0.5"/>
</pattern>
</defs>
<!-- 背景 -->
<rect width="1200" height="800" fill="#060c1a"/>
<rect width="1200" height="800" fill="url(#grid)" opacity="0.6"/>
<!-- ===== 发射器外壳 ===== -->
<g id="emitter">
<rect x="530" y="42" width="140" height="56" rx="6" fill="#14203a" stroke="#1e3358" stroke-width="1.5"/>
<rect x="540" y="50" width="120" height="40" rx="4" fill="#0e1830" stroke="#1a2d50" stroke-width="1"/>
<text x="600" y="75" text-anchor="middle" fill="#4a6a90" font-size="11" font-family="'IBM Plex Mono'">LASER TX</text>
<!-- 激光出口 -->
<rect x="585" y="98" width="30" height="10" rx="2" fill="#0a1220" stroke="#1a2d50" stroke-width="1"/>
<!-- 状态 LED -->
<circle id="emitterLed" cx="555" cy="60" r="4" fill="#1a2a1a" stroke="#0a150a" stroke-width="0.5"/>
</g>
<!-- ===== 入射激光(发射器→棱镜) ===== -->
<line id="beamIn" x1="600" y1="108" x2="600" y2="155" stroke="#00ff88" stroke-width="3" opacity="0" filter="url(#fLaser)"/>
<line id="beamInGlow" x1="600" y1="108" x2="600" y2="155" stroke="#00ff88" stroke-width="10" opacity="0" filter="url(#fLaserWide)"/>
<!-- ===== 旋转棱镜 ===== -->
<g id="prismGroup">
<polygon id="prismShape" fill="url(#gPrism)" stroke="#6699dd" stroke-width="1.2" stroke-linejoin="round"/>
<polygon id="prismInner" fill="none" stroke="#88bbff" stroke-width="0.4" opacity="0.3"/>
</g>
<!-- 棱镜轴心 -->
<circle cx="600" cy="195" r="5" fill="#0e1830" stroke="#3355aa" stroke-width="1"/>
<circle cx="600" cy="195" r="2" fill="#4477cc"/>
<!-- ===== 扫描光束轨迹组 ===== -->
<g id="trailGroup"></g>
<!-- ===== 主扫描光束 ===== -->
<line id="beamOutGlow" x1="600" y1="210" x2="600" y2="660" stroke="#00ff88" stroke-width="14" opacity="0" filter="url(#fLaserWide)"/>
<line id="beamOut" x1="600" y1="210" x2="600" y2="660" stroke="#00ff88" stroke-width="2.5" opacity="0" filter="url(#fLaser)"/>
<!-- ===== 薄膜对象 ===== -->
<g id="filmGroup" visibility="hidden">
<rect id="filmRect" x="0" y="350" width="28" height="190" rx="4" fill="#ff8844" opacity="0.35" stroke="#ff8844" stroke-width="1" stroke-opacity="0.6"/>
<text id="filmLabel" x="0" y="340" text-anchor="middle" fill="#ff8844" font-size="11" font-family="'IBM Plex Mono'" opacity="0.8">薄膜</text>
</g>
<!-- ===== 检测闪光 ===== -->
<circle id="detectFlash" cx="0" cy="0" r="18" fill="#ffffaa" opacity="0" filter="url(#fFlash)"/>
<circle id="detectRing" cx="0" cy="0" r="8" fill="none" stroke="#ffffaa" stroke-width="2" opacity="0"/>
<!-- ===== 接收器 ===== -->
<g id="receiverGroup">
<rect x="165" y="658" width="870" height="32" rx="5" fill="#0e1830" stroke="#1a2d50" stroke-width="1.5"/>
<text x="600" y="680" text-anchor="middle" fill="#3a5575" font-size="10" font-family="'IBM Plex Mono'">LINEAR RECEIVER ARRAY</text>
</g>
<g id="receiverSegments"></g>
<!-- 接收器命中指示 -->
<rect id="recvHit" x="0" y="656" width="6" height="36" rx="2" fill="#00ff88" opacity="0"/>
<!-- ===== 覆盖率条 ===== -->
<g id="coverageGroup" transform="translate(165,710)">
<rect width="870" height="10" rx="3" fill="#0a1220" stroke="#152540" stroke-width="0.5"/>
<rect id="coverageFill" width="0" height="10" rx="3" fill="#00ff88" opacity="0.5"/>
<text id="coverageText" x="880" y="9" fill="#4a6a90" font-size="10" font-family="'IBM Plex Mono'">0%</text>
</g>
<!-- ===== 相位角度表 ===== -->
<g id="phaseDial" transform="translate(105,440)">
<circle r="52" fill="#0a1220" stroke="#1a2d50" stroke-width="1.5"/>
<circle r="46" fill="none" stroke="#152540" stroke-width="0.5"/>
<!-- 刻度 -->
<line x1="0" y1="-46" x2="0" y2="-40" stroke="#3a5575" stroke-width="1"/>
<line x1="46" y1="0" x2="40" y2="0" stroke="#3a5575" stroke-width="1"/>
<line x1="0" y1="46" x2="0" y2="40" stroke="#3a5575" stroke-width="1"/>
<line x1="-46" y1="0" x2="-40" y2="0" stroke="#3a5575" stroke-width="1"/>
<text x="0" y="-52" text-anchor="middle" fill="#4a6a90" font-size="8">0°</text>
<text x="56" y="3" text-anchor="start" fill="#4a6a90" font-size="8">90°</text>
<text x="0" y="62" text-anchor="middle" fill="#4a6a90" font-size="8">180°</text>
<text x="-56" y="3" text-anchor="end" fill="#4a6a90" font-size="8">270°</text>
<!-- 指针 -->
<line id="phaseNeedle" x1="0" y1="0" x2="0" y2="-38" stroke="#00ff88" stroke-width="2" stroke-linecap="round"/>
<circle r="4" fill="#0e1830" stroke="#00ff88" stroke-width="1.5"/>
<text x="0" y="24" text-anchor="middle" fill="#5a7a9a" font-size="9" font-family="'IBM Plex Mono'">相位角</text>
<text id="phaseValue" x="0" y="36" text-anchor="middle" fill="#00ff88" font-size="11" font-weight="600" font-family="'IBM Plex Mono'">0.0°</text>
</g>
<!-- ===== 右侧信息面板 ===== -->
<g transform="translate(1040,120)">
<rect x="-60" y="0" width="180" height="220" rx="8" fill="#0a1220" stroke="#152540" stroke-width="1" opacity="0.9"/>
<text x="30" y="24" text-anchor="middle" fill="#5a7a9a" font-size="10" font-family="'IBM Plex Mono'">实时监测</text>
<line x1="-50" y1="32" x2="110" y2="32" stroke="#152540" stroke-width="0.5"/>
<text x="-48" y="54" fill="#4a6a90" font-size="10">光束位置</text>
<text id="infoPos" x="110" y="54" text-anchor="end" fill="#00ff88" font-size="11" font-weight="600">— mm</text>
<text x="-48" y="80" fill="#4a6a90" font-size="10">扫描频率</text>
<text id="infoFreq" x="110" y="80" text-anchor="end" fill="#00ff88" font-size="11" font-weight="600">1.0 Hz</text>
<text x="-48" y="106" fill="#4a6a90" font-size="10">覆盖率</text>
<text id="infoCov" x="110" y="106" text-anchor="end" fill="#00ff88" font-size="11" font-weight="600">0%</text>
<line x1="-50" y1="118" x2="110" y2="118" stroke="#152540" stroke-width="0.5"/>
<text x="-48" y="140" fill="#4a6a90" font-size="10">检测状态</text>
<text id="infoDetect" x="110" y="140" text-anchor="end" fill="#4a6a90" font-size="11" font-weight="600">待机</text>
<text x="-48" y="166" fill="#4a6a90" font-size="10">检测位置</text>
<text id="infoDetectPos" x="110" y="166" text-anchor="end" fill="#ff8844" font-size="11" font-weight="600">— mm</text>
<text x="-48" y="192" fill="#4a6a90" font-size="10">检测相位</text>
<text id="infoDetectPhase" x="110" y="192" text-anchor="end" fill="#ff8844" font-size="11" font-weight="600">—</text>
</g>
<!-- ===== 标注文字 ===== -->
<text x="600" y="30" text-anchor="middle" fill="#3a5575" font-size="11" font-family="'IBM Plex Mono'" letter-spacing="3">SCANNING LIGHT CURTAIN PRINCIPLE</text>
<text x="600" y="240" text-anchor="middle" fill="#2a4060" font-size="10" font-family="'IBM Plex Mono'" id="scanZoneLabel">扫描光幕区域</text>
<!-- IFR 标语 -->
<g id="ifrBadge" transform="translate(105,620)">
<rect x="-60" y="-14" width="155" height="50" rx="6" fill="rgba(0,255,136,0.05)" stroke="rgba(0,255,136,0.2)" stroke-width="1"/>
<text x="17" y="2" text-anchor="middle" fill="#00ff88" font-size="10" font-weight="600" font-family="'IBM Plex Mono'">1 对传感器</text>
<text x="17" y="16" text-anchor="middle" fill="#5a7a9a" font-size="9" font-family="'IBM Plex Mono'">= 32 对等效覆盖</text>
<text x="17" y="30" text-anchor="middle" fill="#3a5575" font-size="8" font-family="'IBM Plex Mono'">时间换空间 · 零盲区</text>
</g>
<!-- 扫描线数标注 -->
<g id="sweepInfo" opacity="0">
<text id="sweepCountText" x="1095" y="390" text-anchor="middle" fill="#2a4060" font-size="9" font-family="'IBM Plex Mono'">扫掠 #0</text>
</g>
</svg>
</div>
<!-- 控制面板 -->
<div class="ctrl-panel mt-3">
<div class="ctrl-group">
<button id="btnScan" onclick="toggleScan()">启动扫描</button>
</div>
<div class="ctrl-group">
<label>扫描频率</label>
<input type="range" id="sliderFreq" min="0.3" max="4" step="0.1" value="1" oninput="updateFreq(this.value)">
<span class="val" id="valFreq">1.0 Hz</span>
</div>
<div class="ctrl-group">
<button id="btnFilm" onclick="toggleFilm()">投入薄膜</button>
</div>
<div class="ctrl-group">
<label>薄膜位置</label>
<input type="range" id="sliderFilmPos" min="0" max="100" step="1" value="50" oninput="updateFilmPos(this.value)">
<span class="val" id="valFilmPos">160 mm</span>
</div>
<div class="ctrl-group">
<button id="btnReset" onclick="resetCoverage()">重置覆盖</button>
</div>
</div>
<!-- IFR 说明 -->
<div class="ifr-banner mt-3 mb-4">
<strong>最终理想解 (IFR)</strong>:用1对激光传感器配合旋转棱镜的高频扫掠,<strong>以"时间换空间"</strong>替代32个静态传感器的排布。<br>
棱镜每旋转一周即完成一次全视野扫描——<strong>彻底消除盲区,走线锐减至2根</strong>,系统复杂度极小化。
</div>
<script>
/* ===== 常量 ===== */
const SVG_NS = 'http://www.w3.org/2000/svg';
const CX = 600; // 中心X
const EMITTER_Y = 98; // 发射器出口Y
const PRISM_Y = 195; // 棱镜中心Y
const PRISM_R = 42; // 棱镜外接圆半径
const PRISM_SIDES = 8; // 棱镜面数
const BEAM_START_Y = 225; // 扫描光束起点Y(棱镜下方)
const RECV_Y = 660; // 接收器Y
const RECV_X0 = 180; // 接收器左端
const RECV_X1 = 1020; // 接收器右端
const RECV_H = 28; // 接收器高度
const SCAN_AMP = (RECV_X1 - RECV_X0) / 2; // 扫描半幅
const FILM_Y_TOP = 360; // 薄膜顶部Y
const FILM_Y_BOT = 540; // 薄膜底部Y
const FILM_W = 28; // 薄膜宽度
const FILM_H = FILM_Y_BOT - FILM_Y_TOP;
const SEG_COUNT = 70; // 接收器分段数
const TRAIL_N = 18; // 光束轨迹数
const TOTAL_WIDTH_MM = 320; // 等效总宽度mm
/* ===== 状态 ===== */
let scanning = false;
let prismAngle = 0; // 弧度
let freq = 1.0; // 视觉扫描频率Hz
let filmOn = false;
let filmNorm = 0.5; // 0~1
let lastTS = 0;
let coverage = new Float32Array(SEG_COUNT);
let trailXs = [];
let sweepCount = 0;
let prevSign = 1;
let detectFlashTimer = 0;
let lastDetectX = null;
let lastDetectPhase = null;
/* ===== DOM 引用 ===== */
const $ = id => document.getElementById(id);
let prismShape, prismInner, prismGroup;
let beamIn, beamInGlow, beamOut, beamOutGlow;
let trailGroup, filmGroup, filmRect, filmLabel;
let detectFlash, detectRing, recvHit;
let coverageFill, coverageText;
let phaseNeedle, phaseValue;
let emitterLed;
let segRects = [];
let trailLines = [];
/* ===== 初始化 ===== */
function init() {
prismShape = $('prismShape');
prismInner = $('prismInner');
prismGroup = $('prismGroup');
beamIn = $('beamIn');
beamInGlow = $('beamInGlow');
beamOut = $('beamOut');
beamOutGlow = $('beamOutGlow');
trailGroup = $('trailGroup');
filmGroup = $('filmGroup');
filmRect = $('filmRect');
filmLabel = $('filmLabel');
detectFlash = $('detectFlash');
detectRing = $('detectRing');
recvHit = $('recvHit');
coverageFill= $('coverageFill');
coverageText= $('coverageText');
phaseNeedle = $('phaseNeedle');
phaseValue = $('phaseValue');
emitterLed = $('emitterLed');
// 绘制棱镜多边形
updatePrismShape();
// 创建接收器分段
const segG = $('receiverSegments');
const segW = (RECV_X1 - RECV_X0) / SEG_COUNT;
for (let i = 0; i < SEG_COUNT; i++) {
const r = document.createElementNS(SVG_NS, 'rect');
r.setAttribute('x', RECV_X0 + i * segW + 1);
r.setAttribute('y', RECV_Y + 2);
r.setAttribute('width', segW - 2);
r.setAttribute('height', RECV_H - 4);
r.setAttribute('rx', '2');
r.setAttribute('fill', '#0a1525');
r.setAttribute('opacity', '1');
segG.appendChild(r);
segRects.push(r);
}
// 创建光束轨迹线
for (let i = 0; i < TRAIL_N; i++) {
const l = document.createElementNS(SVG_NS, 'line');
l.setAttribute('x1', CX);
l.setAttribute('y1', BEAM_START_Y);
l.setAttribute('x2', CX);
l.setAttribute('y2', RECV_Y);
l.setAttribute('stroke', '#00ff88');
l.setAttribute('stroke-width', '1.5');
l.setAttribute('opacity', '0');
trailGroup.appendChild(l);
trailLines.push(l);
}
requestAnimationFrame(loop);
}
/* 绘制棱镜多边形路径 */
function prismPath(cx, cy, r, n, rotOff) {
let d = '';
for (let i = 0; i < n; i++) {
const a = (2 * Math.PI * i / n) + rotOff - Math.PI / 2;
const x = cx + r * Math.cos(a);
const y = cy + r * Math.sin(a);
d += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
}
return d + 'Z';
}
function updatePrismShape() {
const rotOff = prismAngle;
prismShape.setAttribute('points', prismPoints(CX, PRISM_Y, PRISM_R, PRISM_SIDES, rotOff));
prismInner.setAttribute('points', prismPoints(CX, PRISM_Y, PRISM_R * 0.55, PRISM_SIDES, rotOff));
}
function prismPoints(cx, cy, r, n, rotOff) {
let pts = '';
for (let i = 0; i < n; i++) {
const a = (2 * Math.PI * i / n) + rotOff - Math.PI / 2;
const x = cx + r * Math.cos(a);
const y = cy + r * Math.sin(a);
pts += (i > 0 ? ' ' : '') + x.toFixed(1) + ',' + y.toFixed(1);
}
return pts;
}
/* ===== 动画循环 ===== */
function loop(ts) {
if (!lastTS) lastTS = ts;
const dt = Math.min((ts - lastTS) / 1000, 0.05);
lastTS = ts;
if (scanning) {
// 更新棱镜角度
prismAngle += freq * Math.PI * 2 * dt;
// 计算扫描位置(正弦扫掠)
const scanPhase = Math.sin(prismAngle);
const beamTargetX = CX + SCAN_AMP * scanPhase;
// 扫掠计数
const curSign = scanPhase >= 0 ? 1 : -1;
if (curSign !== prevSign && curSign === 1) sweepCount++;
prevSign = curSign;
// 更新轨迹
trailXs.unshift(beamTargetX);
if (trailXs.length > TRAIL_N) trailXs.pop();
// 更新覆盖率
const segIdx = Math.floor(((beamTargetX - RECV_X0) / (RECV_X1 - RECV_X0)) * SEG_COUNT);
if (segIdx >= 0 && segIdx < SEG_COUNT) coverage[segIdx] = 1;
// 薄膜检测
const filmCX = RECV_X0 + filmNorm * (RECV_X1 - RECV_X0);
const t = (FILM_Y_TOP - BEAM_START_Y) / (RECV_Y - BEAM_START_Y);
const beamXAtFilm = CX + t * (beamTargetX - CX);
const blocked = filmOn && Math.abs(beamXAtFilm - filmCX) < FILM_W * 0.7;
if (blocked && detectFlashTimer <= 0) {
detectFlashTimer = 0.35;
lastDetectX = beamXAtFilm;
const phaseDeg = ((prismAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2)) / (Math.PI * 2) * 360;
lastDetectPhase = phaseDeg;
}
// 渲染
render(beamTargetX, blocked, beamXAtFilm, filmCX, dt);
} else {
renderIdle();
}
// 衰减检测闪光
if (detectFlashTimer > 0) detectFlashTimer -= dt;
requestAnimationFrame(loop);
}
/* ===== 渲染 ===== */
function render(beamTX, blocked, beamXAtFilm, filmCX, dt) {
// 棱镜旋转
updatePrismShape();
// 发射器 LED
emitterLed.setAttribute('fill', '#00ff88');
emitterLed.setAttribute('opacity', 0.6 + 0.4 * Math.sin(performance.now() * 0.005));
// 入射光束
beamIn.setAttribute('opacity', '0.9');
beamInGlow.setAttribute('opacity', '0.25');
// 扫描光束 - 判断是否被遮挡
if (blocked) {
beamOut.setAttribute('x2', beamXAtFilm);
beamOut.setAttribute('y2', FILM_Y_TOP);
beamOutGlow.setAttribute('x2', beamXAtFilm);
beamOutGlow.setAttribute('y2', FILM_Y_TOP);
} else {
beamOut.setAttribute('x2', beamTX);
beamOut.setAttribute('y2', RECV_Y);
beamOutGlow.setAttribute('x2', beamTX);
beamOutGlow.setAttribute('y2', RECV_Y);
}
beamOut.setAttribute('opacity', '0.9');
beamOutGlow.setAttribute('opacity', '0.2');
// 轨迹线
for (let i = 0; i < TRAIL_N; i++) {
if (i < trailXs.length) {
const tx = trailXs[i];
const alpha = Math.max(0, 0.22 * (1 - i / TRAIL_N));
trailLines[i].setAttribute('x2', tx);
trailLines[i].setAttribute('y2', RECV_Y);
trailLines[i].setAttribute('opacity', alpha.toFixed(3));
} else {
trailLines[i].setAttribute('opacity', '0');
}
}
// 接收器命中
recvHit.setAttribute('x', beamTX - 3);
recvHit.setAttribute('opacity', blocked ? '0' : '0.7');
// 接收器分段
for (let i = 0; i < SEG_COUNT; i++) {
if (coverage[i] > 0) {
segRects[i].setAttribute('fill', '#00ff88');
segRects[i].setAttribute('opacity', '0.45');
}
}
// 覆盖率
const litCount = coverage.reduce((s, v) => s + (v > 0 ? 1 : 0), 0);
const covPct = Math.round(litCount / SEG_COUNT * 100);
const barW = (RECV_X1 - RECV_X0) * covPct / 100;
coverageFill.setAttribute('width', barW);
coverageText.textContent = covPct + '%';
$('infoCov').textContent = covPct + '%';
if (covPct >= 98) {
coverageText.setAttribute('fill', '#00ff88');
$('infoCov').setAttribute('fill', '#00ff88');
}
// 相位角度
const phaseDeg = ((prismAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2)) / (Math.PI * 2) * 360;
phaseNeedle.setAttribute('transform', 'rotate(' + phaseDeg.toFixed(1) + ')');
phaseValue.textContent = phaseDeg.toFixed(1) + '°';
// 位置(mm)
const posMm = ((beamTX - RECV_X0) / (RECV_X1 - RECV_X0) * TOTAL_WIDTH_MM).toFixed(1);
$('infoPos').textContent = posMm + ' mm';
$('infoFreq').textContent = freq.toFixed(1) + ' Hz';
// 检测闪光
if (detectFlashTimer > 0) {
const fAlpha = Math.min(1, detectFlashTimer / 0.15);
const fRadius = 18 + (0.35 - detectFlashTimer) * 60;
detectFlash.setAttribute('cx', lastDetectX);
detectFlash.setAttribute('cy', FILM_Y_TOP);
detectFlash.setAttribute('r', fRadius);
detectFlash.setAttribute('opacity', (fAlpha * 0.8).toFixed(2));
detectRing.setAttribute('cx', lastDetectX);
detectRing.setAttribute('cy', FILM_Y_TOP);
detectRing.setAttribute('r', fRadius * 1.5);
detectRing.setAttribute('opacity', (fAlpha * 0.5).toFixed(2));
$('infoDetect').textContent = '遮挡!';
$('infoDetect').setAttribute('fill', '#ffffaa');
if (lastDetectX !== null) {
const dPosMm = ((lastDetectX - RECV_X0) / (RECV_X1 - RECV_X0) * TOTAL_WIDTH_MM).toFixed(1);
$('infoDetectPos').textContent = dPosMm + ' mm';
$('infoDetectPhase').textContent = lastDetectPhase.toFixed(1) + '°';
}
} else {
detectFlash.setAttribute('opacity', '0');
detectRing.setAttribute('opacity', '0');
if (!blocked) {
$('infoDetect').textContent = filmOn ? '扫描中' : '无薄膜';
$('infoDetect').setAttribute('fill', filmOn ? '#00ff88' : '#4a6a90');
}
}
// 扫掠计数
$('sweepCountText').textContent = '扫掠 #' + sweepCount;
$('sweepInfo').setAttribute('opacity', '1');
// 扫描区域标注
$('scanZoneLabel').setAttribute('opacity', '0.4');
}
function renderIdle() {
emitterLed.setAttribute('fill', '#1a2a1a');
emitterLed.setAttribute('opacity', '1');
beamIn.setAttribute('opacity', '0');
beamInGlow.setAttribute('opacity', '0');
beamOut.setAttribute('opacity', '0');
beamOutGlow.setAttribute('opacity', '0');
recvHit.setAttribute('opacity', '0');
for (let i = 0; i < TRAIL_N; i++) trailLines[i].setAttribute('opacity', '0');
detectFlash.setAttribute('opacity', '0');
detectRing.setAttribute('opacity', '0');
$('sweepInfo').setAttribute('opacity', '0');
$('scanZoneLabel').setAttribute('opacity', '0.2');
$('infoDetect').textContent = '待机';
$('infoDetect').setAttribute('fill', '#4a6a90');
}
/* ===== 交互 ===== */
function toggleScan() {
scanning = !scanning;
const btn = $('btnScan');
if (scanning) {
btn.textContent = '停止扫描';
btn.classList.add('active');
lastTS = 0;
trailXs = [];
} else {
btn.textContent = '启动扫描';
btn.classList.remove('active');
}
}
function toggleFilm() {
filmOn = !filmOn;
const btn = $('btnFilm');
if (filmOn) {
btn.textContent = '撤除薄膜';
btn.classList.add('active');
filmGroup.setAttribute('visibility', 'visible');
updateFilmVisual();
} else {
btn.textContent = '投入薄膜';
btn.classList.remove('active');
filmGroup.setAttribute('visibility', 'hidden');
lastDetectX = null;
lastDetectPhase = null;
$('infoDetectPos').textContent = '—';
$('infoDetectPhase').textContent = '—';
}
}
function updateFreq(v) {
freq = parseFloat(v);
$('valFreq').textContent = freq.toFixed(1) + ' Hz';
}
function updateFilmPos(v) {
filmNorm = parseInt(v) / 100;
const posMm = (filmNorm * TOTAL_WIDTH_MM).toFixed(0);
$('valFilmPos').textContent = posMm + ' mm';
if (filmOn) updateFilmVisual();
}
function updateFilmVisual() {
const fx = RECV_X0 + filmNorm * (RECV_X1 - RECV_X0);
filmRect.setAttribute('x', fx - FILM_W / 2);
filmLabel.setAttribute('x', fx);
}
function resetCoverage() {
coverage.fill(0);
for (let i = 0; i < SEG_COUNT; i++) {
segRects[i].setAttribute('fill', '#0a1525');
segRects[i].setAttribute('opacity', '1');
}
coverageFill.setAttribute('width', '0');
coverageText.textContent = '0%';
coverageText.setAttribute('fill', '#4a6a90');
$('infoCov').textContent = '0%';
$('infoCov').setAttribute('fill', '#00ff88');
sweepCount = 0;
trailXs = [];
}
/* ===== 启动 ===== */
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
实现说明:
核心机理可视化:动画完整呈现了"激光 → 旋转棱镜 → 扫掠光幕 → 接收器"的作用链。八面棱镜持续旋转,激光束经折射后在检测区域形成正弦扫掠光幕,轨迹线以渐隐方式呈现"光帘"效果,直观展示全视野覆盖。
IFR 理念传达:
- 左下角 IFR 标识卡突出"1对传感器 = 32对等效覆盖 · 时间换空间 · 零盲区"
- 接收器分段逐一点亮,覆盖率实时攀升至100%,直观证明零盲区
- 底部覆盖率条以填充动画强化"全视野无死角"的理想状态
薄膜检测交互:点击"投入薄膜"后,琥珀色薄膜出现在检测区。当扫掠光束触达薄膜时,光束被截断(只画到薄膜上表面),闪光与涟漪效果标记遮挡点,右侧面板同步显示检测位置(mm)和对应相位角,完整演示"相位角 → 位置"的反推机理。
相位角度表:左侧圆盘仪表实时跟踪棱镜旋转角度,与扫描光束位置严格对应,使"相位推算位置"这一核心创新点可被直接感知。
交互控制:扫描频率滑块可调节扫掠速度,薄膜位置滑块可拖动薄膜至任意水平位置,"重置覆盖"可清空覆盖率重新观察填充过程——用户可亲手验证"任意位置均可被检测到"。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
