独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正交铰接仿生蛇形机器人 - Serpenoid 行波控制与越障原理</title>
<style>
:root {
--bg: #090d16;
--panel-bg: rgba(13, 20, 36, 0.78);
--border: rgba(0, 242, 254, 0.22);
--cyan: #00f2fe;
--amber: #ff9f1c;
--blue: #2575fc;
--text: #e2e8f0;
--muted: #64748b;
--accent-green: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Noto Sans CJK SC", monospace;
overflow: hidden;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
/* 背景工程网格与极光微晕 */
.bg-grid {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 50% 40%, rgba(37, 117, 252, 0.12) 0%, transparent 65%),
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 100% 100%, 40px 40px, 40px 40px;
pointer-events: none;
}
/* 主画布区域:让动画占据绝对视觉中心 */
#stage-container {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
svg#robot-stage {
width: 100%;
height: 100%;
max-width: 1400px;
max-height: 900px;
filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.8));
}
/* 悬浮微型仪表与说明卡片:精简、小字号、边缘靠齐,绝不遮挡中央 */
.hud-header {
position: absolute;
top: 18px;
left: 24px;
z-index: 10;
background: var(--panel-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 16px;
pointer-events: auto;
}
.hud-title {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.8px;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
}
.pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 10px var(--cyan);
animation: pulse 1.6s infinite ease-in-out;
}
.hud-subtitle {
font-size: 11px;
color: var(--muted);
margin-top: 3px;
font-family: monospace;
}
/* 右上角:实时控制律算式与解耦通道指示 */
.hud-math {
position: absolute;
top: 18px;
right: 24px;
z-index: 10;
background: var(--panel-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
font-size: 11px;
display: flex;
flex-direction: column;
gap: 5px;
max-width: 320px;
}
.formula-tag {
color: var(--cyan);
font-family: "Courier New", Courier, monospace;
font-weight: bold;
font-size: 11.5px;
}
.status-badge-group {
display: flex;
gap: 8px;
margin-top: 3px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.5px;
padding: 2px 7px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.3);
}
.badge.yaw { border-color: var(--cyan); color: var(--cyan); }
.badge.pitch { border-color: var(--amber); color: var(--amber); }
/* 底部角落微型交互条 */
.hud-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
background: var(--panel-bg);
backdrop-filter: blur(14px);
border: 1px solid var(--border);
border-radius: 30px;
padding: 6px 14px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
}
.btn-chip {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text);
font-size: 11px;
padding: 5px 12px;
border-radius: 16px;
cursor: pointer;
transition: all 0.25s ease;
display: flex;
align-items: center;
gap: 6px;
}
.btn-chip:hover {
background: rgba(0, 242, 254, 0.15);
border-color: var(--cyan);
color: #fff;
}
.btn-chip.active {
background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(37, 117, 252, 0.3));
border-color: var(--cyan);
color: #fff;
box-shadow: 0 0 12px rgba(0, 242, 254, 0.3);
}
.timeline-progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, var(--cyan), var(--amber));
width: 0%;
transition: width 0.1s linear;
z-index: 20;
}
/* 左下角状态提示 */
.hud-state-label {
position: absolute;
bottom: 20px;
left: 24px;
z-index: 10;
font-size: 11px;
color: var(--muted);
display: flex;
flex-direction: column;
gap: 2px;
}
.hud-state-label b {
color: #e2e8f0;
font-weight: 600;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.85); }
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<!-- 顶部精简抬头与状态 -->
<header class="hud-header">
<div class="hud-title">
<div class="pulse-dot"></div>
<span>正交铰接仿生蛇形机器人 · 行波越障系统</span>
</div>
<div class="hud-subtitle">14 节正交铰接构型 / 20kg·cm 数字舵机 / Serpenoid 连续控制律</div>
</header>
<!-- 右上角控制律与正交自由度解耦解析 -->
<aside class="hud-math">
<div class="formula-tag">θᵢ(t) = A·sin(2πft − 2πki/N) + bᵢ</div>
<div class="status-badge-group">
<span class="badge yaw">奇数关节 · 偏航 (Yaw) 水平行波</span>
<span class="badge pitch">偶数关节 · 俯仰 (Pitch) 升降偏置</span>
</div>
</aside>
<!-- 左下角工况指示 -->
<div class="hud-state-label">
<div>实时步态阶段:<b id="phase-desc">自动巡航与自适应越障中</b></div>
<div style="font-family: monospace;">波幅 A=±40° | 频率 f=1.5Hz | 波数 k=1.2</div>
</div>
<!-- 底部微型步态切换交互 -->
<div class="hud-controls">
<button class="btn-chip active" id="btn-auto" onclick="setMode('auto')">🔄 全自动协同演练</button>
<button class="btn-chip" id="btn-serpentine" onclick="setMode('serpentine')">≈ 纯平面侧向蜿蜒</button>
<button class="btn-chip" id="btn-obstacle" onclick="setMode('obstacle')">⤴ 抬升偏置搭接越障</button>
</div>
<div class="timeline-progress" id="time-bar"></div>
<!-- 核心视口:三维正投影透视 SVG 画布 -->
<div id="stage-container">
<svg id="robot-stage" viewBox="0 0 1200 700" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 线性与径向渐变 -->
<linearGradient id="cyan-glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00f2fe" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#4facfe" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="amber-glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff9f1c" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#ff3838" stop-opacity="0.5"/>
</linearGradient>
<linearGradient id="link-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2a3348"/>
<stop offset="50%" stop-color="#182030"/>
<stop offset="100%" stop-color="#0f1522"/>
</linearGradient>
<linearGradient id="obs-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="rgba(255, 159, 28, 0.15)"/>
<stop offset="100%" stop-color="rgba(255, 159, 28, 0.05)"/>
</linearGradient>
<filter id="laser-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 动态环境底板 (台阶地形 + 辅助坐标网) -->
<g id="terrain-layer"></g>
<!-- 推进波形参考引导线(投影轨迹与正弦波包络) -->
<g id="wave-envelope-layer"></g>
<!-- 蛇形机器人主体层 (14个关节链式装配) -->
<g id="snake-body-layer"></g>
<!-- 头部雷达测距与超声波扫描特效 -->
<g id="sensor-layer"></g>
<!-- 物理特征指示标 (关节转角弧线、推进力矢量动态箭头) -->
<g id="hud-indicators-layer"></g>
</svg>
</div>
<script>
/**
* 仿生蛇形机器人 Serpenoid 行波驱动仿真引擎
* 架构:14 节正交铰接串联机构
* 奇数节 (1, 3, 5, 7, 9, 11, 13): Yaw 水平轴舵机,输出正弦行波推进力
* 偶数节 (2, 4, 6, 8, 10, 12): Pitch 垂直轴舵机,负责起伏俯仰与越障抬升
*/
const N = 14; // 节段数量
const LINK_LEN = 48; // 单节视觉等效中心距 (px)
const RADIUS = 17; // 节段外壳半径
// 控制律默认参数
const params = {
A: 36 * Math.PI / 180, // 波幅 (rad)
freq: 1.3, // 频率 f (Hz)
k: 1.25, // 波数
pitchBias: 0 // 抬头越障俯仰偏置角
};
let mode = 'auto'; // 'auto' | 'serpentine' | 'obstacle'
let globalTime = 0;
let autoCycleTime = 0;
// 障碍物台阶物理参数 (空间坐标)
const obstacle = {
x: 580,
width: 320,
height: 70
};
// 2.5D Isometric 投影变换系统
// 世界坐标 (X: 纵向前进, Y: 横向摆动, Z: 垂直抬高) -> 屏幕平面坐标 (u, v)
function project(x, y, z) {
const originX = 140;
const originY = 460;
const isoX = originX + x * 0.94 - y * 0.35;
const isoY = originY + x * 0.22 + y * 0.62 - z * 1.1;
return { x: isoX, y: isoY };
}
// 初始化环境台阶与辅助线
function drawEnvironment() {
const terrain = document.getElementById('terrain-layer');
let html = '';
// 地面基准基线
const pStart = project(0, -60, 0);
const pEnd = project(1050, -60, 0);
const pStart2 = project(0, 60, 0);
const pEnd2 = project(1050, 60, 0);
// 地面导轨网格
for (let x = 0; x <= 1000; x += 80) {
let pA = project(x, -70, 0);
let pB = project(x, 70, 0);
html += `<line x1="${pA.x}" y1="${pA.y}" x2="${pB.x}" y2="${pB.y}" stroke="rgba(255,255,255,0.04)" stroke-dasharray="2,3" />`;
}
// 障碍物立体台阶渲染
const oP1 = project(obstacle.x, -90, 0);
const oP2 = project(obstacle.x + obstacle.width, -90, 0);
const oP3 = project(obstacle.x + obstacle.width, 90, 0);
const oP4 = project(obstacle.x, 90, 0);
const oP1_top = project(obstacle.x, -90, obstacle.height);
const oP2_top = project(obstacle.x + obstacle.width, -90, obstacle.height);
const oP3_top = project(obstacle.x + obstacle.width, 90, obstacle.height);
const oP4_top = project(obstacle.x, 90, obstacle.height);
// 障碍物顶面
html += `<polygon points="${oP1_top.x},${oP1_top.y} ${oP2_top.x},${oP2_top.y} ${oP3_top.x},${oP3_top.y} ${oP4_top.x},${oP4_top.y}"
fill="url(#obs-grad)" stroke="rgba(255, 159, 28, 0.45)" stroke-width="1.2" />`;
// 障碍物迎撞立面 (前唇)
html += `<polygon points="${oP1.x},${oP1.y} ${oP4.x},${oP4.y} ${oP4_top.x},${oP4_top.y} ${oP1_top.x},${oP1_top.y}"
fill="rgba(255, 159, 28, 0.12)" stroke="rgba(255, 159, 28, 0.6)" stroke-width="1.5" />`;
// 障碍物侧立面
html += `<polygon points="${oP4.x},${oP4.y} ${oP3.x},${oP3.y} ${oP3_top.x},${oP3_top.y} ${oP4_top.x},${oP4_top.y}"
fill="rgba(255, 159, 28, 0.06)" stroke="rgba(255, 159, 28, 0.25)" stroke-width="1" />`;
// 障碍物标签(微型技术注解)
const tagPos = project(obstacle.x + 30, 95, obstacle.height);
html += `<text x="${tagPos.x}" y="${tagPos.y}" fill="#ff9f1c" font-size="10.5" font-family="monospace" letter-spacing="1">OBSTACLE_ELEVATION +70mm</text>`;
terrain.innerHTML = html;
}
// 核心前向运动学计算
function computeSnakeKinematics(t) {
// 整体虚拟前进位移
let speed = 72; // px / sec
let progress = (t * speed) % 950;
let headX = 220 + progress;
// 自动循环时序状态机 (0 ~ 12秒一个大周期)
let autoStage = 'serpentine';
let obstacleTriggerDist = obstacle.x - headX;
if (mode === 'serpentine') {
params.pitchBias = 0;
document.getElementById('phase-desc').innerText = '纯侧向蜿蜒步态 (仅启用奇数水平偏航舵机)';
} else if (mode === 'obstacle') {
params.pitchBias = 32 * Math.PI / 180;
document.getElementById('phase-desc').innerText = '持续抬头越障步态 (偶数俯仰关节叠加正偏置)';
} else {
// 全自动演练模式
if (obstacleTriggerDist < 120 && obstacleTriggerDist > -obstacle.width - 80) {
// 接近并翻越障碍阶段
let blend = Math.sin(Math.min(Math.max((120 - obstacleTriggerDist) / 120, 0), 1) * Math.PI * 0.5);
params.pitchBias = (30 * Math.PI / 180) * blend;
document.getElementById('phase-desc').innerHTML = '<span style="color:#ff9f1c">感知台阶边缘 · 偶数关节叠加俯仰偏置 bᵢ 昂首跨越</span>';
} else {
params.pitchBias = 0;
document.getElementById('phase-desc').innerHTML = '<span style="color:#00f2fe">平地巡航 · 仅奇数节执行 Serpenoid 侧向行波</span>';
}
}
// 计算14个骨骼节点的3D位置与姿态
let joints = [];
let currentX = headX;
for (let i = 0; i < N; i++) {
// Serpenoid 控制律公式计算各节关节角
let phaseLag = (2 * Math.PI * params.k * i) / N;
let wave = Math.sin(2 * Math.PI * params.freq * t - phaseLag);
// 偏航角与俯仰角按奇偶轴彻底解耦
let yaw = (i % 2 === 1) ? params.A * wave : 0;
// 俯仰角:前5节在越障时执行向上翘起
let pitch = 0;
if (i % 2 === 0) {
let headDecay = Math.max(0, 1 - i / 6.5); // 越靠前偏置越大
pitch = params.pitchBias * headDecay;
}
// 横向摆幅计算
let bodyY = Math.sin(2 * Math.PI * params.freq * t - phaseLag) * 44;
// 高度计算 (台阶适应 + 抬头抬升)
let bodyZ = 0;
if (currentX > obstacle.x + 20) {
bodyZ = obstacle.height; // 已登上台阶
} else if (currentX > obstacle.x - 60) {
// 爬坡过渡
let ratio = (currentX - (obstacle.x - 60)) / 80;
bodyZ = obstacle.height * Math.min(Math.max(ratio, 0), 1);
}
// 叠加上抬头俯仰带来的抬升
bodyZ += Math.sin(pitch) * (LINK_LEN * 0.9);
joints.push({
index: i,
worldX: currentX,
worldY: bodyY,
worldZ: Math.max(0, bodyZ),
yaw: yaw,
pitch: pitch,
isYawJoint: i % 2 === 1,
isPitchJoint: i % 2 === 0
});
currentX -= LINK_LEN; // 往尾部后延
}
return joints;
}
// 渲染蛇形机器人高精度机电构件与动态指示
function renderSnake(joints, t) {
const snakeLayer = document.getElementById('snake-body-layer');
const waveLayer = document.getElementById('wave-envelope-layer');
const sensorLayer = document.getElementById('sensor-layer');
const hudLayer = document.getElementById('hud-indicators-layer');
let bodyHtml = '';
let waveHtml = '';
let hudHtml = '';
// 1. 绘制地面行波投影波形虚线(展示机械行波传播方向)
let wavePath = '';
for (let i = 0; i < joints.length; i++) {
let p = project(joints[i].worldX, joints[i].worldY, 0);
wavePath += (i === 0 ? 'M' : 'L') + `${p.x.toFixed(1)},${p.y.toFixed(1)} `;
}
waveHtml += `<path d="${wavePath}" fill="none" stroke="rgba(0, 242, 254, 0.28)" stroke-width="1.8" stroke-dasharray="4,4" />`;
waveLayer.innerHTML = waveHtml;
// 2. 绘制从尾到头的连杆节段(按深度从后向前渲染,保证正交遮挡关系真实)
for (let i = joints.length - 1; i >= 0; i--) {
const j = joints[i];
const screenPos = project(j.worldX, j.worldY, j.worldZ);
// 如果不是最后一节,绘制连接到前一节的正交U形金属铰接支架
if (i < joints.length - 1) {
const next = joints[i + 1];
const nextPos = project(next.worldX, next.worldY, next.worldZ);
// 骨干刚性联轴器
bodyHtml += `
<line x1="${screenPos.x}" y1="${screenPos.y}" x2="${nextPos.x}" y2="${nextPos.y}"
stroke="rgba(255,255,255,0.18)" stroke-width="9" stroke-linecap="round"/>
<line x1="${screenPos.x}" y1="${screenPos.y}" x2="${nextPos.x}" y2="${nextPos.y}"
stroke="#111827" stroke-width="6" stroke-linecap="round"/>
`;
}
// 3D 节段外壳 (钛金圆柱段壳体)
const isOdd = j.isYawJoint;
const mainColor = isOdd ? 'var(--cyan)' : 'var(--amber)';
const shellRadius = (i === 0) ? RADIUS + 2.5 : RADIUS; // 头部略大
// 节段外壳轮廓
bodyHtml += `
<g transform="translate(${screenPos.x}, ${screenPos.y})">
<!-- 底部被动滚轮构件 -->
<ellipse cx="-4" cy="11" rx="4.5" ry="2" fill="#0f172a" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>
<ellipse cx="4" cy="11" rx="4.5" ry="2" fill="#0f172a" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>
<!-- 节段主体外壳 -->
<circle cx="0" cy="0" r="${shellRadius}" fill="url(#link-grad)" stroke="${isOdd ? 'rgba(0, 242, 254, 0.4)' : 'rgba(255, 159, 28, 0.4)'}" stroke-width="1.8"/>
<!-- 正交铰链状态提示环 (奇数节为水平轴承,偶数节为垂直耳座) -->
${isOdd
? `<line x1="-8" y1="0" x2="8" y2="0" stroke="var(--cyan)" stroke-width="2.5" stroke-linecap="round" filter="url(#laser-glow)"/>`
: `<line x1="0" y1="-8" x2="0" y2="8" stroke="var(--amber)" stroke-width="2.5" stroke-linecap="round" filter="url(#laser-glow)"/>`
}
<!-- 关节序号微标 -->
<text x="0" y="3.5" font-size="8" font-family="monospace" fill="${isOdd ? '#00f2fe' : '#ff9f1c'}" text-anchor="middle" font-weight="bold">${i+1}</text>
</g>
`;
// 挑选代表性关节,显示其实时角度仪表与动态应力弧
if (i === 2 || i === 3) {
const angleVal = (isOdd ? j.yaw : j.pitch) * (180 / Math.PI);
const arcColor = isOdd ? 'var(--cyan)' : 'var(--amber)';
const labelText = isOdd ? `水平偏航 J${i+1} : ${angleVal.toFixed(1)}°` : `垂直俯仰 J${i+1} : ${angleVal.toFixed(1)}°`;
hudHtml += `
<g transform="translate(${screenPos.x}, ${screenPos.y - 28})">
<rect x="-42" y="-14" width="84" height="15" rx="3" fill="rgba(10, 16, 28, 0.85)" stroke="${arcColor}" stroke-width="0.8"/>
<text x="0" y="-3.5" font-size="8.5" font-family="monospace" fill="${arcColor}" text-anchor="middle">${labelText}</text>
<line x1="0" y1="1" x2="0" y2="10" stroke="${arcColor}" stroke-width="0.7" stroke-dasharray="2,2"/>
</g>
`;
}
}
snakeLayer.innerHTML = bodyHtml;
hudLayer.innerHTML = hudHtml;
// 3. 头部超声与 IMU 传感器扫描光束特效
const head = joints[0];
const headPos = project(head.worldX, head.worldY, head.worldZ);
const scanEndA = project(head.worldX + 90, head.worldY - 45, head.worldZ);
const scanEndB = project(head.worldX + 90, head.worldY + 45, head.worldZ);
sensorLayer.innerHTML = `
<polygon points="${headPos.x},${headPos.y} ${scanEndA.x},${scanEndA.y} ${scanEndB.x},${scanEndB.y}"
fill="rgba(0, 242, 254, 0.08)" stroke="rgba(0, 242, 254, 0.3)" stroke-width="1" />
<line x1="${headPos.x}" y1="${headPos.y}" x2="${(scanEndA.x + scanEndB.x) / 2}" y2="${(scanEndA.y + scanEndB.y) / 2}"
stroke="#00f2fe" stroke-width="1.2" stroke-dasharray="3,3" opacity="0.8"/>
<circle cx="${headPos.x}" cy="${headPos.y}" r="3.5" fill="#00f2fe" filter="url(#laser-glow)"/>
`;
}
// 模式切换控制器
function setMode(newMode) {
mode = newMode;
document.querySelectorAll('.btn-chip').forEach(btn => btn.classList.remove('active'));
if (newMode === 'auto') document.getElementById('btn-auto').classList.add('active');
if (newMode === 'serpentine') document.getElementById('btn-serpentine').classList.add('active');
if (newMode === 'obstacle') document.getElementById('btn-obstacle').classList.add('active');
}
// 主动画循环 (保证 60 FPS 丝滑自启动)
let lastTimestamp = 0;
function animLoop(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const dt = (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
globalTime += dt;
autoCycleTime = (globalTime % 12);
// 循环时间进度条更新
const progPercent = (autoCycleTime / 12) * 100;
document.getElementById('time-bar').style.width = progPercent + '%';
// 实时动力学求解与图形装配
const joints = computeSnakeKinematics(globalTime);
renderSnake(joints, globalTime);
requestAnimationFrame(animLoop);
}
// 页面加载完成即刻无缝自启
window.addEventListener('DOMContentLoaded', () => {
drawEnvironment();
requestAnimationFrame(animLoop);
});
// 窗口尺寸自适应重绘底板
window.addEventListener('resize', () => {
drawEnvironment();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正交铰接仿生蛇形机器人 - Serpenoid 行波驱动与越障原理</title>
<style>
:root {
--bg-color: #080c14;
--card-bg: rgba(13, 20, 35, 0.82);
--border-color: rgba(0, 242, 254, 0.2);
--cyan: #00f2fe;
--cyan-glow: rgba(0, 242, 254, 0.35);
--amber: #ff9f1c;
--amber-glow: rgba(255, 159, 28, 0.35);
--text-main: #f1f5f9;
--text-sub: #94a3b8;
--accent-green: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", monospace;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* 工程背景网格 */
.bg-canvas {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 50% 45%, rgba(14, 42, 71, 0.3) 0%, transparent 70%),
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 100% 100%, 48px 48px, 48px 48px;
pointer-events: none;
z-index: 0;
}
/* 居中主视口容器 */
#viewport-container {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
svg#serpentine-svg {
width: 100%;
height: 100%;
max-width: 1400px;
max-height: 850px;
filter: drop-shadow(0 15px 35px rgba(0, 0, 0, 0.7));
}
/* 顶部精简抬头卡片 (贴边、不遮挡主中央) */
.header-panel {
position: absolute;
top: 16px;
left: 20px;
z-index: 10;
background: var(--card-bg);
backdrop-filter: blur(14px);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 16px;
pointer-events: auto;
}
.header-title {
font-size: 13.5px;
font-weight: 700;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.indicator-led {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 10px var(--cyan);
animation: ledPulse 2s infinite ease-in-out;
}
.header-desc {
font-size: 11px;
color: var(--text-sub);
margin-top: 3px;
font-family: monospace;
}
/* 右上角控制律与解耦仪表 */
.telemetry-panel {
position: absolute;
top: 16px;
right: 20px;
z-index: 10;
background: var(--card-bg);
backdrop-filter: blur(14px);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 14px;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 290px;
}
.formula-box {
font-family: "Courier New", Courier, monospace;
color: var(--cyan);
font-size: 11.5px;
font-weight: bold;
background: rgba(0, 242, 254, 0.08);
padding: 4px 8px;
border-radius: 4px;
border-left: 2px solid var(--cyan);
}
.dof-tags {
display: flex;
gap: 6px;
}
.dof-chip {
font-size: 10px;
padding: 2px 7px;
border-radius: 3px;
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(0, 0, 0, 0.4);
}
.dof-chip.yaw { border: 1px solid var(--cyan); color: var(--cyan); }
.dof-chip.pitch { border: 1px solid var(--amber); color: var(--amber); }
/* 左下角时序动作指示 */
.sequence-monitor {
position: absolute;
bottom: 24px;
left: 20px;
z-index: 10;
font-size: 11px;
display: flex;
flex-direction: column;
gap: 4px;
background: var(--card-bg);
backdrop-filter: blur(12px);
padding: 8px 14px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.sequence-label {
color: var(--text-sub);
}
.sequence-text {
color: #fff;
font-weight: 600;
}
/* 底部交互控制条 (微型圆角胶囊) */
.control-deck {
position: absolute;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
background: var(--card-bg);
backdrop-filter: blur(16px);
border: 1px solid var(--border-color);
border-radius: 32px;
padding: 6px 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.6);
}
.tab-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-sub);
font-size: 11px;
padding: 5px 12px;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
}
.tab-btn:hover {
color: #fff;
background: rgba(0, 242, 254, 0.12);
border-color: var(--cyan);
}
.tab-btn.active {
color: #fff;
background: linear-gradient(135deg, rgba(0, 242, 254, 0.25), rgba(37, 117, 252, 0.3));
border-color: var(--cyan);
box-shadow: 0 0 10px rgba(0, 242, 254, 0.3);
}
/* 交互滑块微调 */
.slider-wrap {
display: flex;
align-items: center;
gap: 6px;
font-size: 10.5px;
color: var(--text-sub);
border-left: 1px solid rgba(255, 255, 255, 0.1);
padding-left: 10px;
}
.slider-wrap input[type=range] {
width: 75px;
accent-color: var(--cyan);
cursor: pointer;
}
/* 周期时序时间线进度条 */
.timeline-bar {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 0%;
background: linear-gradient(90deg, var(--cyan), var(--amber));
z-index: 20;
}
@keyframes ledPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.35; transform: scale(0.85); }
}
</style>
</head>
<body>
<div class="bg-canvas"></div>
<!-- 顶部系统抬头 -->
<header class="header-panel">
<div class="header-title">
<div class="indicator-led"></div>
<span>正交铰接仿生蛇形机器人 · 行波控制与越障仿真</span>
</div>
<div class="header-desc">14节正交U形铰接 / 20kg·cm 数字舵机 / 底部双被动滚轮推进 / 头部超声感知</div>
</header>
<!-- 右上角控制律数学表达式与状态仪表 -->
<aside class="telemetry-panel">
<div class="formula-box">θᵢ(t) = A·sin(2πft − 2πki/N) + bᵢ</div>
<div class="dof-tags">
<div class="dof-chip yaw">奇数关节 · 偏航 (Yaw) 水平行波</div>
<div class="dof-chip pitch">偶数关节 · 俯仰 (Pitch) 升降偏置</div>
</div>
</aside>
<!-- 左下角时序阶段监视器 -->
<div class="sequence-monitor">
<span class="sequence-label">协同控制动作阶段:</span>
<span class="sequence-text" id="status-phase">自适应巡航与越障演练</span>
</div>
<!-- 底部微型交互控制坞 -->
<nav class="control-deck">
<button class="tab-btn active" id="btn-auto" onclick="setControlMode('auto')">⚡ 完整动作时序演练</button>
<button class="tab-btn" id="btn-serp" onclick="setControlMode('serpentine')">≈ 侧向蜿蜒行波</button>
<button class="tab-btn" id="btn-cross" onclick="setControlMode('obstacle')">⤴ 抬升偏置翻越</button>
<div class="slider-wrap">
<span>波幅 A:</span>
<input type="range" id="param-amp" min="20" max="50" value="40" oninput="updateSliders()">
<span id="amp-val" style="width:24px; font-family:monospace; color:#fff;">40°</span>
</div>
<div class="slider-wrap">
<span>频率 f:</span>
<input type="range" id="param-freq" min="8" max="25" value="15" oninput="updateSliders()">
<span id="freq-val" style="width:28px; font-family:monospace; color:#fff;">1.5Hz</span>
</div>
</nav>
<div class="timeline-bar" id="timeline-progress"></div>
<!-- 主画布视口 (SVG 2.5D Isometric 投影) -->
<main id="viewport-container">
<svg id="serpentine-svg" viewBox="0 0 1300 760" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 材质光晕与发光滤镜 -->
<filter id="glow-effect" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="3.2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- 节段钛灰外壳渐变 -->
<linearGradient id="link-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#334155" />
<stop offset="45%" stop-color="#1e293b" />
<stop offset="100%" stop-color="#0f172a" />
</linearGradient>
<!-- 台阶渐变填充 -->
<linearGradient id="step-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="rgba(255, 159, 28, 0.16)" />
<stop offset="100%" stop-color="rgba(255, 159, 28, 0.04)" />
</linearGradient>
</defs>
<!-- 环境层 (地面导轨与三维台阶障碍物) -->
<g id="env-layer"></g>
<!-- 行波物理包络投影虚线 -->
<g id="envelope-layer"></g>
<!-- 蛇形机器人 14 节机械骨骼装配链 -->
<g id="snake-layer"></g>
<!-- 头部感知雷达与超声波锥形波束 -->
<g id="sensor-layer"></g>
<!-- 动态数据微标与关节弧度动态标注 -->
<g id="overlay-layer"></g>
</svg>
</main>
<script>
/**
* 仿生蛇形机器人 Serpenoid 行波驱动仿真物理引擎
* 遵循正交铰接构型:
* 奇数节 (1, 3, 5, 7, 9, 11, 13) 为 Yaw 关节 (偏航/水平行波)
* 偶数节 (2, 4, 6, 8, 10, 12, 14) 为 Pitch 关节 (俯仰/垂直抬升)
*/
const N = 14; // 节段数量:14 节
const LINK_LENGTH = 46; // 单节等效中心距 (px)
const BODY_RADIUS = 16.5; // 节段半径 (外径 60mm 比例缩小)
// 控制参数
let config = {
amp: 40 * (Math.PI / 180), // 波幅 A (rad)
freq: 1.5, // 频率 f (Hz)
k: 1.2, // 波数 k = 1.2
pitchBias: 0 // 垂直关节偏置 bi
};
let controlMode = 'auto'; // 'auto' | 'serpentine' | 'obstacle'
let animTime = 0;
const CYCLE_DURATION = 13.0; // 全时序自动循环周期:13秒
// 障碍物几何定义 (世界坐标: x, y, z)
const obstacleGeom = {
startX: 620,
width: 320,
height: 72
};
// 2.5D Isometric 等轴测投影变换公式
function project3Dto2D(x, y, z) {
const originX = 170;
const originY = 480;
const u = originX + x * 0.92 - y * 0.36;
const v = originY + x * 0.22 + y * 0.58 - z * 1.08;
return { x: u, y: v };
}
// 绘制环境台阶与地面基准网格
function renderEnvironment() {
const env = document.getElementById('env-layer');
let markup = '';
// 地面导向虚线
for (let x = 0; x <= 1100; x += 85) {
let pA = project3Dto2D(x, -65, 0);
let pB = project3Dto2D(x, 65, 0);
markup += `<line x1="${pA.x}" y1="${pA.y}" x2="${pB.x}" y2="${pB.y}" stroke="rgba(255,255,255,0.035)" stroke-dasharray="2,3"/>`;
}
// 障碍物立体多面体绘制
const p1 = project3Dto2D(obstacleGeom.startX, -90, 0);
const p2 = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, -90, 0);
const p3 = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, 90, 0);
const p4 = project3Dto2D(obstacleGeom.startX, 90, 0);
const p1Top = project3Dto2D(obstacleGeom.startX, -90, obstacleGeom.height);
const p2Top = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, -90, obstacleGeom.height);
const p3Top = project3Dto2D(obstacleGeom.startX + obstacleGeom.width, 90, obstacleGeom.height);
const p4Top = project3Dto2D(obstacleGeom.startX, 90, obstacleGeom.height);
// 顶面
markup += `<polygon points="${p1Top.x},${p1Top.y} ${p2Top.x},${p2Top.y} ${p3Top.x},${p3Top.y} ${p4Top.x},${p4Top.y}"
fill="url(#step-grad)" stroke="rgba(255, 159, 28, 0.45)" stroke-width="1.2" />`;
// 前唇台阶立面 (迎面冲撞受阻面)
markup += `<polygon points="${p1.x},${p1.y} ${p4.x},${p4.y} ${p4Top.x},${p4Top.y} ${p1Top.x},${p1Top.y}"
fill="rgba(255, 159, 28, 0.12)" stroke="rgba(255, 159, 28, 0.65)" stroke-width="1.5" />`;
// 侧立面
markup += `<polygon points="${p4.x},${p4.y} ${p3.x},${p3.y} ${p3Top.x},${p3Top.y} ${p4Top.x},${p4Top.y}"
fill="rgba(255, 159, 28, 0.05)" stroke="rgba(255, 159, 28, 0.25)" stroke-width="1" />`;
// 障碍物高程标识文本
const labelPos = project3Dto2D(obstacleGeom.startX + 20, 96, obstacleGeom.height);
markup += `<text x="${labelPos.x}" y="${labelPos.y}" fill="#ff9f1c" font-size="10" font-family="monospace" letter-spacing="0.5">台阶障碍 +70mm</text>`;
env.innerHTML = markup;
}
// 求解 14 节机构正向动力学与时序姿态
function computeKinematics(t) {
let speed = 76; // 前进位移速率 px/s
let progress = (t * speed) % 1020;
let headX = 220 + progress;
let cycleTime = t % CYCLE_DURATION;
let statusText = '';
let effectiveAmp = config.amp;
// 动作时序与协同过程状态机
if (controlMode === 'auto') {
if (cycleTime < 1.4) {
// 阶段1:上电自检归零
statusText = '<span style="color:#94a3b8">【时序 1/5】系统上电自检 · 各舵机复位归零中...</span>';
effectiveAmp = config.amp * (cycleTime / 1.4);
config.pitchBias = 0;
} else if (headX < obstacleGeom.startX - 90) {
// 阶段2:常规侧向蜿蜒巡航步态
statusText = '<span style="color:#00f2fe">【时序 2/5】侧向蜿蜒巡航 · 仅奇数节执行正弦行波推进</span>';
config.pitchBias = 0;
} else if (headX >= obstacleGeom.startX - 90 && headX < obstacleGeom.startX + 30) {
// 阶段3:头部超声测距发现障碍 -> 偶数节叠加偏置抬高前 4 节
let liftProgress = (headX - (obstacleGeom.startX - 90)) / 120;
liftProgress = Math.min(Math.max(liftProgress, 0), 1);
config.pitchBias = (32 * Math.PI / 180) * Math.sin(liftProgress * Math.PI * 0.5);
statusText = '<span style="color:#ff9f1c">【时序 3/5】超声测距发现障碍 · 垂直轴叠加抬升偏置 bᵢ 昂首探路</span>';
} else if (headX >= obstacleGeom.startX + 30 && headX < obstacleGeom.startX + 180) {
// 阶段4:前端搭上障碍边缘,波幅临时加大 20% 拉拽翻越
effectiveAmp = config.amp * 1.2;
config.pitchBias = 16 * Math.PI / 180;
statusText = '<span style="color:#10b981">【时序 4/5】前身搭上台阶 · 波幅临时加大 20% 爆发拉拽越障</span>';
} else {
// 阶段5:全躯干登上高台,偏置恢复,转入高台正常巡航
statusText = '<span style="color:#00f2fe">【时序 5/5】翻越成功 · 偏置归零恢复标准平地 Serpenoid 步态</span>';
config.pitchBias = 0;
}
} else if (controlMode === 'serpentine') {
config.pitchBias = 0;
statusText = '<span style="color:#00f2fe">模式锁定:纯平面侧向蜿蜒 (仅奇数水平关节摆动)</span>';
} else if (controlMode === 'obstacle') {
config.pitchBias = 32 * Math.PI / 180;
effectiveAmp = config.amp * 1.15;
statusText = '<span style="color:#ff9f1c">模式锁定:持续昂首越障步态 (偶数俯仰关节维持大偏置)</span>';
}
document.getElementById('status-phase').innerHTML = statusText;
// 求解 14 节骨骼的世界坐标及局部铰接角
let joints = [];
let curX = headX;
for (let i = 0; i < N; i++) {
let phaseLag = (2 * Math.PI * config.k * i) / N;
let wave = Math.sin(2 * Math.PI * config.freq * t - phaseLag);
// 正交解耦核心逻辑
let isYawJoint = (i % 2 === 1); // 奇数关节掌控航向 (Yaw)
let isPitchJoint = (i % 2 === 0); // 偶数关节掌控俯仰 (Pitch)
let yawAngle = isYawJoint ? effectiveAmp * wave : 0;
// 抬头偏置主要由前 4~5 节承担
let pitchDecay = Math.max(0, 1 - (i / 4.5));
let pitchAngle = isPitchJoint ? (config.pitchBias * pitchDecay) : 0;
// 计算横向 Y 坐标 (蜿蜒起伏)
let worldY = Math.sin(2 * Math.PI * config.freq * t - phaseLag) * 45;
// 计算垂直 Z 坐标 (台阶高度差 + 自身俯仰翘起)
let worldZ = 0;
if (curX > obstacleGeom.startX + 20) {
worldZ = obstacleGeom.height;
} else if (curX > obstacleGeom.startX - 50) {
let ratio = (curX - (obstacleGeom.startX - 50)) / 70;
worldZ = obstacleGeom.height * Math.min(Math.max(ratio, 0), 1);
}
// 加上主动抬头俯仰高度
worldZ += Math.sin(pitchAngle) * (LINK_LENGTH * 1.05);
joints.push({
index: i,
worldX: curX,
worldY: worldY,
worldZ: Math.max(0, worldZ),
yaw: yawAngle,
pitch: pitchAngle,
isYaw: isYawJoint,
isPitch: isPitchJoint
});
curX -= LINK_LENGTH;
}
return joints;
}
// 渲染蛇身装配构件与机械特征
function renderScene(joints, t) {
const snakeLayer = document.getElementById('snake-layer');
const envLayer = document.getElementById('envelope-layer');
const sensorLayer = document.getElementById('sensor-layer');
const overlayLayer = document.getElementById('overlay-layer');
let snakeSvg = '';
let envSvg = '';
let overlaySvg = '';
// 1. 地面正弦行波包络轨迹绘制
let pathD = '';
for (let i = 0; i < joints.length; i++) {
let pt = project3Dto2D(joints[i].worldX, joints[i].worldY, 0);
pathD += (i === 0 ? 'M' : 'L') + `${pt.x.toFixed(1)},${pt.y.toFixed(1)} `;
}
envSvg += `<path d="${pathD}" fill="none" stroke="rgba(0, 242, 254, 0.22)" stroke-width="1.8" stroke-dasharray="3,4" />`;
envLayer.innerHTML = envSvg;
// 2. 机械节段自尾向头绘制 (保持空间深度遮挡真实)
for (let i = joints.length - 1; i >= 0; i--) {
const curr = joints[i];
const screenCurr = project3Dto2D(curr.worldX, curr.worldY, curr.worldZ);
// 绘制正交U形铰接连接臂
if (i < joints.length - 1) {
const next = joints[i + 1];
const screenNext = project3Dto2D(next.worldX, next.worldY, next.worldZ);
// 铝合金刚性连杆骨架
snakeSvg += `
<line x1="${screenCurr.x}" y1="${screenCurr.y}" x2="${screenNext.x}" y2="${screenNext.y}"
stroke="rgba(255,255,255,0.15)" stroke-width="9" stroke-linecap="round"/>
<line x1="${screenCurr.x}" y1="${screenCurr.y}" x2="${screenNext.x}" y2="${screenNext.y}"
stroke="#0f172a" stroke-width="5" stroke-linecap="round"/>
`;
}
const isHead = (i === 0);
const radius = isHead ? BODY_RADIUS + 3 : BODY_RADIUS;
const mainColor = curr.isYaw ? 'var(--cyan)' : 'var(--amber)';
snakeSvg += `
<g transform="translate(${screenCurr.x}, ${screenCurr.y})">
<!-- 节段底部被动减阻小滚轮 (双轮) -->
<ellipse cx="-4" cy="10.5" rx="4" ry="1.8" fill="#090d16" stroke="rgba(255,255,255,0.25)" stroke-width="0.8"/>
<ellipse cx="4" cy="10.5" rx="4" ry="1.8" fill="#090d16" stroke="rgba(255,255,255,0.25)" stroke-width="0.8"/>
<!-- 节段 3D 打印主体圆柱外壳 -->
<circle cx="0" cy="0" r="${radius}" fill="url(#link-grad)"
stroke="${curr.isYaw ? 'rgba(0, 242, 254, 0.5)' : 'rgba(255, 159, 28, 0.5)'}" stroke-width="1.8"/>
<!-- 舵机输出轴与正交方向键槽 (奇数水平轴,偶数垂直轴) -->
${curr.isYaw
? `<line x1="-7.5" y1="0" x2="7.5" y2="0" stroke="var(--cyan)" stroke-width="2.6" stroke-linecap="round" filter="url(#glow-effect)"/>`
: `<line x1="0" y1="-7.5" x2="0" y2="7.5" stroke="var(--amber)" stroke-width="2.6" stroke-linecap="round" filter="url(#glow-effect)"/>`
}
<!-- 节段序号标注 -->
<text x="0" y="3" font-size="8.5" font-family="monospace" fill="${mainColor}" text-anchor="middle" font-weight="bold">${i+1}</text>
</g>
`;
// 挑选第 2 节 (Pitch 俯仰) 与第 3 节 (Yaw 偏航) 显示微型动态浮动数据标
if (i === 1 || i === 2) {
const angleDeg = (curr.isYaw ? curr.yaw : curr.pitch) * (180 / Math.PI);
const axisName = curr.isYaw ? `J${i+1} 水平偏航` : `J${i+1} 垂直俯仰`;
const badgeColor = curr.isYaw ? 'var(--cyan)' : 'var(--amber)';
overlaySvg += `
<g transform="translate(${screenCurr.x}, ${screenCurr.y - 28})">
<rect x="-42" y="-12" width="84" height="15" rx="3" fill="rgba(8, 12, 20, 0.88)" stroke="${badgeColor}" stroke-width="0.8"/>
<text x="0" y="-1" font-size="8" font-family="monospace" fill="${badgeColor}" text-anchor="middle">${axisName} ${angleDeg.toFixed(1)}°</text>
<line x1="0" y1="3" x2="0" y2="10" stroke="${badgeColor}" stroke-width="0.7" stroke-dasharray="2,2"/>
</g>
`;
}
}
snakeLayer.innerHTML = snakeSvg;
overlayLayer.innerHTML = overlaySvg;
// 3. 头部超声测距雷达波纹与 IMU 视向
const head = joints[0];
const headPt = project3Dto2D(head.worldX, head.worldY, head.worldZ);
const coneA = project3Dto2D(head.worldX + 95, head.worldY - 42, head.worldZ);
const coneB = project3Dto2D(head.worldX + 95, head.worldY + 42, head.worldZ);
sensorLayer.innerHTML = `
<polygon points="${headPt.x},${headPt.y} ${coneA.x},${coneA.y} ${coneB.x},${coneB.y}"
fill="rgba(0, 242, 254, 0.08)" stroke="rgba(0, 242, 254, 0.28)" stroke-width="1"/>
<line x1="${headPt.x}" y1="${headPt.y}" x2="${(coneA.x + coneB.x) / 2}" y2="${(coneA.y + coneB.y) / 2}"
stroke="var(--cyan)" stroke-width="1.2" stroke-dasharray="3,3" opacity="0.85"/>
<circle cx="${headPt.x}" cy="${headPt.y}" r="3.5" fill="var(--cyan)" filter="url(#glow-effect)"/>
`;
}
// 控制模式切换
function setControlMode(m) {
controlMode = m;
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
if (m === 'auto') document.getElementById('btn-auto').classList.add('active');
if (m === 'serpentine') document.getElementById('btn-serp').classList.add('active');
if (m === 'obstacle') document.getElementById('btn-cross').classList.add('active');
}
// 交互滑块更新
function updateSliders() {
const ampSlider = document.getElementById('param-amp');
const freqSlider = document.getElementById('param-freq');
config.amp = parseFloat(ampSlider.value) * (Math.PI / 180);
config.freq = parseFloat(freqSlider.value) / 10;
document.getElementById('amp-val').innerText = ampSlider.value + '°';
document.getElementById('freq-val').innerText = (parseFloat(freqSlider.value) / 10).toFixed(1) + 'Hz';
}
// 主渲染物理引擎循环 (帧率 60FPS 自动启动且连续播放)
let prevStamp = 0;
function animationLoop(timestamp) {
if (!prevStamp) prevStamp = timestamp;
const deltaTime = (timestamp - prevStamp) / 1000;
prevStamp = timestamp;
animTime += deltaTime;
// 周期时间进度条
const cyclePercent = ((animTime % CYCLE_DURATION) / CYCLE_DURATION) * 100;
document.getElementById('timeline-progress').style.width = cyclePercent + '%';
// 运动学求解与画面更新
const joints = computeKinematics(animTime);
renderScene(joints, animTime);
requestAnimationFrame(animationLoop);
}
// 网页加载后自启动(不依赖点击)
window.addEventListener('DOMContentLoaded', () => {
renderEnvironment();
requestAnimationFrame(animationLoop);
});
// 窗口尺寸自适应
window.addEventListener('resize', () => {
renderEnvironment();
});
</script>
</body>
</html>
积分规则:每轮对话扣10分
等待动画代码生成...
