<!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=Syne:wght@400;600;700;800&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #060a12;
--fg: #dce3f0;
--muted: #5a6578;
--accent: #00e5ff;
--accent2: #ff6b2b;
--success: #00e676;
--card: #0c1220;
--border: #172038;
--glass-color: #4fc3f7;
--absorb-color: #5d4037;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
background:var(--bg);color:var(--fg);
font-family:'IBM Plex Mono',monospace;
min-height:100vh;display:flex;flex-direction:column;align-items:center;
overflow-x:hidden;
}
.page-header{
text-align:center;padding:2rem 1rem 0.5rem;width:100%;max-width:1300px;
}
.page-title{
font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(1.4rem,3vw,2.2rem);
letter-spacing:-0.03em;
background:linear-gradient(135deg,var(--accent),#80deea,var(--accent2));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;
}
.page-sub{
color:var(--muted);font-size:0.78rem;font-weight:300;margin-top:0.4rem;
letter-spacing:0.04em;
}
.svg-wrap{
width:100%;max-width:1300px;display:flex;justify-content:center;
padding:1rem;
}
.svg-wrap svg{
width:100%;height:auto;border-radius:14px;
background:var(--card);border:1px solid var(--border);
box-shadow:0 0 60px rgba(0,229,255,0.03),0 0 120px rgba(255,107,43,0.02);
}
.controls-bar{
width:100%;max-width:1300px;
display:flex;flex-wrap:wrap;gap:1.5rem 2.5rem;
justify-content:center;align-items:flex-end;
padding:1.2rem 2rem;
background:var(--card);border-radius:14px;border:1px solid var(--border);
margin:0 1rem 1rem;
}
.ctrl-group{display:flex;flex-direction:column;gap:0.4rem}
.ctrl-label{
font-size:0.68rem;color:var(--muted);text-transform:uppercase;
letter-spacing:0.12em;font-weight:500;
}
.slider-row{display:flex;align-items:center;gap:0.8rem}
input[type=range]{
-webkit-appearance:none;width:220px;height:3px;
background:var(--border);border-radius:2px;outline:none;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:16px;height:16px;
background:var(--accent);border-radius:50%;cursor:pointer;
box-shadow:0 0 8px rgba(0,229,255,0.5);
transition:box-shadow .2s;
}
input[type=range]::-webkit-slider-thumb:hover{
box-shadow:0 0 16px rgba(0,229,255,0.8);
}
.slider-val{
font-size:0.85rem;color:var(--accent);min-width:52px;text-align:right;
font-weight:600;
}
.mat-btns{display:flex;gap:0.4rem}
.mat-btn{
padding:0.45rem 1.1rem;border:1px solid var(--border);
background:transparent;color:var(--muted);
font-family:'IBM Plex Mono',monospace;font-size:0.75rem;
border-radius:6px;cursor:pointer;transition:all .25s;
font-weight:500;
}
.mat-btn.active{
border-color:var(--accent2);color:var(--accent2);
background:rgba(255,107,43,0.08);
box-shadow:0 0 12px rgba(255,107,43,0.1);
}
.mat-btn:hover:not(.active){border-color:var(--fg);color:var(--fg)}
.ifr-insight{
width:100%;max-width:1300px;
text-align:center;padding:1.2rem 2rem;
background:linear-gradient(135deg,rgba(0,229,255,0.04),rgba(255,107,43,0.04));
border-radius:14px;border:1px solid var(--border);
margin:0 1rem 2rem;font-size:0.8rem;line-height:1.7;color:var(--muted);
}
.ifr-insight strong{color:var(--fg);font-weight:600}
.ifr-insight .hl{color:var(--accent2);font-weight:600}
.ifr-insight .hl2{color:var(--accent);font-weight:600}
</style>
</head>
<body>
<header class="page-header">
<div class="page-title">近场主动激励 · 宽频振动感知原理</div>
<div class="page-sub">IFR 最终理想解:障碍物本身即传感器 —— 无需回波,阻抗耦合即感知</div>
</header>
<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 发光滤镜 -->
<filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowOrange" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="8"/>
</filter>
<!-- 盲杖身体渐变 -->
<linearGradient id="caneBodyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3a4560"/>
<stop offset="50%" stop-color="#252d40"/>
<stop offset="100%" stop-color="#1a2030"/>
</linearGradient>
<linearGradient id="caneInnerGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#10151f"/>
<stop offset="100%" stop-color="#0a0e16"/>
</linearGradient>
<linearGradient id="tipGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#546e7a"/>
<stop offset="100%" stop-color="#37474f"/>
</linearGradient>
<linearGradient id="handleGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2c3440"/>
<stop offset="100%" stop-color="#1a1f28"/>
</linearGradient>
<!-- 玻璃墙渐变 -->
<linearGradient id="glassGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="rgba(79,195,247,0.25)"/>
<stop offset="40%" stop-color="rgba(79,195,247,0.08)"/>
<stop offset="100%" stop-color="rgba(79,195,247,0.15)"/>
</linearGradient>
<!-- 吸音墙纹理 -->
<pattern id="absorbPattern" width="8" height="8" patternUnits="userSpaceOnUse">
<rect width="8" height="8" fill="#3e2723"/>
<circle cx="4" cy="4" r="2.5" fill="#2c1a10" opacity="0.7"/>
</pattern>
<!-- 背景网格点 -->
<pattern id="dotGrid" width="30" height="30" patternUnits="userSpaceOnUse">
<circle cx="15" cy="15" r="0.5" fill="rgba(100,120,160,0.15)"/>
</pattern>
<!-- 信号路径箭头 -->
<marker id="arrowCyan" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="rgba(0,229,255,0.6)"/>
</marker>
<marker id="arrowOrange" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="rgba(255,107,43,0.6)"/>
</marker>
</defs>
<!-- 背景网格 -->
<rect width="1200" height="780" fill="url(#dotGrid)"/>
<!-- ===== 信号流向标注 ===== -->
<g id="signalFlow" opacity="0.5">
<!-- 激励信号路径:激振器 → 杖尖 -->
<line x1="700" y1="230" x2="760" y2="230" stroke="rgba(0,229,255,0.3)" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrowCyan)"/>
<text x="730" y="222" fill="rgba(0,229,255,0.5)" font-size="9" text-anchor="middle" font-family="IBM Plex Mono">激励</text>
<!-- 感知信号路径:杖身 → 加速度计 -->
<line x1="700" y1="300" x2="250" y2="300" stroke="rgba(255,107,43,0.3)" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrowOrange)"/>
<text x="475" y="318" fill="rgba(255,107,43,0.5)" font-size="9" text-anchor="middle" font-family="IBM Plex Mono">阻尼变化信号</text>
</g>
<!-- ===== 盲杖主体 ===== -->
<g id="caneGroup">
<!-- 手柄 -->
<rect x="100" y="232" width="80" height="66" rx="10" fill="url(#handleGrad)" stroke="#3a4560" stroke-width="1"/>
<rect x="108" y="240" width="64" height="50" rx="6" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<!-- 手柄握纹 -->
<line x1="115" y1="248" x2="165" y2="248" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="115" y1="256" x2="165" y2="256" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="115" y1="264" x2="165" y2="264" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="115" y1="272" x2="165" y2="272" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="115" y1="280" x2="165" y2="280" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<!-- 杖身主管 -->
<rect x="175" y="245" width="560" height="40" rx="5" fill="url(#caneBodyGrad)" stroke="#3a4560" stroke-width="0.8"/>
<!-- 内腔(剖视) -->
<rect x="185" y="251" width="540" height="28" rx="3" fill="url(#caneInnerGrad)"/>
<!-- 硬质合金杖尖 -->
<path d="M735,245 L775,250 L790,265 L775,280 L735,285 Z" fill="url(#tipGrad)" stroke="#546e7a" stroke-width="0.8"/>
<path d="M740,251 L770,255 L782,265 L770,275 L740,279 Z" fill="#0d1118" stroke="rgba(84,110,122,0.3)" stroke-width="0.5"/>
<!-- 压电陶瓷激振器 -->
<rect id="exciter" x="700" y="253" width="30" height="24" rx="2" fill="#00838f" stroke="#00e5ff" stroke-width="1" opacity="0.9"/>
<rect x="704" y="257" width="22" height="16" rx="1" fill="#004d40" opacity="0.6"/>
<!-- 激振器标识 -->
<text x="715" y="270" fill="#00e5ff" font-size="7" text-anchor="middle" font-family="IBM Plex Mono" font-weight="600">PZT</text>
<!-- 加速度计 -->
<rect id="accelerometer" x="210" y="253" width="28" height="24" rx="2" fill="#bf360c" stroke="#ff6b2b" stroke-width="1" opacity="0.9"/>
<rect x="214" y="257" width="20" height="16" rx="1" fill="#6d1b00" opacity="0.6"/>
<text x="224" y="270" fill="#ff6b2b" font-size="6.5" text-anchor="middle" font-family="IBM Plex Mono" font-weight="600">ACC</text>
<!-- 内部连线 -->
<line x1="238" y1="265" x2="700" y2="265" stroke="rgba(255,255,255,0.06)" stroke-width="0.8" stroke-dasharray="3,4"/>
</g>
<!-- ===== 振动波 (动态生成) ===== -->
<g id="wavesGroup"></g>
<!-- ===== 空气隙粒子 (动态生成) ===== -->
<g id="airGapGroup"></g>
<!-- ===== 阻抗耦合区 (动态) ===== -->
<g id="couplingZone"></g>
<!-- ===== 障碍物墙壁 ===== -->
<g id="wallGroup"></g>
<!-- ===== 距离标注 ===== -->
<g id="distanceAnnotation">
<line id="distLineTop" x1="790" y1="220" x2="950" y2="220" stroke="rgba(255,255,255,0.2)" stroke-width="0.8" stroke-dasharray="3,2"/>
<line id="distLineBot" x1="790" y1="310" x2="950" y2="310" stroke="rgba(255,255,255,0.2)" stroke-width="0.8" stroke-dasharray="3,2"/>
<line id="distLineLeft" x1="790" y1="220" x2="790" y2="310" stroke="rgba(255,255,255,0.15)" stroke-width="0.6"/>
<line id="distLineRight" x1="950" y1="220" x2="950" y2="310" stroke="rgba(255,255,255,0.15)" stroke-width="0.6"/>
<text id="distText" x="870" y="215" fill="rgba(255,255,255,0.4)" font-size="11" text-anchor="middle" font-family="IBM Plex Mono" font-weight="500">40 cm</text>
</g>
<!-- ===== 组件标注 ===== -->
<g id="annotations" font-family="IBM Plex Mono" font-size="10">
<!-- 激振器标注 -->
<line x1="715" y1="250" x2="715" y2="200" stroke="rgba(0,229,255,0.3)" stroke-width="0.8"/>
<text x="715" y="194" fill="rgba(0,229,255,0.7)" text-anchor="middle" font-size="9.5" font-weight="500">宽频激振器</text>
<text x="715" y="182" fill="rgba(0,229,255,0.4)" text-anchor="middle" font-size="8">1kHz - 20kHz</text>
<!-- 加速度计标注 -->
<line x1="224" y1="250" x2="224" y2="200" stroke="rgba(255,107,43,0.3)" stroke-width="0.8"/>
<text x="224" y="194" fill="rgba(255,107,43,0.7)" text-anchor="middle" font-size="9.5" font-weight="500">高灵敏度加速度计</text>
<text x="224" y="182" fill="rgba(255,107,43,0.4)" text-anchor="middle" font-size="8">阻尼/相位感知</text>
<!-- 杖尖标注 -->
<text x="770" y="340" fill="rgba(84,110,122,0.6)" text-anchor="middle" font-size="8.5">硬质合金杖尖</text>
<text x="770" y="352" fill="rgba(84,110,122,0.4)" text-anchor="middle" font-size="7.5">高声阻抗</text>
<!-- 手柄标注 -->
<text x="140" y="320" fill="rgba(255,255,255,0.3)" text-anchor="middle" font-size="8.5">触觉反馈手柄</text>
</g>
<!-- ===== 手柄反馈指示器 ===== -->
<g id="handleFeedback">
<circle id="feedbackDot" cx="140" cy="265" r="0" fill="var(--success)" opacity="0"/>
</g>
<!-- ===== 波形显示区 ===== -->
<g id="waveformPanel">
<rect x="60" y="420" width="480" height="170" rx="8" fill="rgba(10,14,22,0.9)" stroke="var(--border)" stroke-width="1"/>
<text x="80" y="445" fill="var(--muted)" font-size="9" font-family="IBM Plex Mono" font-weight="500">振动状态监测 — 加速度计信号</text>
<line x1="80" y1="505" x2="520" y2="505" stroke="rgba(255,255,255,0.06)" stroke-width="0.8"/>
<!-- 波形标签 -->
<text id="wfLabelFar" x="80" y="460" fill="rgba(0,229,255,0.5)" font-size="8" font-family="IBM Plex Mono">自由振动</text>
<text id="wfLabelNear" x="80" y="460" fill="rgba(255,107,43,0.7)" font-size="8" font-family="IBM Plex Mono" opacity="0">阻尼耦合</text>
<!-- 波形路径 -->
<path id="waveformPath" d="" fill="none" stroke="var(--accent)" stroke-width="1.5" opacity="0.8"/>
<!-- 波形背景发光 -->
<path id="waveformGlow" d="" fill="none" stroke="var(--accent)" stroke-width="4" opacity="0.1" filter="url(#softGlow)"/>
</g>
<!-- ===== 阻抗耦合信息面板 ===== -->
<g id="couplingPanel">
<rect x="580" y="420" width="560" height="170" rx="8" fill="rgba(10,14,22,0.9)" stroke="var(--border)" stroke-width="1"/>
<text x="600" y="445" fill="var(--muted)" font-size="9" font-family="IBM Plex Mono" font-weight="500">近场阻抗耦合状态</text>
<!-- 耦合强度条 -->
<text x="600" y="472" fill="rgba(255,255,255,0.4)" font-size="8.5" font-family="IBM Plex Mono">耦合强度</text>
<rect x="700" y="462" width="200" height="10" rx="3" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.08)" stroke-width="0.5"/>
<rect id="couplingBar" x="700" y="462" width="0" height="10" rx="3" fill="var(--accent2)" opacity="0.8"/>
<text id="couplingPct" x="910" y="472" fill="var(--accent2)" font-size="9" font-family="IBM Plex Mono" font-weight="600">0%</text>
<!-- 阻抗变化示意 -->
<text x="600" y="500" fill="rgba(255,255,255,0.4)" font-size="8.5" font-family="IBM Plex Mono">空气隙状态</text>
<g id="airGapDiagram" transform="translate(700,488)">
<rect width="200" height="14" rx="2" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.06)" stroke-width="0.5"/>
<!-- 空气隙示意粒子 (动态) -->
</g>
<!-- IFR 核心标注 -->
<rect x="600" y="530" width="520" height="48" rx="6" fill="rgba(255,107,43,0.04)" stroke="rgba(255,107,43,0.15)" stroke-width="0.8"/>
<text x="616" y="550" fill="var(--accent2)" font-size="9.5" font-family="Syne" font-weight="700">IFR 理想解</text>
<text x="710" y="550" fill="rgba(255,255,255,0.55)" font-size="8.5" font-family="IBM Plex Mono">障碍物占据空间 → 局部空气阻抗突变 → 阻尼耦合</text>
<text x="616" y="568" fill="rgba(255,255,255,0.35)" font-size="8" font-family="IBM Plex Mono">无需回波反射 · 玻璃/吸音材料一视同仁 · 障碍物本身即传感器</text>
</g>
<!-- ===== 检测状态 ===== -->
<g id="detectionStatus" transform="translate(60,620)">
<rect width="280" height="44" rx="8" fill="rgba(10,14,22,0.9)" stroke="var(--border)" stroke-width="1"/>
<circle id="statusDot" cx="22" cy="22" r="6" fill="var(--muted)" opacity="0.4"/>
<text id="statusText" x="40" y="26" fill="var(--muted)" font-size="10" font-family="IBM Plex Mono" font-weight="500">待机 · 无障碍物</text>
</g>
<!-- ===== 材料对比标注 ===== -->
<g id="materialNote" transform="translate(380,620)">
<rect width="380" height="44" rx="8" fill="rgba(10,14,22,0.9)" stroke="var(--border)" stroke-width="1"/>
<text x="16" y="18" fill="rgba(255,255,255,0.35)" font-size="8" font-family="IBM Plex Mono">传统超声波困境</text>
<text x="16" y="34" fill="rgba(255,82,82,0.5)" font-size="8" font-family="IBM Plex Mono">玻璃→回波偏离 · 吸音→无回波 · 均漏报</text>
<line x1="200" y1="10" x2="200" y2="38" stroke="rgba(255,255,255,0.08)" stroke-width="0.8"/>
<text x="216" y="18" fill="rgba(255,255,255,0.35)" font-size="8" font-family="IBM Plex Mono">近场耦合方案</text>
<text x="216" y="34" fill="rgba(0,230,118,0.5)" font-size="8" font-family="IBM Plex Mono">阻抗突变即感知 · 不依赖反射</text>
</g>
<!-- ===== 时序流程 ===== -->
<g id="sequenceFlow" transform="translate(800,620)">
<rect width="340" height="44" rx="8" fill="rgba(10,14,22,0.9)" stroke="var(--border)" stroke-width="1"/>
<text x="16" y="18" fill="rgba(255,255,255,0.35)" font-size="8" font-family="IBM Plex Mono">动作时序</text>
<text id="seqText" x="16" y="34" fill="var(--accent)" font-size="8" font-family="IBM Plex Mono" opacity="0.6">挥动→激励→空气隙压缩→阻抗耦合→状态突变→预警</text>
</g>
</svg>
</div>
<!-- 控制面板 -->
<div class="controls-bar">
<div class="ctrl-group">
<div class="ctrl-label">杖尖-障碍物距离</div>
<div class="slider-row">
<input type="range" id="distSlider" min="0" max="50" value="40" step="1"/>
<span class="slider-val" id="distVal">40 cm</span>
</div>
</div>
<div class="ctrl-group">
<div class="ctrl-label">障碍物材质</div>
<div class="mat-btns">
<button class="mat-btn active" data-mat="glass">玻璃</button>
<button class="mat-btn" data-mat="absorbent">吸音棉</button>
</div>
</div>
<div class="ctrl-group">
<div class="ctrl-label">激振频率</div>
<div class="slider-row">
<input type="range" id="freqSlider" min="1" max="20" value="8" step="0.5"/>
<span class="slider-val" id="freqVal">8 kHz</span>
</div>
</div>
</div>
<!-- IFR 原理说明 -->
<div class="ifr-insight">
<strong>最终理想解 (IFR):</strong>
系统自行消除了<span class="hl">远场回波路径依赖</span>这一核心矛盾。
障碍物的存在本身改变了局部空气声阻抗,无需障碍物主动"回复"任何信号——
<span class="hl2">近场空气隙的声学/力学耦合</span>让障碍物"成为传感器的一部分"。
无论镜面反射还是强吸音,只要占据空间、改变阻抗,即刻感知。
</div>
<script>
// ===== 全局状态 =====
const state = {
distance: 40, // cm
material: 'glass', // 'glass' | 'absorbent'
freq: 8, // kHz
time: 0,
coupling: 0, // 0~1
detected: false
};
// ===== SVG 元素引用 =====
const svg = document.getElementById('mainSvg');
const wavesGroup = document.getElementById('wavesGroup');
const airGapGroup = document.getElementById('airGapGroup');
const couplingZone = document.getElementById('couplingZone');
const wallGroup = document.getElementById('wallGroup');
const waveformPath = document.getElementById('waveformPath');
const waveformGlow = document.getElementById('waveformGlow');
const couplingBar = document.getElementById('couplingBar');
const couplingPct = document.getElementById('couplingPct');
const feedbackDot = document.getElementById('feedbackDot');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const distText = document.getElementById('distText');
const wfLabelFar = document.getElementById('wfLabelFar');
const wfLabelNear = document.getElementById('wfLabelNear');
// ===== 常量 =====
const TIP_X = 790; // 杖尖右端 x 坐标
const WALL_BASE_X = 950; // 墙壁基准 x(50cm 时)
const CANE_CY = 265; // 盲杖中心 y
const MAX_GAP_PX = 160; // 50cm 对应的像素间距
const COUPLING_THRESHOLD = 30; // cm,耦合阈值
// ===== 初始化振动波元素 =====
const WAVE_COUNT = 8;
const waveEls = [];
for (let i = 0; i < WAVE_COUNT; i++) {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
el.setAttribute('cx', TIP_X);
el.setAttribute('cy', CANE_CY);
el.setAttribute('rx', '0');
el.setAttribute('ry', '0');
el.setAttribute('fill', 'none');
el.setAttribute('stroke', '#00e5ff');
el.setAttribute('stroke-width', '1.2');
el.setAttribute('opacity', '0');
wavesGroup.appendChild(el);
waveEls.push({ el, phase: i / WAVE_COUNT, speed: 0.6 + Math.random() * 0.3 });
}
// ===== 初始化空气隙粒子 =====
const PARTICLE_ROWS = 5;
const PARTICLE_COLS = 12;
const particleEls = [];
for (let r = 0; r < PARTICLE_ROWS; r++) {
for (let c = 0; c < PARTICLE_COLS; c++) {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
el.setAttribute('r', '2');
el.setAttribute('fill', 'rgba(100,140,180,0.3)');
airGapGroup.appendChild(el);
particleEls.push({ el, row: r, col: c });
}
}
// ===== 初始化阻抗耦合区粒子 =====
const COUPLING_PARTICLES = 20;
const couplingParticles = [];
for (let i = 0; i < COUPLING_PARTICLES; i++) {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
el.setAttribute('r', '1.5');
el.setAttribute('fill', '#ff6b2b');
el.setAttribute('opacity', '0');
couplingZone.appendChild(el);
couplingParticles.push({ el, angle: Math.random() * Math.PI * 2, radius: 10 + Math.random() * 30, speed: 0.5 + Math.random() * 1.5 });
}
// ===== 初始化空气隙示意条粒子 =====
const airGapDiagram = document.getElementById('airGapDiagram');
const diagParticles = [];
for (let i = 0; i < 16; i++) {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
el.setAttribute('r', '3');
el.setAttribute('fill', 'rgba(0,229,255,0.4)');
el.setAttribute('cy', '7');
airGapDiagram.appendChild(el);
diagParticles.push(el);
}
// ===== 绘制墙壁 =====
function drawWall() {
// 清空
while (wallGroup.firstChild) wallGroup.removeChild(wallGroup.firstChild);
const wallX = getWallX();
if (state.material === 'glass') {
// 玻璃墙
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', wallX);
rect.setAttribute('y', '160');
rect.setAttribute('width', '24');
rect.setAttribute('height', '220');
rect.setAttribute('rx', '2');
rect.setAttribute('fill', 'url(#glassGrad)');
rect.setAttribute('stroke', 'rgba(79,195,247,0.3)');
rect.setAttribute('stroke-width', '1');
wallGroup.appendChild(rect);
// 反射高光
const hl = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
hl.setAttribute('x', wallX + 4);
hl.setAttribute('y', '170');
hl.setAttribute('width', '3');
hl.setAttribute('height', '200');
hl.setAttribute('rx', '1');
hl.setAttribute('fill', 'rgba(255,255,255,0.12)');
wallGroup.appendChild(hl);
// 标签
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt.setAttribute('x', wallX + 12);
txt.setAttribute('y', '395');
txt.setAttribute('fill', 'rgba(79,195,247,0.5)');
txt.setAttribute('font-size', '9');
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('font-family', 'IBM Plex Mono');
txt.textContent = '玻璃';
wallGroup.appendChild(txt);
// 传统超声波回波偏移示意 (虚线箭头偏转)
const arrowG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
arrowG.setAttribute('opacity', '0.25');
// 入射波
const inc = document.createElementNS('http://www.w3.org/2000/svg', 'line');
inc.setAttribute('x1', TIP_X - 20); inc.setAttribute('y1', CANE_CY - 40);
inc.setAttribute('x2', wallX); inc.setAttribute('y2', CANE_CY - 15);
inc.setAttribute('stroke', '#ff5252'); inc.setAttribute('stroke-width', '1');
inc.setAttribute('stroke-dasharray', '3,2');
arrowG.appendChild(inc);
// 反射波(偏转方向)
const ref = document.createElementNS('http://www.w3.org/2000/svg', 'line');
ref.setAttribute('x1', wallX); ref.setAttribute('y1', CANE_CY - 15);
ref.setAttribute('x2', wallX - 30); ref.setAttribute('y2', CANE_CY - 70);
ref.setAttribute('stroke', '#ff5252'); ref.setAttribute('stroke-width', '1');
ref.setAttribute('stroke-dasharray', '3,2');
arrowG.appendChild(ref);
const refTxt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
refTxt.setAttribute('x', wallX - 40); refTxt.setAttribute('y', CANE_CY - 75);
refTxt.setAttribute('fill', '#ff5252'); refTxt.setAttribute('font-size', '7');
refTxt.setAttribute('font-family', 'IBM Plex Mono');
refTxt.textContent = '回波偏离';
arrowG.appendChild(refTxt);
wallGroup.appendChild(arrowG);
} else {
// 吸音棉墙
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', wallX);
rect.setAttribute('y', '160');
rect.setAttribute('width', '28');
rect.setAttribute('height', '220');
rect.setAttribute('rx', '3');
rect.setAttribute('fill', 'url(#absorbPattern)');
rect.setAttribute('stroke', 'rgba(93,64,55,0.5)');
rect.setAttribute('stroke-width', '1');
wallGroup.appendChild(rect);
// 吸音纹理凸起
for (let row = 0; row < 11; row++) {
for (let col = 0; col < 3; col++) {
const bump = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bump.setAttribute('cx', wallX + 5 + col * 9);
bump.setAttribute('cy', 172 + row * 20);
bump.setAttribute('r', '3');
bump.setAttribute('fill', '#1a0e06');
bump.setAttribute('opacity', '0.6');
wallGroup.appendChild(bump);
}
}
// 标签
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt.setAttribute('x', wallX + 14);
txt.setAttribute('y', '395');
txt.setAttribute('fill', 'rgba(141,110,99,0.5)');
txt.setAttribute('font-size', '9');
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('font-family', 'IBM Plex Mono');
txt.textContent = '吸音棉';
wallGroup.appendChild(txt);
// 传统超声波无回波示意
const arrowG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
arrowG.setAttribute('opacity', '0.25');
const inc = document.createElementNS('http://www.w3.org/2000/svg', 'line');
inc.setAttribute('x1', TIP_X - 20); inc.setAttribute('y1', CANE_CY - 40);
inc.setAttribute('x2', wallX); inc.setAttribute('y2', CANE_CY - 15);
inc.setAttribute('stroke', '#ff5252'); inc.setAttribute('stroke-width', '1');
inc.setAttribute('stroke-dasharray', '3,2');
arrowG.appendChild(inc);
// 被吸收(波消失)
const absMark = document.createElementNS('http://www.w3.org/2000/svg', 'text');
absMark.setAttribute('x', wallX + 4); absMark.setAttribute('y', CANE_CY - 20);
absMark.setAttribute('fill', '#ff5252'); absMark.setAttribute('font-size', '8');
absMark.setAttribute('font-family', 'IBM Plex Mono');
absMark.textContent = '×';
arrowG.appendChild(absMark);
const absTxt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
absTxt.setAttribute('x', wallX - 50); absTxt.setAttribute('y', CANE_CY - 60);
absTxt.setAttribute('fill', '#ff5252'); absTxt.setAttribute('font-size', '7');
absTxt.setAttribute('font-family', 'IBM Plex Mono');
absTxt.textContent = '回波被吸收';
arrowG.appendChild(absTxt);
wallGroup.appendChild(arrowG);
}
}
// ===== 计算墙壁 x 坐标 =====
function getWallX() {
const gapPx = (state.distance / 50) * MAX_GAP_PX;
return TIP_X + Math.max(gapPx, 2);
}
// ===== 计算耦合强度 =====
function calcCoupling() {
if (state.distance >= COUPLING_THRESHOLD) return 0;
return Math.pow(1 - state.distance / COUPLING_THRESHOLD, 1.8);
}
// ===== 更新距离标注 =====
function updateDistanceAnnotation() {
const wallX = getWallX();
const midX = (TIP_X + wallX) / 2;
document.getElementById('distLineTop').setAttribute('x2', wallX);
document.getElementById('distLineBot').setAttribute('x2', wallX);
document.getElementById('distLineRight').setAttribute('x1', wallX);
document.getElementById('distLineRight').setAttribute('x2', wallX);
distText.setAttribute('x', midX);
distText.textContent = state.distance + ' cm';
}
// ===== 更新振动波 =====
function updateWaves(dt) {
const wallX = getWallX();
const maxRadius = wallX - TIP_X;
waveEls.forEach((w, i) => {
w.phase = (w.phase + w.speed * dt * 0.4) % 1;
const t = w.phase;
const rx = t * (maxRadius + 20);
const ry = t * 50;
const opacity = (1 - t) * 0.6 * (0.4 + state.coupling * 0.6);
w.el.setAttribute('rx', Math.max(0, rx));
w.el.setAttribute('ry', Math.max(0, ry));
w.el.setAttribute('opacity', Math.max(0, opacity).toFixed(3));
// 耦合时颜色从青色过渡到橙色
if (state.coupling > 0.1) {
const r = Math.round(0 + 255 * state.coupling);
const g = Math.round(229 - 122 * state.coupling);
const b = Math.round(255 - 212 * state.coupling);
w.el.setAttribute('stroke', `rgb(${r},${g},${b})`);
} else {
w.el.setAttribute('stroke', '#00e5ff');
}
});
}
// ===== 更新空气隙粒子 =====
function updateAirParticles() {
const wallX = getWallX();
const gapWidth = wallX - TIP_X;
const gapTop = CANE_CY - 30;
const gapHeight = 60;
particleEls.forEach(p => {
const baseX = TIP_X + (p.col / (PARTICLE_COLS - 1)) * gapWidth;
const baseY = gapTop + (p.row / (PARTICLE_ROWS - 1)) * gapHeight;
// 耦合时粒子向中心压缩
const compressFactor = state.coupling * 0.4;
const cx = TIP_X + gapWidth / 2;
const cy = CANE_CY;
const dx = baseX - cx;
const dy = baseY - cy;
const x = baseX - dx * compressFactor;
const y = baseY - dy * compressFactor;
// 振动微扰
const jitter = state.coupling > 0.1 ? (1 - state.coupling) * 2 : 0;
const jx = Math.sin(state.time * 5 + p.col) * jitter;
const jy = Math.cos(state.time * 4 + p.row) * jitter;
p.el.setAttribute('cx', (x + jx).toFixed(2));
p.el.setAttribute('cy', (y + jy).toFixed(2));
// 颜色随耦合变化
if (state.coupling > 0.2) {
const alpha = 0.3 + state.coupling * 0.5;
p.el.setAttribute('fill', `rgba(255,107,43,${alpha.toFixed(2)})`);
p.el.setAttribute('r', (2 + state.coupling * 1.5).toFixed(1));
} else {
p.el.setAttribute('fill', 'rgba(100,140,180,0.3)');
p.el.setAttribute('r', '2');
}
});
}
// ===== 更新阻抗耦合区 =====
function updateCouplingZone() {
const wallX = getWallX();
couplingParticles.forEach((cp, i) => {
if (state.coupling < 0.05) {
cp.el.setAttribute('opacity', '0');
return;
}
cp.angle += cp.speed * 0.03;
const centerX = (TIP_X + wallX) / 2;
const centerY = CANE_CY;
const r = cp.radius * (0.5 + state.coupling * 0.8);
const x = centerX + Math.cos(cp.angle + state.time * 0.5) * r * 0.5;
const y = centerY + Math.sin(cp.angle + state.time * 0.7) * r * 0.6;
cp.el.setAttribute('cx', x.toFixed(2));
cp.el.setAttribute('cy', y.toFixed(2));
cp.el.setAttribute('opacity', (state.coupling * 0.5).toFixed(3));
cp.el.setAttribute('r', (1 + state.coupling * 2).toFixed(1));
});
// 耦合区发光矩形
let zoneRect = couplingZone.querySelector('#couplingZoneRect');
if (!zoneRect) {
zoneRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
zoneRect.setAttribute('id', 'couplingZoneRect');
zoneRect.setAttribute('rx', '4');
zoneRect.setAttribute('fill', 'none');
zoneRect.setAttribute('stroke', '#ff6b2b');
zoneRect.setAttribute('stroke-dasharray', '4,3');
couplingZone.insertBefore(zoneRect, couplingZone.firstChild);
}
zoneRect.setAttribute('x', TIP_X - 5);
zoneRect.setAttribute('y', CANE_CY - 40);
zoneRect.setAttribute('width', Math.max(wallX - TIP_X + 10, 5));
zoneRect.setAttribute('height', '80');
zoneRect.setAttribute('opacity', (state.coupling * 0.5).toFixed(3));
zoneRect.setAttribute('stroke-width', (0.5 + state.coupling * 1.5).toFixed(2));
// 耦合区标签
let zoneLabel = couplingZone.querySelector('#couplingZoneLabel');
if (!zoneLabel) {
zoneLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
zoneLabel.setAttribute('id', 'couplingZoneLabel');
zoneLabel.setAttribute('text-anchor', 'middle');
zoneLabel.setAttribute('font-family', 'IBM Plex Mono');
zoneLabel.setAttribute('font-size', '8');
zoneLabel.setAttribute('fill', '#ff6b2b');
couplingZone.appendChild(zoneLabel);
}
zoneLabel.setAttribute('x', ((TIP_X + wallX) / 2).toFixed(2));
zoneLabel.setAttribute('y', (CANE_CY - 48).toFixed(2));
zoneLabel.setAttribute('opacity', (state.coupling * 0.8).toFixed(3));
zoneLabel.textContent = '阻抗耦合区';
}
// ===== 更新波形显示 =====
function updateWaveform() {
const wfX0 = 80, wfX1 = 520;
const wfCY = 505;
const wfAmp = 30;
const points = [];
const glowPoints = [];
const numPoints = 200;
// 阻尼系数随耦合变化
const damping = 1 + state.coupling * 8;
const freqMultiplier = state.freq / 8;
const phaseShift = state.coupling * Math.PI * 0.3;
for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints;
const x = wfX0 + t * (wfX1 - wfX0);
// 阻尼正弦波
const envelope = Math.exp(-damping * t * 2);
const y = wfCY + Math.sin(t * 12 * freqMultiplier + state.time * 3 + phaseShift) * wfAmp * envelope;
points.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`);
glowPoints.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`);
}
const pathD = points.join(' ');
waveformPath.setAttribute('d', pathD);
waveformGlow.setAttribute('d', pathD);
// 波形颜色随耦合变化
if (state.coupling > 0.2) {
waveformPath.setAttribute('stroke', '#ff6b2b');
waveformGlow.setAttribute('stroke', '#ff6b2b');
waveformPath.setAttribute('opacity', '0.9');
} else {
waveformPath.setAttribute('stroke', '#00e5ff');
waveformGlow.setAttribute('stroke', '#00e5ff');
waveformPath.setAttribute('opacity', '0.8');
}
// 波形标签切换
if (state.coupling > 0.2) {
wfLabelFar.setAttribute('opacity', '0');
wfLabelNear.setAttribute('opacity', '1');
} else {
wfLabelFar.setAttribute('opacity', '1');
wfLabelNear.setAttribute('opacity', '0');
}
}
// ===== 更新耦合面板 =====
function updateCouplingPanel() {
const pct = Math.round(state.coupling * 100);
couplingBar.setAttribute('width', (state.coupling * 200).toFixed(1));
couplingPct.textContent = pct + '%';
// 空气隙示意条粒子
const spread = 1 - state.coupling * 0.7;
diagParticles.forEach((el, i) => {
const baseX = 8 + (i / (diagParticles.length - 1)) * 184;
const cx = 100 + (baseX - 100) * spread;
el.setAttribute('cx', cx.toFixed(1));
if (state.coupling > 0.2) {
el.setAttribute('fill', `rgba(255,107,43,${(0.3 + state.coupling * 0.5).toFixed(2)})`);
} else {
el.setAttribute('fill', 'rgba(0,229,255,0.4)');
}
});
}
// ===== 更新手柄反馈 =====
function updateFeedback() {
if (state.coupling > 0.3) {
const pulse = 0.5 + 0.5 * Math.sin(state.time * 8);
const r = 8 + pulse * 5;
feedbackDot.setAttribute('r', r.toFixed(1));
feedbackDot.setAttribute('opacity', (state.coupling * 0.8 * pulse).toFixed(3));
feedbackDot.setAttribute('fill', '#00e676');
} else {
feedbackDot.setAttribute('r', '0');
feedbackDot.setAttribute('opacity', '0');
}
}
// ===== 更新检测状态 =====
function updateStatus() {
state.detected = state.coupling > 0.15;
if (state.coupling > 0.6) {
statusDot.setAttribute('fill', '#00e676');
statusDot.setAttribute('opacity', '1');
statusText.setAttribute('fill', '#00e676');
statusText.textContent = '预警 · 障碍物极近';
} else if (state.coupling > 0.15) {
statusDot.setAttribute('fill', '#ff6b2b');
statusDot.setAttribute('opacity', '0.8');
statusText.setAttribute('fill', '#ff6b2b');
statusText.textContent = '感知 · 阻抗耦合中';
} else {
statusDot.setAttribute('fill', '#5a6578');
statusDot.setAttribute('opacity', '0.4');
statusText.setAttribute('fill', '#5a6578');
statusText.textContent = '待机 · 无障碍物';
}
}
// ===== 激振器脉冲动画 =====
function updateExciterPulse() {
const exciter = document.getElementById('exciter');
const pulse = 0.7 + 0.3 * Math.sin(state.time * state.freq * 0.8);
exciter.setAttribute('opacity', pulse.toFixed(3));
// 耦合时激振器边框变色
if (state.coupling > 0.2) {
exciter.setAttribute('stroke', '#ff6b2b');
} else {
exciter.setAttribute('stroke', '#00e5ff');
}
}
// ===== 加速度计响应动画 =====
function updateAccelResponse() {
const accel = document.getElementById('accelerometer');
if (state.coupling > 0.15) {
const pulse = 0.7 + 0.3 * Math.sin(state.time * 6);
accel.setAttribute('opacity', pulse.toFixed(3));
accel.setAttribute('stroke-width', (1 + state.coupling).toFixed(2));
} else {
accel.setAttribute('opacity', '0.9');
accel.setAttribute('stroke-width', '1');
}
}
// ===== 主动画循环 =====
let lastTime = performance.now();
function animate(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
state.time += dt;
// 计算耦合
state.coupling = calcCoupling();
// 更新所有动态元素
updateWaves(dt);
updateAirParticles();
updateCouplingZone();
updateWaveform();
updateCouplingPanel();
updateFeedback();
updateStatus();
updateExciterPulse();
updateAccelResponse();
updateDistanceAnnotation();
requestAnimationFrame(animate);
}
// ===== 事件绑定 =====
const distSlider = document.getElementById('distSlider');
const distVal = document.getElementById('distVal');
const freqSlider = document.getElementById('freqSlider');
const freqVal = document.getElementById('freqVal');
const matBtns = document.querySelectorAll('.mat-btn');
distSlider.addEventListener('input', () => {
state.distance = parseInt(distSlider.value);
distVal.textContent = state.distance + ' cm';
drawWall();
});
freqSlider.addEventListener('input', () => {
state.freq = parseFloat(freqSlider.value);
freqVal.textContent = state.freq + ' kHz';
});
matBtns.forEach(btn => {
btn.addEventListener('click', () => {
matBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.material = btn.dataset.mat;
drawWall();
});
});
// ===== 启动 =====
drawWall();
requestAnimationFrame(animate);
</script>
</body>
</html>
实现说明
这个高保真 SVG 原理动画围绕 TRIZ 最终理想解 (IFR) 思想设计,核心表达:障碍物本身即传感器——无需回波,阻抗耦合即感知。
视觉架构
- 盲杖剖视图:横向展示内部结构——压电陶瓷激振器 (PZT)、硬质合金杖尖、高灵敏度加速度计 (ACC),信号路径用虚线箭头标注
- 双材质墙壁:玻璃(半透明蓝色+反射高光)与吸音棉(深色凹凸纹理),均附带传统超声波失效的红色虚线示意(回波偏离/被吸收)
- 空气隙粒子:随距离缩小而压缩聚集,颜色从冷蓝过渡到暖橙,直观表达阻抗耦合
- 振动波弧:从激振器发出,颜色随耦合强度从青色渐变为橙色
动态反馈层级
- 细节层:激振器脉冲闪烁、加速度计亮度响应、手柄绿色触觉反馈光圈
- 结构层:波形监测面板实时展示阻尼正弦波(耦合越强衰减越快)、耦合强度进度条
- 场景层:整体色调随耦合状态冷暖转换,IFR 标注面板始终可见
交互控制
- 距离滑块 (0-50cm):控制杖尖与障碍物间距,30cm 内触发耦合效应
- 材质切换:玻璃/吸音棉——两种材质产生相同的耦合效果,验证 IFR 的核心论点
- 频率滑块 (1-20kHz):调节激振频率,影响波形显示的频率特征
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
