独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柔性气动蛇 · 最终理想解原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #050a12;
--fg: #b8c8d8;
--muted: #2a3a50;
--accent: #00e5a0;
--accent2: #ff5e3a;
--accent3: #1a6aff;
--card: #0a1420;
--border: #142038;
--skin: #14332a;
--inflate: #ff5e3a;
--deflate: #162840;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
background:var(--bg);color:var(--fg);
font-family:'Noto Sans SC',sans-serif;
min-height:100vh;display:flex;flex-direction:column;align-items:center;
overflow-x:hidden;
background-image:
radial-gradient(ellipse 80% 60% at 30% 40%, rgba(0,229,160,0.04) 0%, transparent 70%),
radial-gradient(ellipse 60% 50% at 75% 60%, rgba(255,94,58,0.03) 0%, transparent 70%);
}
header{
text-align:center;padding:28px 20px 10px;width:100%;max-width:1200px;
}
header h1{
font-family:'Rajdhani',sans-serif;font-weight:700;font-size:clamp(22px,3.2vw,38px);
letter-spacing:2px;color:#e0eaf4;
background:linear-gradient(90deg,#00e5a0 0%,#e0eaf4 40%,#ff5e3a 100%);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;
}
header p{
font-size:clamp(12px,1.5vw,15px);color:var(--muted);margin-top:4px;
font-weight:300;letter-spacing:1px;
}
.main-wrap{
display:flex;gap:18px;padding:10px 20px 16px;width:100%;max-width:1200px;
flex-wrap:wrap;justify-content:center;
}
.snake-panel{
flex:1 1 680px;min-width:0;
background:var(--card);border:1px solid var(--border);border-radius:14px;
overflow:hidden;position:relative;
}
.snake-panel svg{display:block;width:100%;height:auto;}
.side-panel{
flex:0 0 320px;display:flex;flex-direction:column;gap:14px;
}
.cross-panel{
background:var(--card);border:1px solid var(--border);border-radius:14px;
padding:14px;position:relative;overflow:hidden;
}
.cross-panel h3,.info-panel h3{
font-family:'Rajdhani',sans-serif;font-weight:600;font-size:14px;
color:var(--accent);letter-spacing:1px;margin-bottom:8px;text-transform:uppercase;
}
.cross-panel svg{display:block;width:100%;height:auto;}
.info-panel{
background:var(--card);border:1px solid var(--border);border-radius:14px;
padding:14px;flex:1;
}
.info-row{
display:flex;justify-content:space-between;align-items:center;
padding:5px 0;border-bottom:1px solid var(--border);font-size:13px;
}
.info-row:last-child{border-bottom:none}
.info-label{color:var(--muted);font-weight:400}
.info-value{
font-family:'JetBrains Mono',monospace;font-weight:600;font-size:13px;
color:#e0eaf4;
}
.info-value.hot{color:var(--accent2)}
.info-value.cool{color:var(--accent)}
.controls{
display:flex;gap:20px;padding:8px 20px 24px;width:100%;max-width:1200px;
flex-wrap:wrap;justify-content:center;
}
.ctrl-group{
background:var(--card);border:1px solid var(--border);border-radius:10px;
padding:10px 18px;display:flex;align-items:center;gap:12px;min-width:240px;
}
.ctrl-group label{
font-size:12px;color:var(--muted);white-space:nowrap;min-width:72px;
font-weight:500;letter-spacing:0.5px;
}
.ctrl-group input[type=range]{
flex:1;-webkit-appearance:none;appearance:none;height:4px;
background:var(--border);border-radius:2px;outline:none;cursor:pointer;
}
.ctrl-group input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:16px;height:16px;border-radius:50%;
background:var(--accent);border:2px solid var(--bg);cursor:pointer;
box-shadow:0 0 8px rgba(0,229,160,0.4);
}
.ctrl-group .ctrl-val{
font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--accent);
min-width:52px;text-align:right;
}
.legend{
display:flex;gap:16px;padding:2px 20px 10px;flex-wrap:wrap;justify-content:center;
}
.legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)}
.legend-dot{width:10px;height:10px;border-radius:50%}
@media(max-width:768px){
.side-panel{flex:1 1 100%}
.ctrl-group{min-width:200px}
}
</style>
</head>
<body>
<header>
<h1>PNEUMATIC SOFT SNAKE — IFR</h1>
<p>消除刚性关节 · 柔性形变驱动连续体蜿蜒 · 气动人工肌肉行波蠕动</p>
</header>
<div class="main-wrap">
<!-- 主蛇体动画 -->
<div class="snake-panel">
<svg id="main-svg" viewBox="0 0 900 360" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 发光滤镜 -->
<filter id="glow-o" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-g" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-c" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- 皮肤纹理 -->
<pattern id="scales" width="10" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(-5)">
<path d="M0 8 Q5 2 10 8" stroke="#1a4a3a" stroke-width="0.4" fill="none" opacity="0.5"/>
</pattern>
<!-- 身体填充渐变 (沿 X 轴) -->
<linearGradient id="bodyGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0d1f18"/>
<stop offset="100%" stop-color="#0d1f18"/>
</linearGradient>
</defs>
<!-- 网格背景 -->
<g opacity="0.08" stroke="#3a6a5a" stroke-width="0.5">
<line x1="0" y1="180" x2="900" y2="180"/>
<line x1="0" y1="90" x2="900" y2="90" stroke-dasharray="4,8"/>
<line x1="0" y1="270" x2="900" y2="270" stroke-dasharray="4,8"/>
</g>
<!-- 蛇体元素层 -->
<g id="snake-group">
<path id="body-path" fill="#0f2a20" stroke="#1a4a3a" stroke-width="1"/>
<path id="body-texture" fill="url(#scales)" opacity="0.35"/>
<!-- 左侧肌肉激活指示 -->
<g id="left-indicators"></g>
<!-- 右侧肌肉激活指示 -->
<g id="right-indicators"></g>
<!-- 中心气道 -->
<path id="center-path" fill="none" stroke="#00e5a0" stroke-width="2.5" stroke-linecap="round" opacity="0.7" filter="url(#glow-g)"/>
<!-- 气体粒子 -->
<g id="particles"></g>
<!-- 行波相位标记 -->
<g id="phase-markers"></g>
</g>
<!-- 标注 -->
<g id="annotations" font-family="'Noto Sans SC',sans-serif" font-size="11" fill="#5a7a8a"></g>
</svg>
</div>
<!-- 侧面板 -->
<div class="side-panel">
<div class="cross-panel">
<h3>Cross Section · 截面构型</h3>
<svg id="cross-svg" viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cross-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- 外层弹性皮肤 -->
<circle cx="140" cy="140" r="105" fill="none" stroke="#1a4a3a" stroke-width="2" stroke-dasharray="6,3" opacity="0.6"/>
<circle cx="140" cy="140" r="95" fill="#0a1a14" stroke="#1a3a2a" stroke-width="1.5"/>
<!-- 肌肉组 A (顶部) -->
<path id="muscle-a" fill="#162840" stroke="#2a4a6a" stroke-width="1"/>
<!-- 肌肉组 B (左下) -->
<path id="muscle-b" fill="#162840" stroke="#2a4a6a" stroke-width="1"/>
<!-- 肌肉组 C (右下) -->
<path id="muscle-c" fill="#162840" stroke="#2a4a6a" stroke-width="1"/>
<!-- 中央硅胶管 -->
<circle cx="140" cy="140" r="28" fill="#0a1a14" stroke="#00e5a0" stroke-width="1.5" opacity="0.8"/>
<circle cx="140" cy="140" r="18" fill="none" stroke="#00e5a0" stroke-width="0.8" opacity="0.4" stroke-dasharray="3,3"/>
<!-- 气道标记 -->
<circle cx="140" cy="140" r="5" fill="#00e5a0" opacity="0.6" filter="url(#cross-glow)"/>
<!-- 弯曲方向箭头 -->
<g id="bend-arrow" opacity="0.8">
<line id="bend-line" x1="140" y1="140" x2="140" y2="50" stroke="#ff5e3a" stroke-width="2" stroke-dasharray="4,3"/>
<polygon id="bend-head" points="140,44 135,54 145,54" fill="#ff5e3a"/>
</g>
<!-- 标签 -->
<text x="140" y="22" text-anchor="middle" fill="#5a8a7a" font-size="10" font-family="'Noto Sans SC'">弹性皮肤</text>
<text x="60" y="60" text-anchor="middle" fill="#5a7a9a" font-size="9" font-family="'Noto Sans SC'" id="lbl-a">肌肉 A</text>
<text x="220" y="210" text-anchor="middle" fill="#5a7a9a" font-size="9" font-family="'Noto Sans SC'" id="lbl-b">肌肉 B</text>
<text x="60" y="210" text-anchor="middle" fill="#5a7a9a" font-size="9" font-family="'Noto Sans SC'" id="lbl-c">肌肉 C</text>
<text x="140" y="144" text-anchor="middle" fill="#00e5a0" font-size="8" font-family="'Noto Sans SC'" opacity="0.7">硅胶管</text>
<!-- 120° 标注 -->
<path d="M 140 95 A 45 45 0 0 1 101 117" fill="none" stroke="#3a5a6a" stroke-width="0.8" stroke-dasharray="2,2"/>
<text x="108" y="100" fill="#3a5a6a" font-size="8" font-family="'JetBrains Mono'">120°</text>
</svg>
</div>
<div class="info-panel">
<h3>Parameters · 实时参数</h3>
<div class="info-row"><span class="info-label">工作气压</span><span class="info-value hot" id="val-pressure">0.30 MPa</span></div>
<div class="info-row"><span class="info-label">编织角</span><span class="info-value cool" id="val-angle">30°</span></div>
<div class="info-row"><span class="info-label">行波速度</span><span class="info-value" id="val-speed">1.20 rad/s</span></div>
<div class="info-row"><span class="info-label">最大曲率</span><span class="info-value" id="val-kappa">0.035</span></div>
<div class="info-row"><span class="info-label">活跃肌肉组</span><span class="info-value hot" id="val-active">B + C</span></div>
<div class="info-row"><span class="info-label">弯曲方向</span><span class="info-value cool" id="val-bend">→ 右</span></div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#ff5e3a;box-shadow:0 0 6px #ff5e3a"></div>充气收缩 (轴向缩短)</div>
<div class="legend-item"><div class="legend-dot" style="background:#162840;border:1px solid #2a4a6a"></div>放气松弛 (轴向伸长)</div>
<div class="legend-item"><div class="legend-dot" style="background:#00e5a0;box-shadow:0 0 6px #00e5a0"></div>中央气道 / 硅胶管</div>
<div class="legend-item"><div class="legend-dot" style="background:#ffd700;box-shadow:0 0 6px #ffd700"></div>行波相位前沿</div>
</div>
<div class="controls">
<div class="ctrl-group">
<label>行波速度</label>
<input type="range" id="ctrl-speed" min="0.3" max="3" step="0.1" value="1.2">
<span class="ctrl-val" id="cv-speed">1.2</span>
</div>
<div class="ctrl-group">
<label>工作气压</label>
<input type="range" id="ctrl-pressure" min="0.2" max="0.4" step="0.01" value="0.30">
<span class="ctrl-val" id="cv-pressure">0.30</span>
</div>
<div class="ctrl-group">
<label>螺旋编织角</label>
<input type="range" id="ctrl-angle" min="25" max="35" step="1" value="30">
<span class="ctrl-val" id="cv-angle">30°</span>
</div>
</div>
<script>
/* ===== 配置 ===== */
const CFG = {
segments: 80, // 蛇体分段数
segLen: 3.2, // 每段长度
waveK: 0.09, // 波数
baseRadius: 17, // 基础截面半径
inflateMax: 9, // 最大膨胀增量
headSegs: 5, // 头部渐变段数
tailSegs: 12, // 尾部渐变段数
numParticles: 18, // 气体粒子数
numIndicators: 24, // 每侧激活指示器数
numPhaseMarkers: 5, // 行波相位标记数
};
/* ===== 状态 ===== */
const S = {
time: 0,
speed: 1.2,
pressure: 0.30,
spiralAngle: 30,
maxKappa: 0.035,
};
/* ===== DOM 引用 ===== */
const NS = 'http://www.w3.org/2000/svg';
let mainSvg, bodyPath, bodyTexture, centerPath;
let leftIndicators = [], rightIndicators = [];
let particles = [], phaseMarkers = [];
let annotations;
/* ===== 初始化 ===== */
document.addEventListener('DOMContentLoaded', init);
function init() {
mainSvg = document.getElementById('main-svg');
bodyPath = document.getElementById('body-path');
bodyTexture = document.getElementById('body-texture');
centerPath = document.getElementById('center-path');
annotations = document.getElementById('annotations');
// 创建激活指示器(每侧)
const lGrp = document.getElementById('left-indicators');
const rGrp = document.getElementById('right-indicators');
for (let i = 0; i < CFG.numIndicators; i++) {
const lc = document.createElementNS(NS, 'ellipse');
lc.setAttribute('rx', '4'); lc.setAttribute('ry', '3');
lc.setAttribute('opacity', '0');
lGrp.appendChild(lc);
leftIndicators.push(lc);
const rc = document.createElementNS(NS, 'ellipse');
rc.setAttribute('rx', '4'); rc.setAttribute('ry', '3');
rc.setAttribute('opacity', '0');
rGrp.appendChild(rc);
rightIndicators.push(rc);
}
// 创建气体粒子
const pGrp = document.getElementById('particles');
for (let i = 0; i < CFG.numParticles; i++) {
const c = document.createElementNS(NS, 'circle');
c.setAttribute('r', String(1.5 + Math.random() * 1.5));
c.setAttribute('fill', '#00ffd5');
c.setAttribute('opacity', '0');
pGrp.appendChild(c);
particles.push({ el: c, phase: Math.random(), speed: 0.3 + Math.random() * 0.5 });
}
// 创建行波相位标记
const mGrp = document.getElementById('phase-markers');
for (let i = 0; i < CFG.numPhaseMarkers; i++) {
const d = document.createElementNS(NS, 'polygon');
d.setAttribute('fill', '#ffd700');
d.setAttribute('opacity', '0');
mGrp.appendChild(d);
phaseMarkers.push({ el: d, offset: i / CFG.numPhaseMarkers });
}
// 绑定控件
setupControls();
// 启动动画
requestAnimationFrame(animate);
}
function setupControls() {
const cs = document.getElementById('ctrl-speed');
const cp = document.getElementById('ctrl-pressure');
const ca = document.getElementById('ctrl-angle');
cs.addEventListener('input', () => {
S.speed = parseFloat(cs.value);
document.getElementById('cv-speed').textContent = S.speed.toFixed(1);
});
cp.addEventListener('input', () => {
S.pressure = parseFloat(cp.value);
document.getElementById('cv-pressure').textContent = S.pressure.toFixed(2);
});
ca.addEventListener('input', () => {
S.spiralAngle = parseInt(ca.value);
document.getElementById('cv-angle').textContent = S.spiralAngle + '°';
});
}
/* ===== 动画循环 ===== */
let lastTs = 0;
function animate(ts) {
const dt = lastTs ? Math.min((ts - lastTs) / 1000, 0.05) : 0.016;
lastTs = ts;
S.time += dt * S.speed;
// 气压影响最大曲率
S.maxKappa = 0.015 + (S.pressure - 0.2) / 0.2 * 0.04;
const pts = computeSnake();
updateBodyPath(pts);
updateCenterPath(pts);
updateIndicators(pts);
updateParticles(pts, dt);
updatePhaseMarkers(pts);
updateAnnotations(pts);
updateCrossSection(pts);
updateInfoPanel(pts);
requestAnimationFrame(animate);
}
/* ===== 蛇体中心线计算 (曲率积分法) ===== */
function computeSnake() {
const N = CFG.segments;
const L = CFG.segLen;
const K = CFG.waveK;
const w = S.speed * 2.8;
const k0 = S.maxKappa;
let theta = 0, x = 0, y = 0;
const pts = [{ x, y, theta, kappa: 0 }];
for (let i = 1; i <= N; i++) {
const s = i * L;
// 2.5 个波长的行波
const kappa = k0 * Math.sin(K * s - w * S.time);
theta += kappa * L;
x += Math.cos(theta) * L;
y += Math.sin(theta) * L;
pts.push({ x, y, theta, kappa });
}
// 计算边界并居中
let mnX = Infinity, mxX = -Infinity, mnY = Infinity, mxY = -Infinity;
for (const p of pts) {
mnX = Math.min(mnX, p.x); mxX = Math.max(mxX, p.x);
mnY = Math.min(mnY, p.y); mxY = Math.max(mxY, p.y);
}
const cx = (mnX + mxX) / 2, cy = (mnY + mxY) / 2;
const offX = 450 - cx, offY = 180 - cy;
for (const p of pts) { p.x += offX; p.y += offY; }
return pts;
}
/* ===== 半径计算 (含头尾渐变 & 肌肉膨胀) ===== */
function segRadius(i, side, pts) {
const N = pts.length - 1;
const k0 = Math.max(S.maxKappa, 0.001);
const kappa = pts[i].kappa;
// 左侧肌肉在 kappa>0 时激活 (蛇体右弯), 右侧在 kappa<0 时激活
let activation;
if (side === 'left') {
activation = Math.max(0, kappa / k0);
} else {
activation = Math.max(0, -kappa / k0);
}
// 膨胀量与气压成正比
const inflate = CFG.inflateMax * activation * (S.pressure / 0.3);
// 头尾渐变
let taper = 1;
if (i < CFG.headSegs) taper = 0.4 + 0.6 * (i / CFG.headSegs);
if (i > N - CFG.tailSegs) taper = Math.max(0.05, (N - i) / CFG.tailSegs);
return Math.max(1, (CFG.baseRadius + inflate) * taper);
}
/* ===== 更新蛇体轮廓 ===== */
function updateBodyPath(pts) {
const N = pts.length - 1;
const leftPts = [], rightPts = [];
for (let i = 0; i <= N; i++) {
const p = pts[i];
const nx = -Math.sin(p.theta);
const ny = Math.cos(p.theta);
const lr = segRadius(i, 'left', pts);
const rr = segRadius(i, 'right', pts);
leftPts.push({ x: p.x + nx * lr, y: p.y + ny * lr });
rightPts.push({ x: p.x - nx * rr, y: p.y - ny * rr });
}
// 构建闭合路径
let d = `M ${leftPts[0].x.toFixed(1)} ${leftPts[0].y.toFixed(1)}`;
for (let i = 1; i <= N; i++) {
d += ` L ${leftPts[i].x.toFixed(1)} ${leftPts[i].y.toFixed(1)}`;
}
// 尾端弧线
const tl = leftPts[N], tr = rightPts[N];
d += ` Q ${pts[N].x.toFixed(1)} ${pts[N].y.toFixed(1)} ${tr.x.toFixed(1)} ${tr.y.toFixed(1)}`;
for (let i = N - 1; i >= 0; i--) {
d += ` L ${rightPts[i].x.toFixed(1)} ${rightPts[i].y.toFixed(1)}`;
}
// 头端弧线
const hl = leftPts[0], hr = rightPts[0];
d += ` Q ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)} ${hl.x.toFixed(1)} ${hl.y.toFixed(1)}`;
d += ' Z';
bodyPath.setAttribute('d', d);
bodyTexture.setAttribute('d', d);
}
/* ===== 更新中心气道 ===== */
function updateCenterPath(pts) {
let d = `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)}`;
for (let i = 1; i < pts.length; i++) {
d += ` L ${pts[i].x.toFixed(1)} ${pts[i].y.toFixed(1)}`;
}
centerPath.setAttribute('d', d);
}
/* ===== 更新肌肉激活指示器 ===== */
function updateIndicators(pts) {
const N = pts.length - 1;
const k0 = Math.max(S.maxKappa, 0.001);
const step = Math.floor(N / CFG.numIndicators);
for (let j = 0; j < CFG.numIndicators; j++) {
const i = Math.min(j * step, N);
const p = pts[i];
const nx = -Math.sin(p.theta);
const ny = Math.cos(p.theta);
const leftAct = Math.max(0, p.kappa / k0);
const rightAct = Math.max(0, -p.kappa / k0);
const lr = segRadius(i, 'left', pts);
const rr = segRadius(i, 'right', pts);
// 左侧指示器 - 偏移到体表内侧
const loff = lr * 0.55;
leftIndicators[j].setAttribute('cx', (p.x + nx * loff).toFixed(1));
leftIndicators[j].setAttribute('cy', (p.y + ny * loff).toFixed(1));
leftIndicators[j].setAttribute('rx', String(3 + leftAct * 5));
leftIndicators[j].setAttribute('ry', String(2 + leftAct * 4));
if (leftAct > 0.15) {
const r = Math.round(255 * leftAct + 22 * (1 - leftAct));
const g = Math.round(94 * leftAct + 40 * (1 - leftAct));
const b = Math.round(58 * leftAct + 64 * (1 - leftAct));
leftIndicators[j].setAttribute('fill', `rgb(${r},${g},${b})`);
leftIndicators[j].setAttribute('opacity', String(Math.min(1, leftAct * 1.2)));
leftIndicators[j].setAttribute('filter', leftAct > 0.5 ? 'url(#glow-o)' : '');
} else {
leftIndicators[j].setAttribute('fill', '#162840');
leftIndicators[j].setAttribute('opacity', '0.3');
leftIndicators[j].setAttribute('filter', '');
}
// 右侧指示器
const roff = rr * 0.55;
rightIndicators[j].setAttribute('cx', (p.x - nx * roff).toFixed(1));
rightIndicators[j].setAttribute('cy', (p.y - ny * roff).toFixed(1));
rightIndicators[j].setAttribute('rx', String(3 + rightAct * 5));
rightIndicators[j].setAttribute('ry', String(2 + rightAct * 4));
if (rightAct > 0.15) {
const r = Math.round(255 * rightAct + 22 * (1 - rightAct));
const g = Math.round(94 * rightAct + 40 * (1 - rightAct));
const b = Math.round(58 * rightAct + 64 * (1 - rightAct));
rightIndicators[j].setAttribute('fill', `rgb(${r},${g},${b})`);
rightIndicators[j].setAttribute('opacity', String(Math.min(1, rightAct * 1.2)));
rightIndicators[j].setAttribute('filter', rightAct > 0.5 ? 'url(#glow-o)' : '');
} else {
rightIndicators[j].setAttribute('fill', '#162840');
rightIndicators[j].setAttribute('opacity', '0.3');
rightIndicators[j].setAttribute('filter', '');
}
}
}
/* ===== 更新气体粒子 ===== */
function updateParticles(pts, dt) {
const N = pts.length - 1;
for (const p of particles) {
p.phase += dt * p.speed * S.speed;
if (p.phase > 1) p.phase -= 1;
// 沿中心线运动
const idx = Math.min(Math.floor(p.phase * N), N - 1);
const frac = p.phase * N - idx;
const px = pts[idx].x + (pts[idx + 1].x - pts[idx].x) * frac;
const py = pts[idx].y + (pts[idx + 1].y - pts[idx].y) * frac;
p.el.setAttribute('cx', px.toFixed(1));
p.el.setAttribute('cy', py.toFixed(1));
// 粒子透明度脉动
const pulse = 0.4 + 0.4 * Math.sin(S.time * 4 + p.phase * 12);
p.el.setAttribute('opacity', String(pulse.toFixed(2)));
}
}
/* ===== 更新行波相位标记 ===== */
function updatePhaseMarkers(pts) {
const N = pts.length - 1;
const k0 = Math.max(S.maxKappa, 0.001);
const nx0 = -Math.sin(pts[0].theta);
const ny0 = Math.cos(pts[0].theta);
for (let m = 0; m < phaseMarkers.length; m++) {
const pm = phaseMarkers[m];
// 相位标记位于行波曲率最大处(波峰位置)
// 波峰位置: K*s - w*t = π/2 + 2πn → s = (π/2 + 2πn + w*t) / K
const w = S.speed * 2.8;
const n = m;
let sPos = (Math.PI / 2 + 2 * Math.PI * n + w * S.time) / CFG.waveK;
const totalLen = N * CFG.segLen;
// 归一化
sPos = ((sPos % totalLen) + totalLen) % totalLen;
const idx = Math.min(Math.floor(sPos / CFG.segLen), N - 1);
if (idx < 1 || idx > N - 2) {
pm.el.setAttribute('opacity', '0');
continue;
}
const p = pts[idx];
const nx = -Math.sin(p.theta);
const ny = Math.cos(p.theta);
const size = 5;
// 三角形标记,指向行波传播方向
const dx = Math.cos(p.theta);
const dy = Math.sin(p.theta);
const tipX = p.x + dx * size;
const tipY = p.y + dy * size;
const b1x = p.x - dx * size * 0.4 + nx * size * 0.5;
const b1y = p.y - dy * size * 0.4 + ny * size * 0.5;
const b2x = p.x - dx * size * 0.4 - nx * size * 0.5;
const b2y = p.y - dy * size * 0.4 - ny * size * 0.5;
pm.el.setAttribute('points',
`${tipX.toFixed(1)},${tipY.toFixed(1)} ${b1x.toFixed(1)},${b1y.toFixed(1)} ${b2x.toFixed(1)},${b2y.toFixed(1)}`);
const pulse = 0.5 + 0.4 * Math.sin(S.time * 5 + m * 1.2);
pm.el.setAttribute('opacity', String(pulse.toFixed(2)));
pm.el.setAttribute('filter', 'url(#glow-c)');
}
}
/* ===== 更新标注 ===== */
function updateAnnotations(pts) {
// 清空旧标注
while (annotations.firstChild) annotations.removeChild(annotations.firstChild);
const N = pts.length - 1;
// 头部标注
const head = pts[2];
addLabel(head.x, head.y - 30, '充气→径向膨胀+轴向缩短', '#ff5e3a', 10);
// 中段标注
const mid = pts[Math.floor(N * 0.5)];
addLabel(mid.x, mid.y + 38, '行波蠕动方向 →', '#ffd700', 10);
// 尾部标注
const tail = pts[N - 3];
addLabel(tail.x, tail.y - 28, '放气→恢复伸长', '#4a8a7a', 9);
}
function addLabel(x, y, text, color, size) {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', String(x));
t.setAttribute('y', String(y));
t.setAttribute('text-anchor', 'middle');
t.setAttribute('fill', color);
t.setAttribute('font-size', String(size));
t.setAttribute('font-family', "'Noto Sans SC', sans-serif");
t.setAttribute('opacity', '0.7');
t.textContent = text;
annotations.appendChild(t);
}
/* ===== 更新截面图 ===== */
function updateCrossSection(pts) {
const N = pts.length - 1;
const midIdx = Math.floor(N * 0.4);
const kappa = pts[midIdx].kappa;
const k0 = Math.max(S.maxKappa, 0.001);
// 3组肌肉在截面上的激活状态
// 组A (顶部 90°): 用于垂直弯曲,水平蜿蜒时激活较少
// 组B (左下 210°): kappa>0 时激活(蛇体右弯,左侧肌肉收缩)
// 组C (右下 330°): kappa<0 时激活(蛇体左弯,右侧肌肉收缩)
const actA = 0.15 + 0.1 * Math.sin(S.time * 1.5); // 轻微脉动
const actB = Math.max(0, kappa / k0);
const actC = Math.max(0, -kappa / k0);
drawMuscleArc('muscle-a', 140, 140, 65, 30, 90, actA);
drawMuscleArc('muscle-b', 140, 140, 65, 30, 210, actB);
drawMuscleArc('muscle-c', 140, 140, 65, 30, 330, actC);
// 更新标签颜色
updateMuscleLabel('lbl-a', actA);
updateMuscleLabel('lbl-b', actB);
updateMuscleLabel('lbl-c', actC);
// 更新弯曲方向箭头
const bendAngle = kappa > 0 ? -90 : 90; // kappa>0 → 右弯 → 箭头朝右
const bendMag = Math.min(Math.abs(kappa / k0), 1);
const arrowLen = 40 + bendMag * 50;
const bx = 140 + arrowLen * (kappa > 0 ? 1 : -1) * 0; // 保持中心
const arrowAngle = kappa > 0 ? 0 : 180; // 朝右或朝左
const rad = arrowAngle * Math.PI / 180;
const endX = 140 + Math.cos(rad) * arrowLen;
const endY = 140 + Math.sin(rad) * arrowLen * 0; // 水平
// 实际用角度旋转箭头组
const bendArrow = document.getElementById('bend-arrow');
const rotAngle = kappa > 0 ? 0 : 180;
bendArrow.setAttribute('transform', `rotate(${rotAngle}, 140, 140)`);
bendArrow.setAttribute('opacity', String(0.3 + bendMag * 0.7));
const bendLine = document.getElementById('bend-line');
bendLine.setAttribute('x2', String(140 + arrowLen));
bendLine.setAttribute('y2', '140');
const bendHead = document.getElementById('bend-head');
const hx = 140 + arrowLen;
bendHead.setAttribute('points',
`${hx+6},140 ${hx-4},135 ${hx-4},145`);
}
function drawMuscleArc(id, cx, cy, r, thickness, angleDeg, activation) {
const el = document.getElementById(id);
const startAngle = (angleDeg - 30) * Math.PI / 180;
const endAngle = (angleDeg + 30) * Math.PI / 180;
const rInner = r - thickness / 2;
const rOuter = r + thickness / 2 + activation * 12; // 激活时径向膨胀
const x1 = cx + rInner * Math.cos(startAngle);
const y1 = cy + rInner * Math.sin(startAngle);
const x2 = cx + rInner * Math.cos(endAngle);
const y2 = cy + rInner * Math.sin(endAngle);
const x3 = cx + rOuter * Math.cos(endAngle);
const y3 = cy + rOuter * Math.sin(endAngle);
const x4 = cx + rOuter * Math.cos(startAngle);
const y4 = cy + rOuter * Math.sin(startAngle);
const d = `M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${rInner} ${rInner} 0 0 1 ${x2.toFixed(1)} ${y2.toFixed(1)} L ${x3.toFixed(1)} ${y3.toFixed(1)} A ${rOuter} ${rOuter} 0 0 0 ${x4.toFixed(1)} ${y4.toFixed(1)} Z`;
el.setAttribute('d', d);
// 颜色插值
if (activation > 0.15) {
const r = Math.round(255 * activation + 22 * (1 - activation));
const g = Math.round(94 * activation + 40 * (1 - activation));
const b = Math.round(58 * activation + 64 * (1 - activation));
el.setAttribute('fill', `rgb(${r},${g},${b})`);
el.setAttribute('stroke', `rgba(255,94,58,${(activation * 0.8).toFixed(2)})`);
if (activation > 0.5) {
el.setAttribute('filter', 'url(#cross-glow)');
} else {
el.removeAttribute('filter');
}
} else {
el.setAttribute('fill', '#162840');
el.setAttribute('stroke', '#2a4a6a');
el.removeAttribute('filter');
}
}
function updateMuscleLabel(id, activation) {
const el = document.getElementById(id);
if (activation > 0.4) {
el.setAttribute('fill', '#ff5e3a');
el.setAttribute('font-weight', '700');
} else {
el.setAttribute('fill', '#5a7a9a');
el.setAttribute('font-weight', '400');
}
}
/* ===== 更新信息面板 ===== */
function updateInfoPanel(pts) {
const N = pts.length - 1;
const midIdx = Math.floor(N * 0.4);
const kappa = pts[midIdx].kappa;
const k0 = Math.max(S.maxKappa, 0.001);
document.getElementById('val-pressure').textContent = S.pressure.toFixed(2) + ' MPa';
document.getElementById('val-angle').textContent = S.spiralAngle + '°';
document.getElementById('val-speed').textContent = S.speed.toFixed(2) + ' rad/s';
document.getElementById('val-kappa').textContent = S.maxKappa.toFixed(3);
// 活跃肌肉组
const actB = Math.max(0, kappa / k0);
const actC = Math.max(0, -kappa / k0);
let activeStr = '—';
if (actB > 0.3 && actC < 0.3) activeStr = 'B (左下)';
else if (actC > 0.3 && actB < 0.3) activeStr = 'C (右下)';
else if (actB > 0.3 && actC > 0.3) activeStr = 'B + C';
document.getElementById('val-active').textContent = activeStr;
// 弯曲方向
const bendDir = kappa > 0.005 ? '→ 右' : kappa < -0.005 ? '← 左' : '— 直线';
document.getElementById('val-bend').textContent = bendDir;
}
</script>
</body>
</html>
实现说明
本动画以 TRIZ "最终理想解" 思想为核心,直接展示消除刚性关节后的柔性气动蛇理想工作状态:
蛇体形变原理:采用曲率积分法(而非简单正弦函数)计算蛇体中心线,从曲率逐段积分出姿态角,再积分出坐标,物理上精确还原了"气动肌肉收缩→轴向缩短→截面弯曲"的连续体运动。蛇体轮廓随肌肉膨胀动态变化——充气侧径向膨胀、体壁增厚,对侧保持基础半径,视觉上直观呈现弯曲机理。
肌肉激活可视化:左右两侧各 24 个椭圆指示器实时映射该位置的曲率/激活程度,从冷蓝色(松弛)平滑过渡到亮橙色(充气收缩),高激活区域附带 SVG 辉光滤镜,引导视线聚焦核心创新点。
截面构型联动:右侧截面图展示 120° 分布的三组 McKibben 肌肉,随蛇体中段曲率实时联动——激活组径向膨胀并发出暖色辉光,弯曲方向箭头同步偏转,清晰呈现"3 组肌肉即可实现任意方向弯曲"的精简构型。
行波相位标记:金色三角沿蛇体标示行波波峰传播位置,配合 18 个沿气道流动的青色粒子,直观表达"按行波相位依次充放气 → 连续侧向蠕动波"的协同过程。
交互控制:三个滑块分别调节行波速度、工作气压(0.2–0.4 MPa)和螺旋编织角(25°–35°),参数变化实时反映到蛇体运动幅度、肌肉膨胀程度和截面构型上,让用户亲手验证理想解的动态边界。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
