分享图
A
动画渲染工坊
就绪

这是为您生成的一个展示机械蛇“各向异性摩擦推进”原理的高保真 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: #16161e;
            --surface: #1e1e2a;
            --panel-bg: #1a1a28;
            --text: #d8d8e8;
            --text-secondary: #9898b4;
            --accent-gold: #e8a850;
            --accent-copper: #d4785c;
            --accent-anchor: #ff5c3a;
            --accent-slide: #4ecdc4;
            --accent-wave: #6ea8f0;
            --snake-body: #7a7d8c;
            --snake-body-dark: #555768;
            --ground: #4a3f38;
            --ground-dark: #362e28;
            --border: #2a2a3a;
            --glow-anchor: rgba(255, 92, 58, 0.7);
            --glow-slide: rgba(78, 205, 196, 0.5);
            --font-display: 'Georgia', 'Noto Serif SC', 'STSong', 'SimSun', 'Times New Roman', serif;
            --font-body: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', system-ui, sans-serif;
            --font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', 'Menlo', 'Source Code Pro', monospace;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: var(--bg);
            color: var(--text);
            font-family: var(--font-body);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            overflow-x: hidden;
            -webkit-font-smoothing: antialiased;
            background-image:
                radial-gradient(ellipse at 50% 30%, rgba(100, 130, 180, 0.06) 0%, transparent 60%),
                radial-gradient(ellipse at 70% 70%, rgba(180, 120, 80, 0.04) 0%, transparent 55%),
                radial-gradient(ellipse at 30% 60%, rgba(60, 80, 120, 0.03) 0%, transparent 50%);
        }

        .main-container {
            width: 100%;
            max-width: 1300px;
            padding: 20px 24px;
            display: flex;
            flex-direction: column;
            gap: 18px;
            align-items: center;
        }

        .header {
            text-align: center;
            max-width: 900px;
        }
        .header h1 {
            font-family: var(--font-display);
            font-size: 1.65rem;
            font-weight: 700;
            letter-spacing: 0.04em;
            color: #e8d8c0;
            margin-bottom: 4px;
            line-height: 1.3;
        }
        .header .subtitle {
            font-size: 0.85rem;
            color: var(--text-secondary);
            letter-spacing: 0.06em;
            text-transform: uppercase;
            font-weight: 400;
        }
        .ifr-badge {
            display: inline-block;
            background: rgba(232, 168, 80, 0.12);
            border: 1px solid rgba(232, 168, 80, 0.35);
            color: var(--accent-gold);
            font-size: 0.72rem;
            padding: 3px 12px;
            border-radius: 20px;
            letter-spacing: 0.08em;
            font-weight: 600;
            margin-top: 6px;
        }

        .svg-wrapper {
            width: 100%;
            max-width: 1200px;
            aspect-ratio: 2 / 1;
            background: var(--surface);
            border-radius: 16px;
            border: 1px solid var(--border);
            position: relative;
            overflow: hidden;
            box-shadow:
                0 8px 40px rgba(0, 0, 0, 0.45),
                0 1px 2px rgba(255, 255, 255, 0.03) inset;
            cursor: default;
        }
        .svg-wrapper svg {
            width: 100%;
            height: 100%;
            display: block;
        }

        /* 鳞片高亮脉冲指示器 */
        .scale-indicator {
            position: absolute;
            pointer-events: none;
            border-radius: 50%;
            background: var(--glow-anchor);
            width: 14px;
            height: 14px;
            transform: translate(-50%, -50%);
            animation: pulseIndicator 0.9s ease-out forwards;
            opacity: 0;
            z-index: 5;
        }
        @keyframes pulseIndicator {
            0% {
                transform: translate(-50%, -50%) scale(0.3);
                opacity: 1;
            }
            100% {
                transform: translate(-50%, -50%) scale(4);
                opacity: 0;
            }
        }

        .controls-panel {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            align-items: center;
            justify-content: center;
            background: var(--panel-bg);
            border-radius: 14px;
            padding: 14px 22px;
            border: 1px solid var(--border);
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
            max-width: 900px;
            width: 100%;
        }
        .control-group {
            display: flex;
            flex-direction: column;
            gap: 3px;
            min-width: 100px;
        }
        .control-group label {
            font-size: 0.7rem;
            letter-spacing: 0.05em;
            color: var(--text-secondary);
            text-transform: uppercase;
            font-weight: 600;
            white-space: nowrap;
        }
        .control-group input[type="range"] {
            -webkit-appearance: none;
            width: 130px;
            height: 6px;
            border-radius: 3px;
            background: linear-gradient(90deg, #3a3a50, #5a5a78);
            outline: none;
            cursor: pointer;
        }
        .control-group input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 22px;
            height: 22px;
            border-radius: 50%;
            background: var(--accent-gold);
            border: 2px solid #1a1a28;
            cursor: pointer;
            box-shadow: 0 2px 10px rgba(232, 168, 80, 0.45);
            transition: transform 0.15s;
        }
        .control-group input[type="range"]::-webkit-slider-thumb:active {
            transform: scale(1.2);
        }
        .control-value {
            font-size: 0.75rem;
            color: var(--accent-gold);
            font-family: var(--font-mono);
            font-weight: 600;
            text-align: center;
        }
        .btn {
            padding: 8px 18px;
            border-radius: 22px;
            border: 1px solid var(--border);
            background: var(--surface);
            color: var(--text);
            cursor: pointer;
            font-size: 0.78rem;
            letter-spacing: 0.04em;
            font-weight: 500;
            transition: all 0.25s;
            white-space: nowrap;
            font-family: var(--font-body);
        }
        .btn:hover {
            border-color: var(--accent-gold);
            color: var(--accent-gold);
            box-shadow: 0 2px 14px rgba(232, 168, 80, 0.2);
        }
        .btn.active {
            background: rgba(232, 168, 80, 0.15);
            border-color: var(--accent-gold);
            color: var(--accent-gold);
        }

        .legend-row {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            justify-content: center;
            font-size: 0.7rem;
            color: var(--text-secondary);
            letter-spacing: 0.03em;
            align-items: center;
        }
        .legend-dot {
            display: inline-block;
            width: 9px;
            height: 9px;
            border-radius: 50%;
            margin-right: 5px;
            vertical-align: middle;
            position: relative;
            top: -1px;
        }
        .legend-dot.anchor {
            background: var(--accent-anchor);
            box-shadow: 0 0 8px var(--glow-anchor);
        }
        .legend-dot.slide {
            background: var(--accent-slide);
            box-shadow: 0 0 6px var(--glow-slide);
        }
        .legend-dot.gold {
            background: var(--accent-gold);
        }

        @media (max-width: 768px) {
            .svg-wrapper {
                aspect-ratio: 3/2;
                border-radius: 10px;
            }
            .controls-panel {
                padding: 10px 14px;
                gap: 10px;
            }
            .control-group input[type="range"] {
                width: 80px;
            }
            .header h1 {
                font-size: 1.25rem;
            }
        }
    </style>
</head>
<body>
    <div class="main-container">
        <div class="header">
            <h1>仿生机械蛇 · 被动鳞片各向异性摩擦推进</h1>
            <div class="subtitle">基于 TRIZ 最终理想解(IFR)—— 身体结构自发将弯曲转化为推力</div>
            <span class="ifr-badge">◆ IFR 核心机制:零主动控制 · 纯结构智能</span>
        </div>

        <div class="svg-wrapper" id="svgContainer">
            <svg id="mainSvg" viewBox="0 0 1200 600" xmlns="http://www.w3.org/2000/svg">
                <defs>
                    <!-- 渐变定义 -->
                    <linearGradient id="bgGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="#1c1c2c" />
                        <stop offset="100%" stop-color="#1a1a26" />
                    </linearGradient>
                    <linearGradient id="groundGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="#4a3f38" />
                        <stop offset="30%" stop-color="#3d342e" />
                        <stop offset="100%" stop-color="#2a231f" />
                    </linearGradient>
                    <linearGradient id="snakeBodyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="#8e909e" />
                        <stop offset="35%" stop-color="#757885" />
                        <stop offset="65%" stop-color="#5e606e" />
                        <stop offset="100%" stop-color="#4a4c58" />
                    </linearGradient>
                    <linearGradient id="snakeBodyHighlight" x1="0%" y1="0%" x2="0%" y2="100%">
                        <stop offset="0%" stop-color="rgba(255,255,255,0.15)" />
                        <stop offset="100%" stop-color="rgba(255,255,255,0)" />
                    </linearGradient>
                    <linearGradient id="scaleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
                        <stop offset="0%" stop-color="#e8a850" />
                        <stop offset="40%" stop-color="#d4785c" />
                        <stop offset="100%" stop-color="#b8553a" />
                    </linearGradient>
                    <linearGradient id="scaleGradActive" x1="0%" y1="0%" x2="100%" y2="100%">
                        <stop offset="0%" stop-color="#ffb860" />
                        <stop offset="30%" stop-color="#ff6b3a" />
                        <stop offset="100%" stop-color="#e04020" />
                    </linearGradient>
                    <radialGradient id="anchorGlow" cx="50%" cy="50%" r="50%">
                        <stop offset="0%" stop-color="rgba(255,92,58,0.9)" />
                        <stop offset="40%" stop-color="rgba(255,92,58,0.4)" />
                        <stop offset="100%" stop-color="rgba(255,92,58,0)" />
                    </radialGradient>
                    <radialGradient id="slideGlow" cx="50%" cy="50%" r="50%">
                        <stop offset="0%" stop-color="rgba(78,205,196,0.6)" />
                        <stop offset="50%" stop-color="rgba(78,205,196,0.2)" />
                        <stop offset="100%" stop-color="rgba(78,205,196,0)" />
                    </radialGradient>
                    <!-- 地面纹理 -->
                    <pattern id="groundTexture" patternUnits="userSpaceOnUse" width="30" height="12">
                        <rect width="30" height="12" fill="none" />
                        <circle cx="5" cy="3" r="0.6" fill="rgba(0,0,0,0.25)" />
                        <circle cx="18" cy="7" r="0.4" fill="rgba(0,0,0,0.2)" />
                        <circle cx="26" cy="2" r="0.5" fill="rgba(0,0,0,0.22)" />
                        <circle cx="12" cy="9" r="0.35" fill="rgba(0,0,0,0.18)" />
                    </pattern>
                    <!-- 鳞片形状 -->
                    <g id="scaleShape">
                        <polygon points="0,0 -14,6 -2,10" fill="url(#scaleGrad)" stroke="rgba(0,0,0,0.4)" stroke-width="0.6" stroke-linejoin="round" />
                    </g>
                    <g id="scaleShapeActive">
                        <polygon points="0,0 -16,8 -3,12" fill="url(#scaleGradActive)" stroke="rgba(0,0,0,0.5)" stroke-width="0.8" stroke-linejoin="round" />
                    </g>
                    <!-- 滤波噪点 -->
                    <filter id="noiseFilter" x="0%" y="0%" width="100%" height="100%">
                        <feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="2" stitchTiles="stitch" />
                        <feColorMatrix type="saturate" values="0" />
                        <feBlend in="SourceGraphic" mode="multiply" result="noisy" />
                        <feComponentTransfer>
                            <feFuncA type="linear" slope="0.06" />
                        </feComponentTransfer>
                        <feBlend in="SourceGraphic" mode="normal" />
                    </filter>
                </defs>

                <!-- 背景 -->
                <rect x="0" y="0" width="1200" height="600" fill="url(#bgGrad)" />
                <rect x="0" y="0" width="1200" height="600" fill="rgba(0,0,0,0)" filter="url(#noiseFilter)" />

                <!-- 地面区域 -->
                <rect id="groundRect" x="0" y="415" width="1200" height="185" fill="url(#groundGrad)" />
                <rect x="0" y="415" width="1200" height="185" fill="url(#groundTexture)" opacity="0.7" />
                <!-- 地面线 -->
                <line id="groundLine" x1="0" y1="415" x2="1200" y2="415" stroke="#5c5045" stroke-width="2.5" />
                <line x1="0" y1="416.5" x2="1200" y2="416.5" stroke="rgba(255,255,255,0.04)" stroke-width="1" />

                <!-- 地面标尺标记 -->
                <g id="groundMarkers">
                    <!-- JS动态生成 -->
                </g>

                <!-- 蛇身模块组 -->
                <g id="snakeGroup">
                    <!-- JS动态生成12个模块 -->
                </g>

                <!-- 力矢量组 -->
                <g id="forceVectors">
                    <!-- JS动态更新 -->
                </g>

                <!-- 锚定光效组 -->
                <g id="anchorGlows">
                    <!-- JS动态更新 -->
                </g>

                <!-- CPG波传播指示 -->
                <g id="waveIndicator">
                    <!-- JS动态更新 -->
                </g>

                <!-- 信息叠加层 -->
                <g id="infoOverlay">
                    <text x="30" y="40" font-family="'Georgia','Noto Serif SC',serif" font-size="13" fill="rgba(255,255,255,0.55)" letter-spacing="0.06em">仿生机械蛇 — 侧视图</text>
                    <text x="30" y="62" font-family="'Segoe UI','PingFang SC',sans-serif" font-size="10" fill="rgba(255,255,255,0.3)" letter-spacing="0.04em">被动翻转鳞片 · 各向异性摩擦 · CPG弯曲波</text>
                </g>
            </svg>
        </div>

        <div class="controls-panel" id="controlsPanel">
            <div class="control-group">
                <label>弯曲频率 <span style="color:#e8a850;">(Hz)</span></label>
                <input type="range" id="freqSlider" min="1.0" max="4.5" step="0.1" value="2.5" />
                <span class="control-value" id="freqValue">2.5 Hz</span>
            </div>
            <div class="control-group">
                <label>波幅 <span style="color:#e8a850;">(°) </span></label>
                <input type="range" id="ampSlider" min="10" max="45" step="1" value="28" />
                <span class="control-value" id="ampValue">28°</span>
            </div>
            <div class="control-group">
                <label>鳞片倾角 <span style="color:#e8a850;">(°) </span></label>
                <input type="range" id="tiltSlider" min="8" max="22" step="0.5" value="15" />
                <span class="control-value" id="tiltValue">15°</span>
            </div>
            <button class="btn active" id="playPauseBtn" title="暂停/播放动画">⏯ 播放中</button>
            <button class="btn" id="highlightBtn" title="高亮锚定点">✦ 高亮锚定</button>
            <button class="btn" id="resetBtn" title="重置视角">↺ 重置</button>
        </div>

        <div class="legend-row">
            <span><span class="legend-dot anchor"></span> 高摩擦锚定点(鳞片咬合地面)</span>
            <span><span class="legend-dot slide"></span> 低摩擦滑移(鳞片顺向旋转)</span>
            <span><span class="legend-dot gold"></span> 被动翻转鳞片(核心创新)</span>
            <span style="color:#6ea8f0;">↝ CPG弯曲波传播方向</span>
        </div>
    </div>

    <script>
        (function() {
            // ==================== DOM元素 ====================
            const svg = document.getElementById('mainSvg');
            const snakeGroup = document.getElementById('snakeGroup');
            const forceVectorsGroup = document.getElementById('forceVectors');
            const anchorGlowsGroup = document.getElementById('anchorGlows');
            const waveIndicatorGroup = document.getElementById('waveIndicator');
            const groundMarkersGroup = document.getElementById('groundMarkers');
            const groundLine = document.getElementById('groundLine');
            const groundRect = document.getElementById('groundRect');
            const svgContainer = document.getElementById('svgContainer');

            const freqSlider = document.getElementById('freqSlider');
            const ampSlider = document.getElementById('ampSlider');
            const tiltSlider = document.getElementById('tiltSlider');
            const freqValueEl = document.getElementById('freqValue');
            const ampValueEl = document.getElementById('ampValue');
            const tiltValueEl = document.getElementById('tiltValue');
            const playPauseBtn = document.getElementById('playPauseBtn');
            const highlightBtn = document.getElementById('highlightBtn');
            const resetBtn = document.getElementById('resetBtn');

            // ==================== 参数 ====================
            const NUM_MODULES = 12;
            const MODULE_LENGTH = 52; // 模块主体长度 (SVG px)
            const MODULE_SPACING = 60; // 模块中心间距
            const MODULE_HEIGHT = 36; // 模块主体高度
            const MODULE_RADIUS = 8; // 圆角
            const GROUND_Y = 415; // 地面线Y坐标
            const SNAKE_BASE_Y = 305; // 蛇身基准Y坐标(在地面上方约110px)
            const SNAKE_START_X = 140; // 蛇身起始X坐标
            const SCALE_LENGTH = 13; // 鳞片长度

            let frequency = parseFloat(freqSlider.value); // Hz
            let waveAmplitudeDeg = parseFloat(ampSlider.value); // 关节最大摆角(度)
            let restingTiltDeg = parseFloat(tiltSlider.value); // 鳞片自由倾角(度)
            let isPlaying = true;
            let highlightAnchors = false;

            // 前进速度相关
            let totalForwardDisplacement = 0;
            const BASE_FORWARD_SPEED = 28; // 基础前进速度 (SVG px/s),随频率和波幅变化

            // ==================== 模块数据结构 ====================
            // 每个模块:{ centerX, centerY, angle(弧度,模块纵轴与水平夹角), scaleStates: [{tiltAngle, contactingGround, anchoring}] }
            const modules = [];
            for (let i = 0; i < NUM_MODULES; i++) {
                modules.push({
                    index: i,
                    centerX: SNAKE_START_X + i * MODULE_SPACING,
                    centerY: SNAKE_BASE_Y,
                    angle: 0, // 弧度
                    // 每个模块底部2个鳞片(前、后)
                    scaleStates: [
                        { tiltAngle: restingTiltDeg * Math.PI / 180, contactingGround: false, anchoring: false,
                            hingeX: 0, hingeY: 0 },
                        { tiltAngle: restingTiltDeg * Math.PI / 180, contactingGround: false, anchoring: false,
                            hingeX: 0, hingeY: 0 },
                    ],
                    prevCenterX: SNAKE_START_X + i * MODULE_SPACING,
                    prevCenterY: SNAKE_BASE_Y,
                });
            }

            // ==================== SVG元素缓存 ====================
            const moduleElements = []; // { group, bodyRect, highlightRect, scales: [{group, polygon, hingeCircle}], jointCircle }
            const scaleGlowElements = []; // 锚定光效元素

            function createModuleElements() {
                snakeGroup.innerHTML = '';
                moduleElements.length = 0;
                scaleGlowElements.length = 0;

                for (let i = 0; i < NUM_MODULES; i++) {
                    const modGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                    modGroup.setAttribute('data-module', i);

                    // 模块主体
                    const bodyRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
                    bodyRect.setAttribute('width', MODULE_LENGTH);
                    bodyRect.setAttribute('height', MODULE_HEIGHT);
                    bodyRect.setAttribute('rx', MODULE_RADIUS);
                    bodyRect.setAttribute('ry', MODULE_RADIUS);
                    bodyRect.setAttribute('fill', 'url(#snakeBodyGrad)');
                    bodyRect.setAttribute('stroke', 'rgba(0,0,0,0.45)');
                    bodyRect.setAttribute('stroke-width', '1.2');
                    bodyRect.setAttribute('x', -MODULE_LENGTH / 2);
                    bodyRect.setAttribute('y', -MODULE_HEIGHT / 2);
                    modGroup.appendChild(bodyRect);

                    // 高光条
                    const highlightRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
                    highlightRect.setAttribute('width', MODULE_LENGTH - 6);
                    highlightRect.setAttribute('height', MODULE_HEIGHT * 0.28);
                    highlightRect.setAttribute('rx', 3);
                    highlightRect.setAttribute('ry', 3);
                    highlightRect.setAttribute('fill', 'url(#snakeBodyHighlight)');
                    highlightRect.setAttribute('x', -(MODULE_LENGTH - 6) / 2);
                    highlightRect.setAttribute('y', -MODULE_HEIGHT / 2 + 3);
                    highlightRect.setAttribute('pointer-events', 'none');
                    modGroup.appendChild(highlightRect);

                    // 关节连接点(模块前端的小圆)
                    const jointCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    jointCircle.setAttribute('r', 4.5);
                    jointCircle.setAttribute('fill', '#3a3c48');
                    jointCircle.setAttribute('stroke', 'rgba(0,0,0,0.5)');
                    jointCircle.setAttribute('stroke-width', '1');
                    jointCircle.setAttribute('cx', -MODULE_LENGTH / 2);
                    jointCircle.setAttribute('cy', 0);
                    jointCircle.setAttribute('pointer-events', 'none');
                    modGroup.appendChild(jointCircle);

                    // 尾部连接点
                    const jointCircleRear = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    jointCircleRear.setAttribute('r', 4.5);
                    jointCircleRear.setAttribute('fill', '#3a3c48');
                    jointCircleRear.setAttribute('stroke', 'rgba(0,0,0,0.5)');
                    jointCircleRear.setAttribute('stroke-width', '1');
                    jointCircleRear.setAttribute('cx', MODULE_LENGTH / 2);
                    jointCircleRear.setAttribute('cy', 0);
                    jointCircleRear.setAttribute('pointer-events', 'none');
                    modGroup.appendChild(jointCircleRear);

                    // 鳞片组
                    const scaleGroups = [];
                    const scaleData = [];
                    // 每个模块底部2个鳞片
                    const scaleXOffsets = [-MODULE_LENGTH * 0.28, MODULE_LENGTH * 0.22]; // 前鳞片和后鳞片
                    for (let s = 0; s < 2; s++) {
                        const scaleG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                        const scalePolygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                        scalePolygon.setAttribute('fill', 'url(#scaleGrad)');
                        scalePolygon.setAttribute('stroke', 'rgba(0,0,0,0.5)');
                        scalePolygon.setAttribute('stroke-width', '0.7');
                        scalePolygon.setAttribute('stroke-linejoin', 'round');
                        scaleG.appendChild(scalePolygon);

                        // 铰链小圆
                        const hingeCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                        hingeCircle.setAttribute('r', 2.2);
                        hingeCircle.setAttribute('fill', '#6a5040');
                        hingeCircle.setAttribute('stroke', 'rgba(0,0,0,0.5)');
                        hingeCircle.setAttribute('stroke-width', '0.6');
                        hingeCircle.setAttribute('cx', scaleXOffsets[s]);
                        hingeCircle.setAttribute('cy', MODULE_HEIGHT / 2 - 1);
                        scaleG.appendChild(hingeCircle);

                        modGroup.appendChild(scaleG);
                        scaleGroups.push({ group: scaleG, polygon: scalePolygon, hingeCircle: hingeCircle,
                            xOffset: scaleXOffsets[s] });
                        scaleData.push({ tiltAngle: restingTiltDeg * Math.PI / 180, contactingGround: false,
                            anchoring: false, hingeX: 0, hingeY: 0 });
                    }

                    snakeGroup.appendChild(modGroup);
                    moduleElements.push({
                        group: modGroup,
                        bodyRect: bodyRect,
                        highlightRect: highlightRect,
                        scales: scaleGroups,
                        jointCircle: jointCircle,
                        jointCircleRear: jointCircleRear,
                    });
                }

                // 创建锚定光效元素
                anchorGlowsGroup.innerHTML = '';
                scaleGlowElements.length = 0;
                for (let i = 0; i < NUM_MODULES; i++) {
                    const moduleGlows = [];
                    for (let s = 0; s < 2; s++) {
                        const glowCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                        glowCircle.setAttribute('r', 16);
                        glowCircle.setAttribute('fill', 'url(#anchorGlow)');
                        glowCircle.setAttribute('opacity', '0');
                        glowCircle.setAttribute('pointer-events', 'none');
                        anchorGlowsGroup.appendChild(glowCircle);
                        moduleGlows.push(glowCircle);
                    }
                    scaleGlowElements.push(moduleGlows);
                }

                // 创建滑移光效
                for (let i = 0; i < NUM_MODULES; i++) {
                    for (let s = 0; s < 2; s++) {
                        const slideGlow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                        slideGlow.setAttribute('r', 10);
                        slideGlow.setAttribute('fill', 'url(#slideGlow)');
                        slideGlow.setAttribute('opacity', '0');
                        slideGlow.setAttribute('pointer-events', 'none');
                        slideGlow.setAttribute('data-type', 'slide');
                        anchorGlowsGroup.appendChild(slideGlow);
                        // 存储引用:追加到scaleGlowElements
                        if (!scaleGlowElements[i]._slideGlows) scaleGlowElements[i]._slideGlows = [];
                        scaleGlowElements[i]._slideGlows.push(slideGlow);
                    }
                }
            }

            function createGroundMarkers() {
                groundMarkersGroup.innerHTML = '';
                for (let x = 0; x < 1300; x += 100) {
                    const tickLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    tickLine.setAttribute('x1', x);
                    tickLine.setAttribute('y1', GROUND_Y);
                    tickLine.setAttribute('x2', x);
                    tickLine.setAttribute('y2', GROUND_Y + 8);
                    tickLine.setAttribute('stroke', 'rgba(255,255,255,0.2)');
                    tickLine.setAttribute('stroke-width', '1');
                    tickLine.setAttribute('data-base-x', x);
                    groundMarkersGroup.appendChild(tickLine);

                    if (x % 200 === 0) {
                        const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                        label.setAttribute('x', x);
                        label.setAttribute('y', GROUND_Y + 22);
                        label.setAttribute('text-anchor', 'middle');
                        label.setAttribute('font-size', '9');
                        label.setAttribute('fill', 'rgba(255,255,255,0.3)');
                        label.setAttribute('font-family', "'Segoe UI','PingFang SC',sans-serif");
                        label.setAttribute('data-base-x', x);
                        label.textContent = (x / 100).toFixed(0) + ' dm';
                        groundMarkersGroup.appendChild(label);
                    }
                }
            }

            // ==================== 初始化 ====================
            createModuleElements();
            createGroundMarkers();

            // ==================== 波传播计算 ====================
            function getWaveY(index, time, totalForward) {
                // 正弦弯曲波:身体在垂直方向波动
                // 波长在身体上约1.5个完整波长,身体总长720px,波长≈480px
                const bodyTotalLength = NUM_MODULES * MODULE_SPACING;
                const wavelength = bodyTotalLength / 1.5; // ≈480px
                const k = 2 * Math.PI / wavelength;
                const omega = 2 * Math.PI * frequency;

                // 模块在身体上的位置(从头部算起)
                const posAlongBody = index * MODULE_SPACING;
                // 波从头部向尾部传播:相位随身体位置增加而延迟
                const phase = k * posAlongBody - omega * time;

                // 波幅:将关节角度转换为垂直位移
                // 关节最大摆角 waveAmplitudeDeg,相邻模块间最大角度差
                const maxAngularDeflection = waveAmplitudeDeg * Math.PI / 180;
                // 垂直位移幅值 ≈ 模块间距 * sin(最大角度差)
                const verticalAmplitude = MODULE_SPACING * Math.sin(maxAngularDeflection) * 1.8;

                const waveY = verticalAmplitude * Math.sin(phase);
                return waveY;
            }

            function getModuleAngle(index, time, totalForward) {
                // 使用相邻模块位置差分计算切线角度
                const yCurr = getWaveY(index, time, totalForward);
                const yNext = (index < NUM_MODULES - 1) ? getWaveY(index + 1, time, totalForward) : yCurr;
                const yPrev = (index > 0) ? getWaveY(index - 1, time, totalForward) : yCurr;

                const dx = MODULE_SPACING;
                const dy = (yNext - yPrev) / 2;
                const angle = Math.atan2(dy, dx);
                return angle;
            }

            // ==================== 鳞片物理 ====================
            function updateScaleState(moduleIndex, time, totalForward) {
                const mod = modules[moduleIndex];
                const centerX = mod.centerX;
                const centerY = mod.centerY;
                const bodyAngle = mod.angle;

                // 模块底部在世界坐标中的位置
                const bottomY = centerY + (MODULE_HEIGHT / 2) * Math.cos(bodyAngle);

                // 模块水平速度(相对于地面)
                const dx_dt = (mod.centerX - mod.prevCenterX) / (1 / 60); // 近似速度
                const dy_dt = (mod.centerY - mod.prevCenterY) / (1 / 60);

                for (let s = 0; s < 2; s++) {
                    const scaleElem = moduleElements[moduleIndex].scales[s];
                    const xOffset = scaleElem.xOffset;

                    // 鳞片铰链在世界坐标中的位置
                    const hingeLocalX = xOffset;
                    const hingeLocalY = MODULE_HEIGHT / 2 - 1;
                    const hingeWorldX = centerX + hingeLocalX * Math.cos(bodyAngle) - hingeLocalY * Math.sin(bodyAngle);
                    const hingeWorldY = centerY + hingeLocalX * Math.sin(bodyAngle) + hingeLocalY * Math.cos(bodyAngle);

                    // 检测鳞片尖端是否接近或接触地面
                    const restingTilt = restingTiltDeg * Math.PI / 180;
                    const scaleTipLocalX = hingeLocalX - SCALE_LENGTH * Math.sin(restingTilt);
                    const scaleTipLocalY = hingeLocalY + SCALE_LENGTH * Math.cos(restingTilt);
                    const scaleTipWorldY = centerY + scaleTipLocalX * Math.sin(bodyAngle) + scaleTipLocalY * Math.cos(
                    bodyAngle);

                    const tipToGround = scaleTipWorldY - GROUND_Y;
                    const contacting = tipToGround >= -3; // 接触阈值

                    let targetTilt = restingTilt;
                    let anchoring = false;

                    if (contacting && tipToGround < 8) {
                        // 鳞片接触地面
                        // 判断身体局部运动方向
                        const localHorizVel = dx_dt; // 简化:身体水平速度
                        const downwardVel = dy_dt;

                        if (downwardVel > 2 || (localHorizVel < -3 && tipToGround < 4)) {
                            // 身体向下压或向后推 → 鳞片翻转角度增大 → 锚定
                            const maxTilt = restingTilt + (10 * Math.PI / 180); // 最大翻转+10°
                            const penetrationFactor = Math.min(1, Math.max(0, (4 - tipToGround) / 8));
                            targetTilt = restingTilt + penetrationFactor * (maxTilt - restingTilt);
                            anchoring = penetrationFactor > 0.35;
                        } else if (localHorizVel > 3) {
                            // 身体向前滑动 → 鳞片顺向旋转 → 角度减小
                            const minTilt = Math.max(0, restingTilt - (8 * Math.PI / 180));
                            targetTilt = minTilt;
                            anchoring = false;
                        }
                    }

                    // 平滑过渡
                    const currentTilt = mod.scaleStates[s].tiltAngle;
                    const smoothFactor = 0.35;
                    const newTilt = currentTilt + (targetTilt - currentTilt) * smoothFactor;

                    mod.scaleStates[s].tiltAngle = newTilt;
                    mod.scaleStates[s].contactingGround = contacting;
                    mod.scaleStates[s].anchoring = anchoring;
                    mod.scaleStates[s].hingeX = hingeWorldX;
                    mod.scaleStates[s].hingeY = hingeWorldY;
                }
            }

            // ==================== 渲染 ====================
            function updateScalePolygon(polygon, hingeLocalX, hingeLocalY, tiltAngle, bodyAngle, centerX, centerY) {
                // 鳞片在局部坐标中:铰链在(hingeLocalX, hingeLocalY),尖端向后下方
                // tiltAngle是鳞片与身体纵轴(水平)的夹角
                const tipLocalX = hingeLocalX - SCALE_LENGTH * Math.sin(tiltAngle);
                const tipLocalY = hingeLocalY + SCALE_LENGTH * Math.cos(tiltAngle);
                const baseRearX = hingeLocalX - 2;
                const baseRearY = hingeLocalY + 3;
                const baseFrontX = hingeLocalX + 2;
                const baseFrontY = hingeLocalY + 2;

                const toWorld = (lx, ly) => {
                    const wx = centerX + lx * Math.cos(bodyAngle) - ly * Math.sin(bodyAngle);
                    const wy = centerY + lx * Math.sin(bodyAngle) + ly * Math.cos(bodyAngle);
                    return { x: wx, y: wy };
                };

                const tip = toWorld(tipLocalX, tipLocalY);
                const br = toWorld(baseRearX, baseRearY);
                const bf = toWorld(baseFrontX, baseFrontY);
                const hinge = toWorld(hingeLocalX, hingeLocalY);

                polygon.setAttribute('points', `${hinge.x},${hinge.y} ${tip.x},${tip.y} ${br.x},${br.y} ${bf.x},${bf.y}`);
                return { hinge, tip };
            }

            function renderFrame(time) {
                const effectiveTime = time;

                // 更新前进位移
                if (isPlaying) {
                    const speedMultiplier = (frequency / 2.5) * (waveAmplitudeDeg / 28);
                    totalForwardDisplacement += BASE_FORWARD_SPEED * speedMultiplier * (1 / 60);
                }

                // 更新地面标尺(向后移动模拟前进)
                const groundOffset = totalForwardDisplacement % 200;
                const groundMarkerChildren = groundMarkersGroup.children;
                for (let i = 0; i < groundMarkerChildren.length; i++) {
                    const el = groundMarkerChildren[i];
                    const baseX = parseFloat(el.getAttribute('data-base-x') || '0');
                    let displayX = baseX - groundOffset;
                    if (displayX < -20) displayX += 200;
                    if (displayX > 1220) displayX -= 200;
                    if (el.tagName === 'line') {
                        el.setAttribute('x1', displayX);
                        el.setAttribute('x2', displayX);
                    } else if (el.tagName === 'text') {
                        el.setAttribute('x', displayX);
                    }
                }

                // 更新每个模块
                for (let i = 0; i < NUM_MODULES; i++) {
                    const mod = modules[i];
                    const waveY = getWaveY(i, effectiveTime, totalForwardDisplacement);
                    const bodyAngle = getModuleAngle(i, effectiveTime, totalForwardDisplacement);

                    mod.prevCenterX = mod.centerX;
                    mod.prevCenterY = mod.centerY;
                    mod.centerX = SNAKE_START_X + i * MODULE_SPACING + totalForwardDisplacement;
                    mod.centerY = SNAKE_BASE_Y + waveY;
                    mod.angle = bodyAngle;
                }

                // 更新鳞片状态
                for (let i = 0; i < NUM_MODULES; i++) {
                    updateScaleState(i, effectiveTime, totalForwardDisplacement);
                }

                // 渲染模块
                for (let i = 0; i < NUM_MODULES; i++) {
                    const mod = modules[i];
                    const elem = moduleElements[i];
                    const cx = mod.centerX;
                    const cy = mod.centerY;
                    const angleDeg = mod.angle * 180 / Math.PI;

                    elem.group.setAttribute('transform', `translate(${cx},${cy}) rotate(${angleDeg})`);

                    // 渲染鳞片
                    for (let s = 0; s < 2; s++) {
                        const scaleElem = elem.scales[s];
                        const scaleState = mod.scaleStates[s];
                        const xOff = scaleElem.xOffset;
                        const hingeLocalY = MODULE_HEIGHT / 2 - 1;

                        const result = updateScalePolygon(
                            scaleElem.polygon,
                            xOff,
                            hingeLocalY,
                            scaleState.tiltAngle,
                            mod.angle,
                            cx,
                            cy
                        );

                        // 更新铰链圆位置
                        scaleElem.hingeCircle.setAttribute('cx', xOff);
                        scaleElem.hingeCircle.setAttribute('cy', hingeLocalY);

                        // 鳞片颜色
                        if (scaleState.anchoring) {
                            scaleElem.polygon.setAttribute('fill', 'url(#scaleGradActive)');
                            scaleElem.polygon.setAttribute('stroke', 'rgba(255,60,20,0.8)');
                            scaleElem.polygon.setAttribute('stroke-width', '1.2');
                            scaleElem.hingeCircle.setAttribute('fill', '#ff6b3a');
                        } else if (scaleState.contactingGround) {
                            scaleElem.polygon.setAttribute('fill', 'url(#scaleGrad)');
                            scaleElem.polygon.setAttribute('stroke', 'rgba(200,150,100,0.6)');
                            scaleElem.polygon.setAttribute('stroke-width', '0.8');
                            scaleElem.hingeCircle.setAttribute('fill', '#8a6050');
                        } else {
                            scaleElem.polygon.setAttribute('fill', 'url(#scaleGrad)');
                            scaleElem.polygon.setAttribute('stroke', 'rgba(0,0,0,0.5)');
                            scaleElem.polygon.setAttribute('stroke-width', '0.7');
                            scaleElem.hingeCircle.setAttribute('fill', '#6a5040');
                        }

                        // 更新光效
                        const glowEl = scaleGlowElements[i][s];
                        const slideGlowEl = (scaleGlowElements[i]._slideGlows && scaleGlowElements[i]._slideGlows[s]) ?
                            scaleGlowElements[i]._slideGlows[s] : null;

                        const hingeWorldX = scaleState.hingeX;
                        const hingeWorldY = scaleState.hingeY;

                        if (glowEl) {
                            glowEl.setAttribute('cx', hingeWorldX);
                            glowEl.setAttribute('cy', hingeWorldY);
                            if (scaleState.anchoring && (highlightAnchors || scaleState.anchoring)) {
                                glowEl.setAttribute('opacity',
                                    String(0.55 + 0.45 * Math.sin(effectiveTime * 18 + i * 1.5 + s)));
                                glowEl.setAttribute('r', String(14 + 6 * Math.sin(effectiveTime * 22 + i * 1.2)));
                            } else {
                                glowEl.setAttribute('opacity', '0');
                            }
                        }
                        if (slideGlowEl) {
                            slideGlowEl.setAttribute('cx', hingeWorldX);
                            slideGlowEl.setAttribute('cy', hingeWorldY);
                            if (scaleState.contactingGround && !scaleState.anchoring) {
                                slideGlowEl.setAttribute('opacity', '0.4');
                            } else {
                                slideGlowEl.setAttribute('opacity', '0');
                            }
                        }
                    }
                }

                // 更新力矢量
                updateForceVectors(effectiveTime);

                // 更新波动指示器
                updateWaveIndicator(effectiveTime);
            }

            function updateForceVectors(time) {
                forceVectorsGroup.innerHTML = '';
                const activeAnchors = [];

                for (let i = 0; i < NUM_MODULES; i++) {
                    const mod = modules[i];
                    for (let s = 0; s < 2; s++) {
                        if (mod.scaleStates[s].anchoring) {
                            activeAnchors.push({
                                x: mod.scaleStates[s].hingeX,
                                y: mod.scaleStates[s].hingeY,
                                index: i,
                                scaleIndex: s,
                            });
                        }
                    }
                }

                // 为锚定点绘制推力方向箭头(向前,即蛇身前进方向)
                activeAnchors.forEach(anchor => {
                    const arrowGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');

                    // 推力箭头(水平向前)
                    const arrowLen = 22 + 8 * Math.sin(time * 15 + anchor.index);
                    const startX = anchor.x;
                    const startY = anchor.y - 6;
                    const endX = startX + arrowLen;
                    const endY = startY;

                    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    line.setAttribute('x1', startX);
                    line.setAttribute('y1', startY);
                    line.setAttribute('x2', endX);
                    line.setAttribute('y2', endY);
                    line.setAttribute('stroke', '#f0c060');
                    line.setAttribute('stroke-width', '2.2');
                    line.setAttribute('stroke-linecap', 'round');
                    line.setAttribute('opacity', '0.8');
                    arrowGroup.appendChild(line);

                    // 箭头尖端
                    const arrowTip = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                    arrowTip.setAttribute('points',
                        `${endX},${endY} ${endX-7},${endY-4.5} ${endX-7},${endY+4.5}`);
                    arrowTip.setAttribute('fill', '#f0c060');
                    arrowTip.setAttribute('opacity', '0.85');
                    arrowGroup.appendChild(arrowTip);

                    // 小标签
                    const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                    label.setAttribute('x', endX + 6);
                    label.setAttribute('y', endY + 3);
                    label.setAttribute('font-size', '8');
                    label.setAttribute('fill', '#f0c060');
                    label.setAttribute('font-family', "'Segoe UI','PingFang SC',sans-serif");
                    label.setAttribute('opacity', '0.75');
                    label.textContent = '推力→';
                    arrowGroup.appendChild(label);

                    forceVectorsGroup.appendChild(arrowGroup);
                });

                // 如果有锚定点,在蛇身下方显示合力方向
                if (activeAnchors.length > 0 && activeAnchors.length >= 2) {
                    const avgX = activeAnchors.reduce((s, a) => s + a.x, 0) / activeAnchors.length;
                    const avgY = activeAnchors.reduce((s, a) => s + a.y, 0) / activeAnchors.length;

                    const netForceGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                    const netArrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    netArrow.setAttribute('x1', avgX - 10);
                    netArrow.setAttribute('y1', avgY + 28);
                    netArrow.setAttribute('x2', avgX + 40);
                    netArrow.setAttribute('y2', avgY + 28);
                    netArrow.setAttribute('stroke', '#ffaa40');
                    netArrow.setAttribute('stroke-width', '3');
                    netArrow.setAttribute('stroke-linecap', 'round');
                    netArrow.setAttribute('stroke-dasharray', '6,3');
                    netArrow.setAttribute('opacity', '0.7');
                    netForceGroup.appendChild(netArrow);

                    const netLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                    netLabel.setAttribute('x', avgX + 44);
                    netLabel.setAttribute('y', avgY + 31);
                    netLabel.setAttribute('font-size', '9');
                    netLabel.setAttribute('fill', '#ffaa40');
                    netLabel.setAttribute('font-family', "'Segoe UI','PingFang SC',sans-serif");
                    netLabel.setAttribute('font-weight', '600');
                    netLabel.textContent = '净推力';
                    netForceGroup.appendChild(netLabel);

                    forceVectorsGroup.appendChild(netForceGroup);
                }
            }

            function updateWaveIndicator(time) {
                waveIndicatorGroup.innerHTML = '';

                // 绘制CPG波传播方向(从头部向尾部)
                const headMod = modules[0];
                const tailMod = modules[NUM_MODULES - 1];
                const waveY_top = SNAKE_BASE_Y - 80;

                // 波浪箭头
                const wavePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                const startX = headMod.centerX;
                const endX = tailMod.centerX;
                const midX = (startX + endX) / 2;
                const wavePathStr =
                    `M${startX},${waveY_top} Q${startX+60},${waveY_top-18} ${midX-30},${waveY_top} Q${midX},${waveY_top+18} ${midX+30},${waveY_top} Q${endX-60},${waveY_top-18} ${endX},${waveY_top}`;
                wavePath.setAttribute('d', wavePathStr);
                wavePath.setAttribute('fill', 'none');
                wavePath.setAttribute('stroke', 'rgba(110,168,240,0.45)');
                wavePath.setAttribute('stroke-width', '2');
                wavePath.setAttribute('stroke-dasharray', '8,4');
                wavePath.setAttribute('stroke-linecap', 'round');
                waveIndicatorGroup.appendChild(wavePath);

                // 箭头标记
                const arrowMarker = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                arrowMarker.setAttribute('points', `${endX+6},${waveY_top} ${endX-8},${waveY_top-7} ${endX-8},${waveY_top+7}`);
                arrowMarker.setAttribute('fill', 'rgba(110,168,240,0.55)');
                waveIndicatorGroup.appendChild(arrowMarker);

                // 标签
                const waveLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                waveLabel.setAttribute('x', midX);
                waveLabel.setAttribute('y', waveY_top - 22);
                waveLabel.setAttribute('text-anchor', 'middle');
                waveLabel.setAttribute('font-size', '9');
                waveLabel.setAttribute('fill', 'rgba(110,168,240,0.6)');
                waveLabel.setAttribute('font-family', "'Segoe UI','PingFang SC',sans-serif");
                waveLabel.setAttribute('letter-spacing', '0.05em');
                waveLabel.textContent = `CPG弯曲波 · ${frequency.toFixed(1)}Hz · 波速0.5m/s`;
                waveIndicatorGroup.appendChild(waveLabel);
            }

            // ==================== 动画循环 ====================
            let animationId;
            let lastFrameTime = performance.now();
            let simTime = 0;

            function animate(timestamp) {
                if (!isPlaying) {
                    // 暂停时只渲染静态帧
                    animationId = requestAnimationFrame(animate);
                    return;
                }

                const dt = Math.min((timestamp - lastFrameTime) / 1000, 0.1); // 限制最大步长
                lastFrameTime = timestamp;
                simTime += dt;

                renderFrame(simTime);

                animationId = requestAnimationFrame(animate);
            }

            function renderStaticFrame() {
                renderFrame(simTime);
            }

            // ==================== 事件处理 ====================
            freqSlider.addEventListener('input', () => {
                frequency = parseFloat(freqSlider.value);
                freqValueEl.textContent = frequency.toFixed(1) + ' Hz';
            });

            ampSlider.addEventListener('input', () => {
                waveAmplitudeDeg = parseFloat(ampSlider.value);
                ampValueEl.textContent = Math.round(waveAmplitudeDeg) + '°';
            });

            tiltSlider.addEventListener('input', () => {
                restingTiltDeg = parseFloat(tiltSlider.value);
                tiltValueEl.textContent = restingTiltDeg.toFixed(1) + '°';
            });

            playPauseBtn.addEventListener('click', () => {
                isPlaying = !isPlaying;
                if (isPlaying) {
                    playPauseBtn.textContent = '⏯ 播放中';
                    playPauseBtn.classList.add('active');
                    lastFrameTime = performance.now();
                } else {
                    playPauseBtn.textContent = '▶ 已暂停';
                    playPauseBtn.classList.remove('active');
                    renderStaticFrame();
                }
            });

            highlightBtn.addEventListener('click', () => {
                highlightAnchors = !highlightAnchors;
                if (highlightAnchors) {
                    highlightBtn.classList.add('active');
                    highlightBtn.textContent = '✦ 高亮中';
                } else {
                    highlightBtn.classList.remove('active');
                    highlightBtn.textContent = '✦ 高亮锚定';
                }
            });

            resetBtn.addEventListener('click', () => {
                totalForwardDisplacement = 0;
                simTime = 0;
                frequency = 2.5;
                waveAmplitudeDeg = 28;
                restingTiltDeg = 15;
                freqSlider.value = 2.5;
                ampSlider.value = 28;
                tiltSlider.value = 15;
                freqValueEl.textContent = '2.5 Hz';
                ampValueEl.textContent = '28°';
                tiltValueEl.textContent = '15°';
                highlightAnchors = false;
                highlightBtn.classList.remove('active');
                highlightBtn.textContent = '✦ 高亮锚定';
                for (let i = 0; i < NUM_MODULES; i++) {
                    modules[i].centerX = SNAKE_START_X + i * MODULE_SPACING;
                    modules[i].centerY = SNAKE_BASE_Y;
                    modules[i].angle = 0;
                    modules[i].prevCenterX = modules[i].centerX;
                    modules[i].prevCenterY = modules[i].centerY;
                    for (let s = 0; s < 2; s++) {
                        modules[i].scaleStates[s].tiltAngle = restingTiltDeg * Math.PI / 180;
                        modules[i].scaleStates[s].contactingGround = false;
                        modules[i].scaleStates[s].anchoring = false;
                    }
                }
                createGroundMarkers();
                renderStaticFrame();
                lastFrameTime = performance.now();
            });

            // 键盘控制
            document.addEventListener('keydown', (e) => {
                switch (e.key.toLowerCase()) {
                    case ' ':
                        e.preventDefault();
                        playPauseBtn.click();
                        break;
                    case 'h':
                        highlightBtn.click();
                        break;
                    case 'r':
                        if (!e.ctrlKey && !e.metaKey) {
                            resetBtn.click();
                        }
                        break;
                    case 'arrowup':
                        e.preventDefault();
                        frequency = Math.min(4.5, frequency + 0.2);
                        freqSlider.value = frequency;
                        freqValueEl.textContent = frequency.toFixed(1) + ' Hz';
                        break;
                    case 'arrowdown':
                        e.preventDefault();
                        frequency = Math.max(1.0, frequency - 0.2);
                        freqSlider.value = frequency;
                        freqValueEl.textContent = frequency.toFixed(1) + ' Hz';
                        break;
                    case 'arrowleft':
                        e.preventDefault();
                        waveAmplitudeDeg = Math.max(10, waveAmplitudeDeg - 2);
                        ampSlider.value = waveAmplitudeDeg;
                        ampValueEl.textContent = Math.round(waveAmplitudeDeg) + '°';
                        break;
                    case 'arrowright':
                        e.preventDefault();
                        waveAmplitudeDeg = Math.min(45, waveAmplitudeDeg + 2);
                        ampSlider.value = waveAmplitudeDeg;
                        ampValueEl.textContent = Math.round(waveAmplitudeDeg) + '°';
                        break;
                }
            });

            // ==================== 窗口大小调整 ====================
            function handleResize() {
                // SVG viewBox确保缩放,无需额外处理
                renderStaticFrame();
            }
            window.addEventListener('resize', handleResize);

            // ==================== 启动 ====================
            function init() {
                createGroundMarkers();
                renderStaticFrame();
                lastFrameTime = performance.now();
                animationId = requestAnimationFrame(animate);
            }

            // 初始渲染一帧
            simTime = 0.5; // 给一个初始时间偏移,让蛇身有初始弯曲
            renderFrame(simTime);
            lastFrameTime = performance.now();
            animationId = requestAnimationFrame(animate);

            console.log('🐍 仿生机械蛇 IFR 原理动画已就绪');
            console.log('   · 被动翻转鳞片 — 各向异性摩擦 — 零主动控制');
            console.log('   · 空格键:播放/暂停 | H键:高亮锚定点 | R键:重置');
            console.log('   · 方向键:调节频率(↑↓)和波幅(←→)');
            console.log('   · 拖动滑块可手动控制关键变量');
        })();
    </script>
</body>
</html>

动画展示

这个动画直观展示了机械蛇如何在不增加硬件与算法复杂度的情况下,利用弯曲动作推动身体前进。

核心机制展示:动画聚焦于“被动鳞片-各向异性摩擦-前进推力”的转化过程。蛇身底部带有可翻转的金色鳞片,在接触地面时,鳞片会像船桨一样“锚定”并产生推力(红色光晕),而在离开地面时则顺向滑移(青色光晕)。这正是解决矛盾的关键:利用身体弯曲时产生的侧向力,通过被动鳞片结构差异化为“高摩擦力”(锚定)和“低摩擦力”(滑移),从而将横向推地力转化为纵向推力,无需主动控制鳞片翻转。

操作与反馈:您可以通过底部的控制面板或键盘(空格键暂停/播放,方向键调节频率和波幅)实时改变蛇的弯曲频率波幅鳞片初始倾角。动画会立即响应,展示不同参数下推力生成的变化,让您深入理解理想解的动态原理。

积分规则:第一轮对话扣减6分,后续每轮扣4分