<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仿生鳞片单向推进原理 — IFR 动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #08080E;
--surface: #0E0E1A;
--grid-line: #1A1A30;
--text: #C8C4BE;
--text-dim: #5E5A54;
--grip: #FF5722;
--grip-glow: rgba(255,87,34,0.45);
--glide: #00E5A0;
--glide-glow: rgba(0,229,160,0.30);
--gold: #FFD54F;
--body-fill: #E8E4DC;
--body-stroke: #B0ACA4;
--panel: #0C0C18;
--panel-border: #1E1E36;
}
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; background:var(--bg); color:var(--text); font-family:'Fira Code',monospace; }
#app { display:flex; flex-direction:column; height:100vh; }
/* ---- 顶栏 ---- */
header {
padding:14px 28px 6px;
display:flex; align-items:baseline; gap:18px;
border-bottom:1px solid var(--panel-border);
background:linear-gradient(180deg,rgba(14,14,26,.9),transparent);
flex-shrink:0;
}
header h1 {
font-family:'Cormorant Garamond',serif;
font-weight:700; font-size:22px; letter-spacing:1px;
color:#F0ECE4;
}
header p { font-size:11px; color:var(--text-dim); letter-spacing:.5px; }
/* ---- 主舞台 ---- */
.stage { flex:1; display:flex; justify-content:center; align-items:center; padding:8px 16px; min-height:0; }
.stage svg { width:100%; max-width:1100px; height:100%; }
/* ---- 底栏 ---- */
.bottom {
flex-shrink:0; display:flex; gap:0;
border-top:1px solid var(--panel-border);
background:var(--panel);
height:200px;
}
.panel { padding:14px 20px; border-right:1px solid var(--panel-border); overflow:hidden; }
.panel:last-child { border-right:none; }
.panel h3 { font-family:'Cormorant Garamond',serif; font-size:14px; font-weight:600; color:#D0CCC4; margin-bottom:8px; letter-spacing:.5px; }
.panel-inset { flex:0 0 380px; }
.panel-inset svg { width:100%; height:130px; }
.panel-metrics { flex:0 0 240px; display:flex; flex-direction:column; gap:6px; }
.metric-row { display:flex; justify-content:space-between; align-items:center; font-size:11px; }
.metric-label { color:var(--text-dim); }
.metric-value { font-weight:500; }
.metric-value.grip { color:var(--grip); }
.metric-value.glide { color:var(--glide); }
.metric-value.gold { color:var(--gold); }
.panel-controls { flex:1; display:flex; flex-direction:column; gap:8px; }
.ctrl-row { display:flex; align-items:center; gap:10px; font-size:11px; }
.ctrl-row label { width:80px; color:var(--text-dim); flex-shrink:0; text-align:right; }
.ctrl-row input[type=range] { flex:1; accent-color:var(--gold); height:4px; }
.ctrl-row .val { width:44px; text-align:right; color:var(--text); font-variant-numeric:tabular-nums; }
.btn-row { display:flex; gap:8px; margin-top:4px; }
.btn {
padding:5px 14px; border:1px solid var(--panel-border); background:transparent;
color:var(--text); font-family:inherit; font-size:11px; cursor:pointer;
border-radius:3px; transition:all .2s;
}
.btn:hover { border-color:var(--gold); color:var(--gold); }
.btn.active { background:var(--gold); color:#08080E; border-color:var(--gold); }
.ifr-badge {
display:inline-block; font-size:9px; padding:2px 7px; border-radius:2px;
background:rgba(255,213,79,.12); color:var(--gold); letter-spacing:.5px; margin-top:6px;
}
/* 警告 */
.warn-overlay {
position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
background:rgba(255,87,34,.12); border:1px solid var(--grip);
padding:14px 28px; border-radius:6px; pointer-events:none;
font-size:13px; color:var(--grip); opacity:0; transition:opacity .4s;
z-index:10; text-align:center;
}
.warn-overlay.show { opacity:1; }
</style>
</head>
<body>
<div id="app">
<header>
<h1>仿生鳞片单向推进原理</h1>
<p>Passive Asymmetric Friction → Lateral-to-Forward Thrust</p>
</header>
<div class="stage">
<svg id="main-svg" viewBox="0 0 900 360" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 发光滤镜 -->
<filter id="f-grip" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="f-glide" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="f-gold" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- 地面渐变 -->
<linearGradient id="gnd-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0E0E1A"/>
<stop offset="1" stop-color="#08080E"/>
</linearGradient>
</defs>
<!-- 地面 -->
<g id="ground-group">
<rect x="0" y="290" width="900" height="70" fill="url(#gnd-grad)"/>
<g id="ground-lines" stroke="#1A1A30" stroke-width="1"></g>
</g>
<!-- 轨迹 -->
<polyline id="trail" fill="none" stroke="#FFD54F" stroke-width="1.5" opacity="0.35" stroke-dasharray="6,4"/>
<!-- 蛇身段 -->
<g id="snake-group"></g>
<!-- 力箭头 -->
<g id="force-group"></g>
<!-- 前进大箭头 -->
<g id="thrust-arrow" filter="url(#f-gold)">
<line x1="0" y1="0" x2="0" y2="0" stroke="#FFD54F" stroke-width="3" stroke-linecap="round"/>
<polygon points="0,0 0,0 0,0" fill="#FFD54F"/>
</g>
<!-- IFR 标注 -->
<g id="ifr-anno" transform="translate(730,32)">
<rect x="0" y="0" width="155" height="62" rx="4" fill="rgba(255,213,79,.06)" stroke="rgba(255,213,79,.25)" stroke-width="0.8"/>
<text x="12" y="18" font-size="9" fill="#FFD54F" font-family="'Fira Code',monospace" letter-spacing="1">IFR · 最终理想解</text>
<text x="12" y="33" font-size="8.5" fill="#A8A49E" font-family="'Fira Code',monospace">被动鳞片自调节</text>
<text x="12" y="46" font-size="8.5" fill="#A8A49E" font-family="'Fira Code',monospace">零额外动力推进</text>
<text x="12" y="57" font-size="7.5" fill="#6B6760" font-family="'Fira Code',monospace">摩擦不对称 → 前向推力</text>
</g>
<!-- 刻度尺 -->
<g id="ruler" transform="translate(60,308)">
<line x1="0" y1="0" x2="780" y2="0" stroke="#2A2A44" stroke-width="0.5"/>
</g>
</svg>
</div>
<div class="bottom">
<!-- 鳞片微观 -->
<div class="panel panel-inset">
<h3>鳞片微观机理</h3>
<svg id="inset-svg" viewBox="0 0 380 130">
<defs>
<filter id="f-inset-glow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- 地面 -->
<rect x="0" y="92" width="380" height="38" fill="#0E0E1A" rx="0"/>
<line x1="0" y1="92" x2="380" y2="92" stroke="#2A2A44" stroke-width="1"/>
<!-- 标签 -->
<text id="inset-mode-label" x="190" y="16" text-anchor="middle" font-size="10" fill="#8A8680" font-family="'Fira Code',monospace"></text>
<!-- 身体底面 -->
<rect id="inset-body" x="60" y="60" width="260" height="18" rx="3" fill="#D8D4CC" stroke="#AAA69E" stroke-width="0.8"/>
<!-- 鳞片组 -->
<g id="inset-scales"></g>
<!-- 运动箭头 -->
<g id="inset-arrows"></g>
<!-- 摩擦系数 -->
<text id="inset-mu-label" x="190" y="128" text-anchor="middle" font-size="10" font-family="'Fira Code',monospace"></text>
</svg>
</div>
<!-- 指标 -->
<div class="panel panel-metrics">
<h3>实时参数</h3>
<div class="metric-row"><span class="metric-label">前进距离</span><span class="metric-value gold" id="m-dist">0.0 mm</span></div>
<div class="metric-row"><span class="metric-label">前进速度</span><span class="metric-value gold" id="m-speed">0.0 mm/s</span></div>
<div class="metric-row"><span class="metric-label">μ<sub>高</sub> (抓地)</span><span class="metric-value grip" id="m-mu-h">0.80</span></div>
<div class="metric-row"><span class="metric-label">μ<sub>低</sub> (滑行)</span><span class="metric-value glide" id="m-mu-l">0.20</span></div>
<div class="metric-row"><span class="metric-label">摩擦比</span><span class="metric-value" id="m-ratio" style="color:#FFD54F">4.0</span></div>
<div class="metric-row"><span class="metric-label">表面状态</span><span class="metric-value" id="m-surface" style="color:#00E5A0">正常</span></div>
<div class="ifr-badge">IFR:被动结构 · 自调节摩擦不对称</div>
</div>
<!-- 控制 -->
<div class="panel panel-controls">
<h3>交互控制</h3>
<div class="ctrl-row">
<label>波速</label>
<input type="range" id="c-speed" min="0.3" max="3" step="0.1" value="1.5">
<span class="val" id="v-speed">1.5</span>
</div>
<div class="ctrl-row">
<label>鳞片角度</label>
<input type="range" id="c-angle" min="15" max="75" step="1" value="45">
<span class="val" id="v-angle">45°</span>
</div>
<div class="ctrl-row">
<label>表面粗糙</label>
<input type="range" id="c-rough" min="0" max="1" step="0.05" value="0.85">
<span class="val" id="v-rough">0.85</span>
</div>
<div class="btn-row">
<button class="btn" id="btn-pause">暂停</button>
<button class="btn active" id="btn-force">力向量</button>
<button class="btn" id="btn-reset">重置</button>
</div>
</div>
</div>
<div class="warn-overlay" id="warn-glass">⚠ 极度光滑表面:棘齿无法抓地,摩擦差异消失,方案失效</div>
</div>
<script>
const NS = 'http://www.w3.org/2000/svg';
/* ===== 配置 ===== */
const CFG = {
numSeg: 10,
segLen: 34, // 段长(前进方向)
segW: 24, // 段宽(横向)
segSpacing: 30, // 段中心距
headX: 700, // 头段 x
centerY: 195, // 中心 y
amplitude: 58, // 波幅
waveK: 0.72, // 波数 rad/段
edgeW: 3.5, // 摩擦边宽度
rulerSpacing: 40 // 刻度间距
};
/* ===== 状态 ===== */
const S = {
time: 0,
waveSpeed: 1.5,
scaleAngle: 45,
roughness: 0.85,
showForce: true,
paused: false,
dist: 0,
trail: [],
particles: []
};
/* ===== 工具函数 ===== */
function el(tag, attrs) {
const e = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
return e;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function deg(r) { return r * 180 / Math.PI; }
/* ===== 摩擦系数 ===== */
function muHigh() { return 0.1 + 0.7 * S.roughness; }
function muLow() { return 0.1 + 0.1 * S.roughness; }
function forwardFactor() {
const a = S.scaleAngle * Math.PI / 180;
return Math.sin(2 * a) * (muHigh() - muLow()) / 0.6; // 归一化,45°粗糙=1
}
function forwardSpeed() {
return S.waveSpeed * forwardFactor() * 50; // 像素/秒
}
/* ===== 创建蛇段 SVG ===== */
const segData = [];
const snakeG = document.getElementById('snake-group');
const forceG = document.getElementById('force-group');
for (let i = 0; i < CFG.numSeg; i++) {
const g = el('g');
// 段体
const body = el('rect', {
x: -CFG.segLen / 2, y: -CFG.segW / 2,
width: CFG.segLen, height: CFG.segW,
rx: 5, ry: 5,
fill: '#E8E4DC', stroke: '#B0ACA4', 'stroke-width': 0.8
});
g.appendChild(body);
// 鳞片纹理(底部微倾斜线)
for (let j = -2; j <= 2; j++) {
const ln = el('line', {
x1: j * 7 - 3, y1: CFG.segW / 2 - 1,
x2: j * 7 + 3, y2: CFG.segW / 2 - 5,
stroke: '#C0BCB4', 'stroke-width': 0.6, opacity: 0.5
});
g.appendChild(ln);
}
// 左摩擦边
const leftEdge = el('rect', {
x: -CFG.segLen / 2, y: -CFG.segW / 2,
width: CFG.segLen, height: CFG.edgeW,
rx: 2, fill: '#555', opacity: 0.5
});
g.appendChild(leftEdge);
// 右摩擦边
const rightEdge = el('rect', {
x: -CFG.segLen / 2, y: CFG.segW / 2 - CFG.edgeW,
width: CFG.segLen, height: CFG.edgeW,
rx: 2, fill: '#555', opacity: 0.5
});
g.appendChild(rightEdge);
snakeG.appendChild(g);
// 力箭头
const arrowG = el('g', { opacity: 0 });
const arrowLine = el('line', { x1: 0, y1: 0, x2: 20, y2: 0, stroke: '#FFD54F', 'stroke-width': 2, 'stroke-linecap': 'round' });
const arrowHead = el('polygon', { points: '20,-4 28,0 20,4', fill: '#FFD54F' });
arrowG.appendChild(arrowLine);
arrowG.appendChild(arrowHead);
forceG.appendChild(arrowG);
segData.push({ g, leftEdge, rightEdge, arrowG, arrowLine, arrowHead, cx: 0, cy: 0, angle: 0, vy: 0, leftHigh: false });
}
/* ===== 地面线 ===== */
const groundLinesG = document.getElementById('ground-lines');
const groundLines = [];
for (let i = 0; i < 30; i++) {
const ln = el('line', { x1: i * 40, y1: 290, x2: i * 40, y2: 355, 'stroke-width': 0.6, 'stroke-dasharray': '3,5' });
groundLinesG.appendChild(ln);
groundLines.push(ln);
}
/* ===== 刻度尺标记 ===== */
const rulerG = document.getElementById('ruler');
const rulerMarks = [];
for (let i = 0; i < 25; i++) {
const mk = el('line', { x1: i * 40, y1: -3, x2: i * 40, y2: 3, stroke: '#2A2A44', 'stroke-width': 0.8 });
rulerG.appendChild(mk);
rulerMarks.push(mk);
if (i % 3 === 0) {
const tx = el('text', { x: i * 40, y: 12, 'text-anchor': 'middle', 'font-size': 7, fill: '#3A3A54', 'font-family': "'Fira Code',monospace" });
tx.textContent = i * 40;
rulerG.appendChild(tx);
}
}
/* ===== 轨迹 ===== */
const trailEl = document.getElementById('trail');
/* ===== 鳞片微观 inset ===== */
const insetScalesG = document.getElementById('inset-scales');
const insetArrowsG = document.getElementById('inset-arrows');
const insetScaleEls = [];
for (let i = 0; i < 7; i++) {
const sc = el('polygon', { fill: '#AAA69E', stroke: '#888478', 'stroke-width': 0.6 });
insetScalesG.appendChild(sc);
insetScaleEls.push(sc);
}
/* ===== 粒子池 ===== */
const particleG = el('g');
snakeG.parentNode.insertBefore(particleG, forceG);
const PMAX = 60;
const particles = [];
for (let i = 0; i < PMAX; i++) {
const c = el('circle', { r: 1.8, fill: '#FF5722', opacity: 0 });
particleG.appendChild(c);
particles.push({ el: c, life: 0, x: 0, y: 0, vx: 0, vy: 0 });
}
let pIdx = 0;
function emitParticle(x, y) {
const p = particles[pIdx % PMAX]; pIdx++;
p.x = x; p.y = y;
p.vx = (Math.random() - 0.5) * 30;
p.vy = -Math.random() * 25 - 5;
p.life = 1;
}
/* ===== 计算段状态 ===== */
function computeSegments() {
for (let i = 0; i < CFG.numSeg; i++) {
const phase = S.time - CFG.waveK * i;
const cx = CFG.headX - i * CFG.segSpacing;
const cy = CFG.centerY + CFG.amplitude * Math.sin(phase);
const vy = CFG.amplitude * Math.cos(phase); // ∝ 横向速度
// 切线角
let angle;
if (i === 0) {
const cy1 = CFG.centerY + CFG.amplitude * Math.sin(S.time - CFG.waveK * 1);
angle = Math.atan2(cy1 - cy, (CFG.headX - 1 * CFG.segSpacing) - cx);
} else {
const cxP = CFG.headX - (i - 1) * CFG.segSpacing;
const cyP = CFG.centerY + CFG.amplitude * Math.sin(S.time - CFG.waveK * (i - 1));
angle = Math.atan2(cyP - cy, cxP - cx);
}
// 摩擦状态: vy>0 向下→左侧抓地;vy<0 向上→右侧抓地
const d = segData[i];
d.cx = cx; d.cy = cy; d.angle = angle; d.vy = vy;
d.leftHigh = vy > 0.1;
d.rightHigh = vy < -0.1;
}
}
/* ===== 渲染 ===== */
function render() {
const rough = S.roughness;
const asymmetry = (muHigh() - muLow()) / 0.7; // 0~1
// --- 蛇段 ---
for (let i = 0; i < CFG.numSeg; i++) {
const d = segData[i];
d.g.setAttribute('transform', `translate(${d.cx},${d.cy}) rotate(${deg(d.angle)})`);
// 摩擦边颜色
const leftColor = d.leftHigh ? `rgba(255,87,34,${0.5 + 0.5 * asymmetry})` : `rgba(0,229,160,${0.3 + 0.3 * asymmetry})`;
const rightColor = d.rightHigh ? `rgba(255,87,34,${0.5 + 0.5 * asymmetry})` : `rgba(0,229,160,${0.3 + 0.3 * asymmetry})`;
const leftOp = d.leftHigh ? 0.9 : 0.55;
const rightOp = d.rightHigh ? 0.9 : 0.55;
d.leftEdge.setAttribute('fill', leftColor);
d.leftEdge.setAttribute('opacity', leftOp * asymmetry + 0.15);
d.rightEdge.setAttribute('fill', rightColor);
d.rightEdge.setAttribute('opacity', rightOp * asymmetry + 0.15);
// 力箭头
if (S.showForce && asymmetry > 0.05) {
const fMag = Math.abs(d.vy) / CFG.amplitude * asymmetry * forwardFactor();
const fLen = clamp(fMag * 30, 4, 35);
d.arrowG.setAttribute('opacity', clamp(fMag * 1.5, 0, 0.85));
d.arrowG.setAttribute('transform', `translate(${d.cx},${d.cy}) rotate(${deg(d.angle)})`);
d.arrowLine.setAttribute('x2', fLen);
d.arrowHead.setAttribute('points', `${fLen},-3.5 ${fLen + 7},0 ${fLen},3.5`);
} else {
d.arrowG.setAttribute('opacity', 0);
}
// 粒子发射
if (asymmetry > 0.15) {
const isGripLeft = d.leftHigh && Math.abs(d.vy) > CFG.amplitude * 0.5;
const isGripRight = d.rightHigh && Math.abs(d.vy) > CFG.amplitude * 0.5;
if (isGripLeft && Math.random() < 0.15 * asymmetry) {
const nx = -Math.sin(d.angle) * CFG.segW / 2;
const ny = Math.cos(d.angle) * CFG.segW / 2;
emitParticle(d.cx + nx, d.cy - ny);
}
if (isGripRight && Math.random() < 0.15 * asymmetry) {
const nx = Math.sin(d.angle) * CFG.segW / 2;
const ny = -Math.cos(d.angle) * CFG.segW / 2;
emitParticle(d.cx + nx, d.cy + ny);
}
}
}
// --- 前进大箭头 ---
const thrustG = document.getElementById('thrust-arrow');
const fSpeed = forwardSpeed();
if (fSpeed > 2) {
const arrLen = clamp(fSpeed * 0.5, 10, 60);
const hx = segData[0].cx + 30;
const hy = segData[0].cy;
thrustG.querySelector('line').setAttribute('x1', hx);
thrustG.querySelector('line').setAttribute('y1', hy);
thrustG.querySelector('line').setAttribute('x2', hx + arrLen);
thrustG.querySelector('line').setAttribute('y2', hy);
thrustG.querySelector('polygon').setAttribute('points', `${hx + arrLen},${hy - 5} ${hx + arrLen + 10},${hy} ${hx + arrLen},${hy + 5}`);
thrustG.setAttribute('opacity', clamp(fSpeed / 40, 0.2, 0.8));
} else {
thrustG.setAttribute('opacity', 0);
}
// --- 地面滚动 ---
const gOffset = (S.dist * 0.5) % 40;
for (let i = 0; i < groundLines.length; i++) {
groundLines[i].setAttribute('x1', i * 40 - gOffset);
groundLines[i].setAttribute('x2', i * 40 - gOffset);
}
// 刻度尺
const rOffset = (S.dist * 0.5) % 40;
for (let i = 0; i < rulerMarks.length; i++) {
rulerMarks[i].setAttribute('x1', i * 40 - rOffset);
rulerMarks[i].setAttribute('x2', i * 40 - rOffset);
}
// --- 轨迹 ---
if (segData[0].cx && segData[0].cy) {
S.trail.push({ x: segData[0].cx, y: segData[0].cy });
if (S.trail.length > 300) S.trail.shift();
if (S.trail.length > 2) {
trailEl.setAttribute('points', S.trail.map(p => `${p.x},${p.y}`).join(' '));
}
}
// --- 鳞片微观 ---
renderInset();
// --- 指标 ---
document.getElementById('m-dist').textContent = (S.dist * 0.5).toFixed(1) + ' mm';
document.getElementById('m-speed').textContent = (fSpeed * 0.5).toFixed(1) + ' mm/s';
document.getElementById('m-mu-h').textContent = muHigh().toFixed(2);
document.getElementById('m-mu-l').textContent = muLow().toFixed(2);
const ratio = muLow() > 0.01 ? (muHigh() / muLow()).toFixed(1) : '—';
document.getElementById('m-ratio').textContent = ratio;
const surfEl = document.getElementById('m-surface');
if (S.roughness < 0.15) { surfEl.textContent = '玻璃(失效)'; surfEl.style.color = '#FF5722'; }
else if (S.roughness < 0.4) { surfEl.textContent = '光滑'; surfEl.style.color = '#FFA726'; }
else { surfEl.textContent = '正常'; surfEl.style.color = '#00E5A0'; }
// 警告
document.getElementById('warn-glass').classList.toggle('show', S.roughness < 0.12);
}
/* ===== 鳞片微观渲染 ===== */
function renderInset() {
const headD = segData[0];
const isGripRight = headD.rightHigh; // 右侧抓地
const isGripLeft = headD.leftHigh; // 左侧抓地
const isGrip = isGripLeft || isGripRight;
const asym = (muHigh() - muLow()) / 0.7;
const a = S.scaleAngle;
const rad = a * Math.PI / 180;
const modeLabel = document.getElementById('inset-mode-label');
const muLabel = document.getElementById('inset-mu-label');
if (isGrip) {
modeLabel.textContent = '← 后退方向:鳞片倒刺扎入地面';
modeLabel.setAttribute('fill', '#FF5722');
muLabel.textContent = `μ_静 ≥ ${muHigh().toFixed(2)} (高摩擦锚定)`;
muLabel.setAttribute('fill', '#FF5722');
} else {
modeLabel.textContent = '→ 前进方向:鳞片顺滑放下';
modeLabel.setAttribute('fill', '#00E5A0');
muLabel.textContent = `μ_动 ≤ ${muLow().toFixed(2)} (低摩擦滑行)`;
muLabel.setAttribute('fill', '#00E5A0');
}
// 鳞片几何
const baseY = 78; // 鳞片根部 y
const sLen = 14; // 鳞片长度
const sW = 8; // 鳞片宽度
for (let i = 0; i < insetScaleEls.length; i++) {
const sx = 80 + i * 34;
// 鳞片倾斜方向:向左倾斜45°(倒刺朝左/后方)
const tipX = sx - sLen * Math.cos(rad);
const tipY = baseY + sLen * Math.sin(rad);
const p1x = sx - sW / 2, p1y = baseY;
const p2x = sx + sW / 2, p2y = baseY;
const p3x = tipX + sW / 3 * Math.sin(rad), p3y = tipY + sW / 3 * Math.cos(rad);
const p4x = tipX - sW / 3 * Math.sin(rad), p4y = tipY - sW / 3 * Math.cos(rad);
insetScaleEls[i].setAttribute('points', `${p1x},${p1y} ${p2x},${p2y} ${p3x},${p3y} ${p4x},${p4y}`);
if (isGrip) {
insetScaleEls[i].setAttribute('fill', `rgba(255,87,34,${0.4 + 0.5 * asym})`);
insetScaleEls[i].setAttribute('stroke', '#FF5722');
// 鳞片尖端下压
insetScaleEls[i].setAttribute('transform', `translate(0,${2 * asym})`);
} else {
insetScaleEls[i].setAttribute('fill', `rgba(0,229,160,${0.25 + 0.35 * asym})`);
insetScaleEls[i].setAttribute('stroke', '#00E5A0');
insetScaleEls[i].setAttribute('transform', 'translate(0,0)');
}
}
// 运动箭头
while (insetArrowsG.firstChild) insetArrowsG.removeChild(insetArrowsG.firstChild);
if (isGrip) {
// 身体向左(后退),地面反力向右
const arr = el('line', { x1: 250, y1: 68, x2: 200, y2: 68, stroke: '#FF5722', 'stroke-width': 2.5, 'stroke-linecap': 'round', 'marker-end': '' });
insetArrowsG.appendChild(arr);
const arr2 = el('line', { x1: 130, y1: 100, x2: 190, y2: 100, stroke: '#FFD54F', 'stroke-width': 2, 'stroke-linecap': 'round' });
insetArrowsG.appendChild(arr2);
const tx1 = el('text', { x: 255, y: 66, 'font-size': 8, fill: '#FF5722', 'font-family': "'Fira Code',monospace" });
tx1.textContent = '后退';
insetArrowsG.appendChild(tx1);
const tx2 = el('text', { x: 192, y: 98, 'font-size': 8, fill: '#FFD54F', 'font-family': "'Fira Code',monospace" });
tx2.textContent = '地面反力 →';
insetArrowsG.appendChild(tx2);
} else {
const arr = el('line', { x1: 130, y1: 68, x2: 190, y2: 68, stroke: '#00E5A0', 'stroke-width': 2, 'stroke-linecap': 'round' });
insetArrowsG.appendChild(arr);
const tx1 = el('text', { x: 100, y: 66, 'font-size': 8, fill: '#00E5A0', 'font-family': "'Fira Code',monospace" });
tx1.textContent = '→ 前进滑行';
insetArrowsG.appendChild(tx1);
const tx2 = el('text', { x: 140, y: 100, 'font-size': 8, fill: '#5E5A54', 'font-family': "'Fira Code',monospace" });
tx2.textContent = '极小阻力';
insetArrowsG.appendChild(tx2);
}
// 角度标注
const angleArc = el('path', {
d: `M 80,${baseY} A 12,12 0 0,0 ${80 - 12 * Math.cos(rad)},${baseY + 12 * Math.sin(rad)}`,
fill: 'none', stroke: '#FFD54F', 'stroke-width': 0.8, opacity: 0.7
});
insetArrowsG.appendChild(angleArc);
const angleTx = el('text', { x: 56, y: baseY + 14, 'font-size': 8, fill: '#FFD54F', 'font-family': "'Fira Code',monospace" });
angleTx.textContent = `${a}°`;
insetArrowsG.appendChild(angleTx);
}
/* ===== 粒子更新 ===== */
function updateParticles(dt) {
for (const p of particles) {
if (p.life > 0) {
p.life -= dt * 2.5;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 40 * dt; // 重力
p.el.setAttribute('cx', p.x);
p.el.setAttribute('cy', p.y);
p.el.setAttribute('opacity', clamp(p.life, 0, 1));
p.el.setAttribute('r', 1.2 + p.life * 1.2);
}
}
}
/* ===== 动画循环 ===== */
let lastTS = 0;
function animate(ts) {
const dt = Math.min((ts - lastTS) / 1000, 0.05);
lastTS = ts;
if (!S.paused) {
S.time += dt * S.waveSpeed;
S.dist += forwardSpeed() * dt;
}
computeSegments();
render();
updateParticles(dt);
requestAnimationFrame(animate);
}
/* ===== 控件 ===== */
function initControls() {
const cSpeed = document.getElementById('c-speed');
const cAngle = document.getElementById('c-angle');
const cRough = document.getElementById('c-rough');
cSpeed.addEventListener('input', () => {
S.waveSpeed = parseFloat(cSpeed.value);
document.getElementById('v-speed').textContent = S.waveSpeed.toFixed(1);
});
cAngle.addEventListener('input', () => {
S.scaleAngle = parseInt(cAngle.value);
document.getElementById('v-angle').textContent = S.scaleAngle + '°';
});
cRough.addEventListener('input', () => {
S.roughness = parseFloat(cRough.value);
document.getElementById('v-rough').textContent = S.roughness.toFixed(2);
});
document.getElementById('btn-pause').addEventListener('click', function () {
S.paused = !S.paused;
this.textContent = S.paused ? '继续' : '暂停';
this.classList.toggle('active', S.paused);
});
document.getElementById('btn-force').addEventListener('click', function () {
S.showForce = !S.showForce;
this.classList.toggle('active', S.showForce);
});
document.getElementById('btn-reset').addEventListener('click', () => {
S.time = 0; S.dist = 0; S.trail = [];
trailEl.setAttribute('points', '');
});
}
/* ===== 启动 ===== */
initControls();
requestAnimationFrame(animate);
</script>
</body>
</html>
实现说明
本动画以深色技术美学为基调,完整呈现了仿生鳞片单向推进的 IFR 理想解原理:
核心视觉编码
- **橙红色(#FF5722)**标识高摩擦抓地侧——鳞片倒刺扎入地面,产生锚定力
- **薄荷绿(#00E5A0)**标识低摩擦滑行侧——鳞片顺滑放下,几乎无阻力
- 摩擦边颜色强度随摩擦不对称度(
μ_高 - μ_低)动态缩放,粗糙度越低,两侧颜色越趋同
IFR 体现
- 右上角 IFR 标注框强调"被动鳞片自调节 + 零额外动力"——同一结构根据运动方向自动切换抓地/滑行,无需额外执行器
- 鳞片微观 inset 同步展示当前活跃侧的力学机制:后退时鳞片下压锁死,前进时鳞片翘起滑行
交互控制
- 波速滑块:调节 S 形行波传播速度,直接影响前进推力
- 鳞片角度滑块:15°~75° 可调,45° 时
sin(2α)最大、推力最优,偏离则明显衰减 - 表面粗糙度滑块:降至 0 附近时模拟玻璃表面,摩擦差异消失,蛇体停止前进并弹出失效警告
- 力向量开关:金色箭头显示各段净前向推力分量
- 暂停/重置:便于观察特定时刻的力学状态
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
