分享图
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=IBM+Plex+Mono:wght@300;400;500;600&family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #060a12;
    --bg2: #0b1120;
    --grid: #0f1a2e;
    --outer: #e8943a;
    --outer-glow: rgba(232,148,58,0.25);
    --inner: #2dd4bf;
    --inner-glow: rgba(45,212,191,0.25);
    --gap: #ff4757;
    --gap-glow: rgba(255,71,87,0.18);
    --housing: #334155;
    --housing-fill: #0f172a;
    --plate: #94a3b8;
    --text: #cbd5e1;
    --text-dim: #475569;
    --accent: #fbbf24;
    --path-line: #fbbf24;
  }
  *{margin:0;padding:0;box-sizing:border-box;}
  body{
    background:var(--bg);
    color:var(--text);
    font-family:'IBM Plex Mono',monospace;
    min-height:100vh;
    display:flex;
    flex-direction:column;
    align-items:center;
    overflow:hidden;
  }
  /* 细微噪点纹理 */
  body::before{
    content:'';position:fixed;inset:0;
    background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
    pointer-events:none;z-index:9999;
  }
  header{
    width:100%;padding:18px 32px 10px;
    display:flex;align-items:baseline;gap:18px;
    border-bottom:1px solid var(--grid);
    background:linear-gradient(180deg,rgba(6,10,18,0.95),rgba(6,10,18,0.7));
    backdrop-filter:blur(8px);
    position:relative;z-index:10;
  }
  header h1{
    font-family:'Cormorant Garamond',serif;
    font-weight:700;font-size:22px;letter-spacing:0.04em;
    color:#f1f5f9;
  }
  header .tag{
    font-size:10px;font-weight:500;
    padding:3px 10px;border-radius:20px;
    background:rgba(232,148,58,0.12);color:var(--outer);
    border:1px solid rgba(232,148,58,0.25);
    letter-spacing:0.08em;text-transform:uppercase;
  }
  header .subtitle{
    font-size:12px;color:var(--text-dim);font-weight:300;
  }
  .main-wrap{
    flex:1;display:flex;align-items:center;justify-content:center;
    width:100%;padding:8px 16px;
    min-height:0;
  }
  .svg-container{
    width:100%;max-width:1300px;
    aspect-ratio:3/2;
    max-height:calc(100vh - 160px);
  }
  .svg-container svg{
    width:100%;height:100%;display:block;
  }
  /* 控制面板 */
  .controls{
    width:100%;max-width:900px;
    padding:12px 24px 16px;
    display:flex;align-items:center;gap:28px;
    flex-wrap:wrap;justify-content:center;
    background:linear-gradient(0deg,rgba(6,10,18,0.98),rgba(6,10,18,0.8));
    border-top:1px solid var(--grid);
    position:relative;z-index:10;
  }
  .ctrl-group{
    display:flex;align-items:center;gap:8px;
  }
  .ctrl-group label{
    font-size:11px;color:var(--text-dim);font-weight:400;
    white-space:nowrap;min-width:60px;text-align:right;
  }
  .ctrl-group input[type=range]{
    -webkit-appearance:none;appearance:none;
    width:120px;height:4px;border-radius:2px;
    background:var(--grid);outline:none;cursor:pointer;
  }
  .ctrl-group input[type=range]::-webkit-slider-thumb{
    -webkit-appearance:none;appearance:none;
    width:14px;height:14px;border-radius:50%;
    background:var(--outer);border:2px solid var(--bg);
    box-shadow:0 0 8px var(--outer-glow);
    cursor:pointer;
  }
  .ctrl-group .val{
    font-size:11px;color:var(--outer);font-weight:500;
    min-width:36px;
  }
  .btn-toggle{
    background:none;border:1px solid var(--text-dim);
    color:var(--text);padding:5px 14px;border-radius:4px;
    font-family:inherit;font-size:11px;cursor:pointer;
    transition:all 0.2s;
  }
  .btn-toggle:hover{border-color:var(--outer);color:var(--outer);}
  .btn-toggle.active{background:rgba(232,148,58,0.15);border-color:var(--outer);color:var(--outer);}
  .btn-play{
    width:32px;height:32px;border-radius:50%;
    background:none;border:1.5px solid var(--text-dim);
    color:var(--text);cursor:pointer;display:flex;
    align-items:center;justify-content:center;
    font-size:14px;transition:all 0.2s;
  }
  .btn-play:hover{border-color:var(--inner);color:var(--inner);}
  /* SVG 文字样式 */
  .label-title{font-family:'Cormorant Garamond',serif;font-weight:600;}
  .label-mono{font-family:'IBM Plex Mono',monospace;}
  .label-dim{fill:var(--text-dim);font-family:'IBM Plex Mono',monospace;}
  /* 动画 */
  @keyframes pulse-gap{
    0%,100%{opacity:0.3;}
    50%{opacity:0.7;}
  }
  @keyframes dash-flow{
    to{stroke-dashoffset:-20;}
  }
  @media(prefers-reduced-motion:reduce){
    *{animation:none!important;transition:none!important;}
  }
</style>
</head>
<body>

<header>
  <h1>同轴嵌套减震器</h1>
  <span class="tag">IFR 理想解</span>
  <span class="subtitle">空间维度转换 · 路径折叠原理</span>
</header>

<div class="main-wrap">
  <div class="svg-container">
    <svg id="mainSvg" viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <!-- 网格背景 -->
        <pattern id="gridP" width="40" height="40" patternUnits="userSpaceOnUse">
          <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#0e1726" stroke-width="0.5"/>
        </pattern>
        <!-- 琥珀色辉光 -->
        <filter id="glowA" x="-30%" y="-30%" width="160%" height="160%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <!-- 青色辉光 -->
        <filter id="glowT" x="-30%" y="-30%" width="160%" height="160%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <!-- 红色辉光 -->
        <filter id="glowR" x="-30%" y="-30%" width="160%" height="160%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <!-- 箭头标记 -->
        <marker id="arrowA" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
          <path d="M0,0 L8,3 L0,6" fill="var(--outer)" />
        </marker>
        <marker id="arrowT" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
          <path d="M0,0 L8,3 L0,6" fill="var(--inner)" />
        </marker>
        <marker id="arrowY" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
          <path d="M0,0 L8,3 L0,6" fill="var(--accent)" />
        </marker>
      </defs>

      <!-- 背景网格 -->
      <rect width="1200" height="800" fill="var(--bg)"/>
      <rect width="1200" height="800" fill="url(#gridP)" opacity="0.6"/>

      <!-- ===== 左侧:截面动画区域 ===== -->
      <g id="crossSection">
        <!-- 外壳 -->
        <rect x="195" y="75" width="360" height="660" rx="6"
              fill="var(--housing-fill)" stroke="var(--housing)" stroke-width="2.5"/>
        <!-- 外壳内壁细节线 -->
        <line x1="205" y1="85" x2="205" y2="725" stroke="#1e293b" stroke-width="1"/>
        <line x1="545" y1="85" x2="545" y2="725" stroke="#1e293b" stroke-width="1"/>

        <!-- 外层减震体背景(半透明) -->
        <rect id="outerBgLeft" x="210" y="135" width="70" height="280" rx="3"
              fill="rgba(232,148,58,0.06)" stroke="rgba(232,148,58,0.15)" stroke-width="0.8"/>
        <rect id="outerBgRight" x="470" y="135" width="70" height="280" rx="3"
              fill="rgba(232,148,58,0.06)" stroke="rgba(232,148,58,0.15)" stroke-width="0.8"/>

        <!-- 内层减震体背景 -->
        <rect id="innerBg" x="325" y="435" width="110" height="230" rx="3"
              fill="rgba(45,212,191,0.06)" stroke="rgba(45,212,191,0.15)" stroke-width="0.8"/>

        <!-- 径向间隙高亮区 -->
        <g id="gapHighlight" style="animation:pulse-gap 2s ease-in-out infinite">
          <rect x="280" y="135" width="45" height="280" rx="2"
                fill="var(--gap-glow)" stroke="var(--gap)" stroke-width="0.6" stroke-dasharray="4 3"/>
          <rect x="425" y="135" width="45" height="280" rx="2"
                fill="var(--gap-glow)" stroke="var(--gap)" stroke-width="0.6" stroke-dasharray="4 3"/>
        </g>

        <!-- 顶板 -->
        <rect id="topPlate" x="210" y="110" width="330" height="22" rx="3"
              fill="#475569" stroke="#64748b" stroke-width="1.5"/>
        <!-- 顶板纹理 -->
        <line id="topPlateLine1" x1="220" y1="118" x2="530" y2="118" stroke="#5a6a80" stroke-width="0.5"/>
        <line id="topPlateLine2" x1="220" y1="124" x2="530" y2="124" stroke="#5a6a80" stroke-width="0.5"/>

        <!-- 外层弹簧(左) -->
        <path id="springOL" fill="none" stroke="var(--outer)" stroke-width="2.8" stroke-linecap="round" filter="url(#glowA)"/>
        <!-- 外层弹簧(右) -->
        <path id="springOR" fill="none" stroke="var(--outer)" stroke-width="2.8" stroke-linecap="round" filter="url(#glowA)"/>

        <!-- 中间板 -->
        <rect id="midPlate" x="290" y="410" width="190" height="22" rx="3"
              fill="#475569" stroke="#64748b" stroke-width="1.5"/>
        <line id="midPlateLine" x1="300" y1="421" x2="470" y2="421" stroke="#5a6a80" stroke-width="0.5"/>

        <!-- 内层弹簧 -->
        <path id="springI" fill="none" stroke="var(--inner)" stroke-width="2.8" stroke-linecap="round" filter="url(#glowT)"/>

        <!-- 底板 -->
        <rect id="bottomPlate" x="320" y="660" width="140" height="22" rx="3"
              fill="#475569" stroke="#64748b" stroke-width="1.5"/>
        <line id="bottomPlateLine" x1="328" y1="671" x2="452" y2="671" stroke="#5a6a80" stroke-width="0.5"/>

        <!-- 粒子层 -->
        <g id="particles"></g>

        <!-- 振动输入箭头 -->
        <g id="inputArrows">
          <line x1="375" y1="60" x2="375" y2="100" stroke="var(--outer)" stroke-width="2" marker-end="url(#arrowA)"/>
          <line x1="310" y1="55" x2="310" y2="100" stroke="var(--outer)" stroke-width="1.5" marker-end="url(#arrowA)" opacity="0.6"/>
          <line x1="440" y1="55" x2="440" y2="100" stroke="var(--outer)" stroke-width="1.5" marker-end="url(#arrowA)" opacity="0.6"/>
          <text x="375" y="48" text-anchor="middle" class="label-mono" fill="var(--outer)" font-size="11" font-weight="500">振动输入</text>
        </g>

        <!-- 衰减输出箭头 -->
        <g id="outputArrows">
          <line x1="390" y1="695" x2="390" y2="740" stroke="var(--inner)" stroke-width="1.5" marker-end="url(#arrowT)"/>
          <text x="390" y="756" text-anchor="middle" class="label-mono" fill="var(--inner)" font-size="11" font-weight="500">衰减输出</text>
        </g>

        <!-- 标注:外层 -->
        <g transform="translate(130, 240)">
          <line x1="70" y1="0" x2="85" y2="0" stroke="var(--outer)" stroke-width="1"/>
          <text x="0" y="4" class="label-mono" fill="var(--outer)" font-size="10" font-weight="400">外层环形</text>
          <text x="0" y="18" class="label-mono" fill="var(--outer)" font-size="10" font-weight="400">低刚度</text>
        </g>

        <!-- 标注:内层 -->
        <g transform="translate(130, 530)">
          <line x1="70" y1="0" x2="195" y2="0" stroke="var(--inner)" stroke-width="1" stroke-dasharray="3 2"/>
          <text x="0" y="4" class="label-mono" fill="var(--inner)" font-size="10" font-weight="400">内层柱状</text>
          <text x="0" y="18" class="label-mono" fill="var(--inner)" font-size="10" font-weight="400">高刚度</text>
        </g>

        <!-- 标注:径向间隙 -->
        <g id="gapAnnotation">
          <line x1="303" y1="165" x2="303" y2="200" stroke="var(--gap)" stroke-width="0.8"/>
          <line x1="348" y1="165" x2="348" y2="200" stroke="var(--gap)" stroke-width="0.8"/>
          <line x1="303" y1="180" x2="348" y2="180" stroke="var(--gap)" stroke-width="0.8"/>
          <text x="326" y="215" text-anchor="middle" class="label-mono" fill="var(--gap)" font-size="9">间隙 2-3mm</text>
        </g>
      </g>

      <!-- ===== 振动路径折叠示意 ===== -->
      <g id="foldedPath" opacity="0">
        <path id="foldPathLine" d="" fill="none" stroke="var(--accent)" stroke-width="2"
              stroke-dasharray="6 4" style="animation:dash-flow 0.8s linear infinite"/>
        <text x="375" y="790" text-anchor="middle" class="label-mono" fill="var(--accent)" font-size="10">
          路径折叠:轴向 → 径向 → 轴向
        </text>
      </g>

      <!-- ===== 右侧面板 ===== -->
      <!-- 俯视图 -->
      <g id="topView">
        <text x="940" y="70" text-anchor="middle" class="label-title" fill="#94a3b8" font-size="14">俯视截面</text>
        <!-- 外壳圈 -->
        <circle cx="940" cy="200" r="115" fill="none" stroke="var(--housing)" stroke-width="2.5"/>
        <circle cx="940" cy="200" r="112" fill="var(--housing-fill)" stroke="none"/>
        <!-- 外层阻尼环 -->
        <circle cx="940" cy="200" r="95" fill="none" stroke="var(--outer)" stroke-width="12" opacity="0.7" filter="url(#glowA)"/>
        <!-- 间隙环 -->
        <circle cx="940" cy="200" r="78" fill="none" stroke="var(--gap)" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.6">
          <animate attributeName="opacity" values="0.3;0.7;0.3" dur="2s" repeatCount="indefinite"/>
        </circle>
        <!-- 内层阻尼柱 -->
        <circle cx="940" cy="200" r="60" fill="rgba(45,212,191,0.12)" stroke="var(--inner)" stroke-width="8" opacity="0.8" filter="url(#glowT)"/>
        <!-- 中心轴 -->
        <circle cx="940" cy="200" r="12" fill="#334155" stroke="#64748b" stroke-width="1.5"/>
        <!-- 标注 -->
        <line x1="940" y1="88" x2="940" y2="98" stroke="var(--outer)" stroke-width="1"/>
        <text x="940" y="84" text-anchor="middle" class="label-dim" font-size="9">外壳</text>
        <text x="940" y="308" text-anchor="middle" class="label-mono" fill="var(--outer)" font-size="9">外层环形</text>
        <text x="940" y="320" text-anchor="middle" class="label-mono" fill="var(--gap)" font-size="8">↑ 间隙</text>
        <text x="940" y="335" text-anchor="middle" class="label-mono" fill="var(--inner)" font-size="9">内层柱状</text>
        <!-- 辐射线装饰 -->
        <g stroke="#1e293b" stroke-width="0.5">
          <line x1="940" y1="140" x2="940" y2="88"/>
          <line x1="940" y1="260" x2="940" y2="312"/>
          <line x1="880" y1="200" x2="828" y2="200"/>
          <line x1="1000" y1="200" x2="1052" y2="200"/>
        </g>
      </g>

      <!-- 波形图 -->
      <g id="waveformArea">
        <text x="940" y="385" text-anchor="middle" class="label-title" fill="#94a3b8" font-size="14">实时振动波形</text>
        <!-- 波形背景 -->
        <rect x="775" y="400" width="340" height="140" rx="4"
              fill="rgba(15,23,42,0.6)" stroke="#1e293b" stroke-width="1"/>
        <!-- 零线 -->
        <line x1="780" y1="470" x2="1110" y2="470" stroke="#1e293b" stroke-width="0.8"/>
        <!-- 输入波形 -->
        <polyline id="waveInput" fill="none" stroke="var(--outer)" stroke-width="1.8" opacity="0.85"/>
        <!-- 输出波形 -->
        <polyline id="waveOutput" fill="none" stroke="var(--inner)" stroke-width="1.8" opacity="0.85"/>
        <!-- 标签 -->
        <text x="785" y="418" class="label-mono" fill="var(--outer)" font-size="9">输入</text>
        <text x="785" y="492" class="label-mono" fill="var(--inner)" font-size="9">输出</text>
      </g>

      <!-- 能量衰减柱状图 -->
      <g id="energyBars">
        <text x="940" y="575" text-anchor="middle" class="label-title" fill="#94a3b8" font-size="14">逐级能量衰减</text>
        <!-- 输入能量 -->
        <rect x="830" y="595" width="40" height="0" rx="2" fill="var(--outer)" opacity="0.7" id="bar1"/>
        <text x="850" y="612" text-anchor="middle" class="label-mono" fill="var(--text-dim)" font-size="8" id="barLabel1">100%</text>
        <!-- 外层吸收后 -->
        <rect x="900" y="595" width="40" height="0" rx="2" fill="#c07a2e" opacity="0.6" id="bar2"/>
        <text x="920" y="612" text-anchor="middle" class="label-mono" fill="var(--text-dim)" font-size="8" id="barLabel2">35%</text>
        <!-- 内层吸收后 -->
        <rect x="970" y="595" width="40" height="0" rx="2" fill="var(--inner)" opacity="0.6" id="bar3"/>
        <text x="990" y="612" text-anchor="middle" class="label-mono" fill="var(--text-dim)" font-size="8" id="barLabel3">8%</text>
        <!-- 箭头 -->
        <line x1="875" y1="600" x2="895" y2="600" stroke="var(--text-dim)" stroke-width="1" marker-end="url(#arrowY)"/>
        <line x1="945" y1="600" x2="965" y2="600" stroke="var(--text-dim)" stroke-width="1" marker-end="url(#arrowY)"/>
        <!-- 底部标签 -->
        <text x="850" y="648" text-anchor="middle" class="label-dim" font-size="8">原始</text>
        <text x="920" y="648" text-anchor="middle" class="label-dim" font-size="8">外层后</text>
        <text x="990" y="648" text-anchor="middle" class="label-dim" font-size="8">内层后</text>
      </g>

      <!-- IFR 核心理念文字 -->
      <g id="ifrText">
        <text x="940" y="695" text-anchor="middle" class="label-title" fill="#e2e8f0" font-size="15">最终理想解 (IFR)</text>
        <text x="940" y="715" text-anchor="middle" class="label-mono" fill="var(--accent)" font-size="10">轴向长度 ↓  &nbsp;  径向空间 → 利用</text>
        <text x="940" y="733" text-anchor="middle" class="label-mono" fill="var(--text-dim)" font-size="9">矛盾消解:不增长轴向,完成多级堆叠</text>
        <text x="940" y="750" text-anchor="middle" class="label-mono" fill="var(--text-dim)" font-size="9">代价:径向尺寸略增 · 装配复杂度上升</text>
      </g>

      <!-- 动态数值显示 -->
      <g id="liveData">
        <text x="620" y="100" class="label-mono" fill="var(--text-dim)" font-size="9">轴向位移</text>
        <text id="dispInput" x="700" y="100" class="label-mono" fill="var(--outer)" font-size="11" font-weight="500">0.00mm</text>
        <text x="620" y="118" class="label-mono" fill="var(--text-dim)" font-size="9">中间位移</text>
        <text id="dispMid" x="700" y="118" class="label-mono" fill="#c07a2e" font-size="11" font-weight="500">0.00mm</text>
        <text x="620" y="136" class="label-mono" fill="var(--text-dim)" font-size="9">输出位移</text>
        <text id="dispOutput" x="700" y="136" class="label-mono" fill="var(--inner)" font-size="11" font-weight="500">0.00mm</text>
        <text x="620" y="160" class="label-mono" fill="var(--text-dim)" font-size="9">衰减率</text>
        <text id="attenuation" x="700" y="160" class="label-mono" fill="var(--accent)" font-size="12" font-weight="600">92%</text>
      </g>
    </svg>
  </div>
</div>

<div class="controls">
  <button class="btn-play" id="btnPlay" title="播放/暂停">▶</button>
  <div class="ctrl-group">
    <label>振幅</label>
    <input type="range" id="sliderAmp" min="5" max="40" value="22" step="1"/>
    <span class="val" id="valAmp">22</span>
  </div>
  <div class="ctrl-group">
    <label>频率</label>
    <input type="range" id="sliderFreq" min="0.3" max="3" value="1.2" step="0.1"/>
    <span class="val" id="valFreq">1.2Hz</span>
  </div>
  <div class="ctrl-group">
    <label>外/内刚度比</label>
    <input type="range" id="sliderStiff" min="0.1" max="0.6" value="0.3" step="0.05"/>
    <span class="val" id="valStiff">0.30</span>
  </div>
  <button class="btn-toggle" id="btnPath">路径折叠</button>
  <button class="btn-toggle active" id="btnParticle">能量粒子</button>
</div>

<script>
(function(){
  /* ===== 常量与配置 ===== */
  const SVG_NS = 'http://www.w3.org/2000/svg';
  const DAMPER_CX = 375;            // 减震器中心X
  const TOP_PLATE_Y0 = 110;         // 顶板静止Y
  const MID_PLATE_Y0 = 410;         // 中板静止Y
  const BOTTOM_PLATE_Y0 = 660;      // 底板静止Y
  const SPRING_OL_X = 245;          // 外层左弹簧X
  const SPRING_OR_X = 505;          // 外层右弹簧X
  const SPRING_I_X = 375;           // 内层弹簧X
  const OUTER_COILS = 7;            // 外层弹簧圈数
  const INNER_COILS = 6;            // 内层弹簧圈数
  const OUTER_AMP = 18;             // 外层弹簧横向振幅
  const INNER_AMP = 16;             // 内层弹簧横向振幅
  const WAVE_LEN = 200;             // 波形采样点数
  const PARTICLE_POOL_SIZE = 60;    // 粒子池大小

  /* ===== 状态 ===== */
  let playing = true;
  let showPath = false;
  let showParticles = true;
  let amplitude = 22;
  let frequency = 1.2;
  let stiffRatio = 0.3;             // 外层/内层刚度比(越小外层越软)
  let time = 0;
  let inputWaveData = new Array(WAVE_LEN).fill(0);
  let outputWaveData = new Array(WAVE_LEN).fill(0);
  let particles = [];

  /* ===== DOM 引用 ===== */
  const svg = document.getElementById('mainSvg');
  const springOL = document.getElementById('springOL');
  const springOR = document.getElementById('springOR');
  const springI = document.getElementById('springI');
  const topPlate = document.getElementById('topPlate');
  const midPlate = document.getElementById('midPlate');
  const bottomPlate = document.getElementById('bottomPlate');
  const topPlateLine1 = document.getElementById('topPlateLine1');
  const topPlateLine2 = document.getElementById('topPlateLine2');
  const midPlateLine = document.getElementById('midPlateLine');
  const bottomPlateLine = document.getElementById('bottomPlateLine');
  const outerBgLeft = document.getElementById('outerBgLeft');
  const outerBgRight = document.getElementById('outerBgRight');
  const innerBg = document.getElementById('innerBg');
  const waveInputEl = document.getElementById('waveInput');
  const waveOutputEl = document.getElementById('waveOutput');
  const dispInputEl = document.getElementById('dispInput');
  const dispMidEl = document.getElementById('dispMid');
  const dispOutputEl = document.getElementById('dispOutput');
  const attenuationEl = document.getElementById('attenuation');
  const foldedPathG = document.getElementById('foldedPath');
  const foldPathLine = document.getElementById('foldPathLine');
  const particleGroup = document.getElementById('particles');
  const bar1 = document.getElementById('bar1');
  const bar2 = document.getElementById('bar2');
  const bar3 = document.getElementById('bar3');

  /* ===== 控件绑定 ===== */
  const btnPlay = document.getElementById('btnPlay');
  const btnPath = document.getElementById('btnPath');
  const btnParticle = document.getElementById('btnParticle');
  const sliderAmp = document.getElementById('sliderAmp');
  const sliderFreq = document.getElementById('sliderFreq');
  const sliderStiff = document.getElementById('sliderStiff');

  btnPlay.addEventListener('click', () => {
    playing = !playing;
    btnPlay.textContent = playing ? '⏸' : '▶';
  });
  btnPath.addEventListener('click', () => {
    showPath = !showPath;
    btnPath.classList.toggle('active', showPath);
    foldedPathG.setAttribute('opacity', showPath ? '1' : '0');
  });
  btnParticle.addEventListener('click', () => {
    showParticles = !showParticles;
    btnParticle.classList.toggle('active', showParticles);
  });
  sliderAmp.addEventListener('input', (e) => {
    amplitude = parseFloat(e.target.value);
    document.getElementById('valAmp').textContent = amplitude;
  });
  sliderFreq.addEventListener('input', (e) => {
    frequency = parseFloat(e.target.value);
    document.getElementById('valFreq').textContent = frequency.toFixed(1) + 'Hz';
  });
  sliderStiff.addEventListener('input', (e) => {
    stiffRatio = parseFloat(e.target.value);
    document.getElementById('valStiff').textContent = stiffRatio.toFixed(2);
  });

  /* ===== 弹簧路径生成 ===== */
  function springPath(startY, endY, cx, amp, coils) {
    const minLen = 20;
    const actualEndY = Math.max(startY + minLen, endY);
    const steps = coils * 24;
    let d = '';
    for (let i = 0; i <= steps; i++) {
      const t = i / steps;
      const y = startY + (actualEndY - startY) * t;
      // 两端收束
      const envelope = Math.sin(t * Math.PI);
      const xOff = amp * Math.sin(t * coils * 2 * Math.PI) * Math.min(envelope * 1.2, 1);
      const x = cx + xOff;
      d += (i === 0 ? 'M' : 'L') + ` ${x.toFixed(1)},${y.toFixed(1)}`;
    }
    return d;
  }

  /* ===== 粒子系统 ===== */
  function createParticlePool() {
    for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
      const c = document.createElementNS(SVG_NS, 'circle');
      c.setAttribute('r', '0');
      c.setAttribute('fill', '#fff');
      c.setAttribute('opacity', '0');
      particleGroup.appendChild(c);
      particles.push({
        el: c, active: false,
        x: 0, y: 0, vx: 0, vy: 0,
        life: 0, maxLife: 1,
        color: '#fff', radius: 1
      });
    }
  }

  function emitParticle(x, y, color) {
    if (!showParticles) return;
    for (let p of particles) {
      if (!p.active) {
        p.active = true;
        p.x = x;
        p.y = y;
        p.vx = (Math.random() - 0.5) * 3;
        p.vy = (Math.random() - 0.5) * 3;
        p.life = 1;
        p.maxLife = 0.6 + Math.random() * 0.6;
        p.color = color;
        p.radius = 1.2 + Math.random() * 2;
        return;
      }
    }
  }

  function updateParticles(dt) {
    for (let p of particles) {
      if (!p.active) continue;
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.05; // 微重力
      p.life -= dt / p.maxLife;
      if (p.life <= 0) {
        p.active = false;
        p.el.setAttribute('opacity', '0');
        continue;
      }
      p.el.setAttribute('cx', p.x.toFixed(1));
      p.el.setAttribute('cy', p.y.toFixed(1));
      p.el.setAttribute('r', (p.radius * p.life).toFixed(1));
      p.el.setAttribute('fill', p.color);
      p.el.setAttribute('opacity', (p.life * 0.7).toFixed(2));
    }
  }

  /* ===== 波形更新 ===== */
  function updateWaveforms(inputVal, outputVal) {
    inputWaveData.push(inputVal);
    inputWaveData.shift();
    outputWaveData.push(outputVal);
    outputWaveData.shift();

    let inPts = '';
    let outPts = '';
    const startX = 785;
    const stepX = 320 / WAVE_LEN;
    const centerY = 470;
    const scaleIn = 55;
    const scaleOut = 55;

    for (let i = 0; i < WAVE_LEN; i++) {
      const x = startX + i * stepX;
      inPts += `${x.toFixed(1)},${(centerY - inputWaveData[i] * scaleIn).toFixed(1)} `;
      outPts += `${x.toFixed(1)},${(centerY - outputWaveData[i] * scaleOut).toFixed(1)} `;
    }
    waveInputEl.setAttribute('points', inPts);
    waveOutputEl.setAttribute('points', outPts);
  }

  /* ===== 能量柱状图更新 ===== */
  function updateEnergyBars(inputAbs, midAbs, outputAbs) {
    const maxH = 50;
    const maxVal = Math.max(amplitude, 1);
    const h1 = Math.min(maxH, (inputAbs / maxVal) * maxH);
    const h2 = Math.min(maxH, (midAbs / maxVal) * maxH);
    const h3 = Math.min(maxH, (outputAbs / maxVal) * maxH);

    bar1.setAttribute('height', h1.toFixed(1));
    bar1.setAttribute('y', (640 - h1).toFixed(1));
    bar2.setAttribute('height', h2.toFixed(1));
    bar2.setAttribute('y', (640 - h2).toFixed(1));
    bar3.setAttribute('height', h3.toFixed(1));
    bar3.setAttribute('y', (640 - h3).toFixed(1));

    const pct1 = 100;
    const pct2 = inputAbs > 0.01 ? Math.round((midAbs / inputAbs) * 100) : 0;
    const pct3 = inputAbs > 0.01 ? Math.round((outputAbs / inputAbs) * 100) : 0;
    document.getElementById('barLabel1').textContent = pct1 + '%';
    document.getElementById('barLabel2').textContent = pct2 + '%';
    document.getElementById('barLabel3').textContent = pct3 + '%';
  }

  /* ===== 折叠路径 ===== */
  function updateFoldedPath(topY, midY, bottomY) {
    // 路径:顶板中心 → 下到外层 → 径向内折 → 下到内层 → 底板
    const d = `M ${DAMPER_CX} ${topY + 22} ` +
              `L ${DAMPER_CX} ${midY} ` +
              `L ${SPRING_OL_X} ${midY + 5} ` +
              `L ${SPRING_OL_X} ${topY + 30} ` +
              `L ${SPRING_OL_X - 10} ${topY + 25} ` +
              `M ${SPRING_OL_X} ${midY + 5} ` +
              `L ${SPRING_I_X - 30} ${midY + 11} ` +
              `L ${SPRING_I_X} ${midY + 22} ` +
              `L ${SPRING_I_X} ${bottomY} `;
    foldPathLine.setAttribute('d', d);
  }

  /* ===== 主动画循环 ===== */
  let lastTime = 0;
  let particleTimer = 0;

  function animate(timestamp) {
    const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
    lastTime = timestamp;

    if (playing) {
      time += dt;
    }

    // 计算各板位移
    const omega = 2 * Math.PI * frequency;
    const inputDisp = amplitude * Math.sin(omega * time);
    // 外层吸收后,剩余振动(刚度比决定衰减量)
    const midDisp = inputDisp * stiffRatio * Math.sin(omega * time - 0.25);
    // 内层二次滤波后
    const outputDisp = midDisp * 0.22 * Math.sin(omega * time - 0.45);

    // 各板Y位置
    const topY = TOP_PLATE_Y0 + inputDisp;
    const midY = MID_PLATE_Y0 + midDisp;
    const bottomY = BOTTOM_PLATE_Y0 + outputDisp;

    // 更新板位置
    topPlate.setAttribute('y', topY);
    topPlateLine1.setAttribute('y1', topY + 8);
    topPlateLine1.setAttribute('y2', topY + 8);
    topPlateLine2.setAttribute('y1', topY + 14);
    topPlateLine2.setAttribute('y2', topY + 14);

    midPlate.setAttribute('y', midY);
    midPlateLine.setAttribute('y1', midY + 11);
    midPlateLine.setAttribute('y2', midY + 11);

    bottomPlate.setAttribute('y', bottomY);
    bottomPlateLine.setAttribute('y1', bottomY + 11);
    bottomPlateLine.setAttribute('y2', bottomY + 11);

    // 更新外层背景
    outerBgLeft.setAttribute('y', topY + 22);
    outerBgLeft.setAttribute('height', Math.max(10, midY - topY - 22));
    outerBgRight.setAttribute('y', topY + 22);
    outerBgRight.setAttribute('height', Math.max(10, midY - topY - 22));

    // 更新内层背景
    innerBg.setAttribute('y', midY + 22);
    innerBg.setAttribute('height', Math.max(10, bottomY - midY - 22));

    // 更新弹簧
    const springOLStartY = topY + 22;
    const springOLEndY = midY;
    springOL.setAttribute('d', springPath(springOLStartY, springOLEndY, SPRING_OL_X, OUTER_AMP, OUTER_COILS));
    springOR.setAttribute('d', springPath(springOLStartY, springOLEndY, SPRING_OR_X, OUTER_AMP, OUTER_COILS));

    const springIStartY = midY + 22;
    const springIEndY = bottomY;
    springI.setAttribute('d', springPath(springIStartY, springIEndY, SPRING_I_X, INNER_AMP, INNER_COILS));

    // 更新间隙高亮区
    const gapH = document.getElementById('gapHighlight');
    const gapRects = gapH.querySelectorAll('rect');
    gapRects[0].setAttribute('y', topY + 22);
    gapRects[0].setAttribute('height', Math.max(10, midY - topY - 22));
    gapRects[1].setAttribute('y', topY + 22);
    gapRects[1].setAttribute('height', Math.max(10, midY - topY - 22));

    // 间隙标注
    const gapAnno = document.getElementById('gapAnnotation');
    const gapLines = gapAnno.querySelectorAll('line');
    const gapMidY = (topY + 22 + midY) / 2;
    gapLines[0].setAttribute('y1', gapMidY - 18);
    gapLines[0].setAttribute('y2', gapMidY + 18);
    gapLines[1].setAttribute('y1', gapMidY - 18);
    gapLines[1].setAttribute('y2', gapMidY + 18);
    gapLines[2].setAttribute('y1', gapMidY);
    gapLines[2].setAttribute('y2', gapMidY);
    const gapText = gapAnno.querySelector('text');
    gapText.setAttribute('y', gapMidY + 35);

    // 更新输入箭头位置
    const inputArrows = document.getElementById('inputArrows');
    const iLines = inputArrows.querySelectorAll('line');
    iLines[0].setAttribute('y2', topY);
    iLines[1].setAttribute('y2', topY);
    iLines[2].setAttribute('y2', topY);
    const iText = inputArrows.querySelector('text');
    iText.setAttribute('y', topY - 50);

    // 输出箭头
    const outputArrows = document.getElementById('outputArrows');
    const oLine = outputArrows.querySelector('line');
    oLine.setAttribute('y1', bottomY + 22);
    oLine.setAttribute('y2', bottomY + 55);
    const oText = outputArrows.querySelector('text');
    oText.setAttribute('y', bottomY + 72);

    // 粒子发射
    if (playing && showParticles) {
      particleTimer += dt;
      if (particleTimer > 0.06) {
        particleTimer = 0;
        const compression1 = Math.abs(inputDisp - midDisp);
        const compression2 = Math.abs(midDisp - outputDisp);
        // 外层弹簧处发射粒子(数量与压缩量成正比)
        if (compression1 > 2) {
          emitParticle(SPRING_OL_X + (Math.random()-0.5)*20, (springOLStartY+springOLEndY)/2, '#e8943a');
          emitParticle(SPRING_OR_X + (Math.random()-0.5)*20, (springOLStartY+springOLEndY)/2, '#e8943a');
        }
        if (compression2 > 0.5) {
          emitParticle(SPRING_I_X + (Math.random()-0.5)*16, (springIStartY+springIEndY)/2, '#2dd4bf');
        }
      }
    }
    updateParticles(dt);

    // 波形
    if (playing) {
      updateWaveforms(inputDisp / amplitude, outputDisp / amplitude);
    }

    // 数值显示
    const absIn = Math.abs(inputDisp);
    const absMid = Math.abs(midDisp);
    const absOut = Math.abs(outputDisp);
    dispInputEl.textContent = absIn.toFixed(2) + 'mm';
    dispMidEl.textContent = absMid.toFixed(2) + 'mm';
    dispOutputEl.textContent = absOut.toFixed(2) + 'mm';
    const atten = absIn > 0.1 ? Math.round((1 - absOut / absIn) * 100) : 0;
    attenuationEl.textContent = atten + '%';

    // 能量柱
    updateEnergyBars(absIn, absMid, absOut);

    // 折叠路径
    if (showPath) {
      updateFoldedPath(topY, midY, bottomY);
    }

    // 外层弹簧辉光强度随压缩变化
    const comp1 = Math.min(1, Math.abs(inputDisp) / Math.max(amplitude, 1));
    const comp2 = Math.min(1, Math.abs(midDisp) / Math.max(amplitude * stiffRatio, 0.1));
    springOL.setAttribute('stroke-width', (2.5 + comp1 * 1.5).toFixed(1));
    springOR.setAttribute('stroke-width', (2.5 + comp1 * 1.5).toFixed(1));
    springI.setAttribute('stroke-width', (2.5 + comp2 * 1.5).toFixed(1));

    requestAnimationFrame(animate);
  }

  /* ===== 初始化 ===== */
  createParticlePool();
  lastTime = performance.now();
  requestAnimationFrame(animate);

})();
</script>
</body>
</html>

这份代码实现了一个完整的同轴嵌套减震器 IFR 原理动画,以下是核心设计说明:

视觉架构

  • 左侧为轴向截面动画主体,右侧集成了俯视截面、实时波形和能量衰减柱状图三个辅助面板
  • 深海蓝黑底色搭配琥珀色(外层/低刚度)与青色(内层/高刚度)的双色系统,清晰区分两级减震元件
  • 径向间隙区域以红色脉冲高亮,提示防干涉关键参数

动画机理

  • 顶板随正弦波振荡(振动输入),外层弹簧发生大幅度压缩形变(首级耗能),残余振动传递至中间板
  • 中间板以衰减后的振幅运动,内层弹簧发生微量形变(二级滤波),底板输出近乎静止
  • 能量粒子从弹簧变形区向外发散,直观展示耗能过程;弹簧线宽随压缩量动态变化强化感知

交互控制

  • 振幅滑块:调节输入振动幅度,观察两级衰减的绝对量变化
  • 频率滑块:改变振动频率,体验不同工况下的滤波表现
  • 刚度比滑块:调整外/内级刚度比,直观理解低刚度外层优先吸收大振幅的机理
  • 路径折叠按钮:开启后显示振动传递的"轴向→径向→轴向"折叠路径,突出空间维度转换的核心创新
  • 能量粒子按钮:切换粒子效果,聚焦结构变形或能量流可视化
积分规则:第一轮对话扣减6分,后续每轮扣4分