独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMA仿生蛇 · 无驱动器蜿蜒爬行原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #060a14;
--surface: #0c1424;
--surface-2: #111d30;
--border: #1a2d48;
--text: #c8d8ea;
--text-dim: #4a6080;
--accent: #00e8a2;
--accent-dim: rgba(0,232,162,0.12);
--hot: #ff6b35;
--hot-glow: rgba(255,107,53,0.45);
--cool-left: #3d5a78;
--cool-right: #3d5a78;
--active-left: #ff6b35;
--active-right: #00b8d4;
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;overflow-x:hidden;}
body{
background:var(--bg);
color:var(--text);
font-family:'Noto Sans SC','Rajdhani',sans-serif;
display:flex;flex-direction:column;align-items:center;
min-height:100vh;
}
header{
width:100%;padding:18px 32px 10px;
display:flex;align-items:baseline;gap:18px;flex-wrap:wrap;
border-bottom:1px solid var(--border);
background:linear-gradient(180deg,rgba(12,20,36,0.95),transparent);
position:relative;z-index:2;
}
header h1{
font-family:'Rajdhani','Noto Sans SC',sans-serif;
font-weight:700;font-size:clamp(22px,3.2vw,34px);
letter-spacing:1px;color:#e8f0fa;
}
header h1 .dot{color:var(--accent);}
header p{
font-size:clamp(12px,1.4vw,15px);color:var(--text-dim);
font-weight:300;letter-spacing:0.5px;
}
.main-wrap{
flex:1;width:100%;max-width:1440px;
display:flex;flex-direction:column;
padding:0 16px 16px;
}
.canvas-container{
position:relative;width:100%;
aspect-ratio:2.6/1;min-height:320px;max-height:560px;
border-radius:12px;overflow:hidden;
border:1px solid var(--border);
background:var(--surface);
margin-top:12px;
}
.canvas-container canvas{width:100%;height:100%;display:block;}
.ifs-tag{
position:absolute;top:14px;right:18px;
background:rgba(0,232,162,0.08);border:1px solid rgba(0,232,162,0.25);
border-radius:6px;padding:5px 14px;
font-family:'Rajdhani',sans-serif;font-size:13px;font-weight:600;
color:var(--accent);letter-spacing:1.5px;text-transform:uppercase;
pointer-events:none;
}
.bottom-panel{
display:grid;grid-template-columns:220px 1fr 260px;gap:14px;
margin-top:14px;width:100%;
}
@media(max-width:900px){
.bottom-panel{grid-template-columns:1fr;max-width:520px;}
}
.panel-card{
background:var(--surface);border:1px solid var(--border);
border-radius:10px;padding:14px;overflow:hidden;
}
.panel-card h3{
font-family:'Rajdhani',sans-serif;font-size:13px;font-weight:600;
color:var(--accent);letter-spacing:1.5px;text-transform:uppercase;
margin-bottom:10px;
}
.cross-svg{width:100%;max-width:190px;display:block;margin:0 auto;}
.phase-canvas{width:100%;height:70px;display:block;border-radius:6px;}
.ctrl-group{margin-bottom:12px;}
.ctrl-group:last-child{margin-bottom:0;}
.ctrl-label{
display:flex;justify-content:space-between;align-items:center;
font-size:12px;color:var(--text-dim);margin-bottom:4px;
}
.ctrl-label span.val{color:var(--accent);font-family:'Rajdhani',sans-serif;font-weight:600;}
input[type=range]{
-webkit-appearance:none;width:100%;height:4px;border-radius:2px;
background:var(--border);outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;border-radius:50%;
background:var(--accent);border:2px solid var(--bg);cursor:pointer;
}
.toggle-row{
display:flex;align-items:center;gap:8px;
font-size:12px;color:var(--text-dim);margin-bottom:8px;
}
.toggle-row input[type=checkbox]{accent-color:var(--accent);cursor:pointer;}
.legend-bar{
display:flex;align-items:center;gap:20px;flex-wrap:wrap;
margin-top:14px;padding:10px 16px;
background:var(--surface);border:1px solid var(--border);
border-radius:8px;font-size:12px;color:var(--text-dim);
}
.legend-item{display:flex;align-items:center;gap:6px;}
.legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.legend-line{width:18px;height:3px;border-radius:2px;flex-shrink:0;}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
.main-wrap{animation:fadeIn 0.8s ease-out both;}
.bottom-panel .panel-card:nth-child(1){animation:fadeIn 0.6s 0.2s ease-out both;}
.bottom-panel .panel-card:nth-child(2){animation:fadeIn 0.6s 0.35s ease-out both;}
.bottom-panel .panel-card:nth-child(3){animation:fadeIn 0.6s 0.5s ease-out both;}
</style>
</head>
<body>
<header>
<h1><span class="dot">⬡</span> SMA仿生蛇 · 蜿蜒爬行原理</h1>
<p>形状记忆合金弹簧 × 碳纤维骨架 — 消除一切宏观驱动器的最终理想解</p>
</header>
<div class="main-wrap">
<div class="canvas-container">
<canvas id="mainCanvas"></canvas>
<div class="ifs-tag">IFR · Ideal Final Result</div>
</div>
<div class="bottom-panel">
<!-- 截面图 -->
<div class="panel-card">
<h3>Cross Section · 截面构型</h3>
<svg class="cross-svg" viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg">
<!-- 扁椭圆轮廓 -->
<ellipse cx="100" cy="90" rx="72" ry="36" fill="#141e30" stroke="#2a3e58" stroke-width="1.5"/>
<!-- 碳纤维脊柱 -->
<rect x="38" y="82" width="124" height="6" rx="2" fill="#0e1520" stroke="#1a2d44" stroke-width="0.8"/>
<line x1="50" y1="82" x2="50" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="65" y1="82" x2="65" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="80" y1="82" x2="80" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="95" y1="82" x2="95" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="110" y1="82" x2="110" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="125" y1="82" x2="125" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="140" y1="82" x2="140" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<line x1="155" y1="82" x2="155" y2="88" stroke="#1a2d44" stroke-width="0.5"/>
<!-- 左SMA弹簧 -->
<path d="M34,72 L40,68 L34,64 L40,60 L34,56" fill="none" stroke="#ff6b35" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="34" cy="72" r="2" fill="#ff6b35"/>
<circle cx="34" cy="56" r="2" fill="#ff6b35"/>
<!-- 右SMA弹簧 -->
<path d="M166,72 L160,68 L166,64 L160,60 L166,56" fill="none" stroke="#00b8d4" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="166" cy="72" r="2" fill="#00b8d4"/>
<circle cx="166" cy="56" r="2" fill="#00b8d4"/>
<!-- 腹部倒刺 -->
<line x1="60" y1="124" x2="55" y2="130" stroke="#2a3e58" stroke-width="1"/>
<line x1="72" y1="125" x2="67" y2="132" stroke="#2a3e58" stroke-width="1"/>
<line x1="84" y1="126" x2="79" y2="133" stroke="#2a3e58" stroke-width="1"/>
<line x1="96" y1="126" x2="91" y2="133" stroke="#2a3e58" stroke-width="1"/>
<line x1="108" y1="126" x2="103" y2="133" stroke="#2a3e58" stroke-width="1"/>
<line x1="120" y1="125" x2="115" y2="132" stroke="#2a3e58" stroke-width="1"/>
<line x1="132" y1="124" x2="127" y2="130" stroke="#2a3e58" stroke-width="1"/>
<!-- 标注 -->
<text x="16" y="50" font-size="8" fill="#ff6b35" font-family="Rajdhani,sans-serif" font-weight="600">SMA-L</text>
<text x="162" y="50" font-size="8" fill="#00b8d4" font-family="Rajdhani,sans-serif" font-weight="600">SMA-R</text>
<text x="74" y="79" font-size="7" fill="#4a6080" font-family="Rajdhani,sans-serif">CARBON SPINE</text>
<text x="68" y="146" font-size="7" fill="#4a6080" font-family="Rajdhani,sans-serif">TEFLON BARBS ▸</text>
</svg>
</div>
<!-- 相位图 -->
<div class="panel-card">
<h3>Phase · SMA激活相位</h3>
<canvas id="phaseCanvas" class="phase-canvas"></canvas>
</div>
<!-- 控制 -->
<div class="panel-card">
<h3>Controls · 变量控制</h3>
<div class="ctrl-group">
<div class="ctrl-label"><span>波速 Wave Speed</span><span class="val" id="speedVal">1.0x</span></div>
<input type="range" id="speedSlider" min="0.2" max="3" step="0.1" value="1">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>波幅 Amplitude</span><span class="val" id="ampVal">1.0x</span></div>
<input type="range" id="ampSlider" min="0.2" max="2" step="0.1" value="1">
</div>
<div class="ctrl-group">
<div class="ctrl-label"><span>蜿蜒波数 Waves</span><span class="val" id="waveVal">2.5</span></div>
<input type="range" id="waveSlider" min="1" max="4" step="0.5" value="2.5">
</div>
<div class="toggle-row">
<input type="checkbox" id="showAnnot" checked>
<label for="showAnnot">显示标注 Annotations</label>
</div>
<div class="toggle-row">
<input type="checkbox" id="showForce" checked>
<label for="showForce">显示收缩力 Force Arrows</label>
</div>
</div>
</div>
<div class="legend-bar">
<div class="legend-item"><div class="legend-dot" style="background:#ff6b35;box-shadow:0 0 6px #ff6b35;"></div>左侧SMA收缩 (加热态)</div>
<div class="legend-item"><div class="legend-dot" style="background:#00b8d4;box-shadow:0 0 6px #00b8d4;"></div>右侧SMA收缩 (加热态)</div>
<div class="legend-item"><div class="legend-dot" style="background:#3d5a78;"></div>SMA冷却态 (马氏体)</div>
<div class="legend-item"><div class="legend-line" style="background:#0e1520;border:1px solid #1a2d44;"></div>碳纤维脊柱</div>
<div class="legend-item"><div class="legend-line" style="background:repeating-linear-gradient(90deg,#2a3e58 0 2px,transparent 2px 5px);"></div>腹部倒刺 (特氟龙)</div>
</div>
</div>
<script>
/* ==============================
配置与状态
============================== */
const CFG = {
numSegments: 50, // 蛇体采样点数
snakeLength: 0, // 将在resize时计算
amplitude: 50, // 蜿蜒幅度(px)
numWaves: 2.5, // 蜿蜒波数
omega: 2.0, // 角频率(rad/s)
maxBodyWidth: 15, // 最大半宽(px)
groundSpeed: 40, // 地面滚动速度(px/s)
particleRate: 0.35, // 粒子生成概率
};
const STATE = {
time: 0,
speedMul: 1.0,
ampMul: 1.0,
showAnnot: true,
showForce: true,
particles: [],
dpr: 1,
cw: 0, ch: 0, // canvas逻辑尺寸
};
/* ==============================
Canvas初始化
============================== */
const mainCanvas = document.getElementById('mainCanvas');
const ctx = mainCanvas.getContext('2d');
const phaseCanvas = document.getElementById('phaseCanvas');
const pctx = phaseCanvas.getContext('2d');
function resizeCanvas() {
STATE.dpr = window.devicePixelRatio || 1;
const container = mainCanvas.parentElement;
const rect = container.getBoundingClientRect();
STATE.cw = rect.width;
STATE.ch = rect.height;
mainCanvas.width = rect.width * STATE.dpr;
mainCanvas.height = rect.height * STATE.dpr;
mainCanvas.style.width = rect.width + 'px';
mainCanvas.style.height = rect.height + 'px';
ctx.setTransform(STATE.dpr, 0, 0, STATE.dpr, 0, 0);
// 蛇体长度自适应
CFG.snakeLength = Math.min(STATE.cw * 0.68, 800);
// 相位canvas
const pr = phaseCanvas.parentElement.getBoundingClientRect();
const pw = pr.width - 28;
phaseCanvas.width = pw * STATE.dpr;
phaseCanvas.height = 70 * STATE.dpr;
phaseCanvas.style.width = pw + 'px';
phaseCanvas.style.height = '70px';
pctx.setTransform(STATE.dpr, 0, 0, STATE.dpr, 0, 0);
}
/* ==============================
蛇体计算
============================== */
function computeSnake(time) {
const N = CFG.numSegments;
const L = CFG.snakeLength;
const A = CFG.amplitude * STATE.ampMul;
const kW = CFG.numWaves;
const w = CFG.omega * STATE.speedMul;
const pts = [];
for (let i = 0; i <= N; i++) {
const s = i / N;
const phase = 2 * Math.PI * kW * s - w * time;
const x = s * L;
const y = A * Math.sin(phase);
// 切线方向
const dyds = A * 2 * Math.PI * kW * Math.cos(phase);
const heading = Math.atan2(dyds, L / N * N);
// 曲率符号 → SMA激活
const curvSign = -Math.sin(phase);
pts.push({
x, y, heading, s,
leftAct: Math.max(0, curvSign),
rightAct: Math.max(0, -curvSign),
});
}
return pts;
}
/* 身体半宽(头尾渐缩) */
function bodyHalfW(s) {
const headT = Math.min(1, s * 7);
const tailT = Math.min(1, (1 - s) * 4.5);
return CFG.maxBodyWidth * headT * tailT;
}
/* ==============================
绘图工具
============================== */
/* 绘制平滑闭合路径(左轮廓 → 右轮廓反向) */
function drawSmoothClosedPath(c, leftPts, rightPts) {
c.beginPath();
// 左侧 head→tail
smoothLineTo(c, leftPts);
// 右侧 tail→head
smoothLineTo(c, rightPts.reverse());
c.closePath();
}
function smoothLineTo(c, pts) {
if (pts.length < 2) return;
c.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) {
const prev = pts[i - 1];
const curr = pts[i];
const mx = (prev.x + curr.x) / 2;
const my = (prev.y + curr.y) / 2;
c.quadraticCurveTo(prev.x, prev.y, mx, my);
}
const last = pts[pts.length - 1];
c.lineTo(last.x, last.y);
}
/* 颜色插值 */
function lerpColor(a, b, t) {
t = Math.max(0, Math.min(1, t));
const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16);
const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16);
const r = Math.round(ar + (br - ar) * t);
const g = Math.round(ag + (bg - ag) * t);
const bl = Math.round(ab + (bb - ab) * t);
return `rgb(${r},${g},${bl})`;
}
/* ==============================
粒子系统(SMA加热产生的微光粒子)
============================== */
function spawnParticle(x, y, side) {
STATE.particles.push({
x, y,
vx: (Math.random() - 0.5) * 12,
vy: -Math.random() * 18 - 6,
life: 1.0,
decay: 0.6 + Math.random() * 0.8,
size: 1.5 + Math.random() * 2,
side, // 'left' or 'right'
});
}
function updateParticles(dt) {
for (let i = STATE.particles.length - 1; i >= 0; i--) {
const p = STATE.particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 10 * dt; // 微重力
p.life -= p.decay * dt;
if (p.life <= 0) STATE.particles.splice(i, 1);
}
}
/* ==============================
主绘图
============================== */
function drawScene(pts, time) {
const W = STATE.cw, H = STATE.ch;
const ox = (W - CFG.snakeLength) / 2; // 居中偏移
const oy = H * 0.48;
ctx.clearRect(0, 0, W, H);
// —— 背景渐变 ——
const bgGrad = ctx.createRadialGradient(W/2, H*0.45, 0, W/2, H*0.45, W*0.6);
bgGrad.addColorStop(0, '#0f1a2c');
bgGrad.addColorStop(1, '#060a14');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, W, H);
// —— 地面网格(显示前进运动) ——
drawGround(time, W, H, oy);
ctx.save();
ctx.translate(ox, oy);
// —— 蛇体阴影 ——
drawShadow(pts);
// —— 蛇体轮廓 ——
drawBody(pts);
// —— 碳纤维脊柱 ——
drawSpine(pts);
// —— SMA弹簧 ——
drawSMASprings(pts, time);
// —— 腹部倒刺 ——
drawBarbs(pts);
// —— 收缩力箭头 ——
if (STATE.showForce) drawForceArrows(pts);
// —— 粒子 ——
drawParticles();
// —— 标注 ——
if (STATE.showAnnot) drawAnnotations(pts, time);
ctx.restore();
// —— 前进方向指示 ——
drawForwardIndicator(W, H, oy, time);
}
/* 地面网格 */
function drawGround(time, W, H, oy) {
const spacing = 32;
const offset = (time * CFG.groundSpeed * STATE.speedMul) % spacing;
ctx.save();
ctx.globalAlpha = 0.08;
ctx.strokeStyle = '#3a6090';
ctx.lineWidth = 0.5;
// 竖线
for (let x = -offset; x < W + spacing; x += spacing) {
ctx.beginPath();
ctx.moveTo(x, oy + 50);
ctx.lineTo(x, H);
ctx.stroke();
}
// 横线
for (let y = oy + 50; y < H; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
ctx.restore();
}
/* 阴影 */
function drawShadow(pts) {
if (pts.length < 2) return;
const leftP = [], rightP = [];
for (const p of pts) {
const hw = bodyHalfW(p.s);
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
leftP.push({ x: p.x + nx * hw, y: p.y + ny * hw + 18 });
rightP.push({ x: p.x - nx * hw, y: p.y - ny * hw + 18 });
}
ctx.save();
ctx.globalAlpha = 0.18;
drawSmoothClosedPath(ctx, leftP, rightP.slice());
ctx.fillStyle = '#000000';
ctx.filter = 'blur(8px)';
ctx.fill();
ctx.filter = 'none';
ctx.restore();
}
/* 蛇体 */
function drawBody(pts) {
if (pts.length < 2) return;
const leftP = [], rightP = [];
for (const p of pts) {
const hw = bodyHalfW(p.s);
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
leftP.push({ x: p.x + nx * hw, y: p.y + ny * hw });
rightP.push({ x: p.x - nx * hw, y: p.y - ny * hw });
}
// 主填充
drawSmoothClosedPath(ctx, leftP, rightP.slice());
const bGrad = ctx.createLinearGradient(0, -CFG.maxBodyWidth, 0, CFG.maxBodyWidth);
bGrad.addColorStop(0, '#263548');
bGrad.addColorStop(0.4, '#1a2838');
bGrad.addColorStop(1, '#15202e');
ctx.fillStyle = bGrad;
ctx.fill();
// 边缘高光
ctx.strokeStyle = '#2e4462';
ctx.lineWidth = 0.8;
ctx.stroke();
// 段分割线
ctx.save();
ctx.globalAlpha = 0.15;
ctx.strokeStyle = '#3a5a7a';
ctx.lineWidth = 0.5;
for (let i = 2; i < pts.length - 2; i += 3) {
const p = pts[i];
const hw = bodyHalfW(p.s);
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
ctx.beginPath();
ctx.moveTo(p.x + nx * hw, p.y + ny * hw);
ctx.lineTo(p.x - nx * hw, p.y - ny * hw);
ctx.stroke();
}
ctx.restore();
}
/* 碳纤维脊柱 */
function drawSpine(pts) {
if (pts.length < 2) return;
ctx.save();
// 主线
ctx.beginPath();
smoothLineTo(ctx, pts.map(p => ({ x: p.x, y: p.y })));
ctx.strokeStyle = '#0c1420';
ctx.lineWidth = 2.5;
ctx.stroke();
// 交叉纹
ctx.globalAlpha = 0.25;
ctx.strokeStyle = '#1a2d44';
ctx.lineWidth = 0.6;
for (let i = 2; i < pts.length - 2; i += 2) {
const p = pts[i];
const tx = Math.cos(p.heading);
const ty = Math.sin(p.heading);
const hw = bodyHalfW(p.s) * 0.6;
ctx.beginPath();
ctx.moveTo(p.x - ty * hw, p.y + tx * hw);
ctx.lineTo(p.x + ty * hw, p.y - tx * hw);
ctx.stroke();
}
ctx.restore();
}
/* SMA弹簧 */
function drawSMASprings(pts, time) {
for (let i = 1; i < pts.length - 1; i += 2) {
const p = pts[i];
const pn = pts[Math.min(i + 2, pts.length - 1)];
const hw = bodyHalfW(p.s) * 0.85;
// 法线方向
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
// 左侧弹簧
const lx1 = p.x + nx * hw;
const ly1 = p.y + ny * hw;
const lx2 = pn.x + nx * hw;
const ly2 = pn.y + ny * hw;
drawSpring(lx1, ly1, lx2, ly2, p.leftAct, 'left');
// 右侧弹簧
const rx1 = p.x - nx * hw;
const ry1 = p.y - ny * hw;
const rx2 = pn.x - nx * hw;
const ry2 = pn.y - ny * hw;
drawSpring(rx1, ry1, rx2, ry2, p.rightAct, 'right');
// 粒子生成
if (p.leftAct > 0.5 && Math.random() < CFG.particleRate) {
spawnParticle(lx1, ly1, 'left');
}
if (p.rightAct > 0.5 && Math.random() < CFG.particleRate) {
spawnParticle(rx1, ry1, 'right');
}
}
}
function drawSpring(x1, y1, x2, y2, activation, side) {
const dx = x2 - x1;
const dy = y2 - y1;
const len = Math.max(1, Math.sqrt(dx * dx + dy * dy));
const ux = dx / len;
const uy = dy / len;
const nx = -uy;
const ny = ux;
const coils = 4;
const ampBase = 2.5;
const ampAct = activation * 2.5;
const sAmp = ampBase + ampAct;
// 颜色
const coolColor = '#3d5a78';
const hotColor = side === 'left' ? '#ff6b35' : '#00b8d4';
const color = lerpColor(coolColor, hotColor, activation);
ctx.save();
ctx.beginPath();
ctx.moveTo(x1, y1);
for (let c = 1; c <= coils * 2; c++) {
const t = c / (coils * 2 + 1);
const sign = c % 2 === 1 ? 1 : -1;
const px = x1 + dx * t + nx * sAmp * sign;
const py = y1 + dy * t + ny * sAmp * sign;
ctx.lineTo(px, py);
}
ctx.lineTo(x2, y2);
ctx.strokeStyle = color;
ctx.lineWidth = 1.3 + activation * 0.8;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// 激活发光
if (activation > 0.3) {
ctx.shadowColor = side === 'left' ? 'rgba(255,107,53,0.6)' : 'rgba(0,184,212,0.6)';
ctx.shadowBlur = 6 + activation * 8;
ctx.stroke();
ctx.shadowBlur = 0;
}
ctx.restore();
}
/* 腹部倒刺 */
function drawBarbs(pts) {
ctx.save();
ctx.globalAlpha = 0.3;
ctx.strokeStyle = '#2e4a62';
ctx.lineWidth = 0.8;
for (let i = 3; i < pts.length - 3; i += 4) {
const p = pts[i];
const hw = bodyHalfW(p.s);
// 腹部 = 身体下方
const bx = p.x + Math.sin(p.heading) * hw * 0.7;
const by = p.y + Math.cos(p.heading) * hw * 0.7;
// 倒刺方向:指向尾部+下方
const tx = -Math.cos(p.heading);
const ty = -Math.sin(p.heading);
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx + tx * 4 + ty * 2, by + ty * 4 - tx * 2);
ctx.stroke();
}
ctx.restore();
}
/* 收缩力箭头 */
function drawForceArrows(pts) {
ctx.save();
const step = 6;
for (let i = step; i < pts.length - step; i += step) {
const p = pts[i];
const hw = bodyHalfW(p.s);
// 左侧
if (p.leftAct > 0.4) {
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
const sx = p.x + nx * (hw + 10);
const sy = p.y + ny * (hw + 10);
const ex = p.x + nx * (hw + 2);
const ey = p.y + ny * (hw + 2);
drawArrow(sx, sy, ex, ey, p.leftAct, '#ff6b35');
}
// 右侧
if (p.rightAct > 0.4) {
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
const sx = p.x - nx * (hw + 10);
const sy = p.y - ny * (hw + 10);
const ex = p.x - nx * (hw + 2);
const ey = p.y - ny * (hw + 2);
drawArrow(sx, sy, ex, ey, p.rightAct, '#00b8d4');
}
}
ctx.restore();
}
function drawArrow(x1, y1, x2, y2, alpha, color) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.max(1, Math.sqrt(dx*dx + dy*dy));
const ux = dx/len, uy = dy/len;
ctx.save();
ctx.globalAlpha = alpha * 0.7;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// 箭头
const aLen = 4;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - ux*aLen + uy*2.5, y2 - uy*aLen - ux*2.5);
ctx.lineTo(x2 - ux*aLen - uy*2.5, y2 - uy*aLen + ux*2.5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
/* 粒子绘制 */
function drawParticles() {
for (const p of STATE.particles) {
ctx.save();
const alpha = p.life * 0.8;
const color = p.side === 'left' ? `rgba(255,130,60,${alpha})` : `rgba(0,200,220,${alpha})`;
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
/* 标注 */
function drawAnnotations(pts, time) {
ctx.save();
ctx.font = '600 11px Rajdhani, Noto Sans SC, sans-serif';
// 找到激活最强的左侧和右侧段
let maxL = 0, maxLI = 0, maxR = 0, maxRI = 0;
for (let i = 5; i < pts.length - 5; i++) {
if (pts[i].leftAct > maxL) { maxL = pts[i].leftAct; maxLI = i; }
if (pts[i].rightAct > maxR) { maxR = pts[i].rightAct; maxRI = i; }
}
// 左侧SMA标注
if (maxL > 0.6) {
const p = pts[maxLI];
const hw = bodyHalfW(p.s);
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
const lx = p.x + nx * (hw + 26);
const ly = p.y + ny * (hw + 26);
ctx.fillStyle = '#ff6b35';
ctx.textAlign = 'center';
ctx.fillText('SMA-L 收缩', lx, ly - 2);
ctx.globalAlpha = 0.5;
ctx.strokeStyle = '#ff6b35';
ctx.lineWidth = 0.6;
ctx.setLineDash([3,3]);
ctx.beginPath();
ctx.moveTo(p.x + nx * (hw + 4), p.y + ny * (hw + 4));
ctx.lineTo(lx, ly + 4);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
}
// 右侧SMA标注
if (maxR > 0.6) {
const p = pts[maxRI];
const hw = bodyHalfW(p.s);
const nx = -Math.sin(p.heading);
const ny = Math.cos(p.heading);
const rx = p.x - nx * (hw + 26);
const ry = p.y - ny * (hw + 26);
ctx.fillStyle = '#00b8d4';
ctx.textAlign = 'center';
ctx.fillText('SMA-R 收缩', rx, ry - 2);
ctx.globalAlpha = 0.5;
ctx.strokeStyle = '#00b8d4';
ctx.lineWidth = 0.6;
ctx.setLineDash([3,3]);
ctx.beginPath();
ctx.moveTo(p.x - nx * (hw + 4), p.y - ny * (hw + 4));
ctx.lineTo(rx, ry + 4);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
}
// 脊柱标注
const si = Math.floor(pts.length * 0.2);
const sp = pts[si];
ctx.fillStyle = '#4a6080';
ctx.textAlign = 'left';
ctx.fillText('碳纤维脊柱', sp.x + 8, sp.y - 18);
ctx.globalAlpha = 0.35;
ctx.strokeStyle = '#4a6080';
ctx.lineWidth = 0.5;
ctx.setLineDash([2,2]);
ctx.beginPath();
ctx.moveTo(sp.x, sp.y);
ctx.lineTo(sp.x + 6, sp.y - 12);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
// 波传播方向
const wi = Math.floor(pts.length * 0.75);
const wp = pts[wi];
ctx.fillStyle = '#00e8a2';
ctx.font = '600 10px Rajdhani, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('WAVE →', wp.x, wp.y + bodyHalfW(wp.s) + 28);
ctx.restore();
}
/* 前进方向指示 */
function drawForwardIndicator(W, H, oy, time) {
const cx = W - 70;
const cy = oy - 20;
ctx.save();
ctx.fillStyle = '#00e8a2';
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('FWD', cx, cy - 8);
// 动态箭头
const pulse = 0.5 + 0.5 * Math.sin(time * 3);
ctx.globalAlpha = 0.5 + pulse * 0.4;
ctx.strokeStyle = '#00e8a2';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
const ax = cx - 8 + pulse * 6;
ctx.beginPath();
ctx.moveTo(ax, cy);
ctx.lineTo(ax + 12, cy);
ctx.moveTo(ax + 8, cy - 4);
ctx.lineTo(ax + 12, cy);
ctx.lineTo(ax + 8, cy + 4);
ctx.stroke();
ctx.restore();
}
/* ==============================
相位图绘制
============================== */
function drawPhaseDiagram(pts) {
const w = phaseCanvas.width / STATE.dpr;
const h = 70;
pctx.clearRect(0, 0, w, h);
// 背景
pctx.fillStyle = '#0a1220';
pctx.fillRect(0, 0, w, h);
if (pts.length < 2) return;
const barW = Math.max(1, (w - 20) / pts.length);
const midY = h / 2;
const maxH = midY - 6;
for (let i = 0; i < pts.length; i++) {
const p = pts[i];
const x = 10 + i * barW;
// 左侧SMA (上半)
if (p.leftAct > 0.05) {
const bh = p.leftAct * maxH;
const alpha = 0.3 + p.leftAct * 0.7;
pctx.fillStyle = `rgba(255,107,53,${alpha})`;
pctx.fillRect(x, midY - bh, barW - 0.5, bh);
}
// 右侧SMA (下半)
if (p.rightAct > 0.05) {
const bh = p.rightAct * maxH;
const alpha = 0.3 + p.rightAct * 0.7;
pctx.fillStyle = `rgba(0,184,212,${alpha})`;
pctx.fillRect(x, midY, barW - 0.5, bh);
}
}
// 中线
pctx.strokeStyle = '#1a2d48';
pctx.lineWidth = 0.5;
pctx.beginPath();
pctx.moveTo(10, midY);
pctx.lineTo(w - 10, midY);
pctx.stroke();
// 标签
pctx.font = '600 9px Rajdhani, sans-serif';
pctx.fillStyle = '#ff6b35';
pctx.textAlign = 'left';
pctx.fillText('L', 2, 12);
pctx.fillStyle = '#00b8d4';
pctx.fillText('R', 2, h - 4);
// HEAD/TAIL标签
pctx.fillStyle = '#4a6080';
pctx.font = '500 8px Rajdhani, sans-serif';
pctx.textAlign = 'left';
pctx.fillText('HEAD', 12, h - 3);
pctx.textAlign = 'right';
pctx.fillText('TAIL', w - 12, h - 3);
}
/* ==============================
控件绑定
============================== */
function bindControls() {
const speedSlider = document.getElementById('speedSlider');
const ampSlider = document.getElementById('ampSlider');
const waveSlider = document.getElementById('waveSlider');
const showAnnot = document.getElementById('showAnnot');
const showForce = document.getElementById('showForce');
speedSlider.addEventListener('input', () => {
STATE.speedMul = parseFloat(speedSlider.value);
document.getElementById('speedVal').textContent = STATE.speedMul.toFixed(1) + 'x';
});
ampSlider.addEventListener('input', () => {
STATE.ampMul = parseFloat(ampSlider.value);
document.getElementById('ampVal').textContent = STATE.ampMul.toFixed(1) + 'x';
});
waveSlider.addEventListener('input', () => {
CFG.numWaves = parseFloat(waveSlider.value);
document.getElementById('waveVal').textContent = CFG.numWaves.toFixed(1);
});
showAnnot.addEventListener('change', () => { STATE.showAnnot = showAnnot.checked; });
showForce.addEventListener('change', () => { STATE.showForce = showForce.checked; });
}
/* ==============================
动画主循环
============================== */
let lastTime = 0;
let startTime = 0;
let animStarted = false;
function animate(timestamp) {
if (!animStarted) {
startTime = timestamp;
lastTime = timestamp;
animStarted = true;
}
const dt = Math.min(0.05, (timestamp - lastTime) / 1000);
lastTime = timestamp;
// 入场渐显 (前2秒)
const elapsed = (timestamp - startTime) / 1000;
STATE.time = elapsed;
// 入场波幅渐增
const introFade = Math.min(1, elapsed / 1.8);
const savedAmpMul = STATE.ampMul;
STATE.ampMul *= introFade;
const pts = computeSnake(STATE.time);
STATE.ampMul = savedAmpMul; // 恢复
updateParticles(dt);
drawScene(pts, STATE.time);
drawPhaseDiagram(pts);
requestAnimationFrame(animate);
}
/* ==============================
初始化
============================== */
function init() {
resizeCanvas();
bindControls();
window.addEventListener('resize', () => {
resizeCanvas();
});
requestAnimationFrame(animate);
}
// 页面加载后自动启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>
这个实现包含以下核心要素:
IFR(最终理想解)可视化策略:
- 直接展示消除电机/气泵后的理想工作状态——蛇体仅靠SMA弹簧的交替收缩实现蜿蜒波,无需任何传统驱动器
- 资源利用层面,SMA弹簧同时充当"执行器+结构"的双重角色,碳纤维薄片同时提供背腹柔性与侧向约束,通过截面图和脊柱交叉纹清晰呈现
- 视觉引导上,激活态的SMA弹簧以醒目的暖橙(左侧)/冷青(右侧)发光标记,并伴有微光粒子上浮效果,冷却态回归灰蓝,形成强烈的对比引导
动画核心机制:
- 蛇体中心线由正弦行波
y = A·sin(2πk·s − ωt)驱动,曲率符号决定左右SMA的激活状态 - 弹簧以锯齿线绘制,激活时增粗、加光晕、并发射热辐射粒子;冷却时收细变灰
- 地面网格持续左移暗示蛇体前进,腹部倒刺以小斜线标示单向摩擦
- 收缩力箭头从弹簧外侧指向蛇体,直观展示"通电→收缩→拉弯脊柱"的因果链
交互控制:
- 波速、波幅、波数三个滑块可实时调节蜿蜒参数
- 标注与力箭头可独立开关
- 相位图实时显示从头到尾的SMA激活分布,左右分色清晰呈现行波传递
自动播放:页面加载后1.8秒内波幅从零渐增至满幅,蛇体自然"苏醒"开始蜿蜒爬行,刷新/重新加载即重新播放。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
