这是为您生成的一个高保真SVG原理动画,用于直观展示机械蛇如何通过“扭转→螺旋膨胀波→地面摩擦”的单一驱动实现蜿蜒爬行。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机械蛇 - 螺旋行波推进原理动画 (IFR)</title>
<style>
:root {
--bg: #14181e;
--surface: #1c2128;
--border: #2a3039;
--text: #c8cdd4;
--text-secondary: #88909b;
--accent: #e8953b;
--accent-glow: #f5b461;
--highlight: #ffb347;
--danger: #d4534a;
--steel: #9da5b0;
--rubber: #6b4e3d;
--rubber-light: #8b6a52;
--ground: #3a3530;
--motor: #4d535c;
--wave-glow: #ff9d3c;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #0d1116;
font-family: 'IBM Plex Mono', 'SF Mono', 'Cascadia Code', 'Consolas', 'Menlo', monospace;
color: var(--text);
user-select: none;
-webkit-user-select: none;
overflow-x: hidden;
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
max-width: 1060px;
padding: 20px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 16px;
flex-wrap: wrap;
}
.title-block {
display: flex;
align-items: center;
gap: 12px;
}
.title-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, #e8953b, #d4534a);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.title-text h2 {
font-size: 1.05rem;
font-weight: 600;
letter-spacing: 0.03em;
color: #e8ecf0;
line-height: 1.3;
}
.title-text span {
font-size: 0.7rem;
color: var(--text-secondary);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.68rem;
letter-spacing: 0.04em;
font-weight: 500;
white-space: nowrap;
}
.badge-ifr {
background: #1a2a1f;
color: #5ec97a;
border: 1px solid #2a4a30;
}
.badge-triz {
background: #1f1a2a;
color: #a08cd4;
border: 1px solid #302a45;
}
.svg-wrapper {
position: relative;
width: 100%;
max-width: 1020px;
aspect-ratio: 1020 / 560;
background: var(--surface);
border-radius: 14px;
border: 1px solid var(--border);
overflow: hidden;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.02);
}
svg {
display: block;
width: 100%;
height: 100%;
}
.controls-panel {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 1020px;
background: var(--surface);
border-radius: 12px;
border: 1px solid var(--border);
padding: 14px 22px;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-size: 0.7rem;
letter-spacing: 0.05em;
color: var(--text-secondary);
text-transform: uppercase;
white-space: nowrap;
}
.control-group output {
font-size: 0.75rem;
font-weight: 600;
color: var(--accent-glow);
min-width: 48px;
text-align: center;
}
input[type="range"] {
-webkit-appearance: none;
width: 140px;
height: 6px;
border-radius: 3px;
background: var(--border);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: 2px solid #fff3;
box-shadow: 0 0 12px rgba(232, 149, 59, 0.4);
transition: box-shadow 0.2s;
}
input[type="range"]::-webkit-slider-thumb:active {
box-shadow: 0 0 22px rgba(232, 149, 59, 0.7);
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-family: inherit;
white-space: nowrap;
}
.btn:hover {
background: #2a303b;
border-color: #4a5059;
}
.btn.active-btn {
background: #2a2018;
border-color: #8b5e3c;
color: var(--accent-glow);
box-shadow: 0 0 14px rgba(232, 149, 59, 0.2);
}
.btn-play {
min-width: 80px;
justify-content: center;
}
.legend-row {
display: flex;
gap: 18px;
flex-wrap: wrap;
justify-content: center;
font-size: 0.65rem;
letter-spacing: 0.04em;
color: var(--text-secondary);
}
.legend-dot {
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
margin-right: 4px;
flex-shrink: 0;
}
@keyframes pulse-glow {
0%,
100% {
filter: drop-shadow(0 0 3px rgba(255, 157, 60, 0.5));
}
50% {
filter: drop-shadow(0 0 10px rgba(255, 157, 60, 0.9));
}
}
@keyframes subtle-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
.contact-glow {
animation: pulse-glow 1.5s ease-in-out infinite;
}
@media (max-width: 768px) {
.controls-panel {
gap: 10px;
padding: 10px 14px;
}
input[type="range"] {
width: 90px;
}
.title-text h2 {
font-size: 0.9rem;
}
.btn {
padding: 6px 10px;
font-size: 0.65rem;
}
}
</style>
</head>
<body>
<div class="main-container">
<!-- 头部信息 -->
<div class="header">
<div class="title-block">
<div class="title-icon">⚙️</div>
<div class="title-text">
<h2>螺旋行波推进 · 单驱动机械蛇</h2>
<span>Torsion → Helical Radial Wave → Frictional Thrust</span>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<span class="badge badge-ifr">◈ IFR 最终理想解</span>
<span class="badge badge-triz">△ TRIZ 矛盾破除</span>
</div>
</div>
<!-- SVG 动画区域 -->
<div class="svg-wrapper" id="svgContainer">
<svg id="mainSvg" viewBox="0 0 1020 560" xmlns="http://www.w3.org/2000/svg">
<!-- 定义 -->
<defs>
<!-- 渐变 -->
<radialGradient id="groundGrad" cx="50%" cy="0%" r="80%">
<stop offset="0%" stop-color="#4a423a" />
<stop offset="100%" stop-color="#2a2520" />
</radialGradient>
<linearGradient id="snakeBodyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#7d5c45" />
<stop offset="35%" stop-color="#9b7358" />
<stop offset="60%" stop-color="#6b4a35" />
<stop offset="100%" stop-color="#4a3022" />
</linearGradient>
<linearGradient id="coreGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#d8dce3" />
<stop offset="50%" stop-color="#a8b0ba" />
<stop offset="100%" stop-color="#7a828c" />
</linearGradient>
<linearGradient id="motorGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#5a5f68" />
<stop offset="50%" stop-color="#737982" />
<stop offset="100%" stop-color="#4a4f58" />
</linearGradient>
<linearGradient id="massGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e06050" />
<stop offset="100%" stop-color="#9b2d25" />
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glowFilter" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="softGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="2.5" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="shadowFilter" x="-5%" y="-10%" width="110%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000000" flood-opacity="0.45" />
</filter>
<!-- 箭头标记 -->
<marker id="arrowOrange" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0,0 8,3 0,6" fill="#f5903e" />
</marker>
<marker id="arrowGold" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0,0 7,2.5 0,5" fill="#ffb347" />
</marker>
<marker id="arrowWhite" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0,0 7,2.5 0,5" fill="#c8cdd4" />
</marker>
<!-- 纹理图案 -->
<pattern id="groundTexture" patternUnits="userSpaceOnUse" width="30" height="8">
<line x1="0" y1="4" x2="30" y2="4" stroke="#3a3530" stroke-width="0.6" opacity="0.5" />
<circle cx="8" cy="3" r="0.7" fill="#3a3530" opacity="0.3" />
<circle cx="22" cy="6" r="0.5" fill="#3a3530" opacity="0.3" />
</pattern>
<pattern id="rubberTexture" patternUnits="userSpaceOnUse" width="4" height="8">
<line x1="2" y1="0" x2="2" y2="8" stroke="#3d2518" stroke-width="0.8" opacity="0.5" />
</pattern>
<!-- 裁剪路径 -->
<clipPath id="snakeClip">
<rect id="snakeClipRect" x="95" y="215" width="760" height="95" rx="16" />
</clipPath>
</defs>
<!-- 背景 -->
<rect width="1020" height="560" fill="#14181e" />
<rect x="0" y="0" width="1020" height="560" fill="url(#groundTexture)" opacity="0.15" />
<!-- 网格参考线 -->
<g opacity="0.06" stroke="#88909b" stroke-width="0.5">
<line x1="50" y1="280" x2="970" y2="280" stroke-dasharray="4,12" />
<line x1="50" y1="380" x2="970" y2="380" stroke-dasharray="8,16" />
</g>
<!-- 地面 -->
<g id="groundGroup">
<rect x="30" y="378" width="960" height="70" fill="url(#groundGrad)" rx="3" />
<rect x="30" y="378" width="960" height="70" fill="url(#groundTexture)" opacity="0.6" rx="3" />
<line x1="30" y1="378" x2="990" y2="378" stroke="#5a5248" stroke-width="1.5" />
<!-- 地面摩擦指示纹理 -->
<g opacity="0.35">
<line x1="60" y1="390" x2="78" y2="390" stroke="#6b6258" stroke-width="0.8" />
<line x1="100" y1="394" x2="122" y2="394" stroke="#6b6258" stroke-width="0.8" />
<line x1="160" y1="388" x2="175" y2="388" stroke="#6b6258" stroke-width="0.8" />
<line x1="210" y1="392" x2="232" y2="392" stroke="#6b6258" stroke-width="0.8" />
<line x1="280" y1="386" x2="298" y2="386" stroke="#6b6258" stroke-width="0.8" />
<line x1="340" y1="390" x2="365" y2="390" stroke="#6b6258" stroke-width="0.8" />
<line x1="420" y1="393" x2="440" y2="393" stroke="#6b6258" stroke-width="0.8" />
<line x1="500" y1="387" x2="518" y2="387" stroke="#6b6258" stroke-width="0.8" />
<line x1="570" y1="391" x2="595" y2="391" stroke="#6b6258" stroke-width="0.8" />
<line x1="650" y1="389" x2="668" y2="389" stroke="#6b6258" stroke-width="0.8" />
<line x1="720" y1="393" x2="745" y2="393" stroke="#6b6258" stroke-width="0.8" />
<line x1="800" y1="386" x2="818" y2="386" stroke="#6b6258" stroke-width="0.8" />
<line x1="870" y1="390" x2="895" y2="390" stroke="#6b6258" stroke-width="0.8" />
</g>
</g>
<!-- 蛇身主体组 -->
<g id="snakeGroup" filter="url(#shadowFilter)">
<!-- 皮囊主体 - 上半部分(固定轮廓) -->
<path id="snakeBodyTop"
d="M100,255 Q100,228 130,225 L830,225 Q860,228 860,255 L860,280 Q860,300 830,300 L130,300 Q100,300 100,280 Z"
fill="url(#snakeBodyGrad)" stroke="#3a2518" stroke-width="1.5" />
<!-- 皮囊纵切纹路 -->
<g id="rubberRibs" opacity="0.55" stroke="#3d2015" stroke-width="1.2">
<!-- 动态生成纹路 -->
</g>
<!-- 皮囊底部 - 动态轮廓(随行波变化) -->
<path id="snakeBodyBottom"
d="M100,280 Q100,300 130,300 L830,300 Q860,300 860,280 L860,305 Q860,340 830,340 L130,340 Q100,340 100,305 Z"
fill="#5a3d2b" stroke="#3a2518" stroke-width="1.2" opacity="0.85" />
<!-- 皮囊底部动态凸起轮廓 -->
<path id="snakeBellyContour"
d="M100,300 L130,300 L830,300 L860,300"
fill="none" stroke="#8b5e40" stroke-width="2.5" stroke-linecap="round" opacity="0.9" />
<!-- 行波凸起高亮 -->
<g id="waveHighlights" filter="url(#softGlow)">
<!-- 动态生成 -->
</g>
<!-- 芯轴 -->
<line id="coreShaft" x1="115" y1="272" x2="845" y2="272" stroke="url(#coreGrad)" stroke-width="7"
stroke-linecap="round" />
<line x1="115" y1="272" x2="845" y2="272" stroke="#e8ecf2" stroke-width="1.5" opacity="0.4"
stroke-linecap="round" />
<!-- 芯轴扭转指示线(螺旋缠绕线在侧面视图中的投影) -->
<g id="torsionIndicators" opacity="0.5">
<!-- 动态生成扭转标记 -->
</g>
<!-- 偏心质量块 -->
<g id="massBlocks">
<!-- 动态生成12个三角形质量块 -->
</g>
<!-- 质量块旋转轨迹指示 -->
<g id="orbitIndicators" opacity="0.25">
<!-- 动态生成 -->
</g>
</g>
<!-- 头部电机组 -->
<g id="motorGroup">
<!-- 电机外壳 -->
<rect x="70" y="240" width="48" height="44" rx="10" fill="url(#motorGrad)" stroke="#3a3f46"
stroke-width="1.8" />
<rect x="74" y="244" width="40" height="36" rx="7" fill="none" stroke="#6a7078" stroke-width="0.8"
opacity="0.5" />
<!-- 电机旋转指示环 -->
<circle cx="94" cy="262" r="11" fill="none" stroke="#8a9099" stroke-width="2.5" opacity="0.7"
id="motorRing" />
<!-- 旋转标记 -->
<line id="motorTick" x1="94" y1="251" x2="94" y2="257" stroke="#ffb347" stroke-width="2.5"
stroke-linecap="round" />
<!-- 电机标签 -->
<text x="94" y="298" text-anchor="middle" font-size="8" fill="#88909b" letter-spacing="0.05em">无刷电机</text>
<text x="94" y="310" text-anchor="middle" font-size="7" fill="#6a7078" letter-spacing="0.04em">
BLDC</text>
</g>
<!-- 软性联轴器 -->
<g id="couplingGroup">
<rect x="112" y="260" width="16" height="14" rx="3" fill="#6a5e50" stroke="#4a3e35" stroke-width="1"
opacity="0.8" />
<line x1="114" y1="264" x2="126" y2="264" stroke="#8a7a65" stroke-width="0.7" />
<line x1="114" y1="268" x2="126" y2="268" stroke="#8a7a65" stroke-width="0.7" />
</g>
<!-- 地面接触点(行波凸起与地面接触处) -->
<g id="contactPoints" filter="url(#glowFilter)">
<!-- 动态生成 -->
</g>
<!-- 力流箭头 -->
<g id="forceArrows">
<!-- 动态更新 -->
</g>
<!-- 前进位移指示 -->
<g id="displacementIndicator">
<line id="displacementLine" x1="140" y1="420" x2="140" y2="420" stroke="#ffb347" stroke-width="2"
stroke-dasharray="6,4" opacity="0" />
<text id="displacementText" x="140" y="438" text-anchor="middle" font-size="9" fill="#ffb347"
opacity="0">位移: 0mm</text>
</g>
<!-- 标注说明 -->
<g id="annotations" font-family="'IBM Plex Mono','SF Mono',monospace">
<!-- 芯轴标注 -->
<line x1="480" y1="272" x2="520" y2="195" stroke="#88909b" stroke-width="0.8" stroke-dasharray="3,4"
opacity="0.6" />
<text x="525" y="192" font-size="8.5" fill="#a8b0ba" letter-spacing="0.03em">弹簧钢芯轴</text>
<text x="525" y="203" font-size="7" fill="#6a7078">Ø8mm · 扭转刚度0.5N·m/rad</text>
<!-- 质量块标注 -->
<line x1="360" y1="255" x2="340" y2="170" stroke="#d4534a" stroke-width="0.8" stroke-dasharray="3,4"
opacity="0.6" />
<text x="295" y="166" font-size="8.5" fill="#e06050" letter-spacing="0.03em">偏心质量块</text>
<text x="295" y="177" font-size="7" fill="#b05045">30g · 螺旋错开30°</text>
<!-- 皮囊标注 -->
<line x1="620" y1="305" x2="660" y2="440" stroke="#8b6a52" stroke-width="0.8" stroke-dasharray="3,4"
opacity="0.6" />
<text x="665" y="436" font-size="8.5" fill="#9b7358" letter-spacing="0.03em">橡胶皮囊</text>
<text x="665" y="447" font-size="7" fill="#7d5c45">纵切纹路 · 过盈接触0.3mm</text>
<!-- 行波标注 -->
<text x="445" y="130" font-size="9" fill="#ffb347" letter-spacing="0.05em" text-anchor="middle"
opacity="0.85">▼ 螺旋行波传播方向 ▼</text>
<line x1="200" y1="138" x2="690" y2="138" stroke="#ffb347" stroke-width="1" opacity="0.4"
stroke-dasharray="8,5" marker-end="url(#arrowGold)" />
</g>
<!-- 图例区 -->
<g id="legendGroup" transform="translate(810, 60)" opacity="0.8">
<rect x="0" y="0" width="175" height="115" rx="8" fill="#1c2128" stroke="#2a3039" stroke-width="1" />
<text x="12" y="18" font-size="8" fill="#88909b" letter-spacing="0.05em">图例</text>
<circle cx="16" cy="34" r="4" fill="#e06050" />
<text x="26" y="37" font-size="7.5" fill="#a8b0ba">偏心质量块</text>
<circle cx="16" cy="52" r="4" fill="#ffb347" filter="url(#softGlow)" />
<text x="26" y="55" font-size="7.5" fill="#a8b0ba">行波凸起/接触点</text>
<line x1="12" y1="70" x2="20" y2="70" stroke="#ffb347" stroke-width="2" marker-end="url(#arrowGold)" />
<text x="26" y="73" font-size="7.5" fill="#a8b0ba">力流方向</text>
<circle cx="16" cy="90" r="3.5" fill="none" stroke="#9da5b0" stroke-width="1.5" />
<text x="26" y="93" font-size="7.5" fill="#a8b0ba">芯轴旋转轨迹</text>
</g>
</svg>
</div>
<!-- 控制面板 -->
<div class="controls-panel" id="controlsPanel">
<div class="control-group">
<label>⚡ 电机转速</label>
<input type="range" id="rpmSlider" min="200" max="600" value="360" step="10" />
<output id="rpmOutput">360 RPM</output>
</div>
<button class="btn btn-play active-btn" id="btnPlay" title="播放/暂停">
<span id="playIcon">⏸</span> <span id="playText">暂停</span>
</button>
<button class="btn" id="btnReset" title="重置动画">↺ 重置</button>
<button class="btn" id="btnShowInternals" title="显示/隐藏内部结构">🔍 内部结构</button>
<div class="control-group">
<label>🐍 速度倍率</label>
<input type="range" id="speedScaleSlider" min="0.2" max="2.0" value="0.6" step="0.1" />
<output id="speedOutput">0.6×</output>
</div>
</div>
<!-- 底部图例行 -->
<div class="legend-row">
<span><span class="legend-dot" style="background:#e06050;"></span>偏心质量块(12个·螺旋排列)</span>
<span><span class="legend-dot" style="background:#ffb347;"></span>行波凸起·地面接触点</span>
<span><span class="legend-dot" style="background:#9da5b0;"></span>弹簧钢芯轴Ø8mm</span>
<span><span class="legend-dot" style="background:#8b6a52;"></span>橡胶皮囊(纵切纹路)</span>
</div>
</div>
<script>
(function() {
// ============ DOM 引用 ============
const svg = document.getElementById('mainSvg');
const snakeGroup = document.getElementById('snakeGroup');
const massBlocksGroup = document.getElementById('massBlocks');
const orbitIndicatorsGroup = document.getElementById('orbitIndicators');
const torsionIndicatorsGroup = document.getElementById('torsionIndicators');
const rubberRibsGroup = document.getElementById('rubberRibs');
const waveHighlightsGroup = document.getElementById('waveHighlights');
const contactPointsGroup = document.getElementById('contactPoints');
const forceArrowsGroup = document.getElementById('forceArrows');
const snakeBellyContour = document.getElementById('snakeBellyContour');
const motorTick = document.getElementById('motorTick');
const displacementLine = document.getElementById('displacementLine');
const displacementText = document.getElementById('displacementText');
// 控件
const rpmSlider = document.getElementById('rpmSlider');
const rpmOutput = document.getElementById('rpmOutput');
const speedScaleSlider = document.getElementById('speedScaleSlider');
const speedOutput = document.getElementById('speedOutput');
const btnPlay = document.getElementById('btnPlay');
const playIcon = document.getElementById('playIcon');
const playText = document.getElementById('playText');
const btnReset = document.getElementById('btnReset');
const btnShowInternals = document.getElementById('btnShowInternals');
// ============ 参数 ============
const NUM_MASSES = 12; // 质量块数量
const SNAKE_LENGTH = 720; // 蛇身长度 px (对应720mm)
const MASS_SPACING = SNAKE_LENGTH / NUM_MASSES; // 质量块间距 ≈60px
const CORE_Y = 272; // 芯轴Y坐标
const ECCENTRICITY = 15; // 偏心距 px (对应15mm)
const BELLY_NORMAL_Y = 300; // 皮囊底部正常Y
const BELLY_MAX_DEFLECT = 42; // 皮囊最大形变幅度(放大展示)
const GROUND_Y = 378; // 地面Y坐标
const SNAKE_START_X = 120; // 蛇身起始X(第一个质量块位置)
const MOTOR_CX = 94; // 电机中心X
const MOTOR_CY = 262; // 电机中心Y
// ============ 状态 ============
let motorAngle = 0; // 电机旋转角度 (rad)
let rpm = 360; // 当前转速
let speedScale = 0.6; // 速度倍率
let isPlaying = true;
let showInternals = true;
let snakeDisplacement = 0; // 蛇身前进总位移 (px)
let lastTime = null;
let animId = null;
// 存储质量块DOM引用
let massElements = [];
let orbitElements = [];
let torsionElements = [];
let ribElements = [];
// ============ 初始化SVG元素 ============
function initSVGElements() {
// 清空动态组
massBlocksGroup.innerHTML = '';
orbitIndicatorsGroup.innerHTML = '';
torsionIndicatorsGroup.innerHTML = '';
rubberRibsGroup.innerHTML = '';
waveHighlightsGroup.innerHTML = '';
contactPointsGroup.innerHTML = '';
forceArrowsGroup.innerHTML = '';
massElements = [];
orbitElements = [];
torsionElements = [];
ribElements = [];
// 创建12个偏心质量块(三角形)
for (let i = 0; i < NUM_MASSES; i++) {
const cx = SNAKE_START_X + i * MASS_SPACING;
// 三角形质量块
const triangle = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
triangle.setAttribute('fill', 'url(#massGrad)');
triangle.setAttribute('stroke', '#6b1a14');
triangle.setAttribute('stroke-width', '1.2');
triangle.setAttribute('data-index', i);
triangle.style.transition = 'opacity 0.3s';
massBlocksGroup.appendChild(triangle);
massElements.push(triangle);
// 旋转轨迹圆(虚线)
const orbit = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
orbit.setAttribute('cx', cx);
orbit.setAttribute('cy', CORE_Y);
orbit.setAttribute('r', ECCENTRICITY);
orbit.setAttribute('fill', 'none');
orbit.setAttribute('stroke', '#6a7078');
orbit.setAttribute('stroke-width', '0.8');
orbit.setAttribute('stroke-dasharray', '3,4');
orbit.setAttribute('data-index', i);
orbitIndicatorsGroup.appendChild(orbit);
orbitElements.push(orbit);
// 扭转指示标记(芯轴上的短线)
const tLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
tLine.setAttribute('x1', cx);
tLine.setAttribute('y1', CORE_Y - 5);
tLine.setAttribute('x2', cx);
tLine.setAttribute('y2', CORE_Y + 5);
tLine.setAttribute('stroke', '#d8dce3');
tLine.setAttribute('stroke-width', '1.2');
tLine.setAttribute('opacity', '0.55');
tLine.setAttribute('data-index', i);
torsionIndicatorsGroup.appendChild(tLine);
torsionElements.push(tLine);
}
// 创建皮囊纵切纹路(沿蛇身分布)
const ribCount = 60;
for (let i = 0; i <= ribCount; i++) {
const rx = 100 + (i / ribCount) * 760;
const ribLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
ribLine.setAttribute('x1', rx);
ribLine.setAttribute('y1', 228);
ribLine.setAttribute('x2', rx);
ribLine.setAttribute('y2', 300);
ribLine.setAttribute('stroke', '#3d2015');
ribLine.setAttribute('stroke-width', '0.9');
ribLine.setAttribute('opacity', '0.45');
ribLine.setAttribute('data-index', i);
rubberRibsGroup.appendChild(ribLine);
ribElements.push({ element: ribLine, baseX: rx, baseY1: 228, baseY2: 300 });
}
// 创建行波高亮元素
for (let i = 0; i < 6; i++) {
const glowDot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
glowDot.setAttribute('r', '5');
glowDot.setAttribute('fill', '#ffb347');
glowDot.setAttribute('opacity', '0');
glowDot.setAttribute('data-wave-index', i);
waveHighlightsGroup.appendChild(glowDot);
}
// 创建接触点元素
for (let i = 0; i < 6; i++) {
const cp = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
cp.setAttribute('rx', '6');
cp.setAttribute('ry', '3');
cp.setAttribute('fill', '#ff9d3c');
cp.setAttribute('opacity', '0');
cp.setAttribute('data-cp-index', i);
cp.classList.add('contact-glow');
contactPointsGroup.appendChild(cp);
}
// 力流箭头
for (let i = 0; i < 8; i++) {
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
arrow.setAttribute('stroke', '#f5903e');
arrow.setAttribute('stroke-width', '1.8');
arrow.setAttribute('marker-end', 'url(#arrowOrange)');
arrow.setAttribute('opacity', '0');
arrow.setAttribute('data-arrow-index', i);
forceArrowsGroup.appendChild(arrow);
}
}
// ============ 计算三角形顶点 ============
function calcTriangleVertices(cx, cy, angle, size = 10) {
// 三角形绕(cx,cy)旋转,重心在偏心位置
const eccX = cx + ECCENTRICITY * Math.cos(angle);
const eccY = cy + ECCENTRICITY * Math.sin(angle);
// 三角形顶点:一个指向外(径向),两个在内侧
const tipAngle = angle; // 尖端指向偏心方向
const tipX = eccX + size * Math.cos(tipAngle);
const tipY = eccY + size * Math.sin(tipAngle);
const base1X = eccX + size * 0.55 * Math.cos(tipAngle + 2.2);
const base1Y = eccY + size * 0.55 * Math.sin(tipAngle + 2.2);
const base2X = eccX + size * 0.55 * Math.cos(tipAngle - 2.2);
const base2Y = eccY + size * 0.55 * Math.sin(tipAngle - 2.2);
return `${tipX},${tipY} ${base1X},${base1Y} ${base2X},${base2Y}`;
}
// ============ 更新皮囊底部轮廓 ============
function updateBellyContour(offsets) {
// offsets: 12个质量块处的皮囊底部偏移(正值=向下凸起)
const points = [];
const n = NUM_MASSES;
// 在质量块之间进行更精细的采样
const samplesPerSegment = 4;
const totalSamples = n * samplesPerSegment;
for (let s = 0; s <= totalSamples; s++) {
const t = s / totalSamples;
const x = SNAKE_START_X + t * SNAKE_LENGTH;
// 使用Catmull-Rom样条插值
const idxFloat = t * (n - 1);
const idx0 = Math.floor(idxFloat);
const frac = idxFloat - idx0;
// 获取相邻4个控制点的偏移值
const getOffset = (i) => {
const clamped = Math.max(0, Math.min(n - 1, i));
return offsets[clamped];
};
const o0 = getOffset(idx0 - 1);
const o1 = getOffset(idx0);
const o2 = getOffset(idx0 + 1);
const o3 = getOffset(idx0 + 2);
// Catmull-Rom插值
const t2 = frac * frac;
const t3 = t2 * frac;
const y = BELLY_NORMAL_Y - (
0.5 * ((2 * o1) +
(-o0 + o2) * frac +
(2 * o0 - 5 * o1 + 4 * o2 - o3) * t2 +
(-o0 + 3 * o1 - 3 * o2 + o3) * t3)
);
points.push(`${x},${Math.round(y * 10) / 10}`);
}
snakeBellyContour.setAttribute('d', `M${points.join(' L')}`);
}
// ============ 主动画循环 ============
function animate(timestamp) {
if (!isPlaying) {
lastTime = null;
animId = requestAnimationFrame(animate);
return;
}
if (lastTime === null) {
lastTime = timestamp;
animId = requestAnimationFrame(animate);
return;
}
const dt = Math.min((timestamp - lastTime) / 1000, 0.1); // 限制最大步长
lastTime = timestamp;
// 更新电机角度
const angularVelocity = (rpm / 60) * 2 * Math.PI * speedScale; // rad/s
motorAngle += angularVelocity * dt;
// 保持角度在0-2π范围内
const normalizedAngle = motorAngle % (2 * Math.PI);
// 计算蛇身前进速度(基于行波推进的简化模型)
// 前进速度约为行波速度的8-15%(取决于摩擦特性)
const waveSpeed = angularVelocity * SNAKE_LENGTH / (2 * Math.PI); // 行波速度 px/s
const advanceEfficiency = 0.1; // 前进效率
const advanceSpeed = waveSpeed * advanceEfficiency;
snakeDisplacement += advanceSpeed * dt;
// 限制位移显示范围
if (snakeDisplacement > 300) snakeDisplacement -= 300;
// 计算12个质量块的偏移
const offsets = [];
const massPhases = [];
for (let i = 0; i < NUM_MASSES; i++) {
const phase = motorAngle + i * (Math.PI / 6); // 30度 = π/6 错开
massPhases.push(phase);
const sinVal = Math.sin(phase);
// 偏移:sin>0时向下推挤皮囊
const deflection = sinVal > 0 ? sinVal * BELLY_MAX_DEFLECT : sinVal * BELLY_MAX_DEFLECT * 0.25;
offsets.push(deflection);
}
// 更新皮囊底部轮廓
updateBellyContour(offsets);
// 更新质量块三角形
for (let i = 0; i < NUM_MASSES; i++) {
const cx = SNAKE_START_X + i * MASS_SPACING;
const phase = massPhases[i];
const verts = calcTriangleVertices(cx, CORE_Y, phase, 9);
massElements[i].setAttribute('points', verts);
massElements[i].setAttribute('opacity', showInternals ? '0.9' : '0.15');
// 更新轨道
orbitElements[i].setAttribute('opacity', showInternals ? '0.3' : '0.05');
// 更新扭转指示
const twistOffset = Math.sin(phase) * 4;
torsionElements[i].setAttribute('x1', cx + twistOffset);
torsionElements[i].setAttribute('x2', cx - twistOffset);
torsionElements[i].setAttribute('opacity', showInternals ? '0.5' : '0.08');
}
// 更新皮囊纵切纹路(底部随凸起变化)
for (let i = 0; i < ribElements.length; i++) {
const rib = ribElements[i];
const rx = rib.baseX;
// 找到该纹路位置对应的偏移
const t = (rx - SNAKE_START_X) / SNAKE_LENGTH;
const clampedT = Math.max(0, Math.min(1, t));
const idxFloat = clampedT * (NUM_MASSES - 1);
const idx0 = Math.floor(idxFloat);
const frac = idxFloat - idx0;
const o0 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, idx0))];
const o1 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, idx0 + 1))];
const localDeflection = o0 + (o1 - o0) * frac;
const bottomY = BELLY_NORMAL_Y - localDeflection;
rib.element.setAttribute('y2', Math.max(228, bottomY));
}
// 更新行波高亮
const waveHighlightDots = waveHighlightsGroup.querySelectorAll('circle[data-wave-index]');
const numHighlights = waveHighlightDots.length;
for (let i = 0; i < numHighlights; i++) {
const dot = waveHighlightDots[i];
// 高亮位置跟随行波波峰
const wavePhase = normalizedAngle + i * (2 * Math.PI / numHighlights);
const wavePos = (wavePhase % (2 * Math.PI)) / (2 * Math.PI); // 0-1
const hx = SNAKE_START_X + wavePos * SNAKE_LENGTH;
// 找到该位置的偏移
const ht = wavePos;
const hidxFloat = ht * (NUM_MASSES - 1);
const hidx0 = Math.floor(hidxFloat);
const hfrac = hidxFloat - hidx0;
const ho0 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, hidx0))];
const ho1 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, hidx0 + 1))];
const hdeflection = ho0 + (ho1 - ho0) * hfrac;
const hy = BELLY_NORMAL_Y - hdeflection;
dot.setAttribute('cx', hx);
dot.setAttribute('cy', hy);
// 只在凸起明显时显示
const brightness = hdeflection > 5 ? Math.min(1, hdeflection / BELLY_MAX_DEFLECT) : 0;
dot.setAttribute('opacity', brightness * 0.85);
dot.setAttribute('r', 3 + brightness * 5);
}
// 更新接触点
const contactDots = contactPointsGroup.querySelectorAll('ellipse[data-cp-index]');
const numContacts = contactDots.length;
for (let i = 0; i < numContacts; i++) {
const cp = contactDots[i];
const wavePhase = normalizedAngle + i * (2 * Math.PI / numContacts) + Math.PI / numContacts;
const wavePos = (wavePhase % (2 * Math.PI)) / (2 * Math.PI);
const cx = SNAKE_START_X + wavePos * SNAKE_LENGTH;
const ct = wavePos;
const cidxFloat = ct * (NUM_MASSES - 1);
const cidx0 = Math.floor(cidxFloat);
const cfrac = cidxFloat - cidx0;
const co0 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, cidx0))];
const co1 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, cidx0 + 1))];
const cdeflection = co0 + (co1 - co0) * cfrac;
const bellyY = BELLY_NORMAL_Y - cdeflection;
cp.setAttribute('cx', cx);
cp.setAttribute('cy', GROUND_Y - 1);
// 当皮囊凸起接近地面时显示接触点
const contactStrength = bellyY >= GROUND_Y - 8 ? Math.min(1, (bellyY - GROUND_Y + 16) / 16) : 0;
cp.setAttribute('opacity', contactStrength * 0.9);
cp.setAttribute('rx', 4 + contactStrength * 5);
cp.setAttribute('ry', 1.5 + contactStrength * 3);
}
// 更新力流箭头
const arrowElements = forceArrowsGroup.querySelectorAll('line[data-arrow-index]');
for (let i = 0; i < arrowElements.length; i++) {
const arrow = arrowElements[i];
const wavePhase = normalizedAngle + i * (2 * Math.PI / arrowElements.length);
const wavePos = (wavePhase % (2 * Math.PI)) / (2 * Math.PI);
const ax = SNAKE_START_X + wavePos * SNAKE_LENGTH;
const at = wavePos;
const aidxFloat = at * (NUM_MASSES - 1);
const aidx0 = Math.floor(aidxFloat);
const afrac = aidxFloat - aidx0;
const ao0 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, aidx0))];
const ao1 = offsets[Math.max(0, Math.min(NUM_MASSES - 1, aidx0 + 1))];
const adeflection = ao0 + (ao1 - ao0) * afrac;
const bellyY = BELLY_NORMAL_Y - adeflection;
const isContacting = bellyY >= GROUND_Y - 10;
if (isContacting && adeflection > 10) {
arrow.setAttribute('x1', ax);
arrow.setAttribute('y1', Math.min(bellyY, GROUND_Y));
arrow.setAttribute('x2', ax + 28);
arrow.setAttribute('y2', Math.min(bellyY, GROUND_Y) - 5);
arrow.setAttribute('opacity', '0.7');
} else {
arrow.setAttribute('opacity', '0');
}
}
// 更新电机旋转标记
const motorTickAngle = normalizedAngle;
const tickLen = 9;
const tx = MOTOR_CX + tickLen * Math.cos(motorTickAngle);
const ty = MOTOR_CY + tickLen * Math.sin(motorTickAngle);
motorTick.setAttribute('x1', MOTOR_CX);
motorTick.setAttribute('y1', MOTOR_CY);
motorTick.setAttribute('x2', tx);
motorTick.setAttribute('y2', ty);
// 更新位移指示
const dispPx = snakeDisplacement % 300;
displacementLine.setAttribute('x1', 130 + dispPx);
displacementLine.setAttribute('x2', 130 + dispPx);
displacementLine.setAttribute('y1', 408);
displacementLine.setAttribute('y2', 424);
displacementLine.setAttribute('opacity', '0.7');
displacementText.setAttribute('x', 130 + dispPx);
displacementText.setAttribute('y', 438);
displacementText.setAttribute('opacity', '0.8');
displacementText.textContent = `前进 ≈${(snakeDisplacement*0.9).toFixed(0)}mm`;
animId = requestAnimationFrame(animate);
}
// ============ 控件事件 ============
rpmSlider.addEventListener('input', () => {
rpm = parseInt(rpmSlider.value);
rpmOutput.textContent = rpm + ' RPM';
});
speedScaleSlider.addEventListener('input', () => {
speedScale = parseFloat(speedScaleSlider.value);
speedOutput.textContent = speedScale.toFixed(1) + '×';
});
btnPlay.addEventListener('click', () => {
isPlaying = !isPlaying;
if (isPlaying) {
playIcon.textContent = '⏸';
playText.textContent = '暂停';
btnPlay.classList.add('active-btn');
lastTime = null;
} else {
playIcon.textContent = '▶';
playText.textContent = '播放';
btnPlay.classList.remove('active-btn');
}
});
btnReset.addEventListener('click', () => {
motorAngle = 0;
snakeDisplacement = 0;
lastTime = null;
rpm = 360;
speedScale = 0.6;
rpmSlider.value = 360;
rpmOutput.textContent = '360 RPM';
speedScaleSlider.value = 0.6;
speedOutput.textContent = '0.6×';
if (!isPlaying) {
isPlaying = true;
playIcon.textContent = '⏸';
playText.textContent = '暂停';
btnPlay.classList.add('active-btn');
}
// 重新初始化
initSVGElements();
});
btnShowInternals.addEventListener('click', () => {
showInternals = !showInternals;
if (showInternals) {
btnShowInternals.classList.add('active-btn');
btnShowInternals.textContent = '🔍 内部结构: 显示';
} else {
btnShowInternals.classList.remove('active-btn');
btnShowInternals.textContent = '🔍 内部结构: 隐藏';
}
// 更新质量块和轨道的透明度
for (let i = 0; i < NUM_MASSES; i++) {
massElements[i].setAttribute('opacity', showInternals ? '0.9' : '0.12');
orbitElements[i].setAttribute('opacity', showInternals ? '0.3' : '0.04');
torsionElements[i].setAttribute('opacity', showInternals ? '0.5' : '0.06');
}
});
// 初始状态
btnShowInternals.classList.add('active-btn');
btnShowInternals.textContent = '🔍 内部结构: 显示';
// ============ 键盘快捷键 ============
document.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
btnPlay.click();
break;
case 'r':
if (!e.ctrlKey && !e.metaKey) {
btnReset.click();
}
break;
case 'i':
if (!e.ctrlKey && !e.metaKey) {
btnShowInternals.click();
}
break;
case 'arrowleft':
e.preventDefault();
rpm = Math.max(200, rpm - 30);
rpmSlider.value = rpm;
rpmOutput.textContent = rpm + ' RPM';
break;
case 'arrowright':
e.preventDefault();
rpm = Math.min(600, rpm + 30);
rpmSlider.value = rpm;
rpmOutput.textContent = rpm + ' RPM';
break;
case 'arrowup':
e.preventDefault();
speedScale = Math.min(2.0, speedScale + 0.1);
speedScaleSlider.value = speedScale;
speedOutput.textContent = speedScale.toFixed(1) + '×';
break;
case 'arrowdown':
e.preventDefault();
speedScale = Math.max(0.2, speedScale - 0.1);
speedScaleSlider.value = speedScale;
speedOutput.textContent = speedScale.toFixed(1) + '×';
break;
}
});
// ============ 启动 ============
initSVGElements();
animId = requestAnimationFrame(animate);
// ============ 响应式处理 ============
window.addEventListener('resize', () => {
// SVG viewBox保持不变,自动缩放
});
console.log('🐍 机械蛇螺旋行波推进动画已就绪');
console.log(' IFR理念: 弹性脊柱 + 螺旋质量块 → 行波推进');
console.log(' 操作: 空格键暂停 | R重置 | I切换内部结构 | ←→调节转速 | ↑↓调节速度');
})();
</script>
</body>
</html>
动画解读
这个页面不是一个普通的演示,而是一个围绕最终理想解,演示“扭转-扩张-前进”如何完成一体式推进的思路模型。
1. 核心机制:从“关节弯曲”到“螺旋行波”
动画通过机械蛇的剖面视图,直观展示了“弹簧钢芯轴”在电机驱动下发生扭转。这根芯轴上的偏心质量块依次挤压“橡胶皮囊”,形成不断向前移动的螺旋形凸起。这模拟了力从内部旋转转化为外部蠕动波的过程,代替了传统机械蛇复杂的多关节协调运动。
2. 视觉与交互设计:突出核心矛盾与资源利用
- 视觉焦点:动画使用高亮发光点标记皮囊上正在与地面接触的“推进点”,并用橙色箭头指示地面反作用力的方向。这直观地强调了“橡胶皮囊的局部形变”是如何被巧妙利用,成为推进抓地力的关键资源。
- 操作与控制:底部的控制面板让您可以动态调整电机转速(200-600 RPM),并切换显示内部结构。您可以亲手改变转速,观察行波传播速度与前进效率的关系,深入体验动态原理。
- 信息标注:画面中直接标注了“弹簧钢芯轴”、“偏心质量块”等关键部件及其参数(如螺旋错开30°),将设计细节与视觉呈现紧密结合。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
