<!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">轴向长度 ↓ 径向空间 → 利用</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分
等待动画代码生成...
