分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>蛇形机器人 - 纵向摩擦推进原理</title>
    <style>
        :root {
            --bg-color: #05070a;
            --grid-color: #121a2f;
            --chassis-fill: rgba(10, 20, 35, 0.8);
            --chassis-stroke: #00f3ff;
            --joint-color: #ffffff;
            --runner-color: #ffaa00;
            --force-forward: #00ff66;
            --force-lateral: #ff0055;
            --text-main: #c7d2fe;
            --text-accent: #00f3ff;
            --hud-bg: rgba(5, 7, 10, 0.65);
            --hud-border: rgba(0, 243, 255, 0.15);
        }

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

        body, html {
            width: 100%;
            height: 100%;
            background-color: var(--bg-color);
            font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, sans-serif;
            overflow: hidden;
            color: var(--text-main);
        }

        #app-container {
            position: relative;
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 50px 50px;
            background-position: 0 0;
        }

        /* 核心动画区域 */
        svg {
            width: 100%;
            height: 100%;
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 10;
            filter: drop-shadow(0 0 20px rgba(0, 243, 255, 0.1));
        }

        /* UI 面板 - 角落布局,避免遮挡 */
        .hud-panel {
            position: absolute;
            background: var(--hud-bg);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            border: 1px solid var(--hud-border);
            border-radius: 8px;
            padding: 16px;
            z-index: 20;
            pointer-events: none; /* 让鼠标穿透,不影响潜在的拖拽交互 */
        }

        .hud-top-left {
            top: 24px;
            left: 24px;
            width: 280px;
        }

        .hud-bottom-right {
            bottom: 24px;
            right: 24px;
            pointer-events: auto; /* 允许操作滑块 */
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .hud-title {
            font-size: 14px;
            font-weight: 600;
            color: var(--text-accent);
            letter-spacing: 1px;
            margin-bottom: 8px;
            text-transform: uppercase;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .hud-title::before {
            content: '';
            display: block;
            width: 6px;
            height: 6px;
            background: var(--text-accent);
            border-radius: 50%;
            box-shadow: 0 0 8px var(--text-accent);
        }

        .hud-text {
            font-size: 12px;
            line-height: 1.6;
            color: rgba(199, 210, 254, 0.8);
            margin-bottom: 8px;
        }

        .data-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 12px;
            margin-top: 4px;
            font-family: 'JetBrains Mono', 'Courier New', monospace;
        }

        .data-label { color: rgba(199, 210, 254, 0.6); }
        .data-value { color: #fff; font-weight: 500; }

        /* 图例 */
        .legend {
            display: flex;
            flex-direction: column;
            gap: 6px;
            margin-top: 12px;
            padding-top: 12px;
            border-top: 1px solid rgba(255,255,255,0.05);
        }
        
        .legend-item {
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 11px;
        }

        .color-box {
            width: 12px;
            height: 4px;
            border-radius: 2px;
        }

        /* 控件 */
        .control-group {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        .control-header {
            display: flex;
            justify-content: space-between;
            font-size: 11px;
            color: var(--text-accent);
        }

        input[type=range] {
            -webkit-appearance: none;
            width: 200px;
            background: transparent;
        }

        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            height: 12px;
            width: 12px;
            border-radius: 50%;
            background: var(--text-accent);
            cursor: pointer;
            margin-top: -5px;
            box-shadow: 0 0 10px rgba(0, 243, 255, 0.5);
        }

        input[type=range]::-webkit-slider-runnable-track {
            width: 100%;
            height: 2px;
            cursor: pointer;
            background: rgba(0, 243, 255, 0.2);
            border-radius: 1px;
        }

        /* SVG 滤镜和动画类 */
        .glow-effect { filter: url(#neon-glow); }
        
        @keyframes pulse {
            0% { opacity: 0.6; }
            50% { opacity: 1; }
            100% { opacity: 0.6; }
        }
        .pulsing { animation: pulse 1.5s infinite ease-in-out; }
    </style>
</head>
<body>

<div id="app-container">
    
    <svg id="canvas" viewBox="-500 -300 1000 600" preserveAspectRatio="xMidYMid meet">
        <defs>
            <!-- 发光滤镜 -->
            <filter id="neon-glow" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>
            
            <filter id="force-glow" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur stdDeviation="4" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>

            <!-- 箭头标记 -->
            <marker id="arrow-forward" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
                <path d="M 0 2 L 10 5 L 0 8 z" fill="var(--force-forward)" />
            </marker>
            <marker id="arrow-lateral" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
                <path d="M 0 2 L 10 5 L 0 8 z" fill="var(--force-lateral)" />
            </marker>
            
            <!-- 摩擦力阻挡标记 (X形) -->
            <g id="friction-block">
                <line x1="-4" y1="-4" x2="4" y2="4" stroke="var(--force-lateral)" stroke-width="2" stroke-linecap="round"/>
                <line x1="4" y1="-4" x2="-4" y2="4" stroke="var(--force-lateral)" stroke-width="2" stroke-linecap="round"/>
            </g>
        </defs>

        <!-- 历史轨迹线 -->
        <path id="trajectory" fill="none" stroke="rgba(0, 243, 255, 0.15)" stroke-width="2" stroke-dasharray="4 4" />

        <!-- 机器蛇主干容器 -->
        <g id="snake-system"></g>
    </svg>

    <!-- 信息面板 -->
    <div class="hud-panel hud-top-left">
        <div class="hud-title">Lateral Undulation System</div>
        <div class="hud-text">
            最终理想解 (IFR):利用具有方向选择性的被动接触件,将电机产生的横向摆动势能,通过“横向锁死、纵向滑动”的机制,100%转化为单向推进力。
        </div>
        <div class="legend">
            <div class="legend-item">
                <div class="color-box" style="background: var(--runner-color); box-shadow: 0 0 5px var(--runner-color);"></div>
                <span>被动滑片/直排轮 (极低纵向阻力)</span>
            </div>
            <div class="legend-item">
                <div class="color-box" style="background: var(--force-lateral); box-shadow: 0 0 5px var(--force-lateral);"></div>
                <span>横向推力被地面摩擦力完全抵消</span>
            </div>
            <div class="legend-item">
                <div class="color-box" style="background: var(--force-forward); box-shadow: 0 0 5px var(--force-forward);"></div>
                <span>沿身体轴向的有效净推进力</span>
            </div>
        </div>
    </div>

    <!-- 控制面板与实时数据 -->
    <div class="hud-panel hud-bottom-right">
        <div class="hud-title">Telemetries</div>
        <div class="data-row">
            <span class="data-label">Forward Velocity</span>
            <span class="data-value" id="val-vel">0.00 m/s</span>
        </div>
        <div class="data-row">
            <span class="data-label">Phase Difference</span>
            <span class="data-value" id="val-phase">40.0°</span>
        </div>
        <div class="data-row" style="margin-bottom: 8px;">
            <span class="data-label">Friction Ratio (μ_x / μ_y)</span>
            <span class="data-value">0.05</span>
        </div>
        
        <div class="control-group">
            <div class="control-header">
                <span>Wave Frequency</span>
                <span id="lbl-freq">1.2 Hz</span>
            </div>
            <input type="range" id="ctrl-freq" min="0.5" max="3" step="0.1" value="1.5">
        </div>
    </div>

</div>

<script>
/**
 * 核心逻辑:基于Serpenoid曲线的运动学模拟
 * 为了视觉稳定性,采用“跑步机”模式:蛇身在屏幕中央游动,背景网格向后移动以体现位移。
 */

const N_SEGMENTS = 14;      // 舱段数量
const SEG_LENGTH = 36;      // 单个舱段长度
const SEG_WIDTH = 18;       // 单个舱段宽度
let AMPLITUDE = 75;         // 波动幅度
let WAVELENGTH = 350;       // 波长
let WAVE_SPEED = 1.5;       // 波传播速度 (受滑块控制)

const snakeSystem = document.getElementById('snake-system');
const trajectoryPath = document.getElementById('trajectory');
const appContainer = document.getElementById('app-container');

// UI 元素
const valVel = document.getElementById('val-vel');
const ctrlFreq = document.getElementById('ctrl-freq');
const lblFreq = document.getElementById('lbl-freq');

let time = 0;
let globalDistanceX = 0; // 累计前进距离
let segments = [];
let forceArrows = [];

// 初始化 DOM 结构
function initSnake() {
    // 创建尾迹路径,横跨整个画布
    let pathD = `M -600 0 L 600 0`;
    trajectoryPath.setAttribute('d', pathD);

    for (let i = 0; i < N_SEGMENTS; i++) {
        const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
        g.classList.add('glow-effect');

        // 1. 被动滑片/直排轮 (底盘两侧)
        const runner1 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
        runner1.setAttribute("x", -SEG_LENGTH/2 + 4);
        runner1.setAttribute("y", -SEG_WIDTH/2 - 4);
        runner1.setAttribute("width", SEG_LENGTH - 8);
        runner1.setAttribute("height", 3);
        runner1.setAttribute("fill", "var(--runner-color)");
        runner1.setAttribute("rx", "1.5");
        
        const runner2 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
        runner2.setAttribute("x", -SEG_LENGTH/2 + 4);
        runner2.setAttribute("y", SEG_WIDTH/2 + 1);
        runner2.setAttribute("width", SEG_LENGTH - 8);
        runner2.setAttribute("height", 3);
        runner2.setAttribute("fill", "var(--runner-color)");
        runner2.setAttribute("rx", "1.5");

        // 2. 刚性舱段主体
        const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
        rect.setAttribute("x", -SEG_LENGTH/2);
        rect.setAttribute("y", -SEG_WIDTH/2);
        rect.setAttribute("width", SEG_LENGTH);
        rect.setAttribute("height", SEG_WIDTH);
        rect.setAttribute("rx", "4");
        rect.setAttribute("fill", "var(--chassis-fill)");
        rect.setAttribute("stroke", "var(--chassis-stroke)");
        rect.setAttribute("stroke-width", "1.5");

        // 3. 关节 (除了最后一个)
        const joint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        joint.setAttribute("cx", SEG_LENGTH/2);
        joint.setAttribute("cy", 0);
        joint.setAttribute("r", 4);
        joint.setAttribute("fill", "var(--joint-color)");
        
        g.appendChild(runner1);
        g.appendChild(runner2);
        g.appendChild(rect);
        if (i < N_SEGMENTS - 1) g.appendChild(joint);

        // 4. 力学矢量分析组 (仅在关键分段展示)
        if (i === 3 || i === 7 || i === 11) {
            const forceGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
            forceGroup.setAttribute("style", "filter: url(#force-glow);");
            
            // 纵向推进力 (绿色)
            const forwardForce = document.createElementNS("http://www.w3.org/2000/svg", "line");
            forwardForce.setAttribute("x1", 0);
            forwardForce.setAttribute("y1", 0);
            forwardForce.setAttribute("x2", 40);
            forwardForce.setAttribute("y2", 0);
            forwardForce.setAttribute("stroke", "var(--force-forward)");
            forwardForce.setAttribute("stroke-width", "2.5");
            forwardForce.setAttribute("marker-end", "url(#arrow-forward)");
            
            // 横向抵消力 (红色)
            const lateralForce = document.createElementNS("http://www.w3.org/2000/svg", "line");
            lateralForce.setAttribute("x1", 0);
            lateralForce.setAttribute("y1", 0);
            lateralForce.setAttribute("x2", 0);
            lateralForce.setAttribute("y2", 40);
            lateralForce.setAttribute("stroke", "var(--force-lateral)");
            lateralForce.setAttribute("stroke-width", "2");
            lateralForce.setAttribute("marker-end", "url(#arrow-lateral)");
            lateralForce.setAttribute("stroke-dasharray", "4 2"); // 虚线表示被锁死

            // 锁死标记
            const blockMark = document.createElementNS("http://www.w3.org/2000/svg", "use");
            blockMark.setAttribute("href", "#friction-block");
            
            forceGroup.appendChild(lateralForce);
            forceGroup.appendChild(blockMark);
            forceGroup.appendChild(forwardForce);
            
            g.appendChild(forceGroup);
            
            forceArrows.push({
                group: forceGroup,
                forward: forwardForce,
                lateral: lateralForce,
                block: blockMark,
                segmentIndex: i
            });
        }

        snakeSystem.appendChild(g);
        segments.push({ element: g });
    }
}

// 物理与运动更新循环
function animate() {
    time += 0.016 * WAVE_SPEED; // 模拟 delta time

    const k = 2 * Math.PI / WAVELENGTH;
    const omega = 2 * Math.PI; // 基准角频率
    
    // 计算每个段的位置和角度。采用拟合正弦曲线的方式实现平滑动画。
    // 在真实物理中是由关节角度推导全局坐标,这里为了表现“理想解”的宏观形态,由曲线映射坐标。
    
    // 假设头部在 X=150 的位置,身体向左延伸
    const headX = 150;
    
    let pathD = `M -600 ${AMPLITUDE * Math.sin(k * (-600 - globalDistanceX) - omega * time)}`;
    for (let px = -590; px <= 600; px+=10) {
        let py = AMPLITUDE * Math.sin(k * (px - globalDistanceX) - omega * time);
        pathD += ` L ${px} ${py}`;
    }
    trajectoryPath.setAttribute('d', pathD);

    for (let i = 0; i < N_SEGMENTS; i++) {
        // 计算沿着曲线的近似弧长积分位置 (简化版:直接按X轴等距分布)
        // 实际上为了保持刚体长度,应迭代计算。此处用精细近似法。
        const arcPos = headX - i * SEG_LENGTH; 
        
        // 运动学方程: y = A * sin(k*x - omega*t)
        // 由于是向前移动,相位的传播使得身体呈现波动。
        const phase = k * arcPos - omega * time;
        const x = arcPos;
        const y = AMPLITUDE * Math.sin(phase);
        
        // 导数求切线角度
        const dy_dx = AMPLITUDE * k * Math.cos(phase);
        const theta = Math.atan(dy_dx);
        const thetaDeg = theta * (180 / Math.PI);

        // 应用变换
        segments[i].element.setAttribute("transform", `translate(${x}, ${y}) rotate(${thetaDeg})`);
        segments[i].theta = theta;
        segments[i].phase = phase;
    }

    // 更新受力分析箭头的动态表现
    // IFR核心:横向摆动速度最大处(跨越中轴线时),横向摩擦力发挥最大锁定作用;
    // 波峰/波谷处,纵向推进力最大。
    forceArrows.forEach(fa => {
        const seg = segments[fa.segmentIndex];
        // 简单模型:推进力与摆动角度的绝对值成反比(越直推进越快),横向力与角度成正比
        const absTheta = Math.abs(seg.theta);
        const maxTheta = Math.atan(AMPLITUDE * k);
        const normalizedAngle = absTheta / maxTheta; // 0 to 1

        // 动态计算力的大小
        const latForceMag = 20 + 30 * normalizedAngle; 
        const fwdForceMag = 20 + 35 * (1 - normalizedAngle);

        // 判断横向力方向 (向曲线凹面推)
        const sign = Math.sin(seg.phase) > 0 ? -1 : 1;
        
        fa.lateral.setAttribute("y2", latForceMag * sign);
        fa.block.setAttribute("x", 0);
        fa.block.setAttribute("y", (latForceMag + 6) * sign);
        
        fa.forward.setAttribute("x2", fwdForceMag);
        
        // 只有当有明显的推力时才高亮显示
        fa.group.style.opacity = 0.5 + 0.5 * normalizedAngle;
    });

    // 计算宏观前进速度并更新UI
    // 理论速度 v = f * lambda * 效率系数 (受滑移和结构限制)
    // 这里做个视觉映射
    const simulatedVel = (WAVE_SPEED * 0.45).toFixed(2);
    valVel.innerText = `${simulatedVel} m/s`;
    
    // 背景网格移动形成前进错觉
    globalDistanceX += simulatedVel * 1.5;
    appContainer.style.backgroundPosition = `${-globalDistanceX}px 0`;

    requestAnimationFrame(animate);
}

// 交互事件绑定
ctrlFreq.addEventListener('input', (e) => {
    WAVE_SPEED = parseFloat(e.target.value);
    lblFreq.innerText = `${WAVE_SPEED.toFixed(1)} Hz`;
});

// 初始化并启动动画 (满足重开即播,无需点击)
window.addEventListener('DOMContentLoaded', () => {
    initSnake();
    animate();
});

</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分