分享图
A
动画渲染工坊
就绪
<!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),信号路径用虚线箭头标注
  • 双材质墙壁:玻璃(半透明蓝色+反射高光)与吸音棉(深色凹凸纹理),均附带传统超声波失效的红色虚线示意(回波偏离/被吸收)
  • 空气隙粒子:随距离缩小而压缩聚集,颜色从冷蓝过渡到暖橙,直观表达阻抗耦合
  • 振动波弧:从激振器发出,颜色随耦合强度从青色渐变为橙色

动态反馈层级

  1. 细节层:激振器脉冲闪烁、加速度计亮度响应、手柄绿色触觉反馈光圈
  2. 结构层:波形监测面板实时展示阻尼正弦波(耦合越强衰减越快)、耦合强度进度条
  3. 场景层:整体色调随耦合状态冷暖转换,IFR 标注面板始终可见

交互控制

  • 距离滑块 (0-50cm):控制杖尖与障碍物间距,30cm 内触发耦合效应
  • 材质切换:玻璃/吸音棉——两种材质产生相同的耦合效果,验证 IFR 的核心论点
  • 频率滑块 (1-20kHz):调节激振频率,影响波形显示的频率特征
积分规则:第一轮对话扣减6分,后续每轮扣4分