独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>蛇形机器人 - 纵向摩擦推进原理</title>
<style>
:root {
--bg-color: #05070a;
--grid-color: #121a2f;
--chassis-fill: rgba(10, 20, 35, 0.8);
--chassis-stroke: #00f3ff;
--joint-color: #ffffff;
--runner-color: #ffaa00;
--force-forward: #00ff66;
--force-lateral: #ff0055;
--text-main: #c7d2fe;
--text-accent: #00f3ff;
--hud-bg: rgba(5, 7, 10, 0.65);
--hud-border: rgba(0, 243, 255, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body, html {
width: 100%;
height: 100%;
background-color: var(--bg-color);
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, sans-serif;
overflow: hidden;
color: var(--text-main);
}
#app-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 50px 50px;
background-position: 0 0;
}
/* 核心动画区域 */
svg {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 10;
filter: drop-shadow(0 0 20px rgba(0, 243, 255, 0.1));
}
/* UI 面板 - 角落布局,避免遮挡 */
.hud-panel {
position: absolute;
background: var(--hud-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--hud-border);
border-radius: 8px;
padding: 16px;
z-index: 20;
pointer-events: none; /* 让鼠标穿透,不影响潜在的拖拽交互 */
}
.hud-top-left {
top: 24px;
left: 24px;
width: 280px;
}
.hud-bottom-right {
bottom: 24px;
right: 24px;
pointer-events: auto; /* 允许操作滑块 */
display: flex;
flex-direction: column;
gap: 12px;
}
.hud-title {
font-size: 14px;
font-weight: 600;
color: var(--text-accent);
letter-spacing: 1px;
margin-bottom: 8px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 8px;
}
.hud-title::before {
content: '';
display: block;
width: 6px;
height: 6px;
background: var(--text-accent);
border-radius: 50%;
box-shadow: 0 0 8px var(--text-accent);
}
.hud-text {
font-size: 12px;
line-height: 1.6;
color: rgba(199, 210, 254, 0.8);
margin-bottom: 8px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-top: 4px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.data-label { color: rgba(199, 210, 254, 0.6); }
.data-value { color: #fff; font-weight: 500; }
/* 图例 */
.legend {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,0.05);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.color-box {
width: 12px;
height: 4px;
border-radius: 2px;
}
/* 控件 */
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-header {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--text-accent);
}
input[type=range] {
-webkit-appearance: none;
width: 200px;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: var(--text-accent);
cursor: pointer;
margin-top: -5px;
box-shadow: 0 0 10px rgba(0, 243, 255, 0.5);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: rgba(0, 243, 255, 0.2);
border-radius: 1px;
}
/* SVG 滤镜和动画类 */
.glow-effect { filter: url(#neon-glow); }
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.pulsing { animation: pulse 1.5s infinite ease-in-out; }
</style>
</head>
<body>
<div id="app-container">
<svg id="canvas" viewBox="-500 -300 1000 600" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 发光滤镜 -->
<filter id="neon-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="force-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- 箭头标记 -->
<marker id="arrow-forward" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 2 L 10 5 L 0 8 z" fill="var(--force-forward)" />
</marker>
<marker id="arrow-lateral" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M 0 2 L 10 5 L 0 8 z" fill="var(--force-lateral)" />
</marker>
<!-- 摩擦力阻挡标记 (X形) -->
<g id="friction-block">
<line x1="-4" y1="-4" x2="4" y2="4" stroke="var(--force-lateral)" stroke-width="2" stroke-linecap="round"/>
<line x1="4" y1="-4" x2="-4" y2="4" stroke="var(--force-lateral)" stroke-width="2" stroke-linecap="round"/>
</g>
</defs>
<!-- 历史轨迹线 -->
<path id="trajectory" fill="none" stroke="rgba(0, 243, 255, 0.15)" stroke-width="2" stroke-dasharray="4 4" />
<!-- 机器蛇主干容器 -->
<g id="snake-system"></g>
</svg>
<!-- 信息面板 -->
<div class="hud-panel hud-top-left">
<div class="hud-title">Lateral Undulation System</div>
<div class="hud-text">
最终理想解 (IFR):利用具有方向选择性的被动接触件,将电机产生的横向摆动势能,通过“横向锁死、纵向滑动”的机制,100%转化为单向推进力。
</div>
<div class="legend">
<div class="legend-item">
<div class="color-box" style="background: var(--runner-color); box-shadow: 0 0 5px var(--runner-color);"></div>
<span>被动滑片/直排轮 (极低纵向阻力)</span>
</div>
<div class="legend-item">
<div class="color-box" style="background: var(--force-lateral); box-shadow: 0 0 5px var(--force-lateral);"></div>
<span>横向推力被地面摩擦力完全抵消</span>
</div>
<div class="legend-item">
<div class="color-box" style="background: var(--force-forward); box-shadow: 0 0 5px var(--force-forward);"></div>
<span>沿身体轴向的有效净推进力</span>
</div>
</div>
</div>
<!-- 控制面板与实时数据 -->
<div class="hud-panel hud-bottom-right">
<div class="hud-title">Telemetries</div>
<div class="data-row">
<span class="data-label">Forward Velocity</span>
<span class="data-value" id="val-vel">0.00 m/s</span>
</div>
<div class="data-row">
<span class="data-label">Phase Difference</span>
<span class="data-value" id="val-phase">40.0°</span>
</div>
<div class="data-row" style="margin-bottom: 8px;">
<span class="data-label">Friction Ratio (μ_x / μ_y)</span>
<span class="data-value">0.05</span>
</div>
<div class="control-group">
<div class="control-header">
<span>Wave Frequency</span>
<span id="lbl-freq">1.2 Hz</span>
</div>
<input type="range" id="ctrl-freq" min="0.5" max="3" step="0.1" value="1.5">
</div>
</div>
</div>
<script>
/**
* 核心逻辑:基于Serpenoid曲线的运动学模拟
* 为了视觉稳定性,采用“跑步机”模式:蛇身在屏幕中央游动,背景网格向后移动以体现位移。
*/
const N_SEGMENTS = 14; // 舱段数量
const SEG_LENGTH = 36; // 单个舱段长度
const SEG_WIDTH = 18; // 单个舱段宽度
let AMPLITUDE = 75; // 波动幅度
let WAVELENGTH = 350; // 波长
let WAVE_SPEED = 1.5; // 波传播速度 (受滑块控制)
const snakeSystem = document.getElementById('snake-system');
const trajectoryPath = document.getElementById('trajectory');
const appContainer = document.getElementById('app-container');
// UI 元素
const valVel = document.getElementById('val-vel');
const ctrlFreq = document.getElementById('ctrl-freq');
const lblFreq = document.getElementById('lbl-freq');
let time = 0;
let globalDistanceX = 0; // 累计前进距离
let segments = [];
let forceArrows = [];
// 初始化 DOM 结构
function initSnake() {
// 创建尾迹路径,横跨整个画布
let pathD = `M -600 0 L 600 0`;
trajectoryPath.setAttribute('d', pathD);
for (let i = 0; i < N_SEGMENTS; i++) {
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.classList.add('glow-effect');
// 1. 被动滑片/直排轮 (底盘两侧)
const runner1 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
runner1.setAttribute("x", -SEG_LENGTH/2 + 4);
runner1.setAttribute("y", -SEG_WIDTH/2 - 4);
runner1.setAttribute("width", SEG_LENGTH - 8);
runner1.setAttribute("height", 3);
runner1.setAttribute("fill", "var(--runner-color)");
runner1.setAttribute("rx", "1.5");
const runner2 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
runner2.setAttribute("x", -SEG_LENGTH/2 + 4);
runner2.setAttribute("y", SEG_WIDTH/2 + 1);
runner2.setAttribute("width", SEG_LENGTH - 8);
runner2.setAttribute("height", 3);
runner2.setAttribute("fill", "var(--runner-color)");
runner2.setAttribute("rx", "1.5");
// 2. 刚性舱段主体
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", -SEG_LENGTH/2);
rect.setAttribute("y", -SEG_WIDTH/2);
rect.setAttribute("width", SEG_LENGTH);
rect.setAttribute("height", SEG_WIDTH);
rect.setAttribute("rx", "4");
rect.setAttribute("fill", "var(--chassis-fill)");
rect.setAttribute("stroke", "var(--chassis-stroke)");
rect.setAttribute("stroke-width", "1.5");
// 3. 关节 (除了最后一个)
const joint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
joint.setAttribute("cx", SEG_LENGTH/2);
joint.setAttribute("cy", 0);
joint.setAttribute("r", 4);
joint.setAttribute("fill", "var(--joint-color)");
g.appendChild(runner1);
g.appendChild(runner2);
g.appendChild(rect);
if (i < N_SEGMENTS - 1) g.appendChild(joint);
// 4. 力学矢量分析组 (仅在关键分段展示)
if (i === 3 || i === 7 || i === 11) {
const forceGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
forceGroup.setAttribute("style", "filter: url(#force-glow);");
// 纵向推进力 (绿色)
const forwardForce = document.createElementNS("http://www.w3.org/2000/svg", "line");
forwardForce.setAttribute("x1", 0);
forwardForce.setAttribute("y1", 0);
forwardForce.setAttribute("x2", 40);
forwardForce.setAttribute("y2", 0);
forwardForce.setAttribute("stroke", "var(--force-forward)");
forwardForce.setAttribute("stroke-width", "2.5");
forwardForce.setAttribute("marker-end", "url(#arrow-forward)");
// 横向抵消力 (红色)
const lateralForce = document.createElementNS("http://www.w3.org/2000/svg", "line");
lateralForce.setAttribute("x1", 0);
lateralForce.setAttribute("y1", 0);
lateralForce.setAttribute("x2", 0);
lateralForce.setAttribute("y2", 40);
lateralForce.setAttribute("stroke", "var(--force-lateral)");
lateralForce.setAttribute("stroke-width", "2");
lateralForce.setAttribute("marker-end", "url(#arrow-lateral)");
lateralForce.setAttribute("stroke-dasharray", "4 2"); // 虚线表示被锁死
// 锁死标记
const blockMark = document.createElementNS("http://www.w3.org/2000/svg", "use");
blockMark.setAttribute("href", "#friction-block");
forceGroup.appendChild(lateralForce);
forceGroup.appendChild(blockMark);
forceGroup.appendChild(forwardForce);
g.appendChild(forceGroup);
forceArrows.push({
group: forceGroup,
forward: forwardForce,
lateral: lateralForce,
block: blockMark,
segmentIndex: i
});
}
snakeSystem.appendChild(g);
segments.push({ element: g });
}
}
// 物理与运动更新循环
function animate() {
time += 0.016 * WAVE_SPEED; // 模拟 delta time
const k = 2 * Math.PI / WAVELENGTH;
const omega = 2 * Math.PI; // 基准角频率
// 计算每个段的位置和角度。采用拟合正弦曲线的方式实现平滑动画。
// 在真实物理中是由关节角度推导全局坐标,这里为了表现“理想解”的宏观形态,由曲线映射坐标。
// 假设头部在 X=150 的位置,身体向左延伸
const headX = 150;
let pathD = `M -600 ${AMPLITUDE * Math.sin(k * (-600 - globalDistanceX) - omega * time)}`;
for (let px = -590; px <= 600; px+=10) {
let py = AMPLITUDE * Math.sin(k * (px - globalDistanceX) - omega * time);
pathD += ` L ${px} ${py}`;
}
trajectoryPath.setAttribute('d', pathD);
for (let i = 0; i < N_SEGMENTS; i++) {
// 计算沿着曲线的近似弧长积分位置 (简化版:直接按X轴等距分布)
// 实际上为了保持刚体长度,应迭代计算。此处用精细近似法。
const arcPos = headX - i * SEG_LENGTH;
// 运动学方程: y = A * sin(k*x - omega*t)
// 由于是向前移动,相位的传播使得身体呈现波动。
const phase = k * arcPos - omega * time;
const x = arcPos;
const y = AMPLITUDE * Math.sin(phase);
// 导数求切线角度
const dy_dx = AMPLITUDE * k * Math.cos(phase);
const theta = Math.atan(dy_dx);
const thetaDeg = theta * (180 / Math.PI);
// 应用变换
segments[i].element.setAttribute("transform", `translate(${x}, ${y}) rotate(${thetaDeg})`);
segments[i].theta = theta;
segments[i].phase = phase;
}
// 更新受力分析箭头的动态表现
// IFR核心:横向摆动速度最大处(跨越中轴线时),横向摩擦力发挥最大锁定作用;
// 波峰/波谷处,纵向推进力最大。
forceArrows.forEach(fa => {
const seg = segments[fa.segmentIndex];
// 简单模型:推进力与摆动角度的绝对值成反比(越直推进越快),横向力与角度成正比
const absTheta = Math.abs(seg.theta);
const maxTheta = Math.atan(AMPLITUDE * k);
const normalizedAngle = absTheta / maxTheta; // 0 to 1
// 动态计算力的大小
const latForceMag = 20 + 30 * normalizedAngle;
const fwdForceMag = 20 + 35 * (1 - normalizedAngle);
// 判断横向力方向 (向曲线凹面推)
const sign = Math.sin(seg.phase) > 0 ? -1 : 1;
fa.lateral.setAttribute("y2", latForceMag * sign);
fa.block.setAttribute("x", 0);
fa.block.setAttribute("y", (latForceMag + 6) * sign);
fa.forward.setAttribute("x2", fwdForceMag);
// 只有当有明显的推力时才高亮显示
fa.group.style.opacity = 0.5 + 0.5 * normalizedAngle;
});
// 计算宏观前进速度并更新UI
// 理论速度 v = f * lambda * 效率系数 (受滑移和结构限制)
// 这里做个视觉映射
const simulatedVel = (WAVE_SPEED * 0.45).toFixed(2);
valVel.innerText = `${simulatedVel} m/s`;
// 背景网格移动形成前进错觉
globalDistanceX += simulatedVel * 1.5;
appContainer.style.backgroundPosition = `${-globalDistanceX}px 0`;
requestAnimationFrame(animate);
}
// 交互事件绑定
ctrlFreq.addEventListener('input', (e) => {
WAVE_SPEED = parseFloat(e.target.value);
lblFreq.innerText = `${WAVE_SPEED.toFixed(1)} Hz`;
});
// 初始化并启动动画 (满足重开即播,无需点击)
window.addEventListener('DOMContentLoaded', () => {
initSnake();
animate();
});
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
