独立渲染引擎就绪就绪
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMP 热致变刚度机械鸟 · IFR 原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg: #060b18; --fg: #dce4f0; --muted: #5a6a82;
--accent-cold: #00e5ff; --accent-hot: #ff9100;
--card: rgba(12,20,40,0.85); --border: rgba(90,106,130,0.25);
}
body {
background: var(--bg); color: var(--fg);
font-family: 'Fira Code', monospace;
min-height: 100vh; display: flex; flex-direction: column;
align-items: center; justify-content: center;
overflow: hidden;
}
#scene-wrap {
width: 96vw; max-width: 1400px; aspect-ratio: 14/9;
position: relative; border-radius: 16px; overflow: hidden;
box-shadow: 0 0 80px rgba(0,229,255,0.06), 0 0 200px rgba(255,145,0,0.04);
border: 1px solid var(--border);
}
#scene { width:100%; height:100%; display:block; }
/* 控制面板 */
#controls {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 20px;
background: var(--card); backdrop-filter: blur(16px);
border: 1px solid var(--border); border-radius: 12px;
padding: 12px 24px; z-index: 10;
}
#controls label {
font-family: 'Syne', sans-serif; font-size: 13px;
font-weight: 600; color: var(--muted); white-space: nowrap;
}
#tempSlider {
-webkit-appearance: none; width: 200px; height: 6px;
border-radius: 3px; outline: none; cursor: pointer;
background: linear-gradient(to right, var(--accent-cold), var(--accent-hot));
}
#tempSlider::-webkit-slider-thumb {
-webkit-appearance: none; width: 18px; height: 18px;
border-radius: 50%; border: 2px solid var(--fg);
background: var(--bg); cursor: pointer;
box-shadow: 0 0 8px rgba(0,229,255,0.5);
}
#tempValue {
font-size: 14px; font-weight: 500; min-width: 48px;
color: var(--accent-cold); text-align: right;
}
.mode-btn {
font-family: 'Syne', sans-serif; font-size: 12px; font-weight: 700;
padding: 6px 14px; border-radius: 6px; cursor: pointer;
border: 1px solid var(--border); background: transparent;
color: var(--muted); transition: all 0.3s;
text-transform: uppercase; letter-spacing: 0.5px;
}
.mode-btn.active {
background: rgba(0,229,255,0.12); color: var(--accent-cold);
border-color: var(--accent-cold); box-shadow: 0 0 12px rgba(0,229,255,0.15);
}
/* 阶段标签 */
#phaseLabel {
position: absolute; top: 24px; left: 50%; transform: translateX(-50%);
font-family: 'Syne', sans-serif; font-size: 18px; font-weight: 800;
letter-spacing: 1px; text-transform: uppercase;
color: var(--fg); text-align: center;
text-shadow: 0 0 20px rgba(0,229,255,0.3);
transition: color 0.5s, text-shadow 0.5s;
z-index: 10; white-space: nowrap;
}
/* IFR 标注 */
#ifr-badge {
position: absolute; top: 24px; right: 24px;
font-family: 'Syne', sans-serif; font-size: 11px; font-weight: 700;
padding: 6px 12px; border-radius: 6px;
background: rgba(255,145,0,0.1); border: 1px solid rgba(255,145,0,0.3);
color: var(--accent-hot); letter-spacing: 0.8px;
text-transform: uppercase; z-index: 10;
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition-duration: 0s !important; }
}
@media (max-width: 768px) {
#controls { flex-wrap: wrap; justify-content: center; padding: 10px 16px; gap: 10px; }
#tempSlider { width: 140px; }
#phaseLabel { font-size: 14px; }
}
</style>
</head>
<body>
<div id="scene-wrap">
<svg id="scene" viewBox="0 0 1400 900" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 背景渐变 -->
<radialGradient id="bgGrad" cx="50%" cy="60%" r="65%">
<stop offset="0%" stop-color="#0c1528"/>
<stop offset="100%" stop-color="#040810"/>
</radialGradient>
<!-- 热气流渐变 -->
<linearGradient id="thermalGrad" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#ff9100" stop-opacity="0.35"/>
<stop offset="40%" stop-color="#ff6d00" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#ff6d00" stop-opacity="0"/>
</linearGradient>
<!-- 热气流光晕 -->
<radialGradient id="thermalGlow" cx="50%" cy="80%" r="50%">
<stop offset="0%" stop-color="#ff9100" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#ff9100" stop-opacity="0"/>
</radialGradient>
<!-- SMP 冷色 -->
<radialGradient id="smpCold">
<stop offset="0%" stop-color="#00e5ff"/>
<stop offset="100%" stop-color="#0097a7"/>
</radialGradient>
<!-- SMP 热色 -->
<radialGradient id="smpHot">
<stop offset="0%" stop-color="#ffab40"/>
<stop offset="100%" stop-color="#ff6d00"/>
</radialGradient>
<!-- 机翼渐变 -->
<linearGradient id="wingGrad" x1="1" y1="0" x2="0" y2="0">
<stop offset="0%" stop-color="#7a8a9e"/>
<stop offset="100%" stop-color="#5a6a7e"/>
</linearGradient>
<!-- 机身渐变 -->
<linearGradient id="bodyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#b0bcc8"/>
<stop offset="100%" stop-color="#7a8a9e"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feFlood flood-color="#00e5ff" flood-opacity="0.6" result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowAmber" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feFlood flood-color="#ff9100" flood-opacity="0.6" result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- 背景 -->
<rect width="1400" height="900" fill="url(#bgGrad)"/>
<!-- 网格线 -->
<g id="grid" opacity="0.06" stroke="#4a6a8a" stroke-width="0.5">
<line x1="0" y1="150" x2="1400" y2="150"/>
<line x1="0" y1="300" x2="1400" y2="300"/>
<line x1="0" y1="450" x2="1400" y2="450"/>
<line x1="0" y1="600" x2="1400" y2="600"/>
<line x1="0" y1="750" x2="1400" y2="750"/>
<line x1="200" y1="0" x2="200" y2="900"/>
<line x1="400" y1="0" x2="400" y2="900"/>
<line x1="600" y1="0" x2="600" y2="900"/>
<line x1="800" y1="0" x2="800" y2="900"/>
<line x1="1000" y1="0" x2="1000" y2="900"/>
<line x1="1200" y1="0" x2="1200" y2="900"/>
</g>
<!-- 热气流列 -->
<g id="thermalGroup" opacity="0">
<ellipse cx="700" cy="820" rx="220" ry="280" fill="url(#thermalGlow)"/>
<rect x="580" y="500" width="240" height="400" fill="url(#thermalGrad)" rx="120"/>
<!-- 热气流粒子容器 -->
<g id="thermalParticles"></g>
<!-- 上升箭头 -->
<g id="thermalArrows" opacity="0.5">
<path d="M660,750 L660,650 L648,665 M660,650 L672,665" stroke="#ff9100" stroke-width="2" fill="none" opacity="0.6"/>
<path d="M700,780 L700,660 L688,675 M700,660 L712,675" stroke="#ff9100" stroke-width="2.5" fill="none" opacity="0.8"/>
<path d="M740,750 L740,650 L728,665 M740,650 L752,665" stroke="#ff9100" stroke-width="2" fill="none" opacity="0.6"/>
</g>
</g>
<!-- 机械鸟主体 -->
<g id="birdGroup">
<!-- 左翼 -->
<g id="leftWing">
<!-- 内翼段 -->
<path id="leftInner" d="M0,-17 L-195,-9 L-195,9 L0,17 Z" fill="url(#wingGrad)" stroke="#8a9aae" stroke-width="0.5" opacity="0.92"/>
<!-- 翼面纹理线 -->
<line x1="-50" y1="-12" x2="-50" y2="12" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<line x1="-100" y1="-10" x2="-100" y2="10" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<line x1="-150" y1="-9" x2="-150" y2="9" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<!-- SMP 铰链 -->
<circle id="leftSMP" cx="-195" cy="0" r="7" fill="url(#smpCold)" filter="url(#glowCyan)"/>
<circle cx="-195" cy="0" r="3" fill="#0a1020" opacity="0.5"/>
<!-- 外翼段组 -->
<g id="leftOuterWing">
<path id="leftOuter" d="M0,-9 L-175,-3 L-175,3 L0,9 Z" fill="url(#wingGrad)" stroke="#8a9aae" stroke-width="0.5" opacity="0.88"/>
<line x1="-60" y1="-6" x2="-60" y2="6" stroke="#6a7a8e" stroke-width="0.3" opacity="0.3"/>
<line x1="-120" y1="-4" x2="-120" y2="4" stroke="#6a7a8e" stroke-width="0.3" opacity="0.3"/>
<!-- 被动偏流小翼 -->
<path d="M-175,-3 L-192,-16 L-180,0 Z" fill="#8a9aae" stroke="#9aaabe" stroke-width="0.5" opacity="0.8"/>
</g>
</g>
<!-- 右翼(镜像) -->
<g id="rightWing">
<path id="rightInner" d="M0,-17 L-195,-9 L-195,9 L0,17 Z" fill="url(#wingGrad)" stroke="#8a9aae" stroke-width="0.5" opacity="0.92"/>
<line x1="-50" y1="-12" x2="-50" y2="12" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<line x1="-100" y1="-10" x2="-100" y2="10" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<line x1="-150" y1="-9" x2="-150" y2="9" stroke="#6a7a8e" stroke-width="0.4" opacity="0.4"/>
<circle id="rightSMP" cx="-195" cy="0" r="7" fill="url(#smpCold)" filter="url(#glowCyan)"/>
<circle cx="-195" cy="0" r="3" fill="#0a1020" opacity="0.5"/>
<g id="rightOuterWing">
<path id="rightOuter" d="M0,-9 L-175,-3 L-175,3 L0,9 Z" fill="url(#wingGrad)" stroke="#8a9aae" stroke-width="0.5" opacity="0.88"/>
<line x1="-60" y1="-6" x2="-60" y2="6" stroke="#6a7a8e" stroke-width="0.3" opacity="0.3"/>
<line x1="-120" y1="-4" x2="-120" y2="4" stroke="#6a7a8e" stroke-width="0.3" opacity="0.3"/>
<path d="M-175,-3 L-192,-16 L-180,0 Z" fill="#8a9aae" stroke="#9aaabe" stroke-width="0.5" opacity="0.8"/>
</g>
</g>
<!-- 机身 -->
<path id="body" d="M700,310 Q720,350 718,390 Q716,440 700,470 Q684,440 682,390 Q680,350 700,310 Z"
fill="url(#bodyGrad)" stroke="#9aaabe" stroke-width="0.8"/>
<!-- 机身中线 -->
<line x1="700" y1="320" x2="700" y2="460" stroke="#8a9aae" stroke-width="0.5" opacity="0.3"/>
<!-- 头部 -->
<ellipse cx="700" cy="315" rx="12" ry="16" fill="#b0bcc8" stroke="#9aaabe" stroke-width="0.5"/>
<!-- 眼睛 -->
<circle cx="694" cy="310" r="2.5" fill="#0a1020"/>
<circle cx="706" cy="310" r="2.5" fill="#0a1020"/>
<circle cx="694.5" cy="309.5" r="0.8" fill="#4a6a8a"/>
<circle cx="706.5" cy="309.5" r="0.8" fill="#4a6a8a"/>
<!-- 喙 -->
<path d="M697,300 L700,285 L703,300 Z" fill="#d4a056" stroke="#c4903a" stroke-width="0.5"/>
<!-- 尾翼 -->
<path d="M690,465 L675,500 L685,490 L700,505 L715,490 L725,500 L710,465 Z"
fill="#7a8a9e" stroke="#8a9aae" stroke-width="0.5"/>
<!-- 腹部微压传感器阵列 -->
<g id="sensorArray">
<circle class="sensor" cx="692" cy="380" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="708" cy="380" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="692" cy="400" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="708" cy="400" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="700" cy="420" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="694" cy="440" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
<circle class="sensor" cx="706" cy="440" r="3" fill="#0a1828" stroke="#00e5ff" stroke-width="1" opacity="0.6"/>
</g>
</g>
<!-- 标注线与标签 -->
<g id="annotations" font-family="'Syne', sans-serif" font-size="12" font-weight="600">
<!-- SMP 铰链标注 -->
<g id="annoSMP" opacity="0.9">
<line x1="480" y1="390" x2="380" y2="440" stroke="#00e5ff" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>
<rect x="260" y="430" width="120" height="24" rx="4" fill="rgba(0,229,255,0.08)" stroke="rgba(0,229,255,0.3)" stroke-width="0.8"/>
<text x="320" y="447" text-anchor="middle" fill="#00e5ff" font-size="11">SMP 铰链</text>
</g>
<!-- 传感器标注 -->
<g id="annoSensor" opacity="0.9">
<line x1="708" y1="400" x2="800" y2="460" stroke="#00e5ff" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>
<rect x="770" y="450" width="150" height="24" rx="4" fill="rgba(0,229,255,0.08)" stroke="rgba(0,229,255,0.3)" stroke-width="0.8"/>
<text x="845" y="467" text-anchor="middle" fill="#00e5ff" font-size="11">微压传感器阵列</text>
</g>
<!-- 小翼标注 -->
<g id="annoWinglet" opacity="0.9">
<line x1="350" y1="358" x2="280" y2="310" stroke="#9aaabe" stroke-width="1" stroke-dasharray="4,3" opacity="0.4"/>
<rect x="180" y="298" width="100" height="24" rx="4" fill="rgba(154,170,190,0.08)" stroke="rgba(154,170,190,0.25)" stroke-width="0.8"/>
<text x="230" y="315" text-anchor="middle" fill="#8a9aae" font-size="11">偏流小翼</text>
</g>
</g>
<!-- HUD:温度指示 -->
<g id="hudTemp" transform="translate(60, 200)">
<rect x="0" y="0" width="36" height="260" rx="18" fill="rgba(10,20,40,0.7)" stroke="rgba(90,106,130,0.3)" stroke-width="1"/>
<rect id="tempBar" x="4" y="200" width="28" height="56" rx="14" fill="url(#smpCold)" opacity="0.8"/>
<text x="18" y="290" text-anchor="middle" fill="#5a6a82" font-family="'Fira Code', monospace" font-size="10">T</text>
<text id="tempText" x="18" y="170" text-anchor="middle" fill="#00e5ff" font-family="'Fira Code', monospace" font-size="11" font-weight="500">25℃</text>
</g>
<!-- HUD:刚度比 -->
<g id="hudStiffness" transform="translate(60, 520)">
<text x="18" y="0" text-anchor="middle" fill="#5a6a82" font-family="'Syne', sans-serif" font-size="10" font-weight="600">刚度</text>
<text id="stiffText" x="18" y="22" text-anchor="middle" fill="#00e5ff" font-family="'Fira Code', monospace" font-size="13" font-weight="500">100:1</text>
</g>
<!-- HUD:高度 -->
<g id="hudAlt" transform="translate(1304, 200)">
<text x="18" y="0" text-anchor="middle" fill="#5a6a82" font-family="'Syne', sans-serif" font-size="10" font-weight="600">ALT</text>
<text id="altText" x="18" y="24" text-anchor="middle" fill="#8a9aae" font-family="'Fira Code', monospace" font-size="14" font-weight="500">120m</text>
</g>
<!-- HUD:模式 -->
<g id="hudMode" transform="translate(1304, 280)">
<text x="18" y="0" text-anchor="middle" fill="#5a6a82" font-family="'Syne', sans-serif" font-size="10" font-weight="600">MODE</text>
<text id="modeText" x="18" y="24" text-anchor="middle" fill="#00e5ff" font-family="'Fira Code', monospace" font-size="12" font-weight="500">FLAP</text>
</g>
<!-- IFR 核心原理解读 -->
<g id="ifrNote" transform="translate(700, 810)" opacity="0" font-family="'Syne', sans-serif">
<rect x="-280" y="-18" width="560" height="36" rx="8"
fill="rgba(255,145,0,0.06)" stroke="rgba(255,145,0,0.25)" stroke-width="0.8"/>
<text x="0" y="5" text-anchor="middle" fill="#ff9100" font-size="13" font-weight="600">
环境热能 → SMP 刚度转变 → 气动载荷驱动构型切换 · 零主动能耗
</text>
</g>
</svg>
<div id="phaseLabel">扑动飞行模式</div>
<div id="ifr-badge">IFR · 最终理想解</div>
<div id="controls">
<button class="mode-btn active" id="autoBtn" aria-label="自动模式">AUTO</button>
<button class="mode-btn" id="manualBtn" aria-label="手动模式">MANUAL</button>
<label for="tempSlider">环境温度</label>
<input type="range" id="tempSlider" min="20" max="55" value="25" step="0.5" aria-label="温度控制"/>
<span id="tempValue">25℃</span>
</div>
</div>
<script>
(function() {
'use strict';
/* ===== 常量 ===== */
const CYCLE = 16000; // 完整循环时长 ms
const SMP_Tg = 45; // 玻璃化转变温度
const T_MIN = 20, T_MAX = 55; // 温度范围
const SWEEP_MAX = 55; // 最大后掠角(度)
const FLAP_AMP = 8; // 扑动振幅(度)
const FLAP_FREQ = 5; // 扑动频率 Hz
const BIRD_CX = 700, BIRD_CY = 400; // 鸟体中心
/* ===== DOM 引用 ===== */
const svg = document.getElementById('scene');
const leftWing = document.getElementById('leftWing');
const rightWing = document.getElementById('rightWing');
const leftOuterWing = document.getElementById('leftOuterWing');
const rightOuterWing = document.getElementById('rightOuterWing');
const leftSMP = document.getElementById('leftSMP');
const rightSMP = document.getElementById('rightSMP');
const thermalGroup = document.getElementById('thermalGroup');
const thermalParticles = document.getElementById('thermalParticles');
const sensorArray = document.getElementById('sensorArray');
const phaseLabel = document.getElementById('phaseLabel');
const ifrNote = document.getElementById('ifrNote');
const tempSlider = document.getElementById('tempSlider');
const tempValueEl = document.getElementById('tempValue');
const tempBar = document.getElementById('tempBar');
const tempText = document.getElementById('tempText');
const stiffText = document.getElementById('stiffText');
const altText = document.getElementById('altText');
const modeText = document.getElementById('modeText');
const autoBtn = document.getElementById('autoBtn');
const manualBtn = document.getElementById('manualBtn');
const birdGroup = document.getElementById('birdGroup');
/* ===== 状态 ===== */
let autoMode = true;
let manualTemp = 25;
let startTime = null;
let particles = [];
/* ===== 工具函数 ===== */
function lerp(a, b, t) { return a + (b - a) * Math.max(0, Math.min(1, t)); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function smoothstep(e0, e1, x) {
const t = clamp((x - e0) / (e1 - e0), 0, 1);
return t * t * (3 - 2 * t);
}
/* ===== 热气流粒子系统 ===== */
function initParticles() {
for (let i = 0; i < 50; i++) {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('r', String(Math.random() * 3 + 1.5));
c.setAttribute('fill', '#ff9100');
c.setAttribute('opacity', '0');
thermalParticles.appendChild(c);
particles.push({
el: c,
x: 600 + Math.random() * 200,
y: 850 + Math.random() * 100,
vy: -(0.4 + Math.random() * 0.6),
vx: (Math.random() - 0.5) * 0.3,
life: Math.random(),
maxLife: 0.7 + Math.random() * 0.3
});
}
}
function updateParticles(dt, thermalOpacity) {
particles.forEach(p => {
p.life += dt * 0.0008;
if (p.life > p.maxLife) {
p.life = 0;
p.x = 600 + Math.random() * 200;
p.y = 830 + Math.random() * 70;
p.vy = -(0.4 + Math.random() * 0.6);
p.vx = (Math.random() - 0.5) * 0.3;
}
p.x += p.vx + Math.sin(p.life * 8) * 0.15;
p.y += p.vy;
const fade = 1 - (p.life / p.maxLife);
const op = fade * thermalOpacity * 0.6;
p.el.setAttribute('cx', String(p.x));
p.el.setAttribute('cy', String(p.y));
p.el.setAttribute('opacity', String(Math.max(0, op)));
});
}
/* ===== 相位计算 ===== */
// 返回 { temp, sweep, flapActive, thermalIntensity, altitude, phaseName, smpStiffness }
function getPhaseState(cycleProgress) {
const t = cycleProgress; // 0~1
let temp, sweep, flapActive, thermalIntensity, altitude, phaseName, smpStiffness;
if (t < 0.18) {
// 扑动飞行(冷环境)
temp = lerp(22, 28, t / 0.18);
sweep = 0; flapActive = true; thermalIntensity = 0;
altitude = lerp(120, 125, t / 0.18);
phaseName = '扑动飞行模式';
smpStiffness = 1;
} else if (t < 0.30) {
// 进入热气流,传感器检测
const p = (t - 0.18) / 0.12;
temp = lerp(28, 42, p);
sweep = 0; flapActive = true;
thermalIntensity = smoothstep(0, 1, p);
altitude = lerp(125, 135, p);
phaseName = '检测到热上升气流';
smpStiffness = lerp(1, 0.4, p);
} else if (t < 0.45) {
// SMP 软化,翼面开始后掠
const p = (t - 0.30) / 0.15;
temp = lerp(42, 50, p);
sweep = smoothstep(0, 1, p) * SWEEP_MAX;
flapActive = p < 0.5;
thermalIntensity = lerp(0.6, 1, p);
altitude = lerp(135, 180, p);
phaseName = 'SMP 软化 · 构型切换';
smpStiffness = lerp(0.4, 0.02, p);
} else if (t < 0.65) {
// 盘旋滑翔(完全后掠)
const p = (t - 0.45) / 0.20;
temp = lerp(50, 52, Math.sin(p * Math.PI));
sweep = SWEEP_MAX; flapActive = false;
thermalIntensity = 1;
altitude = lerp(180, 350, p);
phaseName = '热气流盘旋滑翔';
smpStiffness = 0.02;
} else if (t < 0.78) {
// 脱离热气流
const p = (t - 0.65) / 0.13;
temp = lerp(52, 38, p);
sweep = SWEEP_MAX; flapActive = false;
thermalIntensity = lerp(1, 0.3, p);
altitude = lerp(350, 340, p);
phaseName = '脱离热气流 · 降温中';
smpStiffness = lerp(0.02, 0.3, p);
} else if (t < 0.92) {
// SMP 硬化,恢复平直翼
const p = (t - 0.78) / 0.14;
temp = lerp(38, 25, p);
sweep = lerp(SWEEP_MAX, 0, smoothstep(0, 1, p));
flapActive = p > 0.6;
thermalIntensity = lerp(0.3, 0, p);
altitude = lerp(340, 130, p);
phaseName = 'SMP 硬化 · 恢复扑动';
smpStiffness = lerp(0.3, 1, p);
} else {
// 恢复扑动
const p = (t - 0.92) / 0.08;
temp = lerp(25, 22, p);
sweep = 0; flapActive = true;
thermalIntensity = 0;
altitude = lerp(130, 120, p);
phaseName = '扑动飞行模式';
smpStiffness = 1;
}
return { temp, sweep, flapActive, thermalIntensity, altitude, phaseName, smpStiffness };
}
/* ===== 从温度直接计算状态(手动模式) ===== */
function getStateFromTemp(temp) {
const softness = smoothstep(SMP_Tg - 5, SMP_Tg + 5, temp);
const sweep = softness * SWEEP_MAX;
const flapActive = softness < 0.5;
const thermalIntensity = smoothstep(28, 42, temp);
const smpStiffness = lerp(1, 0.02, softness);
let phaseName;
if (softness < 0.1) phaseName = '扑动飞行模式';
else if (softness < 0.5) phaseName = 'SMP 软化 · 构型切换';
else if (softness < 0.9) phaseName = '热气流盘旋滑翔';
else phaseName = '热气流盘旋滑翔';
const altitude = lerp(120, 350, softness);
return { temp, sweep, flapActive, thermalIntensity, altitude, phaseName, smpStiffness };
}
/* ===== 颜色插值 ===== */
function smpColor(stiffness) {
// stiffness 1=cold(cyan), 0=hot(amber)
const t = 1 - stiffness;
const r = Math.round(lerp(0, 255, t));
const g = Math.round(lerp(229, 145, t));
const b = Math.round(lerp(255, 0, t));
return `rgb(${r},${g},${b})`;
}
function smpFilter(stiffness) {
return stiffness < 0.5 ? 'url(#glowAmber)' : 'url(#glowCyan)';
}
/* ===== 传感器脉冲 ===== */
function updateSensors(time, thermalIntensity) {
const sensors = sensorArray.querySelectorAll('.sensor');
sensors.forEach((s, i) => {
const pulse = thermalIntensity > 0.1
? 0.5 + 0.5 * Math.sin(time * 0.006 + i * 1.2)
: 0.3;
const op = lerp(0.3, 1, pulse * thermalIntensity);
s.setAttribute('opacity', String(op));
s.setAttribute('stroke', thermalIntensity > 0.3 ? '#ff9100' : '#00e5ff');
s.setAttribute('stroke-width', String(lerp(0.8, 2, pulse * thermalIntensity)));
});
}
/* ===== 渲染 ===== */
function render(time, state) {
const { temp, sweep, flapActive, thermalIntensity, altitude, phaseName, smpStiffness } = state;
// 鸟体位置(高度变化时微微上下浮动)
const birdY = BIRD_CY - (altitude - 120) * 0.15;
const bankAngle = thermalIntensity > 0.8 ? Math.sin(time * 0.002) * 5 : 0;
birdGroup.setAttribute('transform', `translate(0, ${BIRD_CY - birdY}) rotate(${bankAngle}, ${BIRD_CX}, ${birdY})`);
// 扑动角度
const flapAngle = flapActive ? Math.sin(time * 0.001 * FLAP_FREQ * Math.PI * 2) * FLAP_AMP : 0;
// 左翼
const lwx = BIRD_CX - 22, lwy = birdY - 10;
leftWing.setAttribute('transform', `translate(${lwx}, ${lwy}) rotate(${flapAngle})`);
leftOuterWing.setAttribute('transform', `translate(-195, 0) rotate(${sweep})`);
// 右翼(镜像)
const rwx = BIRD_CX + 22, rwy = birdY - 10;
rightWing.setAttribute('transform', `translate(${rwx}, ${rwy}) scale(-1, 1) rotate(${-flapAngle})`);
rightOuterWing.setAttribute('transform', `translate(-195, 0) rotate(${-sweep})`);
// SMP 铰链颜色
const smpCol = smpColor(smpStiffness);
const smpFilt = smpFilter(smpStiffness);
[leftSMP, rightSMP].forEach(smp => {
smp.setAttribute('fill', smpCol);
smp.setAttribute('filter', smpFilt);
});
// 热气流可见度
const thermOp = smoothstep(0, 0.3, thermalIntensity);
thermalGroup.setAttribute('opacity', String(thermOp));
// 传感器
updateSensors(time, thermalIntensity);
// HUD
const tempNorm = clamp((temp - T_MIN) / (T_MAX - T_MIN), 0, 1);
const barH = lerp(30, 240, tempNorm);
const barY = 260 - barH + 4;
tempBar.setAttribute('y', String(barY));
tempBar.setAttribute('height', String(barH));
tempBar.setAttribute('fill', smpCol);
tempText.textContent = Math.round(temp) + '℃';
tempText.setAttribute('fill', smpCol);
const stiffRatio = Math.round(smpStiffness * 100);
stiffText.textContent = stiffRatio + ':1';
stiffText.setAttribute('fill', smpCol);
altText.textContent = Math.round(altitude) + 'm';
const modeStr = flapActive ? 'FLAP' : (sweep > 5 ? 'SOAR' : 'FLAP');
modeText.textContent = modeStr;
modeText.setAttribute('fill', thermalIntensity > 0.5 ? '#ff9100' : '#00e5ff');
// 阶段标签
phaseLabel.textContent = phaseName;
phaseLabel.style.color = smpCol;
phaseLabel.style.textShadow = `0 0 20px ${smpCol}40`;
// IFR 注释(在热气流阶段显示)
const ifrOp = smoothstep(0.3, 0.6, thermalIntensity);
ifrNote.setAttribute('opacity', String(ifrOp));
// 标注颜色跟随 SMP
const annoSMP = document.getElementById('annoSMP');
if (annoSMP) {
annoSMP.querySelectorAll('line').forEach(l => l.setAttribute('stroke', smpCol));
annoSMP.querySelectorAll('rect').forEach(r => {
r.setAttribute('stroke', smpCol + '4d');
r.setAttribute('fill', smpCol + '14');
});
annoSMP.querySelectorAll('text').forEach(t => t.setAttribute('fill', smpCol));
}
// 温度滑块值(自动模式下同步)
if (autoMode) {
tempSlider.value = temp;
tempValueEl.textContent = Math.round(temp) + '℃';
tempValueEl.style.color = smpCol;
}
}
/* ===== 主动画循环 ===== */
function animate(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
let state;
if (autoMode) {
const cycleProgress = (elapsed % CYCLE) / CYCLE;
state = getPhaseState(cycleProgress);
} else {
state = getStateFromTemp(manualTemp);
}
// 更新粒子
updateParticles(16, state.thermalIntensity);
// 渲染
render(elapsed, state);
requestAnimationFrame(animate);
}
/* ===== 模式切换 ===== */
autoBtn.addEventListener('click', function() {
autoMode = true;
autoBtn.classList.add('active');
manualBtn.classList.remove('active');
startTime = null; // 重置计时
});
manualBtn.addEventListener('click', function() {
autoMode = false;
manualBtn.classList.add('active');
autoBtn.classList.remove('active');
});
/* ===== 温度滑块 ===== */
tempSlider.addEventListener('input', function() {
if (!autoMode) {
manualTemp = parseFloat(this.value);
const s = getStateFromTemp(manualTemp);
const smpCol = smpColor(s.smpStiffness);
tempValueEl.textContent = Math.round(manualTemp) + '℃';
tempValueEl.style.color = smpCol;
}
});
/* ===== 初始化 ===== */
function init() {
initParticles();
requestAnimationFrame(animate);
}
// 页面加载后自动开始
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>
实现说明
这个动画完整呈现了 SMP 热致变刚度机械鸟 的 IFR(最终理想解)工作原理,核心设计要点如下:
视觉叙事逻辑
动画以 16 秒为一个完整循环,依次展示:扑动飞行 → 检测热气流 → SMP 软化/构型切换 → 盘旋滑翔 → 脱离降温 → SMP 硬化/恢复扑动。整个过程中没有"问题状态"的对比展示,直接呈现理想解的运作——环境热能被被动捕获,驱动构型切换,零主动能耗。
核心创新点的视觉引导
- SMP 铰链:颜色从冷青(刚性锁定)到琥珀色(软化屈服)连续渐变,配合辉光滤镜切换,是全场视觉焦点
- 后掠角变化:外翼段在腕关节处旋转最大 55°,清晰展示气动载荷驱动的被动折叠
- 传感器阵列:腹部传感器在检测到热气流时脉冲闪烁,颜色从青切换到琥珀
- 热气流粒子:50 个粒子构成上升热流柱,透明度与温度联动
- IFR 底部注释:在盘旋阶段浮现"环境热能 → SMP 刚度转变 → 气动载荷驱动构型切换 · 零主动能耗"
交互控制
- AUTO / MANUAL 切换:自动模式循环播放完整飞行场景;手动模式下拖动温度滑块可直接控制环境温度,实时观察 SMP 响应、翼面后掠和飞行模式切换
- HUD 仪表:左侧温度条、刚度比、右侧高度和模式指示,数据实时更新
工程细节
- 右翼通过
scale(-1,1)镜像实现,旋转角度取反确保双翼对称运动 - 所有状态过渡使用
smoothstep插值,避免生硬跳变 - 粒子系统循环复用 50 个 SVG 元素,无内存泄漏
- 支持
prefers-reduced-motion媒体查询
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMP 热致变刚度机械鸟 · IFR 原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#07080e;--fg:#d8e0ec;--muted:#506078;
--cold:#2dd4bf;--hot:#f43f5e;
--card:rgba(10,14,26,0.88);--border:rgba(80,96,120,0.22);
--struct:#6b7a90;--sand:#c4a06a;
}
body{
background:var(--bg);color:var(--fg);
font-family:'JetBrains Mono',monospace;
min-height:100vh;display:flex;align-items:center;justify-content:center;
overflow:hidden;
}
#wrap{
width:97vw;max-width:1480px;aspect-ratio:14/9;
position:relative;border-radius:14px;overflow:hidden;
border:1px solid var(--border);
box-shadow:0 0 60px rgba(45,212,191,0.035),0 0 140px rgba(244,63,94,0.025);
}
#scene{width:100%;height:100%;display:block}
#phase{
position:absolute;top:20px;left:50%;transform:translateX(-50%);
font-family:'Chakra Petch',sans-serif;font-size:16px;font-weight:700;
letter-spacing:1.4px;text-transform:uppercase;
color:var(--cold);text-shadow:0 0 16px rgba(45,212,191,0.3);
transition:color .55s,text-shadow .55s;z-index:5;white-space:nowrap;
}
#ifr-badge{
position:absolute;top:18px;right:20px;
font-family:'Chakra Petch',sans-serif;font-size:10px;font-weight:700;
letter-spacing:1px;padding:5px 10px;border-radius:5px;
background:rgba(196,160,106,0.07);border:1px solid rgba(196,160,106,0.28);
color:var(--sand);z-index:5;text-transform:uppercase;
}
#controls{
position:absolute;bottom:16px;left:50%;transform:translateX(-50%);
display:flex;align-items:center;gap:14px;
background:var(--card);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:10px;
padding:9px 20px;z-index:5;
}
.btn{
font-family:'Chakra Petch',sans-serif;font-size:10px;font-weight:700;
letter-spacing:.7px;padding:5px 11px;border-radius:5px;
border:1px solid var(--border);background:transparent;
color:var(--muted);cursor:pointer;transition:all .22s;text-transform:uppercase;
}
.btn.on{
background:rgba(45,212,191,0.1);color:var(--cold);
border-color:var(--cold);box-shadow:0 0 10px rgba(45,212,191,0.1);
}
.cl{font-family:'Chakra Petch',sans-serif;font-size:10px;font-weight:600;color:var(--muted);white-space:nowrap}
#slider{
-webkit-appearance:none;width:170px;height:5px;border-radius:3px;
background:linear-gradient(to right,var(--cold),var(--hot));
outline:none;cursor:pointer;
}
#slider::-webkit-slider-thumb{
-webkit-appearance:none;width:15px;height:15px;border-radius:50%;
border:2px solid var(--fg);background:var(--bg);cursor:pointer;
box-shadow:0 0 6px rgba(45,212,191,0.35);
}
#tv{font-size:12px;font-weight:500;min-width:40px;text-align:right;color:var(--cold)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:0s!important}}
@media(max-width:768px){
#controls{flex-wrap:wrap;justify-content:center;gap:8px;padding:8px 12px}
#slider{width:110px}#phase{font-size:12px}
}
</style>
</head>
<body>
<div id="wrap">
<svg id="scene" viewBox="0 0 1400 900" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bgG" cx="50%" cy="42%" r="68%">
<stop offset="0%" stop-color="#0d1228"/><stop offset="100%" stop-color="#050710"/>
</radialGradient>
<linearGradient id="thG" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#f43f5e" stop-opacity=".28"/>
<stop offset="55%" stop-color="#fb923c" stop-opacity=".1"/>
<stop offset="100%" stop-color="#fb923c" stop-opacity="0"/>
</linearGradient>
<radialGradient id="thGl" cx="50%" cy="78%" r="48%">
<stop offset="0%" stop-color="#f43f5e" stop-opacity=".09"/>
<stop offset="100%" stop-color="#f43f5e" stop-opacity="0"/>
</radialGradient>
<radialGradient id="smpC"><stop offset="0%" stop-color="#5eead4"/><stop offset="100%" stop-color="#14b8a6"/></radialGradient>
<radialGradient id="smpH"><stop offset="0%" stop-color="#fda4af"/><stop offset="100%" stop-color="#e11d48"/></radialGradient>
<linearGradient id="wG" x1="1" y1="0" x2="0" y2="0">
<stop offset="0%" stop-color="#7a8a9e"/><stop offset="100%" stop-color="#586878"/>
</linearGradient>
<linearGradient id="wGr" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#7a8a9e"/><stop offset="100%" stop-color="#586878"/>
</linearGradient>
<linearGradient id="bG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#94a3b8"/><stop offset="100%" stop-color="#64748b"/>
</linearGradient>
<filter id="gC" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="5" result="b"/><feFlood flood-color="#2dd4bf" flood-opacity=".55" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gH" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="7" result="b"/><feFlood flood-color="#f43f5e" flood-opacity=".55" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gS" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="2.5"/></filter>
</defs>
<!-- 背景 -->
<rect width="1400" height="900" fill="url(#bgG)"/>
<!-- 网格 -->
<g opacity=".035" stroke="#4a5a7a" stroke-width=".5">
<line x1="0" y1="100" x2="1400" y2="100"/><line x1="0" y1="200" x2="1400" y2="200"/>
<line x1="0" y1="300" x2="1400" y2="300"/><line x1="0" y1="450" x2="1400" y2="450"/>
<line x1="0" y1="600" x2="1400" y2="600"/><line x1="0" y1="700" x2="1400" y2="700"/>
<line x1="0" y1="800" x2="1400" y2="800"/>
<line x1="200" y1="0" x2="200" y2="900"/><line x1="400" y1="0" x2="400" y2="900"/>
<line x1="600" y1="0" x2="600" y2="900"/><line x1="800" y1="0" x2="800" y2="900"/>
<line x1="1000" y1="0" x2="1000" y2="900"/><line x1="1200" y1="0" x2="1200" y2="900"/>
</g>
<!-- 热气流柱 -->
<g id="thC" opacity="0">
<ellipse cx="700" cy="790" rx="250" ry="270" fill="url(#thGl)"/>
<rect x="575" y="470" width="250" height="430" fill="url(#thG)" rx="125"/>
<g opacity=".4">
<path d="M655,740 L655,645 L643,660 M655,645 L667,660" stroke="#f43f5e" stroke-width="1.5" fill="none"/>
<path d="M700,770 L700,635 L688,650 M700,635 L712,650" stroke="#f43f5e" stroke-width="2" fill="none"/>
<path d="M745,740 L745,645 L733,660 M745,645 L757,660" stroke="#f43f5e" stroke-width="1.5" fill="none"/>
</g>
<g id="ptc"></g>
</g>
<!-- 能量流向线 -->
<g id="eFlow" opacity="0">
<path id="eL" d="M660,510 C640,470 560,430 510,398" fill="none" stroke="#f43f5e" stroke-width="1.4" stroke-dasharray="5,4" opacity=".45"/>
<path id="eR" d="M740,510 C760,470 840,430 890,398" fill="none" stroke="#f43f5e" stroke-width="1.4" stroke-dasharray="5,4" opacity=".45"/>
<text x="590" y="462" fill="#f43f5e" font-family="'Chakra Petch',sans-serif" font-size="8.5" font-weight="600" opacity=".65">热能驱动</text>
<text x="790" y="462" fill="#f43f5e" font-family="'Chakra Petch',sans-serif" font-size="8.5" font-weight="600" opacity=".65">热能驱动</text>
</g>
<!-- ========= 机械鸟 ========= -->
<g id="bird">
<!-- 左翼 -->
<g id="lw">
<path d="M0,-14 L-140,-7 L-148,0 L-140,7 L0,14 Z" fill="url(#wG)" stroke="#8a96a8" stroke-width=".6" opacity=".9"/>
<line x1="-40" y1="-10" x2="-40" y2="10" stroke="#506078" stroke-width=".4" opacity=".3"/>
<line x1="-80" y1="-8.5" x2="-80" y2="8.5" stroke="#506078" stroke-width=".4" opacity=".3"/>
<line x1="-120" y1="-7.5" x2="-120" y2="7.5" stroke="#506078" stroke-width=".4" opacity=".3"/>
<circle cx="-30" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<circle cx="-70" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<circle cx="-110" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<!-- SMP 铰链 -->
<g id="lsmp">
<circle cx="-148" cy="0" r="9" fill="url(#smpC)" filter="url(#gC)"/>
<circle cx="-148" cy="0" r="3.8" fill="#080c18" opacity=".55"/>
<circle id="lr1" cx="-148" cy="0" r="9" fill="none" stroke="#2dd4bf" stroke-width="1.5" opacity="0"/>
<circle id="lr2" cx="-148" cy="0" r="13" fill="none" stroke="#2dd4bf" stroke-width=".8" opacity="0"/>
</g>
<!-- 外翼 -->
<g id="low">
<path d="M0,-7 L-125,-2 L-132,0 L-125,2 L0,7 Z" fill="url(#wG)" stroke="#8a96a8" stroke-width=".5" opacity=".85"/>
<line x1="-50" y1="-4" x2="-50" y2="4" stroke="#506078" stroke-width=".3" opacity=".22"/>
<line x1="-90" y1="-3" x2="-90" y2="3" stroke="#506078" stroke-width=".3" opacity=".22"/>
<!-- 偏流小翼 -->
<path d="M-132,0 L-150,-14 L-138,-1 Z" fill="#8a96a8" stroke="#9aa6b8" stroke-width=".4" opacity=".72"/>
</g>
<!-- 后掠角指示弧 -->
<g id="sweepGauge" transform="translate(-148,0)">
<path id="sweepArc" d="" fill="none" stroke="#2dd4bf" stroke-width="1" opacity=".35"/>
<line id="sweepNeedle" x1="0" y1="0" x2="-28" y2="0" stroke="#2dd4bf" stroke-width="1.2" opacity=".7"/>
<text id="sweepLabel" x="-18" y="-18" fill="#2dd4bf" font-family="'JetBrains Mono',monospace" font-size="8" opacity=".6">0°</text>
</g>
</g>
<!-- 右翼 -->
<g id="rw">
<path d="M0,-14 L140,-7 L148,0 L140,7 L0,14 Z" fill="url(#wGr)" stroke="#8a96a8" stroke-width=".6" opacity=".9"/>
<line x1="40" y1="-10" x2="40" y2="10" stroke="#506078" stroke-width=".4" opacity=".3"/>
<line x1="80" y1="-8.5" x2="80" y2="8.5" stroke="#506078" stroke-width=".4" opacity=".3"/>
<line x1="120" y1="-7.5" x2="120" y2="7.5" stroke="#506078" stroke-width=".4" opacity=".3"/>
<circle cx="30" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<circle cx="70" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<circle cx="110" cy="0" r="1.4" fill="#506078" opacity=".45"/>
<g id="rsmp">
<circle cx="148" cy="0" r="9" fill="url(#smpC)" filter="url(#gC)"/>
<circle cx="148" cy="0" r="3.8" fill="#080c18" opacity=".55"/>
<circle id="rr1" cx="148" cy="0" r="9" fill="none" stroke="#2dd4bf" stroke-width="1.5" opacity="0"/>
<circle id="rr2" cx="148" cy="0" r="13" fill="none" stroke="#2dd4bf" stroke-width=".8" opacity="0"/>
</g>
<g id="row">
<path d="M0,-7 L125,-2 L132,0 L125,2 L0,7 Z" fill="url(#wGr)" stroke="#8a96a8" stroke-width=".5" opacity=".85"/>
<line x1="50" y1="-4" x2="50" y2="4" stroke="#506078" stroke-width=".3" opacity=".22"/>
<line x1="90" y1="-3" x2="90" y2="3" stroke="#506078" stroke-width=".3" opacity=".22"/>
<path d="M132,0 L150,-14 L138,-1 Z" fill="#8a96a8" stroke="#9aa6b8" stroke-width=".4" opacity=".72"/>
</g>
</g>
<!-- 机身 -->
<path d="M700,340 Q718,375 716,410 Q714,452 700,478 Q686,452 684,410 Q682,375 700,340 Z"
fill="url(#bG)" stroke="#8a96a8" stroke-width=".7"/>
<line x1="700" y1="350" x2="700" y2="468" stroke="#64748b" stroke-width=".35" opacity=".2"/>
<line x1="693" y1="372" x2="693" y2="452" stroke="#64748b" stroke-width=".25" opacity=".12"/>
<line x1="707" y1="372" x2="707" y2="452" stroke="#64748b" stroke-width=".25" opacity=".12"/>
<!-- 头部 -->
<ellipse cx="700" cy="332" rx="11" ry="14" fill="#94a3b8" stroke="#8a96a8" stroke-width=".5"/>
<circle cx="694" cy="327" r="2.2" fill="#080c18"/><circle cx="706" cy="327" r="2.2" fill="#080c18"/>
<circle cx="694.5" cy="326.5" r=".7" fill="#3a4a60"/><circle cx="706.5" cy="326.5" r=".7" fill="#3a4a60"/>
<path d="M697,320 L700,306 L703,320 Z" fill="#c4a06a" stroke="#a88550" stroke-width=".5"/>
<!-- 尾翼 -->
<path d="M690,473 L672,508 L684,497 L700,512 L716,497 L728,508 L710,473 Z"
fill="#64748b" stroke="#78859b" stroke-width=".5"/>
<!-- 腹部传感器阵列 -->
<g id="senG">
<circle class="sen" cx="693" cy="385" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="707" cy="385" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="693" cy="405" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="707" cy="405" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="700" cy="425" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="694" cy="445" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
<circle class="sen" cx="706" cy="445" r="2.6" fill="#080c18" stroke="#2dd4bf" stroke-width=".85" opacity=".45"/>
</g>
</g>
<!-- 标注 -->
<g id="annos" font-family="'Chakra Petch',sans-serif" font-weight="600">
<g id="aSMP">
<line x1="510" y1="398" x2="400" y2="445" stroke="#2dd4bf" stroke-width=".8" stroke-dasharray="4,3" opacity=".4"/>
<rect x="288" y="435" width="112" height="22" rx="4" fill="rgba(45,212,191,.05)" stroke="rgba(45,212,191,.22)" stroke-width=".7"/>
<text x="344" y="451" text-anchor="middle" fill="#2dd4bf" font-size="10">SMP 铰链</text>
</g>
<g id="aSen">
<line x1="707" y1="405" x2="820" y2="462" stroke="#2dd4bf" stroke-width=".8" stroke-dasharray="4,3" opacity=".4"/>
<rect x="790" y="452" width="145" height="22" rx="4" fill="rgba(45,212,191,.05)" stroke="rgba(45,212,191,.22)" stroke-width=".7"/>
<text x="862" y="468" text-anchor="middle" fill="#2dd4bf" font-size="10">微压传感器阵列</text>
</g>
<g id="aWl">
<line x1="378" y1="375" x2="300" y2="325" stroke="#6b7a90" stroke-width=".7" stroke-dasharray="4,3" opacity=".3"/>
<rect x="215" y="313" width="85" height="22" rx="4" fill="rgba(107,122,144,.05)" stroke="rgba(107,122,144,.18)" stroke-width=".7"/>
<text x="257" y="329" text-anchor="middle" fill="#6b7a90" font-size="10">偏流小翼</text>
</g>
</g>
<!-- HUD 左侧:温度 -->
<g transform="translate(52,175)">
<rect x="0" y="0" width="30" height="290" rx="15" fill="rgba(8,12,24,.6)" stroke="rgba(80,96,120,.18)" stroke-width=".7"/>
<rect id="tBar" x="4" y="230" width="22" height="56" rx="11" fill="#2dd4bf" opacity=".72"/>
<text x="15" y="316" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">T</text>
<text id="tTxt" x="15" y="190" text-anchor="middle" fill="#2dd4bf" font-size="10" font-weight="500">25℃</text>
</g>
<!-- 刚度比 -->
<g transform="translate(52,525)">
<text x="15" y="0" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">刚度比</text>
<text id="kTxt" x="15" y="17" text-anchor="middle" fill="#2dd4bf" font-size="11.5" font-weight="500">100:1</text>
</g>
<!-- 后掠角 -->
<g transform="translate(52,575)">
<text x="15" y="0" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">后掠角</text>
<text id="swTxt" x="15" y="17" text-anchor="middle" fill="#2dd4bf" font-size="11.5" font-weight="500">0°</text>
</g>
<!-- HUD 右侧 -->
<g transform="translate(1318,175)">
<text x="16" y="0" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">ALT</text>
<text id="altTxt" x="16" y="18" text-anchor="middle" fill="#6b7a90" font-size="12.5" font-weight="500">120m</text>
</g>
<g transform="translate(1318,225)">
<text x="16" y="0" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">MODE</text>
<text id="mTxt" x="16" y="18" text-anchor="middle" fill="#2dd4bf" font-size="10.5" font-weight="500">FLAP</text>
</g>
<g transform="translate(1318,275)">
<text x="16" y="0" text-anchor="middle" fill="#506078" font-size="8.5" font-weight="600">ENERGY</text>
<text id="eTxt" x="16" y="18" text-anchor="middle" fill="#2dd4bf" font-size="10.5" font-weight="500">主动</text>
</g>
<!-- IFR 原理注解 -->
<g id="ifrN" transform="translate(700,830)" opacity="0" font-family="'Chakra Petch',sans-serif">
<rect x="-300" y="-16" width="600" height="32" rx="7"
fill="rgba(244,63,94,.04)" stroke="rgba(244,63,94,.2)" stroke-width=".7"/>
<text x="0" y="5" text-anchor="middle" fill="#f43f5e" font-size="11.5" font-weight="600">
环境热能 → SMP 刚度转变 → 气动载荷驱动构型切换 · 零主动能耗
</text>
</g>
<!-- 参数面板 -->
<g transform="translate(1200,650)" font-family="'JetBrains Mono',monospace" font-size="9" opacity=".55">
<rect x="0" y="0" width="160" height="68" rx="6" fill="rgba(8,12,24,.5)" stroke="rgba(80,96,120,.15)" stroke-width=".6"/>
<text x="12" y="18" fill="#506078" font-weight="500">Tg = 45℃</text>
<text x="12" y="34" fill="#506078" font-weight="500">刚度比 > 100:1</text>
<text x="12" y="50" fill="#506078" font-weight="500">后掠角 0° ~ 55°</text>
<text x="12" y="63" fill="#506078" font-weight="400" font-size="7.5" opacity=".7">SMP: Shape Memory Polymer</text>
</g>
</svg>
<div id="phase">扑动飞行模式</div>
<div id="ifr-badge">IFR · 最终理想解</div>
<div id="controls">
<button class="btn on" id="aBtn" aria-label="自动循环">AUTO</button>
<button class="btn" id="mBtn" aria-label="手动控温">MANUAL</button>
<label class="cl" for="slider">环境温度</label>
<input type="range" id="slider" min="20" max="55" value="25" step=".5" aria-label="温度滑块"/>
<span id="tv">25℃</span>
</div>
</div>
<script>
(function(){
'use strict';
/* ===== 常量 ===== */
const CYCLE=16000, SMP_Tg=45, T_MIN=20, T_MAX=55;
const SWEEP_MAX=55, FLAP_AMP=7, FLAP_FREQ=4.2;
const CX=700, CY=400;
/* ===== DOM ===== */
const $=id=>document.getElementById(id);
const bird=$('bird'), lw=$('lw'), rw=$('rw'), low=$('low'), row=$('row');
const lsmp=$('lsmp'), rsmp=$('rsmp');
const lr1=$('lr1'), lr2=$('lr2'), rr1=$('rr1'), rr2=$('rr2');
const thC=$('thC'), ptc=$('ptc'), eFlow=$('eFlow');
const sweepArc=$('sweepArc'), sweepNeedle=$('sweepNeedle'), sweepLabel=$('sweepLabel');
const tBar=$('tBar'), tTxt=$('tTxt'), kTxt=$('kTxt'), swTxt=$('swTxt');
const altTxt=$('altTxt'), mTxt=$('mTxt'), eTxt=$('eTxt');
const phaseEl=$('phase'), ifrN=$('ifrN');
const slider=$('slider'), tv=$('tv'), aBtn=$('aBtn'), mBtn=$('mBtn');
const sensors=document.querySelectorAll('.sen');
const aSMP=$('aSMP');
/* ===== 状态 ===== */
let auto=true, mTemp=25, t0=null, particles=[];
/* ===== 工具 ===== */
function lerp(a,b,t){return a+(b-a)*Math.max(0,Math.min(1,t))}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}
function ss(e0,e1,x){const t=clamp((x-e0)/(e1-e0),0,1);return t*t*(3-2*t)}
function smpCol(stiff){
const t=1-stiff;
return`rgb(${Math.round(lerp(45,244,t))},${Math.round(lerp(212,63,t))},${Math.round(lerp(191,94,t))})`
}
function degToRad(d){return d*Math.PI/180}
/* ===== 弧线路径生成 ===== */
function arcPath(cx,cy,r,startDeg,endDeg){
const s=degToRad(startDeg-90), e=degToRad(endDeg-90);
const x1=cx+r*Math.cos(s), y1=cy+r*Math.sin(s);
const x2=cx+r*Math.cos(e), y2=cy+r*Math.sin(e);
const large=Math.abs(endDeg-startDeg)>180?1:0;
return`M${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2}`
}
/* ===== 粒子系统 ===== */
function initPtc(){
for(let i=0;i<48;i++){
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('r',String(1.2+Math.random()*2.8));
c.setAttribute('fill','#f43f5e');c.setAttribute('opacity','0');
ptc.appendChild(c);
particles.push({el:c,x:605+Math.random()*190,y:830+Math.random()*70,
vy:-(0.3+Math.random()*0.5),vx:(Math.random()-.5)*.2,
life:Math.random(),max:.6+Math.random()*.4});
}
}
function updatePtc(dt,thOp){
particles.forEach(p=>{
p.life+=dt*.00065;
if(p.life>p.max){
p.life=0;p.x=605+Math.random()*190;p.y=830+Math.random()*70;
p.vy=-(0.3+Math.random()*0.5);p.vx=(Math.random()-.5)*.2;
}
p.x+=p.vx+Math.sin(p.life*6.5)*.1; p.y+=p.vy;
const fade=1-(p.life/p.max);
p.el.setAttribute('cx',String(p.x));
p.el.setAttribute('cy',String(p.y));
p.el.setAttribute('opacity',String(Math.max(0,fade*thOp*.5)));
});
}
/* ===== 相位状态机 ===== */
function phaseState(t){
let temp,sweep,flap,therm,alt,name,stiff;
if(t<.15){
temp=lerp(22,27,t/.15);sweep=0;flap=true;therm=0;
alt=lerp(120,125,t/.15);name='扑动飞行模式';stiff=1;
}else if(t<.25){
const p=(t-.15)/.1;
temp=lerp(27,40,p);sweep=0;flap=true;therm=ss(0,1,p);
alt=lerp(125,135,p);name='传感器检测 · 热上升气流';stiff=lerp(1,.4,p);
}else if(t<.42){
const p=(t-.25)/.17;
temp=lerp(40,50,p);sweep=ss(0,1,p)*SWEEP_MAX;flap=p<.4;
therm=lerp(.55,1,p);alt=lerp(135,195,p);
name='SMP 软化 · 构型切换';stiff=lerp(.4,.015,p);
}else if(t<.62){
const p=(t-.42)/.2;
temp=lerp(50,52,Math.sin(p*Math.PI));sweep=SWEEP_MAX;flap=false;
therm=1;alt=lerp(195,370,p);name='热气流盘旋滑翔';stiff=.015;
}else if(t<.73){
const p=(t-.62)/.11;
temp=lerp(52,35,p);sweep=SWEEP_MAX;flap=false;
therm=lerp(1,.2,p);alt=lerp(370,358,p);
name='脱离热气流 · 降温中';stiff=lerp(.015,.28,p);
}else if(t<.89){
const p=(t-.73)/.16;
temp=lerp(35,23,p);sweep=lerp(SWEEP_MAX,0,ss(0,1,p));flap=p>.5;
therm=lerp(.2,0,p);alt=lerp(358,125,p);
name='SMP 硬化 · 恢复扑动';stiff=lerp(.28,1,p);
}else{
const p=(t-.89)/.11;
temp=lerp(23,22,p);sweep=0;flap=true;therm=0;
alt=lerp(125,120,p);name='扑动飞行模式';stiff=1;
}
return{temp,sweep,flap,therm,alt,name,stiff};
}
function stateFromTemp(temp){
const soft=ss(SMP_Tg-5,SMP_Tg+5,temp);
const sweep=soft*SWEEP_MAX,flap=soft<.4,therm=ss(28,42,temp);
const stiff=lerp(1,.015,soft);
let name;
if(soft<.08)name='扑动飞行模式';
else if(soft<.5)name='SMP 软化 · 构型切换';
else name='热气流盘旋滑翔';
return{temp,sweep,flap,therm,alt:lerp(120,370,soft),name,stiff};
}
/* ===== 传感器 ===== */
function updateSensors(time,therm){
sensors.forEach((s,i)=>{
const pulse=therm>.08?.5+.5*Math.sin(time*.005+i*1.2):.2;
const op=lerp(.15,1,pulse*therm);
s.setAttribute('opacity',String(op));
s.setAttribute('stroke',therm>.25?'#f43f5e':'#2dd4bf');
s.setAttribute('stroke-width',String(lerp(.7,2.2,pulse*therm)));
});
}
/* ===== 渲染 ===== */
function render(time,s){
const{temp,sweep,flap,therm,alt,name,stiff}=s;
const col=smpCol(stiff);
/* 鸟体位置 */
const by=CY-(alt-120)*.1;
const bank=therm>.75?Math.sin(time*.0016)*4.5:0;
bird.setAttribute('transform',`translate(0,${CY-by}) rotate(${bank},${CX},${by})`);
/* 扑动 */
const fA=flap?Math.sin(time*.001*FLAP_FREQ*Math.PI*2)*FLAP_AMP:0;
const lwx=CX-20,lwy=by-8,rwx=CX+20,rwy=by-8;
lw.setAttribute('transform',`translate(${lwx},${lwy}) rotate(${fA})`);
rw.setAttribute('transform',`translate(${rwx},${rwy}) rotate(${-fA})`);
/* 外翼后掠:左翼逆时针(负角),右翼顺时针(正角) → 翼尖向尾方 */
low.setAttribute('transform',`translate(-148,0) rotate(${-sweep})`);
row.setAttribute('transform',`translate(148,0) rotate(${sweep})`);
/* SMP 铰链视觉 */
const smpFill=stiff<.5?'url(#smpH)':'url(#smpC)';
const smpFilt=stiff<.5?'url(#gH)':'url(#gC)';
[lsmp,rsmp].forEach(g=>{
g.children[0].setAttribute('fill',smpFill);
g.children[0].setAttribute('filter',smpFilt);
});
/* SMP 脉冲环 */
const trans=stiff>.02&&stiff<.96;
const ringOp=trans?.35+.35*Math.sin(time*.007):0;
const ringR1=trans?9+4*Math.sin(time*.005):9;
const ringR2=trans?13+6*Math.sin(time*.004):13;
[lr1,rr1].forEach(r=>{r.setAttribute('opacity',String(ringOp));r.setAttribute('r',String(ringR1));r.setAttribute('stroke',col)});
[lr2,rr2].forEach(r=>{r.setAttribute('opacity',String(ringOp*.5));r.setAttribute('r',String(ringR2));r.setAttribute('stroke',col)});
/* 后掠角指示弧 */
if(sweep>1){
sweepArc.setAttribute('d',arcPath(0,0,28,180,180+sweep));
sweepArc.setAttribute('stroke',col);sweepArc.setAttribute('opacity',String(.3+therm*.2));
const needleAngle=degToRad(180+sweep-90);
const nx=28*Math.cos(needleAngle),ny=28*Math.sin(needleAngle);
sweepNeedle.setAttribute('x2',String(nx));sweepNeedle.setAttribute('y2',String(ny));
sweepNeedle.setAttribute('stroke',col);sweepNeedle.setAttribute('opacity',String(.5+therm*.3));
sweepLabel.textContent=Math.round(sweep)+'°';
sweepLabel.setAttribute('fill',col);sweepLabel.setAttribute('opacity',String(.4+therm*.3));
}else{
sweepArc.setAttribute('d','');sweepNeedle.setAttribute('opacity','0');sweepLabel.setAttribute('opacity','0');
}
/* 热气流 */
thC.setAttribute('opacity',String(ss(0,.3,therm)));
/* 传感器 */
updateSensors(time,therm);
/* 能量流向线动画偏移 */
if(therm>.3){
const dashOff=time*.03;
document.getElementById('eL').setAttribute('stroke-dashoffset',String(-dashOff));
document.getElementById('eR').setAttribute('stroke-dashoffset',String(-dashOff));
}
eFlow.setAttribute('opacity',String(ss(.35,.65,therm)*.75));
/* HUD */
const tN=clamp((temp-T_MIN)/(T_MAX-T_MIN),0,1);
const barH=lerp(24,245,tN),barY=282-barH+4;
tBar.setAttribute('y',String(barY));tBar.setAttribute('height',String(barH));tBar.setAttribute('fill',col);
tTxt.textContent=Math.round(temp)+'℃';tTxt.setAttribute('fill',col);
kTxt.textContent=Math.round(stiff*100)+':1';kTxt.setAttribute('fill',col);
swTxt.textContent=Math.round(sweep)+'°';swTxt.setAttribute('fill',col);
altTxt.textContent=Math.round(alt)+'m';
const mode=flap?'FLAP':(sweep>5?'SOAR':'FLAP');
mTxt.textContent=mode;mTxt.setAttribute('fill',therm>.45?'#f43f5e':'#2dd4bf');
eTxt.textContent=therm>.5?'寄生':'主动';eTxt.setAttribute('fill',therm>.5?'#f43f5e':'#2dd4bf');
/* 阶段标签 */
phaseEl.textContent=name;phaseEl.style.color=col;
phaseEl.style.textShadow=`0 0 14px ${col}35`;
/* IFR 注解 */
ifrN.setAttribute('opacity',String(ss(.25,.55,therm)));
/* 标注颜色联动 */
if(aSMP){
aSMP.querySelectorAll('line').forEach(l=>l.setAttribute('stroke',col));
aSMP.querySelectorAll('rect').forEach(r=>{r.setAttribute('stroke',col+'38');r.setAttribute('fill',col+'0d')});
aSMP.querySelectorAll('text').forEach(t=>t.setAttribute('fill',col));
}
/* 滑块同步 */
if(auto){slider.value=temp;tv.textContent=Math.round(temp)+'℃';tv.style.color=col}
}
/* ===== 动画循环 ===== */
function loop(ts){
if(!t0)t0=ts;
const elapsed=ts-t0;
let s;
if(auto){s=phaseState((elapsed%CYCLE)/CYCLE)}
else{s=stateFromTemp(mTemp)}
updatePtc(16,s.therm);
render(elapsed,s);
requestAnimationFrame(loop);
}
/* ===== 交互 ===== */
aBtn.addEventListener('click',()=>{auto=true;aBtn.classList.add('on');mBtn.classList.remove('on');t0=null});
mBtn.addEventListener('click',()=>{auto=false;mBtn.classList.add('on');aBtn.classList.remove('on')});
slider.addEventListener('input',function(){
if(!auto){
mTemp=parseFloat(this.value);
const s=stateFromTemp(mTemp);const col=smpCol(s.stiff);
tv.textContent=Math.round(mTemp)+'℃';tv.style.color=col;
}
});
/* ===== 初始化 ===== */
function init(){initPtc();requestAnimationFrame(loop)}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);
else init();
})();
</script>
</body>
</html>
实现说明
本次动画从零重构,围绕 TRIZ 最终理想解 (IFR) 思想,聚焦呈现「环境热能直接驱动构型切换」的核心创新。
设计决策
色彩体系:摒弃常见的青-橙配色,采用 薄荷青 (#2dd4bf) → 玫瑰红 (#f43f5e) 的温度映射。冷态锁定时铰链呈薄荷青并带冷色辉光,进入热气流后渐变为玫瑰红并切换暖色辉光,视觉反差强烈且不落俗套。
鸟体构型:俯视拓扑视图,双翼左右对称展开。左翼/右翼分别绘制(不使用 scale(-1,1) 镜像),确保后掠旋转方向精确——左外翼 rotate(-sweep)、右外翼 rotate(+sweep),翼尖均向尾方偏转,物理行为正确。
IFR 视觉引导层次
- SMP 铰链:全场视觉焦点——颜色渐变 + 辉光滤镜切换 + 双层脉冲环(过渡态时半径与透明度正弦振荡),三重信号叠加确保无法忽略
- 后掠角指示弧:左翼腕关节处的弧形量角器 + 旋转指针 + 实时角度数值,将抽象的角度变化具象为仪表读数
- 能量流向线:热气流柱到双侧 SMP 铰链的虚线曲线,
stroke-dashoffset持续动画模拟能量流动方向 - 传感器脉冲:腹部 7 枚传感器在检测到热气流时同步闪烁,颜色从青切换到红
- ENERGY 指示:右侧 HUD 显示"主动 → 寄生"切换,直白表达 IFR 的能量寄生本质
动画时序(16 秒循环)
| 阶段 | 温度 | 刚度比 | 后掠角 | 模式 |
|---|---|---|---|---|
| 扑动飞行 | 22~27℃ | 100:1 | 0° | FLAP |
| 传感器检测 | 27~40℃ | 100→40:1 | 0° | FLAP |
| SMP软化·构型切换 | 40~50℃ | 40→1.5:1 | 0→55° | FLAP→SOAR |
| 热气流盘旋滑翔 | ~52℃ | 1.5:1 | 55° | SOAR |
| 脱离热气流 | 52→35℃ | 1.5→28:1 | 55° | SOAR |
| SMP硬化·恢复扑动 | 35→23℃ | 28→100:1 | 55→0° | SOAR→FLAP |
交互
- AUTO:自动循环完整飞行场景
- MANUAL:拖动温度滑块直接控制环境温度,实时观察 SMP 刚度响应、翼面后掠与飞行模式切换——重点体验 45℃ 附近的相变临界区
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMP 热致变刚度机械鸟 · IFR 原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#060B18;--fg:#E2DAD0;--muted:#5A6B7E;
--cold:#22D3EE;--hot:#F59E0B;--thermal:#DC6B14;
--card:rgba(8,16,32,0.88);--border:rgba(90,107,126,0.18);
}
body{
background:var(--bg);color:var(--fg);
font-family:'JetBrains Mono',monospace;
min-height:100vh;display:flex;align-items:center;justify-content:center;
overflow:hidden;
}
#app{
width:97vw;max-width:1500px;aspect-ratio:14/9;
position:relative;border-radius:14px;overflow:hidden;
box-shadow:0 0 80px rgba(34,211,238,0.04),0 0 160px rgba(245,158,11,0.025);
border:1px solid var(--border);
}
#scene{width:100%;height:100%;display:block}
#phaseLabel{
position:absolute;top:18px;left:50%;transform:translateX(-50%);
font-family:'Cormorant Garamond',serif;font-size:24px;font-weight:700;
letter-spacing:2px;color:var(--cold);
text-shadow:0 0 28px rgba(34,211,238,0.35);
transition:color .5s,text-shadow .5s;z-index:10;
text-align:center;white-space:nowrap;
}
#ifrBadge{
position:absolute;top:18px;right:20px;
font-size:9px;font-weight:600;padding:5px 10px;border-radius:4px;
background:rgba(245,158,11,0.07);border:1px solid rgba(245,158,11,0.22);
color:var(--hot);letter-spacing:1.8px;text-transform:uppercase;z-index:10;
}
#controls{
position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
display:flex;align-items:center;gap:14px;
background:var(--card);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:9px;
padding:9px 18px;z-index:10;
}
.cb{
font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600;
padding:4px 11px;border-radius:4px;cursor:pointer;
border:1px solid var(--border);background:transparent;
color:var(--muted);transition:all .25s;letter-spacing:.6px;
}
.cb:focus-visible{outline:2px solid var(--cold);outline-offset:2px}
.cb.active{background:rgba(34,211,238,0.09);color:var(--cold);border-color:rgba(34,211,238,0.35)}
#controls label{font-size:10px;font-weight:500;color:var(--muted);white-space:nowrap}
#tempSlider{
-webkit-appearance:none;width:170px;height:3px;border-radius:2px;
outline:none;cursor:pointer;background:linear-gradient(to right,var(--cold),var(--hot));
}
#tempSlider::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;border-radius:50%;
border:2px solid var(--fg);background:var(--bg);cursor:pointer;
box-shadow:0 0 6px rgba(34,211,238,0.35);
}
#tempVal{font-size:11px;font-weight:500;min-width:40px;color:var(--cold);text-align:right}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:0s!important}}
@media(max-width:768px){
#controls{flex-wrap:wrap;justify-content:center;padding:7px 10px;gap:8px}
#tempSlider{width:110px}#phaseLabel{font-size:17px}
}
</style>
</head>
<body>
<div id="app">
<svg id="scene" viewBox="0 0 1400 900" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="skyG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#030710"/><stop offset="55%" stop-color="#0A1225"/>
<stop offset="82%" stop-color="#18152A"/><stop offset="100%" stop-color="#2D1B0E"/>
</linearGradient>
<linearGradient id="groundG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2D1B0E"/><stop offset="100%" stop-color="#150A04"/>
</linearGradient>
<linearGradient id="thermG" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#DC6B14" stop-opacity="0.38"/>
<stop offset="25%" stop-color="#DC6B14" stop-opacity="0.18"/>
<stop offset="65%" stop-color="#F59E0B" stop-opacity="0.06"/>
<stop offset="100%" stop-color="#F59E0B" stop-opacity="0"/>
</linearGradient>
<radialGradient id="thermGlow" cx="50%" cy="100%" r="55%">
<stop offset="0%" stop-color="#DC6B14" stop-opacity="0.14"/>
<stop offset="100%" stop-color="#DC6B14" stop-opacity="0"/>
</radialGradient>
<radialGradient id="heatSpot" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#F59E0B" stop-opacity="0.55"/>
<stop offset="55%" stop-color="#DC6B14" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#DC6B14" stop-opacity="0"/>
</radialGradient>
<radialGradient id="smpColdG"><stop offset="0%" stop-color="#67E8F9"/><stop offset="100%" stop-color="#22D3EE"/></radialGradient>
<radialGradient id="smpHotG"><stop offset="0%" stop-color="#FCD34D"/><stop offset="100%" stop-color="#F59E0B"/></radialGradient>
<linearGradient id="wingG" x1="1" y1="0" x2="0" y2="0">
<stop offset="0%" stop-color="#A8B2C0"/><stop offset="50%" stop-color="#8A94A4"/><stop offset="100%" stop-color="#6A7585"/>
</linearGradient>
<linearGradient id="bodyG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#C8D0DC"/><stop offset="50%" stop-color="#A0AAB8"/><stop offset="100%" stop-color="#7A8494"/>
</linearGradient>
<filter id="gCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="b"/><feFlood flood-color="#22D3EE" flood-opacity="0.65" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gAmber" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="7" result="b"/><feFlood flood-color="#F59E0B" flood-opacity="0.65" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<clipPath id="smpClip"><circle cx="0" cy="0" r="48"/></clipPath>
</defs>
<!-- 背景 -->
<rect width="1400" height="900" fill="url(#skyG)"/>
<g id="stars"></g>
<!-- 地面 -->
<rect x="0" y="780" width="1400" height="120" fill="url(#groundG)"/>
<path d="M0,780 Q200,774 400,780 Q600,786 800,780 Q1000,774 1200,780 Q1300,783 1400,780" stroke="#3D2B15" stroke-width="1" fill="none" opacity="0.5"/>
<ellipse id="heatSpotEl" cx="700" cy="800" rx="130" ry="25" fill="url(#heatSpot)" opacity="0"/>
<!-- 热气流 -->
<g id="thermalG" opacity="0">
<ellipse cx="700" cy="780" rx="200" ry="260" fill="url(#thermGlow)"/>
<rect x="600" y="180" width="200" height="600" fill="url(#thermG)" rx="100" opacity="0.85"/>
<g id="thermArrows" opacity="0.45">
<path d="M665,700 L665,620 L656,632 M665,620 L674,632" stroke="#F59E0B" stroke-width="1.5" fill="none" opacity="0.55"/>
<path d="M700,720 L700,590 L691,602 M700,590 L709,602" stroke="#F59E0B" stroke-width="2" fill="none" opacity="0.75"/>
<path d="M735,700 L735,620 L726,632 M735,620 L744,632" stroke="#F59E0B" stroke-width="1.5" fill="none" opacity="0.55"/>
</g>
<g id="shimmerLines"></g>
<g id="thermParticles"></g>
</g>
<!-- 轨迹圈 -->
<ellipse id="orbitPath" cx="700" cy="340" rx="90" ry="32" fill="none" stroke="#F59E0B" stroke-width="0.8" stroke-dasharray="5,4" opacity="0"/>
<!-- ===== 机械鸟 ===== -->
<g id="birdG">
<!-- 左翼 -->
<g id="lWing">
<path d="M0,-16 L-168,-7 L-168,7 L0,16 Z" fill="url(#wingG)" stroke="#9AA4B2" stroke-width="0.5" opacity="0.92"/>
<line x1="-45" y1="-12" x2="-45" y2="12" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="-90" y1="-10" x2="-90" y2="10" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="-135" y1="-8" x2="-135" y2="8" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="0" y1="0" x2="-168" y2="0" stroke="#8A94A4" stroke-width="0.7" opacity="0.15"/>
<circle id="lSMP" cx="-168" cy="0" r="8" fill="url(#smpColdG)" filter="url(#gCyan)"/>
<circle cx="-168" cy="0" r="3.5" fill="#0A1225" opacity="0.45"/>
<circle cx="-168" cy="0" r="1.8" fill="#22D3EE" opacity="0.25"/>
<g id="lOuter">
<path d="M0,-7 L-152,-2 L-152,2 L0,7 Z" fill="url(#wingG)" stroke="#9AA4B2" stroke-width="0.45" opacity="0.87"/>
<line x1="-55" y1="-4.5" x2="-55" y2="4.5" stroke="#7A8494" stroke-width="0.3" opacity="0.2"/>
<line x1="-105" y1="-3" x2="-105" y2="3" stroke="#7A8494" stroke-width="0.3" opacity="0.2"/>
<path d="M-152,-2 L-168,-16 L-157,0 Z" fill="#9AA4B2" stroke="#AAB4C2" stroke-width="0.4" opacity="0.72"/>
</g>
</g>
<!-- 右翼 -->
<g id="rWing">
<path d="M0,-16 L-168,-7 L-168,7 L0,16 Z" fill="url(#wingG)" stroke="#9AA4B2" stroke-width="0.5" opacity="0.92"/>
<line x1="-45" y1="-12" x2="-45" y2="12" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="-90" y1="-10" x2="-90" y2="10" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="-135" y1="-8" x2="-135" y2="8" stroke="#7A8494" stroke-width="0.35" opacity="0.25"/>
<line x1="0" y1="0" x2="-168" y2="0" stroke="#8A94A4" stroke-width="0.7" opacity="0.15"/>
<circle id="rSMP" cx="-168" cy="0" r="8" fill="url(#smpColdG)" filter="url(#gCyan)"/>
<circle cx="-168" cy="0" r="3.5" fill="#0A1225" opacity="0.45"/>
<circle cx="-168" cy="0" r="1.8" fill="#22D3EE" opacity="0.25"/>
<g id="rOuter">
<path d="M0,-7 L-152,-2 L-152,2 L0,7 Z" fill="url(#wingG)" stroke="#9AA4B2" stroke-width="0.45" opacity="0.87"/>
<line x1="-55" y1="-4.5" x2="-55" y2="4.5" stroke="#7A8494" stroke-width="0.3" opacity="0.2"/>
<line x1="-105" y1="-3" x2="-105" y2="3" stroke="#7A8494" stroke-width="0.3" opacity="0.2"/>
<path d="M-152,-2 L-168,-16 L-157,0 Z" fill="#9AA4B2" stroke="#AAB4C2" stroke-width="0.4" opacity="0.72"/>
</g>
</g>
<!-- 机身 -->
<path d="M0,-75 C17,-63 20,-28 19,14 C17,48 9,68 0,75 C-9,68 -17,48 -19,14 C-20,-28 -17,-63 0,-75 Z" fill="url(#bodyG)" stroke="#B0BAC8" stroke-width="0.6" opacity="0.95"/>
<line x1="0" y1="-65" x2="0" y2="65" stroke="#9AA4B2" stroke-width="0.35" opacity="0.12"/>
<path d="M-13,-38 C-10,-18 -10,18 -13,38" stroke="#8A94A4" stroke-width="0.3" opacity="0.12" fill="none"/>
<path d="M13,-38 C10,-18 10,18 13,38" stroke="#8A94A4" stroke-width="0.3" opacity="0.12" fill="none"/>
<!-- 头 -->
<ellipse cx="0" cy="-78" rx="10" ry="13" fill="#B8C2D0" stroke="#A0AAB8" stroke-width="0.4"/>
<circle cx="-4.5" cy="-82" r="2" fill="#0A1225"/><circle cx="4.5" cy="-82" r="2" fill="#0A1225"/>
<circle cx="-4" cy="-82.5" r="0.6" fill="#4A6A8A"/><circle cx="5" cy="-82.5" r="0.6" fill="#4A6A8A"/>
<path d="M-2.5,-91 L0,-104 L2.5,-91 Z" fill="#D4A050" stroke="#C49040" stroke-width="0.35"/>
<!-- 尾 -->
<path d="M-7,70 L-20,98 L-10,90 L0,106 L10,90 L20,98 L7,70 Z" fill="#8892A0" stroke="#9AA4B2" stroke-width="0.4" opacity="0.82"/>
<!-- 传感器 -->
<g id="sensors">
<circle class="sen" cx="-4.5" cy="-28" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="4.5" cy="-28" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="-4.5" cy="-8" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="4.5" cy="-8" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="0" cy="12" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="-3.5" cy="32" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
<circle class="sen" cx="3.5" cy="32" r="2.2" fill="#0A1828" stroke="#22D3EE" stroke-width="0.7" opacity="0.45"/>
</g>
</g>
<!-- 标注 -->
<g id="labels" font-family="'JetBrains Mono',monospace" font-size="10" font-weight="500">
<g id="lblSMP">
<line id="smpLd" x1="620" y1="365" x2="510" y2="420" stroke="#22D3EE" stroke-width="0.7" stroke-dasharray="3,3" opacity="0.35"/>
<rect id="smpBox" x="410" y="410" width="100" height="20" rx="3" fill="rgba(34,211,238,0.05)" stroke="rgba(34,211,238,0.22)" stroke-width="0.5"/>
<text id="smpTxt" x="460" y="424" text-anchor="middle" fill="#22D3EE" font-size="9.5">SMP 铰链</text>
</g>
<g id="lblSen">
<line x1="705" y1="375" x2="800" y2="435" stroke="#22D3EE" stroke-width="0.7" stroke-dasharray="3,3" opacity="0.35"/>
<rect x="775" y="425" width="120" height="20" rx="3" fill="rgba(34,211,238,0.05)" stroke="rgba(34,211,238,0.22)" stroke-width="0.5"/>
<text x="835" y="439" text-anchor="middle" fill="#22D3EE" font-size="9.5">微压传感器阵列</text>
</g>
<g id="lblWing">
<line x1="480" y1="348" x2="390" y2="300" stroke="#8A94A4" stroke-width="0.7" stroke-dasharray="3,3" opacity="0.25"/>
<rect x="310" y="289" width="80" height="20" rx="3" fill="rgba(138,148,164,0.05)" stroke="rgba(138,148,164,0.18)" stroke-width="0.5"/>
<text x="350" y="303" text-anchor="middle" fill="#8A94A4" font-size="9.5">偏流小翼</text>
</g>
</g>
<!-- SMP 微观放大镜 -->
<g id="smpInset" transform="translate(120,680)" opacity="0">
<circle cx="0" cy="0" r="52" fill="#080E1E" stroke="#4A5A6E" stroke-width="0.8" opacity="0.92"/>
<circle cx="0" cy="0" r="52" fill="none" stroke="#22D3EE" stroke-width="0.4" opacity="0.2"/>
<g id="smpGridG" clip-path="url(#smpClip)"></g>
<text x="0" y="68" text-anchor="middle" fill="#5A6B7E" font-family="'JetBrains Mono',monospace" font-size="8" font-weight="500">SMP 微观结构</text>
<line id="insetLine" x1="48" y1="-28" x2="530" y2="-320" stroke="#22D3EE" stroke-width="0.5" stroke-dasharray="3,4" opacity="0.15"/>
</g>
<!-- HUD 左 -->
<g id="hudL" transform="translate(42,170)" font-family="'JetBrains Mono',monospace">
<text x="16" y="0" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">TEMP</text>
<rect x="3" y="8" width="26" height="180" rx="13" fill="rgba(8,14,30,0.55)" stroke="rgba(90,107,126,0.15)" stroke-width="0.6"/>
<rect id="tBar" x="5" y="148" width="22" height="38" rx="11" fill="#22D3EE" opacity="0.65"/>
<text id="tTxt" x="16" y="208" text-anchor="middle" fill="#22D3EE" font-size="10" font-weight="500">25℃</text>
<text x="16" y="238" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">STIFF</text>
<text id="stTxt" x="16" y="256" text-anchor="middle" fill="#22D3EE" font-size="13" font-weight="600">100:1</text>
<text x="16" y="286" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">SWEEP</text>
<text id="swTxt" x="16" y="304" text-anchor="middle" fill="#22D3EE" font-size="13" font-weight="600">0°</text>
</g>
<!-- HUD 右 -->
<g id="hudR" transform="translate(1342,170)" font-family="'JetBrains Mono',monospace">
<text x="16" y="0" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">ALT</text>
<text id="altTxt" x="16" y="22" text-anchor="middle" fill="#8A94A4" font-size="14" font-weight="500">120m</text>
<text x="16" y="55" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">MODE</text>
<text id="modeTxt" x="16" y="75" text-anchor="middle" fill="#22D3EE" font-size="12" font-weight="600">FLAP</text>
<text x="16" y="108" text-anchor="middle" fill="#5A6B7E" font-size="8" font-weight="600" letter-spacing="1.2">ENERGY</text>
<text id="nrgTxt" x="16" y="128" text-anchor="middle" fill="#5A6B7E" font-size="10" font-weight="500">ACTIVE</text>
</g>
<!-- IFR 底部注释 -->
<g id="ifrNote" transform="translate(700,755)" opacity="0" font-family="'JetBrains Mono',monospace">
<rect x="-280" y="-14" width="560" height="28" rx="5" fill="rgba(245,158,11,0.04)" stroke="rgba(245,158,11,0.18)" stroke-width="0.5"/>
<text x="0" y="4" text-anchor="middle" fill="#F59E0B" font-size="11" font-weight="500">环境热能 → SMP 刚度转变 → 气动载荷驱动构型 · 零伺服能耗</text>
</g>
<!-- 时间线 -->
<g id="timeline" transform="translate(100,870)" font-family="'JetBrains Mono',monospace" font-size="7">
<rect x="0" y="-3" width="1200" height="6" rx="3" fill="rgba(90,107,126,0.08)"/>
<rect x="0" y="-3" width="180" height="6" rx="1.5" fill="rgba(34,211,238,0.12)"/>
<rect x="180" y="-3" width="120" height="6" fill="rgba(34,211,238,0.2)"/>
<rect x="300" y="-3" width="180" height="6" fill="rgba(245,158,11,0.15)"/>
<rect x="480" y="-3" width="300" height="6" fill="rgba(245,158,11,0.28)"/>
<rect x="780" y="-3" width="180" height="6" fill="rgba(34,211,238,0.15)"/>
<rect x="960" y="-3" width="240" height="6" rx="1.5" fill="rgba(34,211,238,0.12)"/>
<rect id="playhead" x="0" y="-5" width="2.5" height="10" rx="1.25" fill="#E2DAD0" opacity="0.75"/>
<text x="90" y="15" text-anchor="middle" fill="#4A5A6E">扑动</text>
<text x="240" y="15" text-anchor="middle" fill="#4A5A6E">探测</text>
<text x="390" y="15" text-anchor="middle" fill="#4A5A6E">SMP转化</text>
<text x="630" y="15" text-anchor="middle" fill="#4A5A6E">盘旋滑翔</text>
<text x="870" y="15" text-anchor="middle" fill="#4A5A6E">SMP恢复</text>
<text x="1080" y="15" text-anchor="middle" fill="#4A5A6E">扑动</text>
</g>
</svg>
<div id="phaseLabel">扑动飞行模式</div>
<div id="ifrBadge">IFR · 最终理想解</div>
<div id="controls">
<button class="cb active" id="autoBtn" aria-label="自动播放">AUTO</button>
<button class="cb" id="manBtn" aria-label="手动控制">MANUAL</button>
<label for="tempSlider">环境温度</label>
<input type="range" id="tempSlider" min="20" max="55" value="25" step="0.5" aria-label="温度控制"/>
<span id="tempVal">25℃</span>
</div>
</div>
<script>
(function(){
'use strict';
/* ===== 常量 ===== */
var CYCLE=16000, SMP_Tg=45, T_MIN=20, T_MAX=55;
var SWEEP_MAX=55, FLAP_AMP=6, FLAP_FREQ=4.5;
var BCX=700, BCY=350;
/* ===== DOM ===== */
var svg=document.getElementById('scene');
var birdG=document.getElementById('birdG');
var lWing=document.getElementById('lWing');
var rWing=document.getElementById('rWing');
var lOuter=document.getElementById('lOuter');
var rOuter=document.getElementById('rOuter');
var lSMP=document.getElementById('lSMP');
var rSMP=document.getElementById('rSMP');
var thermalG=document.getElementById('thermalG');
var thermParticlesG=document.getElementById('thermParticles');
var shimmerG=document.getElementById('shimmerLines');
var sensors=document.getElementById('sensors');
var orbitPath=document.getElementById('orbitPath');
var heatSpotEl=document.getElementById('heatSpotEl');
var smpInset=document.getElementById('smpInset');
var smpGridG=document.getElementById('smpGridG');
var ifrNote=document.getElementById('ifrNote');
var phaseLabel=document.getElementById('phaseLabel');
var playhead=document.getElementById('playhead');
var tBar=document.getElementById('tBar');
var tTxt=document.getElementById('tTxt');
var stTxt=document.getElementById('stTxt');
var swTxt=document.getElementById('swTxt');
var altTxt=document.getElementById('altTxt');
var modeTxt=document.getElementById('modeTxt');
var nrgTxt=document.getElementById('nrgTxt');
var tempSlider=document.getElementById('tempSlider');
var tempValEl=document.getElementById('tempVal');
var autoBtn=document.getElementById('autoBtn');
var manBtn=document.getElementById('manBtn');
var smpLd=document.getElementById('smpLd');
var smpBox=document.getElementById('smpBox');
var smpTxt=document.getElementById('smpTxt');
/* ===== 状态 ===== */
var autoMode=true, manualTemp=25, startTime=null;
var particles=[], gridLines=[], shimmers=[];
/* ===== 工具 ===== */
function lerp(a,b,t){return a+(b-a)*clamp01(t)}
function clamp01(t){return Math.max(0,Math.min(1,t))}
function smoothstep(e0,e1,x){var t=clamp01((x-e0)/(e1-e0));return t*t*(3-2*t)}
/* ===== 初始化:星空 ===== */
function initStars(){
var g=document.getElementById('stars');
for(var i=0;i<70;i++){
var c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',String(Math.random()*1400));
c.setAttribute('cy',String(Math.random()*700));
c.setAttribute('r',String(Math.random()*1.1+0.3));
c.setAttribute('fill','#E2DAD0');
c.setAttribute('opacity',String(Math.random()*0.25+0.04));
g.appendChild(c);
}
}
/* ===== 初始化:热气流粒子 ===== */
function initParticles(){
for(var i=0;i<55;i++){
var c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('r',String(Math.random()*2.5+1));
c.setAttribute('fill',Math.random()>0.5?'#F59E0B':'#DC6B14');
c.setAttribute('opacity','0');
thermParticlesG.appendChild(c);
particles.push({
el:c, x:620+Math.random()*160, y:770+Math.random()*40,
vy:-(0.45+Math.random()*0.7), vx:(Math.random()-0.5)*0.18,
ph:Math.random()*Math.PI*2, life:Math.random(), ml:0.55+Math.random()*0.45
});
}
}
/* ===== 初始化:热浪线 ===== */
function initShimmers(){
for(var i=0;i<6;i++){
var p=document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('stroke','#F59E0B');
p.setAttribute('stroke-width','0.6');
p.setAttribute('fill','none');
p.setAttribute('opacity','0');
shimmerG.appendChild(p);
shimmers.push({el:p, baseY:300+i*70, amp:3+i*1.5, freq:0.008+i*0.002, speed:0.0012+i*0.0003});
}
}
/* ===== 初始化:SMP 网格 ===== */
function initGrid(){
var sp=11, rng=44;
for(var i=-rng;i<=rng;i+=sp){
var lh=document.createElementNS('http://www.w3.org/2000/svg','line');
lh.setAttribute('x1',String(-rng));lh.setAttribute('y1',String(i));
lh.setAttribute('x2',String(rng));lh.setAttribute('y2',String(i));
lh.setAttribute('stroke','#22D3EE');lh.setAttribute('stroke-width','0.55');lh.setAttribute('opacity','0.45');
smpGridG.appendChild(lh);gridLines.push({el:lh,t:'h',i:i});
var lv=document.createElementNS('http://www.w3.org/2000/svg','line');
lv.setAttribute('x1',String(i));lv.setAttribute('y1',String(-rng));
lv.setAttribute('x2',String(i));lv.setAttribute('y2',String(rng));
lv.setAttribute('stroke','#22D3EE');lv.setAttribute('stroke-width','0.55');lv.setAttribute('opacity','0.45');
smpGridG.appendChild(lv);gridLines.push({el:lv,t:'v',i:i});
}
/* 交叉节点 */
for(var ix=-rng;ix<=rng;ix+=sp){
for(var iy=-rng;iy<=rng;iy+=sp){
if(ix*ix+iy*iy>48*48)continue;
var dot=document.createElementNS('http://www.w3.org/2000/svg','circle');
dot.setAttribute('cx',String(ix));dot.setAttribute('cy',String(iy));
dot.setAttribute('r','1.2');dot.setAttribute('fill','#22D3EE');dot.setAttribute('opacity','0.3');
smpGridG.appendChild(dot);gridLines.push({el:dot,t:'d',ix:ix,iy:iy});
}
}
}
/* ===== 更新:热气流粒子 ===== */
function updateParticles(dt,tOp){
particles.forEach(function(p){
p.life+=dt*0.00065;
if(p.life>p.ml){p.life=0;p.x=620+Math.random()*160;p.y=760+Math.random()*40;p.vy=-(0.45+Math.random()*0.7);p.vx=(Math.random()-0.5)*0.18;}
p.x+=p.vx+Math.sin(p.life*5+p.ph)*0.18;
p.y+=p.vy;
var fade=1-(p.life/p.ml);
var op=fade*tOp*0.45;
p.el.setAttribute('cx',String(p.x));p.el.setAttribute('cy',String(p.y));
p.el.setAttribute('opacity',String(Math.max(0,op)));
});
}
/* ===== 更新:热浪线 ===== */
function updateShimmers(time,tOp){
shimmers.forEach(function(s){
if(tOp<0.05){s.el.setAttribute('opacity','0');return;}
var d='';var by=s.baseY;
for(var x=600;x<=800;x+=4){
var y=by+s.amp*Math.sin(x*s.freq+time*s.speed);
d+=(x===600?'M':'L')+x.toFixed(1)+','+y.toFixed(1);
}
s.el.setAttribute('d',d);
s.el.setAttribute('opacity',String(tOp*0.25));
});
}
/* ===== 更新:SMP 网格 ===== */
function updateGrid(softness,time){
gridLines.forEach(function(l){
var el=l.el;
if(l.t==='h'){
var w=softness*4.5*Math.sin(l.i*0.28+time*0.0025);
el.setAttribute('y1',String(l.i+w));el.setAttribute('y2',String(l.i+w));
var dx=softness*3*Math.sin(time*0.0018+l.i*0.45);
el.setAttribute('x1',String(-44+dx));el.setAttribute('x2',String(44+dx));
}else if(l.t==='v'){
var w=softness*4.5*Math.sin(l.i*0.28+time*0.0025+1.2);
el.setAttribute('x1',String(l.i+w));el.setAttribute('x2',String(l.i+w));
var dy=softness*3*Math.sin(time*0.0018+l.i*0.45+2.1);
el.setAttribute('y1',String(-44+dy));el.setAttribute('y2',String(44+dy));
}else{
var wx=softness*3*Math.sin(l.ix*0.3+time*0.003);
var wy=softness*3*Math.sin(l.iy*0.3+time*0.003+1);
el.setAttribute('cx',String(l.ix+wx));el.setAttribute('cy',String(l.iy+wy));
}
var r=Math.round(lerp(34,245,softness));
var g=Math.round(lerp(211,158,softness));
var b=Math.round(lerp(238,11,softness));
el.setAttribute('stroke'||(l.t==='d'?'fill':'stroke'), 'rgb('+r+','+g+','+b+')');
if(l.t==='d'){el.setAttribute('fill','rgb('+r+','+g+','+b+')');}
else{el.setAttribute('stroke','rgb('+r+','+g+','+b+')');}
el.setAttribute('opacity',String(lerp(0.45,0.25,softness)));
});
}
/* ===== 相位状态计算 ===== */
function getPhase(cp){
var t=cp,temp,sweep,flap,therm,alt,name,stiff,orbit;
if(t<0.15){
var p=t/0.15;temp=lerp(22,26,p);sweep=0;flap=true;therm=0;alt=lerp(120,130,p);name='扑动飞行模式';stiff=1;orbit=0;
}else if(t<0.25){
var p=(t-0.15)/0.10;temp=lerp(26,40,p);sweep=0;flap=true;therm=smoothstep(0,0.55,p);alt=lerp(130,150,p);name='传感器探测热气流';stiff=lerp(1,0.45,p);orbit=0;
}else if(t<0.40){
var p=(t-0.25)/0.15;temp=lerp(40,50,p);sweep=smoothstep(0,1,p)*SWEEP_MAX;flap=p<0.4;therm=lerp(0.55,1,p);alt=lerp(150,220,p);name='SMP 软化 · 构型切换';stiff=lerp(0.45,0.02,p);orbit=0;
}else if(t<0.65){
var p=(t-0.40)/0.25;temp=lerp(50,52,Math.sin(p*Math.PI));sweep=SWEEP_MAX;flap=false;therm=1;alt=lerp(220,400,p);name='热气流盘旋滑翔';stiff=0.02;orbit=p*Math.PI*4;
}else if(t<0.80){
var p=(t-0.65)/0.15;temp=lerp(52,34,p);sweep=lerp(SWEEP_MAX,0,smoothstep(0,1,p));flap=p>0.5;therm=lerp(1,0,p);alt=lerp(400,190,p);name='脱离热气流 · SMP 硬化';stiff=lerp(0.02,0.75,p);orbit=0;
}else{
var p=(t-0.80)/0.20;temp=lerp(34,22,p);sweep=0;flap=true;therm=0;alt=lerp(190,120,p);name='扑动飞行模式';stiff=lerp(0.75,1,p);orbit=0;
}
return{temp:temp,sweep:sweep,flap:flap,therm:therm,alt:alt,name:name,stiff:stiff,orbit:orbit};
}
/* 手动模式状态 */
function getManual(temp){
var soft=smoothstep(SMP_Tg-5,SMP_Tg+5,temp);
var sweep=soft*SWEEP_MAX;var flap=soft<0.5;var therm=smoothstep(28,45,temp);
var stiff=lerp(1,0.02,soft);var alt=lerp(120,400,soft);
var name=soft<0.05?'扑动飞行模式':soft<0.3?'SMP 软化 · 构型切换':'热气流盘旋滑翔';
return{temp:temp,sweep:sweep,flap:flap,therm:therm,alt:alt,name:name,stiff:stiff,orbit:0};
}
/* ===== 颜色工具 ===== */
function smpCol(s){
var t=1-s;
return'rgb('+Math.round(lerp(34,245,t))+','+Math.round(lerp(211,158,t))+','+Math.round(lerp(238,11,t))+')';
}
function smpFilt(s){return s<0.4?'url(#gAmber)':'url(#gCyan)'}
/* ===== 传感器脉冲 ===== */
function updateSensors(time,therm){
var ss=sensors.querySelectorAll('.sen');
ss.forEach(function(s,i){
var pulse=therm>0.1?0.5+0.5*Math.sin(time*0.005+i*1.4):0.2;
var op=lerp(0.15,0.85,pulse*Math.max(0.08,therm));
s.setAttribute('opacity',String(op));
var act=therm>0.25;
s.setAttribute('stroke',act?'#F59E0B':'#22D3EE');
s.setAttribute('stroke-width',String(act?lerp(0.7,1.8,pulse):0.7));
});
}
/* ===== 主渲染 ===== */
function render(time,st){
var temp=st.temp,sweep=st.sweep,flap=st.flap,therm=st.therm;
var alt=st.alt,name=st.name,stiff=st.stiff,orbit=st.orbit;
/* 鸟位置 */
var birdY=BCY-(alt-120)*0.28;
var birdX=BCX;
if(orbit!==0){birdX+=Math.cos(orbit)*75;birdY+=Math.sin(orbit)*22;}
var bank=therm>0.65?Math.sin(orbit||0)*7:0;
birdG.setAttribute('transform','translate('+birdX.toFixed(1)+','+birdY.toFixed(1)+') rotate('+bank.toFixed(1)+')');
/* 扑动 */
var flapA=flap?Math.sin(time*0.001*FLAP_FREQ*Math.PI*2)*FLAP_AMP:0;
var lax=-20,lay=-13;
lWing.setAttribute('transform','translate('+lax+','+lay+') rotate('+flapA.toFixed(2)+')');
lOuter.setAttribute('transform','translate(-168,0) rotate('+(-sweep).toFixed(2)+')');
var rax=20,ray=-13;
rWing.setAttribute('transform','translate('+rax+','+ray+') scale(-1,1) rotate('+(-flapA).toFixed(2)+')');
rOuter.setAttribute('transform','translate(-168,0) rotate('+sweep.toFixed(2)+')');
/* SMP 颜色 */
var sc=smpCol(stiff);var sf=smpFilt(stiff);
[lSMP,rSMP].forEach(function(s){s.setAttribute('fill',sc);s.setAttribute('filter',sf);});
/* 热气流 */
var tOp=smoothstep(0,0.25,therm);
thermalG.setAttribute('opacity',String(tOp));
heatSpotEl.setAttribute('opacity',String(tOp*0.75));
orbitPath.setAttribute('opacity',String(smoothstep(0.55,0.85,therm)*0.35));
/* 传感器 */
updateSensors(time,therm);
/* SMP 放大镜 */
var insetOp=smoothstep(0.15,0.4,1-stiff);
smpInset.setAttribute('opacity',String(insetOp));
if(insetOp>0.05)updateGrid(1-stiff,time);
/* IFR 注释 */
ifrNote.setAttribute('opacity',String(smoothstep(0.35,0.6,therm)));
/* 标注颜色 */
smpTxt.setAttribute('fill',sc);
smpLd.setAttribute('stroke',sc);
smpBox.setAttribute('stroke',sc+'38');
smpBox.setAttribute('fill',sc+'0d');
/* HUD */
var tn=clamp01((temp-T_MIN)/(T_MAX-T_MIN));
var bH=lerp(25,170,tn);var bY=186-bH;
tBar.setAttribute('y',String(bY));tBar.setAttribute('height',String(bH));tBar.setAttribute('fill',sc);
tTxt.textContent=Math.round(temp)+'℃';tTxt.setAttribute('fill',sc);
stTxt.textContent=Math.round(stiff*100)+':1';stTxt.setAttribute('fill',sc);
swTxt.textContent=Math.round(sweep)+'°';swTxt.setAttribute('fill',sc);
altTxt.textContent=Math.round(alt)+'m';
modeTxt.textContent=flap?'FLAP':'SOAR';modeTxt.setAttribute('fill',therm>0.45?'#F59E0B':'#22D3EE');
nrgTxt.textContent=therm>0.25?'PARASITIC':'ACTIVE';nrgTxt.setAttribute('fill',therm>0.25?'#F59E0B':'#5A6B7E');
/* 阶段标签 */
phaseLabel.textContent=name;phaseLabel.style.color=sc;
phaseLabel.style.textShadow='0 0 24px '+sc+'44';
/* 同步滑块 */
if(autoMode){tempSlider.value=temp;tempValEl.textContent=Math.round(temp)+'℃';tempValEl.style.color=sc;}
}
/* ===== 主循环 ===== */
function animate(ts){
if(!startTime)startTime=ts;
var elapsed=ts-startTime;
var st;
if(autoMode){
var cp=(elapsed%CYCLE)/CYCLE;
st=getPhase(cp);
playhead.setAttribute('x',String(cp*1200));
}else{
st=getManual(manualTemp);
}
updateParticles(16,st.therm);
updateShimmers(elapsed,st.therm);
render(elapsed,st);
requestAnimationFrame(animate);
}
/* ===== 控制 ===== */
autoBtn.addEventListener('click',function(){
autoMode=true;autoBtn.classList.add('active');manBtn.classList.remove('active');startTime=null;
});
manBtn.addEventListener('click',function(){
autoMode=false;manBtn.classList.add('active');autoBtn.classList.remove('active');
});
tempSlider.addEventListener('input',function(){
if(!autoMode){
manualTemp=parseFloat(this.value);
var s=getManual(manualTemp);var sc=smpCol(s.stiff);
tempValEl.textContent=Math.round(manualTemp)+'℃';tempValEl.style.color=sc;
}
});
/* ===== 初始化 ===== */
function init(){
initStars();initParticles();initShimmers();initGrid();
requestAnimationFrame(animate);
}
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}
else{init();}
})();
</script>
</body>
</html>
实现说明
本动画围绕 TRIZ 最终理想解(IFR) 思想,完整呈现 SMP 热致变刚度机械鸟的工作原理,核心设计要点如下:
IFR 叙事架构
动画以 16 秒为周期自动循环,直接展示理想解的运作——环境热能被动驱动构型切换,零伺服能耗。底部 IFR 注释在盘旋阶段浮现,明确点出"环境热能 → SMP 刚度转变 → 气动载荷驱动构型 · 零伺服能耗"的核心逻辑。
核心创新的视觉引导
- SMP 铰链:全场视觉焦点,颜色从青色(刚性锁定)到琥珀色(软化屈服)连续渐变,配合辉光滤镜切换,后掠时 SMP 放大镜视口同步显示微观网格从规则→波浪的形变过程
- 翼面后掠:外翼段在腕关节处旋转最大 55°,清晰展示气动载荷驱动的被动折叠
- 传感器阵列:腹部传感器在检测到热气流时脉冲闪烁,颜色从青→琥珀切换
- 热气流可视化:上升粒子 + 热浪线 + 地面热斑三重联动,营造强烈的环境能量氛围
- HUD 能量指示:盘旋时显示"PARASITIC"(寄生利用),扑动时显示"ACTIVE",直指 IFR 资源利用内核
交互设计
- AUTO / MANUAL 模式:自动模式循环播放完整飞行场景;手动模式下拖动温度滑块可直接控制环境温度(20℃–55℃),实时观察 SMP 相变、翼面后掠和飞行模式切换
- 底部时间线:显示当前播放位置和各阶段分段,帮助理解动作时序
大气与细节
- 深空星空背景 + 地形线 + 热斑,营造高空飞行氛围
- 盘旋阶段鸟体沿椭圆轨道运动并带有银行角倾斜,模拟真实盘旋
- SMP 微观放大镜通过连接线指向实际铰链位置,建立宏观-微观视觉关联
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMP 热致变刚度仿生鸟 · IFR 原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#090d18;--fg:#c8d6e5;--muted:#4a5c72;
--cold:#00d2b4;--hot:#ff6b35;--ifr:#fbbf24;
--card:rgba(12,18,36,0.92);--border:rgba(74,92,114,0.22);
--surface:#131b2e;
}
body{
background:var(--bg);color:var(--fg);
font-family:'Chakra Petch',sans-serif;
min-height:100vh;display:flex;flex-direction:column;
align-items:center;justify-content:center;
overflow:hidden;
}
#wrap{
width:97vw;max-width:1500px;aspect-ratio:16/9;
position:relative;border-radius:14px;overflow:hidden;
box-shadow:0 0 60px rgba(0,210,180,0.04),0 0 160px rgba(255,107,53,0.03);
border:1px solid var(--border);
}
#scene{width:100%;height:100%;display:block}
#phaseBar{
position:absolute;top:20px;left:50%;transform:translateX(-50%);
font-size:15px;font-weight:700;letter-spacing:1.2px;
text-transform:uppercase;text-align:center;
transition:color .4s,text-shadow .4s;z-index:10;white-space:nowrap;
padding:6px 22px;border-radius:8px;
background:rgba(9,13,24,0.7);backdrop-filter:blur(8px);
border:1px solid var(--border);
}
#ifrTag{
position:absolute;top:20px;right:22px;
font-size:10px;font-weight:700;letter-spacing:1px;
padding:5px 12px;border-radius:5px;
background:rgba(251,191,36,0.08);border:1px solid rgba(251,191,36,0.3);
color:var(--ifr);text-transform:uppercase;z-index:10;
}
#ctrl{
position:absolute;bottom:18px;left:50%;transform:translateX(-50%);
display:flex;align-items:center;gap:16px;
background:var(--card);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:10px;
padding:10px 22px;z-index:10;
}
#ctrl label{font-size:12px;font-weight:600;color:var(--muted);white-space:nowrap}
#tSlider{
-webkit-appearance:none;width:180px;height:5px;
border-radius:3px;outline:none;cursor:pointer;
background:linear-gradient(to right,var(--cold),var(--hot));
}
#tSlider::-webkit-slider-thumb{
-webkit-appearance:none;width:16px;height:16px;
border-radius:50%;border:2px solid var(--fg);
background:var(--bg);cursor:pointer;
box-shadow:0 0 6px rgba(0,210,180,0.5);
}
#tVal{font-family:'Space Mono',monospace;font-size:13px;min-width:44px;text-align:right;color:var(--cold)}
.mbtn{
font-family:'Chakra Petch',sans-serif;font-size:11px;font-weight:700;
padding:5px 12px;border-radius:5px;cursor:pointer;
border:1px solid var(--border);background:transparent;
color:var(--muted);transition:all .25s;
text-transform:uppercase;letter-spacing:.5px;
}
.mbtn.on{
background:rgba(0,210,180,0.1);color:var(--cold);
border-color:var(--cold);box-shadow:0 0 10px rgba(0,210,180,0.12);
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:0s!important}}
@media(max-width:800px){
#ctrl{flex-wrap:wrap;justify-content:center;gap:8px;padding:8px 14px}
#tSlider{width:120px}
#phaseBar{font-size:12px}
}
</style>
</head>
<body>
<div id="wrap">
<svg id="scene" viewBox="0 0 1600 900" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 背景渐变 -->
<radialGradient id="bgG" cx="50%" cy="55%" r="70%">
<stop offset="0%" stop-color="#0f1628"/>
<stop offset="100%" stop-color="#060910"/>
</radialGradient>
<!-- 地面渐变 -->
<linearGradient id="gndG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0c1220" stop-opacity="0"/>
<stop offset="60%" stop-color="#101820" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#141e28" stop-opacity="0.7"/>
</linearGradient>
<!-- 热区渐变 -->
<radialGradient id="thG" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff6b35" stop-opacity="0.22"/>
<stop offset="50%" stop-color="#ff6b35" stop-opacity="0.08"/>
<stop offset="100%" stop-color="#ff6b35" stop-opacity="0"/>
</radialGradient>
<!-- SMP 冷色 -->
<radialGradient id="smpC">
<stop offset="0%" stop-color="#00ffd5"/>
<stop offset="100%" stop-color="#00a89c"/>
</radialGradient>
<!-- SMP 热色 -->
<radialGradient id="smpH">
<stop offset="0%" stop-color="#ffab40"/>
<stop offset="100%" stop-color="#ff5722"/>
</radialGradient>
<!-- 翼面 -->
<linearGradient id="wgG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#8a9aae"/>
<stop offset="100%" stop-color="#5c6e82"/>
</linearGradient>
<linearGradient id="bdG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#b8c4d0"/>
<stop offset="100%" stop-color="#7a8c9e"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="gC" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="b"/>
<feFlood flood-color="#00d2b4" flood-opacity="0.7" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/>
<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gH" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="7" result="b"/>
<feFlood flood-color="#ff6b35" flood-opacity="0.7" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/>
<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="gIfr" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="3" result="b"/>
<feFlood flood-color="#fbbf24" flood-opacity="0.5" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/>
<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- 背景层 -->
<rect width="1600" height="900" fill="url(#bgG)"/>
<rect y="600" width="1600" height="300" fill="url(#gndG)"/>
<!-- 网格 -->
<g opacity="0.04" stroke="#4a6a8a" stroke-width="0.5">
<line x1="0" y1="180" x2="1600" y2="180"/><line x1="0" y1="360" x2="1600" y2="360"/>
<line x1="0" y1="540" x2="1600" y2="540"/><line x1="0" y1="720" x2="1600" y2="720"/>
<line x1="200" y1="0" x2="200" y2="900"/><line x1="400" y1="0" x2="400" y2="900"/>
<line x1="600" y1="0" x2="600" y2="900"/><line x1="800" y1="0" x2="800" y2="900"/>
<line x1="1000" y1="0" x2="1000" y2="900"/><line x1="1200" y1="0" x2="1200" y2="900"/>
<line x1="1400" y1="0" x2="1400" y2="900"/>
</g>
<!-- 热气流区域 -->
<g id="thZone" opacity="0">
<ellipse cx="900" cy="430" rx="300" ry="260" fill="url(#thG)"/>
<!-- 热气流核心 -->
<ellipse cx="900" cy="430" rx="120" ry="100" fill="#ff6b35" opacity="0.04"/>
<!-- 扩展波纹容器 -->
<g id="thRipples"></g>
<!-- 上升粒子容器 -->
<g id="thParts"></g>
<!-- 上升箭头 -->
<g opacity="0.35" stroke="#ff6b35" stroke-width="1.8" fill="none">
<path d="M840,520 L840,440 L830,455 M840,440 L850,455"/>
<path d="M900,550 L900,420 L890,435 M900,420 L910,435" stroke-width="2.2"/>
<path d="M960,520 L960,440 L950,455 M960,440 L970,455"/>
</g>
<!-- 热区标注 -->
<text x="900" y="700" text-anchor="middle" fill="#ff6b35" font-family="'Chakra Petch',sans-serif" font-size="13" font-weight="600" opacity="0.6">
热上升气流区域
</text>
</g>
<!-- 机械鸟主体 -->
<g id="bird">
<!-- 阴影(随高度偏移) -->
<ellipse id="bShadow" cx="0" cy="0" rx="90" ry="20" fill="#000" opacity="0.15"/>
<!-- 左翼组 -->
<g id="lwG">
<!-- 内翼 -->
<path d="M0,-8 L-14,-160 L14,-160 L0,8 Z" fill="url(#wgG)" stroke="#8a9aae" stroke-width="0.6" opacity="0.92"/>
<!-- 翼肋线 -->
<line x1="-4" y1="-50" x2="4" y2="-50" stroke="#6a7a8e" stroke-width="0.4" opacity="0.35"/>
<line x1="-7" y1="-90" x2="7" y2="-90" stroke="#6a7a8e" stroke-width="0.4" opacity="0.3"/>
<line x1="-10" y1="-130" x2="10" y2="-130" stroke="#6a7a8e" stroke-width="0.4" opacity="0.25"/>
<!-- SMP 铰链 -->
<circle id="lSmp" cx="0" cy="-160" r="10" fill="url(#smpC)" filter="url(#gC)"/>
<circle cx="0" cy="-160" r="4" fill="#0a0f1c" opacity="0.4"/>
<!-- 铰链内环 -->
<circle cx="0" cy="-160" r="6.5" fill="none" stroke="#0a0f1c" stroke-width="0.8" opacity="0.25"/>
<!-- 外翼组 -->
<g id="lwO">
<path d="M-10,-160 L-7,-295 L7,-295 L10,-160 Z" fill="url(#wgG)" stroke="#8a9aae" stroke-width="0.5" opacity="0.88"/>
<line x1="-5" y1="-200" x2="5" y2="-200" stroke="#6a7a8e" stroke-width="0.3" opacity="0.25"/>
<line x1="-4" y1="-250" x2="4" y2="-250" stroke="#6a7a8e" stroke-width="0.3" opacity="0.2"/>
<!-- 被动偏流小翼 -->
<path d="M-7,-295 L-22,-318 L-2,-295 Z" fill="#8a9aae" stroke="#9aaabe" stroke-width="0.5" opacity="0.75"/>
</g>
</g>
<!-- 右翼组(镜像) -->
<g id="rwG">
<path d="M0,-8 L-14,-160 L14,-160 L0,8 Z" fill="url(#wgG)" stroke="#8a9aae" stroke-width="0.6" opacity="0.92"/>
<line x1="-4" y1="-50" x2="4" y2="-50" stroke="#6a7a8e" stroke-width="0.4" opacity="0.35"/>
<line x1="-7" y1="-90" x2="7" y2="-90" stroke="#6a7a8e" stroke-width="0.4" opacity="0.3"/>
<line x1="-10" y1="-130" x2="10" y2="-130" stroke="#6a7a8e" stroke-width="0.4" opacity="0.25"/>
<circle id="rSmp" cx="0" cy="-160" r="10" fill="url(#smpC)" filter="url(#gC)"/>
<circle cx="0" cy="-160" r="4" fill="#0a0f1c" opacity="0.4"/>
<circle cx="0" cy="-160" r="6.5" fill="none" stroke="#0a0f1c" stroke-width="0.8" opacity="0.25"/>
<g id="rwO">
<path d="M-10,-160 L-7,-295 L7,-295 L10,-160 Z" fill="url(#wgG)" stroke="#8a9aae" stroke-width="0.5" opacity="0.88"/>
<line x1="-5" y1="-200" x2="5" y2="-200" stroke="#6a7a8e" stroke-width="0.3" opacity="0.25"/>
<line x1="-4" y1="-250" x2="4" y2="-250" stroke="#6a7a8e" stroke-width="0.3" opacity="0.2"/>
<path d="M-7,-295 L-22,-318 L-2,-295 Z" fill="#8a9aae" stroke="#9aaabe" stroke-width="0.5" opacity="0.75"/>
</g>
</g>
<!-- 机身 -->
<path d="M-80,0 C-80,-15 -25,-17 65,-11 L88,0 L65,11 C-25,17 -80,15 -80,0 Z" fill="url(#bdG)" stroke="#9aaabe" stroke-width="0.7"/>
<!-- 机身结构线 -->
<line x1="-40" y1="-13" x2="-40" y2="13" stroke="#8a9aae" stroke-width="0.4" opacity="0.2"/>
<line x1="0" y1="-15" x2="0" y2="15" stroke="#8a9aae" stroke-width="0.4" opacity="0.2"/>
<line x1="40" y1="-13" x2="40" y2="13" stroke="#8a9aae" stroke-width="0.4" opacity="0.2"/>
<!-- 头部 -->
<ellipse cx="92" cy="0" rx="22" ry="15" fill="#b0bcc8" stroke="#9aaabe" stroke-width="0.5"/>
<!-- 眼睛 -->
<circle cx="100" cy="-5" r="3" fill="#0a0f1c"/>
<circle cx="100" cy="5" r="3" fill="#0a0f1c"/>
<circle cx="101" cy="-5.5" r="1" fill="#3a5a7a"/>
<circle cx="101" cy="4.5" r="1" fill="#3a5a7a"/>
<!-- 喙 -->
<path d="M112,-4 L130,0 L112,4 Z" fill="#d4a050" stroke="#c49040" stroke-width="0.5"/>
<!-- 尾翼 -->
<path d="M-80,0 L-118,-28 L-98,0 L-118,28 Z" fill="#6a7c90" stroke="#7a8c9e" stroke-width="0.5"/>
<line x1="-98" y1="0" x2="-118" y2="-28" stroke="#5a6c80" stroke-width="0.4" opacity="0.3"/>
<line x1="-98" y1="0" x2="-118" y2="28" stroke="#5a6c80" stroke-width="0.4" opacity="0.3"/>
<!-- 腹部微压传感器阵列 -->
<g id="sensG">
<circle class="sen" cx="-30" cy="0" r="3.5" fill="#0a1428" stroke="#00d2b4" stroke-width="1" opacity="0.5"/>
<circle class="sen" cx="0" cy="0" r="3.5" fill="#0a1428" stroke="#00d2b4" stroke-width="1" opacity="0.5"/>
<circle class="sen" cx="30" cy="0" r="3.5" fill="#0a1428" stroke="#00d2b4" stroke-width="1" opacity="0.5"/>
<circle class="sen" cx="-15" cy="10" r="3" fill="#0a1428" stroke="#00d2b4" stroke-width="0.8" opacity="0.45"/>
<circle class="sen" cx="15" cy="10" r="3" fill="#0a1428" stroke="#00d2b4" stroke-width="0.8" opacity="0.45"/>
<circle class="sen" cx="0" cy="-10" r="3" fill="#0a1428" stroke="#00d2b4" stroke-width="0.8" opacity="0.45"/>
<circle class="sen" cx="-45" cy="5" r="2.5" fill="#0a1428" stroke="#00d2b4" stroke-width="0.7" opacity="0.4"/>
<circle class="sen" cx="45" cy="5" r="2.5" fill="#0a1428" stroke="#00d2b4" stroke-width="0.7" opacity="0.4"/>
</g>
</g>
<!-- 标注 -->
<g id="annos" font-family="'Chakra Petch',sans-serif" font-size="12" font-weight="600">
<g id="anSmp">
<line x1="0" y1="0" x2="0" y2="0" stroke="#00d2b4" stroke-width="1" stroke-dasharray="4,3" opacity="0.5" id="anSmpLine"/>
<rect x="0" y="0" width="0" height="0" rx="4" fill="rgba(0,210,180,0.06)" stroke="rgba(0,210,180,0.3)" stroke-width="0.8" id="anSmpBox"/>
<text x="0" y="0" text-anchor="middle" fill="#00d2b4" font-size="11" id="anSmpTxt">SMP 铰链</text>
</g>
<g id="anSen">
<line x1="0" y1="0" x2="0" y2="0" stroke="#00d2b4" stroke-width="1" stroke-dasharray="4,3" opacity="0.4" id="anSenLine"/>
<rect x="0" y="0" width="0" height="0" rx="4" fill="rgba(0,210,180,0.06)" stroke="rgba(0,210,180,0.25)" stroke-width="0.8" id="anSenBox"/>
<text x="0" y="0" text-anchor="middle" fill="#00d2b4" font-size="11" id="anSenTxt">微压传感器</text>
</g>
<g id="anWng">
<line x1="0" y1="0" x2="0" y2="0" stroke="#8a9aae" stroke-width="1" stroke-dasharray="4,3" opacity="0.3" id="anWngLine"/>
<rect x="0" y="0" width="0" height="0" rx="4" fill="rgba(138,154,174,0.06)" stroke="rgba(138,154,174,0.2)" stroke-width="0.8" id="anWngBox"/>
<text x="0" y="0" text-anchor="middle" fill="#8a9aae" font-size="11" id="anWngTxt">偏流小翼</text>
</g>
</g>
<!-- IFR 能量流图 -->
<g id="ifrFlow" opacity="0" font-family="'Chakra Petch',sans-serif">
<rect x="370" y="770" width="860" height="52" rx="10" fill="rgba(251,191,36,0.04)" stroke="rgba(251,191,36,0.2)" stroke-width="0.8"/>
<!-- 节点 -->
<g id="ifrNodes">
<rect x="395" y="782" width="130" height="28" rx="5" fill="rgba(255,107,53,0.1)" stroke="rgba(255,107,53,0.4)" stroke-width="0.8"/>
<text x="460" y="801" text-anchor="middle" fill="#ff6b35" font-size="11" font-weight="700">环境热能</text>
<path d="M535,796 L565,796 L558,790 M565,796 L558,802" stroke="#fbbf24" stroke-width="1.5" fill="none" opacity="0.7" id="ifrA1"/>
<rect x="575" y="782" width="130" height="28" rx="5" fill="rgba(0,210,180,0.1)" stroke="rgba(0,210,180,0.4)" stroke-width="0.8"/>
<text x="640" y="801" text-anchor="middle" fill="#00d2b4" font-size="11" font-weight="700">SMP 相变</text>
<path d="M715,796 L745,796 L738,790 M745,796 L738,802" stroke="#fbbf24" stroke-width="1.5" fill="none" opacity="0.7" id="ifrA2"/>
<rect x="755" y="782" width="130" height="28" rx="5" fill="rgba(138,154,174,0.1)" stroke="rgba(138,154,174,0.4)" stroke-width="0.8"/>
<text x="820" y="801" text-anchor="middle" fill="#8a9aae" font-size="11" font-weight="700">气动载荷</text>
<path d="M895,796 L925,796 L918,790 M925,796 L918,802" stroke="#fbbf24" stroke-width="1.5" fill="none" opacity="0.7" id="ifrA3"/>
<rect x="935" y="782" width="140" height="28" rx="5" fill="rgba(251,191,36,0.1)" stroke="rgba(251,191,36,0.4)" stroke-width="0.8" filter="url(#gIfr)"/>
<text x="1005" y="801" text-anchor="middle" fill="#fbbf24" font-size="11" font-weight="700">构型切换</text>
</g>
<!-- 核心标注 -->
<text x="800" y="845" text-anchor="middle" fill="#fbbf24" font-size="10" font-weight="600" opacity="0.6">
零主动能耗 · 环境资源寄生利用 · IFR 最终理想解
</text>
</g>
<!-- HUD 温度 -->
<g id="hudT" transform="translate(58,180)">
<rect x="0" y="0" width="34" height="280" rx="17" fill="rgba(10,16,30,0.75)" stroke="rgba(74,92,114,0.25)" stroke-width="1"/>
<rect id="tBar" x="4" y="220" width="26" height="56" rx="13" fill="#00d2b4" opacity="0.8"/>
<text x="17" y="308" text-anchor="middle" fill="#4a5c72" font-family="'Space Mono',monospace" font-size="9">T</text>
<text id="tTxt" x="17" y="190" text-anchor="middle" fill="#00d2b4" font-family="'Space Mono',monospace" font-size="11" font-weight="700">25℃</text>
<!-- Tg 标记 -->
<line x1="32" y1="106" x2="40" y2="106" stroke="#ff6b35" stroke-width="1" opacity="0.5"/>
<text x="48" y="110" fill="#ff6b35" font-family="'Space Mono',monospace" font-size="8" opacity="0.5">Tg 45℃</text>
</g>
<!-- HUD 刚度 -->
<g id="hudK" transform="translate(58,510)">
<text x="17" y="0" text-anchor="middle" fill="#4a5c72" font-family="'Chakra Petch',sans-serif" font-size="10" font-weight="600">刚度比</text>
<text id="kTxt" x="17" y="22" text-anchor="middle" fill="#00d2b4" font-family="'Space Mono',monospace" font-size="14" font-weight="700">100:1</text>
</g>
<!-- HUD 高度 -->
<g id="hudA" transform="translate(1508,180)">
<text x="17" y="0" text-anchor="middle" fill="#4a5c72" font-family="'Chakra Petch',sans-serif" font-size="10" font-weight="600">ALT</text>
<text id="aTxt" x="17" y="24" text-anchor="middle" fill="#8a9aae" font-family="'Space Mono',monospace" font-size="15" font-weight="700">100m</text>
</g>
<!-- HUD 模式 -->
<g id="hudM" transform="translate(1508,250)">
<text x="17" y="0" text-anchor="middle" fill="#4a5c72" font-family="'Chakra Petch',sans-serif" font-size="10" font-weight="600">MODE</text>
<text id="mTxt" x="17" y="24" text-anchor="middle" fill="#00d2b4" font-family="'Space Mono',monospace" font-size="13" font-weight="700">FLAP</text>
</g>
<!-- HUD 能耗指示 -->
<g id="hudE" transform="translate(1508,320)">
<text x="17" y="0" text-anchor="middle" fill="#4a5c72" font-family="'Chakra Petch',sans-serif" font-size="10" font-weight="600">能耗</text>
<text id="eTxt" x="17" y="22" text-anchor="middle" fill="#00d2b4" font-family="'Space Mono',monospace" font-size="12" font-weight="700">--</text>
</g>
<!-- 侧视高度剖面 -->
<g id="sideView" transform="translate(1340,560)">
<rect x="0" y="0" width="220" height="160" rx="8" fill="rgba(10,16,30,0.7)" stroke="rgba(74,92,114,0.2)" stroke-width="0.8"/>
<text x="110" y="18" text-anchor="middle" fill="#4a5c72" font-family="'Chakra Petch',sans-serif" font-size="9" font-weight="600">侧视剖面</text>
<!-- 地面 -->
<line x1="15" y1="140" x2="205" y2="140" stroke="#3a4a5a" stroke-width="1"/>
<!-- 热气流 -->
<g id="svThermal" opacity="0">
<rect x="80" y="40" width="60" height="100" rx="30" fill="rgba(255,107,53,0.08)"/>
<path d="M95,135 L95,85 L90,92 M95,85 L100,92" stroke="#ff6b35" stroke-width="1" fill="none" opacity="0.4"/>
<path d="M110,135 L110,50 L105,57 M110,50 L115,57" stroke="#ff6b35" stroke-width="1.2" fill="none" opacity="0.5"/>
<path d="M125,135 L125,85 L120,92 M125,85 L130,92" stroke="#ff6b35" stroke-width="1" fill="none" opacity="0.4"/>
</g>
<!-- 鸟标记 -->
<g id="svBird">
<path d="M0,-5 L8,0 L0,5 L-4,0 Z" fill="#b0bcc8" stroke="#8a9aae" stroke-width="0.5"/>
</g>
<!-- 高度刻度 -->
<text x="12" y="143" fill="#3a4a5a" font-family="'Space Mono',monospace" font-size="7">0</text>
<text x="8" y="48" fill="#3a4a5a" font-family="'Space Mono',monospace" font-size="7">400m</text>
</g>
</svg>
<div id="phaseBar">扑动飞行模式</div>
<div id="ifrTag">IFR · 最终理想解</div>
<div id="ctrl">
<button class="mbtn on" id="aBtn" aria-label="自动模式">AUTO</button>
<button class="mbtn" id="mBtn" aria-label="手动模式">MANUAL</button>
<label for="tSlider">环境温度</label>
<input type="range" id="tSlider" min="20" max="55" value="25" step="0.5" aria-label="温度控制"/>
<span id="tVal">25℃</span>
</div>
</div>
<script>
(function(){
'use strict';
/* ===== 常量 ===== */
const CYC = 18000; // 循环时长 ms
const Tg = 45; // SMP 玻璃化转变温度
const T_MIN = 20, T_MAX = 55;
const SWEEP_MAX = 55; // 最大后掠角
const FLAP_HZ = 4.5; // 扑动频率
const FLAP_AMP_MAX = 10; // 最大扑动振幅(度)
const BIRD_CX = 750, BIRD_CY = 420;
/* ===== DOM ===== */
const $ = id => document.getElementById(id);
const scene = $('scene');
const birdG = $('bird');
const lwG = $('lwG'), rwG = $('rwG');
const lwO = $('lwO'), rwO = $('rwO');
const lSmp = $('lSmp'), rSmp = $('rSmp');
const thZone = $('thZone');
const thParts = $('thParts');
const thRipples = $('thRipples');
const sensG = $('sensG');
const ifrFlow = $('ifrFlow');
const phaseBar = $('phaseBar');
const tSlider = $('tSlider'), tValEl = $('tVal');
const aBtn = $('aBtn'), mBtn = $('mBtn');
const tBar = $('tBar'), tTxt = $('tTxt'), kTxt = $('kTxt');
const aTxt = $('aTxt'), mTxt = $('mTxt'), eTxt = $('eTxt');
const bShadow = $('bShadow');
const svBird = $('svBird'), svThermal = $('svThermal');
const anSmpLine = $('anSmpLine'), anSmpBox = $('anSmpBox'), anSmpTxt = $('anSmpTxt');
const anSenLine = $('anSenLine'), anSenBox = $('anSenBox'), anSenTxt = $('anSenTxt');
const anWngLine = $('anWngLine'), anWngBox = $('anWngBox'), anWngTxt = $('anWngTxt');
/* ===== 状态 ===== */
let autoMode = true, manualT = 25, t0 = null;
let particles = [], ripples = [];
/* ===== 工具 ===== */
const lerp = (a,b,t) => a + (b-a) * Math.max(0, Math.min(1, t));
const clamp = (v,lo,hi) => Math.max(lo, Math.min(hi, v));
const ss = (e0,e1,x) => { const t = clamp((x-e0)/(e1-e0),0,1); return t*t*(3-2*t); };
/* ===== 粒子初始化 ===== */
function initParts(){
for(let i=0;i<45;i++){
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('r', String(1.5 + Math.random()*2.5));
c.setAttribute('fill','#ff6b35');
c.setAttribute('opacity','0');
thParts.appendChild(c);
particles.push({
el:c,
x: 780 + Math.random()*240,
y: 600 + Math.random()*150,
vy: -(0.3 + Math.random()*0.5),
vx: (Math.random()-0.5)*0.2,
life: Math.random(),
ml: 0.6 + Math.random()*0.4
});
}
// 扩展波纹
for(let i=0;i<4;i++){
const e = document.createElementNS('http://www.w3.org/2000/svg','ellipse');
e.setAttribute('fill','none');
e.setAttribute('stroke','#ff6b35');
e.setAttribute('stroke-width','0.8');
e.setAttribute('opacity','0');
thRipples.appendChild(e);
ripples.push({ el:e, phase: i/4 });
}
}
function tickParts(dt, inten){
particles.forEach(p => {
p.life += dt*0.0007;
if(p.life > p.ml){
p.life = 0;
p.x = 780 + Math.random()*240;
p.y = 580 + Math.random()*120;
p.vy = -(0.3+Math.random()*0.5);
p.vx = (Math.random()-0.5)*0.2;
}
p.x += p.vx + Math.sin(p.life*6)*0.12;
p.y += p.vy;
const fade = 1 - p.life/p.ml;
const op = fade * inten * 0.55;
p.el.setAttribute('cx', String(p.x));
p.el.setAttribute('cy', String(p.y));
p.el.setAttribute('opacity', String(Math.max(0, op)));
});
// 波纹
const now = performance.now()*0.001;
ripples.forEach(r => {
const cyc = (now*0.3 + r.phase) % 1;
const rx = 40 + cyc*200;
const ry = 35 + cyc*170;
const op = (1-cyc) * inten * 0.2;
r.el.setAttribute('cx','900');
r.el.setAttribute('cy','430');
r.el.setAttribute('rx', String(rx));
r.el.setAttribute('ry', String(ry));
r.el.setAttribute('opacity', String(Math.max(0, op)));
});
}
/* ===== 状态计算 ===== */
function getCycleState(p){
// p: 0~1
let temp, sweep, flapAmp, thermal, alt, stiff, bx, name;
if(p < 0.18){
const q = p/0.18;
temp=lerp(22,25,q); sweep=0; flapAmp=FLAP_AMP_MAX; thermal=0;
alt=lerp(100,105,q); stiff=1; bx=lerp(600,640,q); name='扑动飞行模式';
} else if(p < 0.28){
const q=(p-0.18)/0.10;
temp=lerp(25,40,q); sweep=0; flapAmp=lerp(FLAP_AMP_MAX,6,q);
thermal=ss(0,1,q); alt=lerp(105,120,q); stiff=lerp(1,0.5,q);
bx=lerp(640,730,q); name='探测到热上升气流';
} else if(p < 0.42){
const q=(p-0.28)/0.14;
temp=lerp(40,52,q); sweep=ss(0,1,q)*SWEEP_MAX;
flapAmp=lerp(6,0,ss(0,0.7,q)); thermal=lerp(0.5,1,q);
alt=lerp(120,220,q); stiff=lerp(0.5,0.01,q);
bx=lerp(730,800,q); name='SMP 软化 · 构型切换';
} else if(p < 0.62){
const q=(p-0.42)/0.20;
temp=lerp(52,50,0.5+0.5*Math.sin(q*Math.PI));
sweep=SWEEP_MAX; flapAmp=0; thermal=1;
alt=lerp(220,400,q); stiff=0.01;
bx=800+Math.sin(q*Math.PI*2)*35; name='热气流盘旋滑翔';
} else if(p < 0.72){
const q=(p-0.62)/0.10;
temp=lerp(50,35,q); sweep=SWEEP_MAX; flapAmp=0;
thermal=lerp(1,0.25,q); alt=lerp(400,370,q); stiff=lerp(0.01,0.25,q);
bx=lerp(800,700,q); name='脱离热气流 · 降温中';
} else if(p < 0.86){
const q=(p-0.72)/0.14;
temp=lerp(35,24,q); sweep=lerp(SWEEP_MAX,0,ss(0,1,q));
flapAmp=lerp(0,FLAP_AMP_MAX*0.8,ss(0.4,1,q));
thermal=lerp(0.25,0,q); alt=lerp(370,120,q); stiff=lerp(0.25,1,q);
bx=lerp(700,630,q); name='SMP 硬化 · 恢复扑动';
} else {
const q=(p-0.86)/0.14;
temp=lerp(24,22,q); sweep=0; flapAmp=lerp(FLAP_AMP_MAX*0.8,FLAP_AMP_MAX,q);
thermal=0; alt=lerp(120,100,q); stiff=1;
bx=lerp(630,600,q); name='扑动飞行模式';
}
return {temp,sweep,flapAmp,thermal,alt,stiff,bx,name};
}
function getManualState(temp){
const soft = ss(Tg-5, Tg+5, temp);
return {
temp, sweep: soft*SWEEP_MAX,
flapAmp: soft<0.5 ? lerp(FLAP_AMP_MAX,0,soft*2) : 0,
thermal: ss(28,42,temp), alt: lerp(100,400,soft),
stiff: lerp(1,0.01,soft), bx: lerp(650,800,soft),
name: soft<0.1?'扑动飞行模式':soft<0.5?'SMP 软化 · 构型切换':'热气流盘旋滑翔'
};
}
/* ===== 颜色 ===== */
function smpCol(stiff){
const t=1-stiff;
return `rgb(${Math.round(lerp(0,255,t))},${Math.round(lerp(210,107,t))},${Math.round(lerp(180,53,t))})`;
}
function smpFilt(stiff){ return stiff<0.5?'url(#gH)':'url(#gC)'; }
/* ===== 传感器脉冲 ===== */
function tickSensors(time, thermal){
const sens = sensG.querySelectorAll('.sen');
sens.forEach((s,i) => {
const pulse = thermal>0.1 ? 0.5+0.5*Math.sin(time*0.005+i*1.3) : 0.25;
const op = lerp(0.2, 1, pulse*thermal);
s.setAttribute('opacity', String(op));
s.setAttribute('stroke', thermal>0.3 ? '#ff6b35' : '#00d2b4');
s.setAttribute('stroke-width', String(lerp(0.8, 2.2, pulse*thermal)));
});
}
/* ===== 标注位置更新 ===== */
function tickAnnos(bx, by, sweep, stiff){
// 鸟的世界坐标
const lSmpX = bx + 5, lSmpY = by - 14 - 160; // 左SMP约略位置
const senX = bx, senY = by + 5;
const wngX = bx - 22*Math.cos(sweep*Math.PI/180)*0.3;
const wngY = by - 14 - 295 + (295-160)*(1-Math.cos(sweep*Math.PI/180));
// SMP标注
anSmpLine.setAttribute('x1', lSmpX-10); anSmpLine.setAttribute('y1', lSmpY);
anSmpLine.setAttribute('x2', lSmpX-70); anSmpLine.setAttribute('y2', lSmpY-50);
anSmpBox.setAttribute('x', lSmpX-155); anSmpBox.setAttribute('y', lSmpY-65);
anSmpBox.setAttribute('width', 82); anSmpBox.setAttribute('height', 24);
anSmpTxt.setAttribute('x', lSmpX-114); anSmpTxt.setAttribute('y', lSmpY-48);
const col = smpCol(stiff);
anSmpLine.setAttribute('stroke', col);
anSmpBox.setAttribute('stroke', col+'4d'); anSmpBox.setAttribute('fill', col+'14');
anSmpTxt.setAttribute('fill', col);
// 传感器标注
anSenLine.setAttribute('x1', senX+15); anSenLine.setAttribute('y1', senY);
anSenLine.setAttribute('x2', senX+60); anSenLine.setAttribute('y2', senY+45);
anSenBox.setAttribute('x', senX+55); anSenBox.setAttribute('y', senY+38);
anSenBox.setAttribute('width', 105); anSenBox.setAttribute('height', 24);
anSenTxt.setAttribute('x', senX+107); anSenTxt.setAttribute('y', senY+55);
// 小翼标注
anWngLine.setAttribute('x1', lSmpX-20); anWngLine.setAttribute('y1', wngY-15);
anWngLine.setAttribute('x2', lSmpX-55); anWngLine.setAttribute('y2', wngY-55);
anWngBox.setAttribute('x', lSmpX-115); anWngBox.setAttribute('y', wngY-68);
anWngBox.setAttribute('width', 82); anWngBox.setAttribute('height', 24);
anWngTxt.setAttribute('x', lSmpX-74); anWngTxt.setAttribute('y', wngY-51);
}
/* ===== 渲染 ===== */
function render(time, s){
const {temp,sweep,flapAmp,thermal,alt,stiff,bx,name} = s;
const by = BIRD_CY;
// 鸟整体位置
const birdScale = 1 + (alt-100)*0.0003; // 高度越高略大
const bankAngle = thermal>0.8 ? Math.sin(time*0.0018)*6 : 0;
birdG.setAttribute('transform',
`translate(${bx}, ${by}) scale(${birdScale}) rotate(${bankAngle})`);
// 阴影偏移(高度越高阴影越远越淡)
const shadowOff = (alt-100)*0.12;
const shadowOp = lerp(0.18, 0.03, (alt-100)/350);
bShadow.setAttribute('cx', -shadowOff*0.3);
bShadow.setAttribute('cy', shadowOff);
bShadow.setAttribute('rx', lerp(90,60,(alt-100)/350));
bShadow.setAttribute('opacity', String(shadowOp));
// 扑动角度
const flapAng = flapAmp * Math.sin(time*0.001*FLAP_HZ*Math.PI*2);
// 左翼
lwG.setAttribute('transform', `translate(5, ${-14+flapAng*0.3})`);
lwO.setAttribute('transform', `rotate(${sweep}, 0, -160)`);
// 右翼(镜像)
rwG.setAttribute('transform', `translate(5, ${14-flapAng*0.3}) scale(1, -1)`);
rwO.setAttribute('transform', `rotate(${sweep}, 0, -160)`);
// SMP 颜色
const col = smpCol(stiff);
const filt = smpFilt(stiff);
[lSmp, rSmp].forEach(s => {
s.setAttribute('fill', col);
s.setAttribute('filter', filt);
});
// 热区
thZone.setAttribute('opacity', String(ss(0, 0.25, thermal)));
// 传感器
tickSensors(time, thermal);
// 标注
tickAnnos(bx, by, sweep, stiff);
// IFR 流程图(在过渡阶段显示)
const ifrOp = ss(0.2, 0.4, thermal) * (1 - ss(0.85, 1, thermal));
ifrFlow.setAttribute('opacity', String(ifrOp));
// HUD 温度
const tn = clamp((temp-T_MIN)/(T_MAX-T_MIN), 0, 1);
const barH = lerp(25, 250, tn);
tBar.setAttribute('y', String(270-barH));
tBar.setAttribute('height', String(barH));
tBar.setAttribute('fill', col);
tTxt.textContent = Math.round(temp)+'℃';
tTxt.setAttribute('fill', col);
// 刚度
const kr = Math.round(lerp(1, 100, stiff));
kTxt.textContent = kr+':1';
kTxt.setAttribute('fill', col);
// 高度
aTxt.textContent = Math.round(alt)+'m';
// 模式
const mode = flapAmp>2 ? 'FLAP' : (sweep>5 ? 'SOAR' : 'FLAP');
mTxt.textContent = mode;
mTxt.setAttribute('fill', thermal>0.4?'#ff6b35':'#00d2b4');
// 能耗
const eCost = flapAmp>2 ? 'HIGH' : (sweep>5 ? 'ZERO' : 'LOW');
eTxt.textContent = eCost;
eTxt.setAttribute('fill', eCost==='ZERO'?'#fbbf24': eCost==='HIGH'?'#ff6b35':'#00d2b4');
// 侧视剖面
const svAlt = lerp(130, 30, (alt-100)/350);
svBird.setAttribute('transform', `translate(${lerp(30,110,(alt-100)/350)}, ${svAlt})`);
svThermal.setAttribute('opacity', String(ss(0.15, 0.35, thermal)));
// 阶段标签
phaseBar.textContent = name;
phaseBar.style.color = col;
phaseBar.style.borderColor = col+'30';
// 同步滑块(自动模式)
if(autoMode){
tSlider.value = temp;
tValEl.textContent = Math.round(temp)+'℃';
tValEl.style.color = col;
}
}
/* ===== 主循环 ===== */
function loop(ts){
if(!t0) t0 = ts;
const elapsed = ts - t0;
let state;
if(autoMode){
const p = (elapsed%CYC)/CYC;
state = getCycleState(p);
} else {
state = getManualState(manualT);
}
tickParts(16, state.thermal);
render(elapsed, state);
requestAnimationFrame(loop);
}
/* ===== 交互 ===== */
aBtn.onclick = () => { autoMode=true; aBtn.classList.add('on'); mBtn.classList.remove('on'); t0=null; };
mBtn.onclick = () => { autoMode=false; mBtn.classList.add('on'); aBtn.classList.remove('on'); };
tSlider.oninput = function(){
if(!autoMode){
manualT = parseFloat(this.value);
const s = getManualState(manualT);
const col = smpCol(s.stiff);
tValEl.textContent = Math.round(manualT)+'℃';
tValEl.style.color = col;
}
};
/* ===== 启动 ===== */
function init(){
initParts();
requestAnimationFrame(loop);
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script>
</body>
</html>
实现说明
本次动画从零重新设计,以全新视觉语言呈现 SMP 热致变刚度仿生鸟 的 IFR 原理:
设计美学
- 色调:深空蓝底色搭配青绿(
#00d2b4,冷态/刚性)与炽橙(#ff6b35,热态/软化)的双色系统,SMP 铰链的颜色渐变是全场视觉锚点 - 字体:Chakra Petch(棱角分明的技术感标签)+ Space Mono(等宽数据读出),避开了常见的 Inter/Roboto
- 层次:背景网格 → 地面渐变 → 热气流区域 → 鸟体阴影 → 机械鸟 → 标注 → HUD → IFR 流程图,共 7 层景深
IFR 叙事核心
- 能量流图:在热气流阶段浮现
环境热能 → SMP 相变 → 气动载荷 → 构型切换四步链路,最终节点以金色辉光标注"零主动能耗" - 能耗 HUD:实时显示
HIGH / ZERO / LOW,盘旋阶段显示金色ZERO,直观对比扑动的高耗能与滑翔的零能耗 - 侧视剖面:右下角小窗口同步展示高度变化与热气流柱,弥补俯视图的高度感知不足
关键动画逻辑
- 18 秒完整循环:扑动 → 探测 → SMP软化/后掠 → 盘旋上升 → 脱离降温 → SMP硬化/翼展 → 恢复扑动
- SMP 铰链:颜色在青绿↔炽橙间连续插值,辉光滤镜同步切换,是破除矛盾的核心视觉焦点
- 双翼镜像:右翼通过
scale(1,-1)镜像,外翼段绕 SMP 关节旋转最大 55°后掠角 - 热气流粒子:45 个粒子循环上升 + 4 圈扩展波纹,强度与温度联动
- 传感器脉冲:8 个腹部传感器在检测到热梯度时频率闪烁,颜色从青切换到橙
交互
- AUTO / MANUAL:自动模式循环播放;手动模式拖动温度滑块直接驱动 SMP 响应
- 温度计 HUD:左侧温度条含 Tg=45℃ 标记线,实时反映当前环境温度
- 阴影系统:鸟体阴影随高度偏移与淡化,增强空间感
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
