分享图
动画工坊
引擎就绪
<!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: #060b13;
            --grid-color: rgba(0, 240, 255, 0.08);
            --neon-cyan: #00f3ff;
            --neon-magenta: #ff0055;
            --neon-blue: #0066ff;
            --text-main: #c7e3ff;
            --text-dim: #5c8bba;
            --panel-bg: rgba(6, 11, 19, 0.75);
            --panel-border: rgba(0, 243, 255, 0.2);
        }

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

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

        /* 核心动画区域容器 */
        #animation-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1;
        }

        /* 动态网格背景(模拟前进) */
        #dynamic-grid {
            position: absolute;
            top: -50%;
            left: -50%;
            width: 200%;
            height: 200%;
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 60px 60px;
            z-index: 0;
            transform: perspective(600px) rotateX(20deg);
            transform-origin: center center;
        }

        /* 覆盖层UI面板 */
        .ui-panel {
            position: absolute;
            background: var(--panel-bg);
            border: 1px solid var(--panel-border);
            border-radius: 6px;
            padding: 12px 16px;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5), inset 0 0 12px rgba(0, 243, 255, 0.05);
            z-index: 10;
        }

        .panel-top-left { top: 20px; left: 20px; max-width: 280px; }
        .panel-top-right { top: 20px; right: 20px; width: 240px; }
        .panel-bottom-left { bottom: 20px; left: 20px; width: 280px; }
        .panel-bottom-right { bottom: 20px; right: 20px; }

        /* 文字排版,极致缩小以避免遮挡 */
        h1 {
            font-size: 14px;
            font-weight: 600;
            color: var(--neon-cyan);
            margin-bottom: 6px;
            letter-spacing: 1px;
            text-transform: uppercase;
        }

        p, .label {
            font-size: 11px;
            line-height: 1.5;
            color: var(--text-dim);
            margin-bottom: 4px;
        }

        .highlight { color: var(--neon-cyan); font-weight: bold; }
        .warn { color: var(--neon-magenta); }

        /* 交互控件 */
        .control-group {
            margin-top: 10px;
        }
        
        .control-header {
            display: flex;
            justify-content: space-between;
            font-size: 11px;
            margin-bottom: 4px;
            color: var(--text-main);
        }

        input[type=range] {
            -webkit-appearance: none;
            width: 100%;
            background: transparent;
            margin-bottom: 8px;
        }
        
        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            height: 12px;
            width: 12px;
            border-radius: 50%;
            background: var(--neon-cyan);
            cursor: pointer;
            margin-top: -4px;
            box-shadow: 0 0 8px var(--neon-cyan);
        }
        
        input[type=range]::-webkit-slider-runnable-track {
            width: 100%;
            height: 3px;
            cursor: pointer;
            background: rgba(0, 243, 255, 0.2);
            border-radius: 2px;
        }

        /* 遥测图表 */
        canvas.telemetry {
            width: 100%;
            height: 60px;
            background: rgba(0,0,0,0.4);
            border: 1px solid rgba(255,255,255,0.05);
            border-radius: 4px;
            margin-top: 6px;
        }

        .legend-item {
            display: flex;
            align-items: center;
            font-size: 10px;
            margin-top: 4px;
        }
        .legend-color {
            width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
        }

        /* SVG 样式 */
        svg { overflow: visible; }
        .snake-segment { fill: #111a24; stroke: #1a2b3c; stroke-width: 2; }
        .snake-wheel { fill: #000; stroke: #333; stroke-width: 1; }
        .snake-wheel-active { stroke: var(--neon-cyan); filter: drop-shadow(0 0 3px var(--neon-cyan)); }
        .snake-joint { fill: #060b13; stroke: var(--neon-blue); stroke-width: 2; transition: fill 0.1s; }
        
        /* 发光滤镜 */
        .glow-cyan { filter: drop-shadow(0 0 6px rgba(0, 240, 255, 0.6)); }
        .glow-magenta { filter: drop-shadow(0 0 8px rgba(255, 0, 85, 0.8)); }
    </style>
</head>
<body>

    <!-- 动态背景 -->
    <div id="dynamic-grid"></div>

    <!-- 核心动画容器 -->
    <div id="animation-container">
        <svg id="robot-canvas" width="600" height="800" viewBox="-300 -400 600 800">
            <defs>
                <filter id="neon-glow" x="-50%" y="-50%" width="200%" height="200%">
                    <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur1" />
                    <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur2" />
                    <feMerge>
                        <feMergeNode in="blur2" />
                        <feMergeNode in="blur1" />
                        <feMergeNode in="SourceGraphic" />
                    </feMerge>
                </filter>
                
                <!-- 侧向受力阻力箭头定义 -->
                <marker id="arrow-red" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
                    <path d="M 0 0 L 10 5 L 0 10 Z" fill="#ff0055" />
                </marker>
                
                <!-- 纵向推进受力箭头定义 -->
                <marker id="arrow-cyan" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
                    <path d="M 0 0 L 10 5 L 0 10 Z" fill="#00f3ff" />
                </marker>
            </defs>

            <!-- 蛇形机体组 -->
            <g id="snake-body-group"></g>
            <!-- 视觉引导组:轨迹与受力分析 -->
            <g id="analysis-layer"></g>
        </svg>
    </div>

    <!-- UI: 左上 说明与机理 -->
    <div class="ui-panel panel-top-left">
        <h1>驻波推进机体 (IFR模型)</h1>
        <p>通过 <span class="highlight">1-DOF 伺服关节串联</span> 模拟脊椎,破除刚性限制。</p>
        <p>利用Serpenoid曲线生成相位差,产生自头至尾的横向驻波。</p>
        <div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(255,255,255,0.1);">
            <p class="label">资源利用创新点 (TRIZ):</p>
            <p>底置<span class="highlight">被动轮/特氟龙滑块</span>。利用极大的横向摩擦力阻挡侧滑,从而将横向波动转化为极小的纵向摩擦推进力。</p>
        </div>
    </div>

    <!-- UI: 右上 相位遥测 -->
    <div class="ui-panel panel-top-right">
        <h1 style="font-size: 12px;">关节相位差遥测</h1>
        <canvas id="phase-graph" class="telemetry"></canvas>
        <div class="legend-item"><div class="legend-color" style="background:#00f3ff;"></div> 关节 1 (头部)</div>
        <div class="legend-item"><div class="legend-color" style="background:#ff0055;"></div> 关节 2</div>
        <div class="legend-item"><div class="legend-color" style="background:#ffcc00;"></div> 关节 3</div>
    </div>

    <!-- UI: 左下 交互控制 -->
    <div class="ui-panel panel-bottom-left">
        <h1 style="font-size: 12px;">波形参数控制</h1>
        
        <div class="control-group">
            <div class="control-header">
                <span>运动频率 (波速)</span>
                <span id="val-freq">2.0 Hz</span>
            </div>
            <input type="range" id="ctrl-freq" min="0" max="4" step="0.1" value="2.0">
        </div>
        
        <div class="control-group">
            <div class="control-header">
                <span>蛇行振幅 (偏转角)</span>
                <span id="val-amp">±35°</span>
            </div>
            <input type="range" id="ctrl-amp" min="0.2" max="0.8" step="0.05" value="0.6">
        </div>

        <div class="control-group" style="margin-bottom:0;">
            <div class="control-header">
                <span>相邻相位差 (驻波比)</span>
                <span id="val-phase">0.7 rad</span>
            </div>
            <input type="range" id="ctrl-phase" min="0.2" max="1.5" step="0.1" value="0.7">
        </div>
    </div>
    
    <!-- UI: 右下 状态图例 -->
    <div class="ui-panel panel-bottom-right" style="width: 180px;">
        <h1 style="font-size: 12px;">力学图示</h1>
        <div class="legend-item">
            <svg width="20" height="10" style="margin-right:4px;"><line x1="0" y1="5" x2="16" y2="5" stroke="#ff0055" stroke-width="2" marker-end="url(#arrow-red)"/></svg>
            横向抗滑阻力
        </div>
        <div class="legend-item">
            <svg width="20" height="10" style="margin-right:4px;"><line x1="0" y1="5" x2="16" y2="5" stroke="#00f3ff" stroke-width="2" marker-end="url(#arrow-cyan)"/></svg>
            前向推进分力
        </div>
        <div class="legend-item" style="margin-top: 8px;">
            <div class="legend-color" style="background:#ff0055; box-shadow: 0 0 6px #ff0055;"></div> 峰值力矩输出 (电机)
        </div>
    </div>

<script>
/**
 * 刚性机体驻波推进器模拟核心
 */

// --- 参数配置 ---
const NUM_SEGMENTS = 12;      // 舱段数量 (>10的刚性圆柱)
const SEG_LENGTH = 36;        // 舱段视觉长度
const SEG_WIDTH = 14;         // 舱段宽度
const WHEEL_W = 4;
const WHEEL_H = 16;

let params = {
    freq: 2.0,       // 时间频率 w
    amp: 0.6,        // 振幅 alpha
    phaseShift: 0.7  // 相位差 k (空间频率)
};

// --- DOM元素获取 ---
const svgGroup = document.getElementById('snake-body-group');
const analysisLayer = document.getElementById('analysis-layer');
const grid = document.getElementById('dynamic-grid');
const phaseCanvas = document.getElementById('phase-graph');
const ctx = phaseCanvas.getContext('2d');

// 控件更新逻辑
document.getElementById('ctrl-freq').addEventListener('input', e => { params.freq = parseFloat(e.target.value); document.getElementById('val-freq').innerText = params.freq.toFixed(1) + ' Hz'; });
document.getElementById('ctrl-amp').addEventListener('input', e => { params.amp = parseFloat(e.target.value); document.getElementById('val-amp').innerText = '±' + Math.round(params.amp * 180 / Math.PI) + '°'; });
document.getElementById('ctrl-phase').addEventListener('input', e => { params.phaseShift = parseFloat(e.target.value); document.getElementById('val-phase').innerText = params.phaseShift.toFixed(1) + ' rad'; });

// 响应式画布
function resizeCanvas() {
    phaseCanvas.width = phaseCanvas.parentElement.clientWidth - 32;
    phaseCanvas.height = 60;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();


// --- 构建 SVG 元素 ---
const segments = [];
for (let i = 0; i < NUM_SEGMENTS; i++) {
    // 舱段组 (包含本体,两侧被动轮)
    const gSeg = document.createElementNS("http://www.w3.org/2000/svg", "g");
    
    // 主体刚性圆柱
    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", -SEG_WIDTH/2);
    rect.setAttribute("y", -SEG_LENGTH/2);
    rect.setAttribute("width", SEG_WIDTH);
    rect.setAttribute("height", SEG_LENGTH);
    rect.setAttribute("rx", 6);
    rect.setAttribute("class", "snake-segment");
    gSeg.appendChild(rect);

    // 左侧被动轮
    const wheelL = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    wheelL.setAttribute("x", -SEG_WIDTH/2 - WHEEL_W - 1);
    wheelL.setAttribute("y", -WHEEL_H/2);
    wheelL.setAttribute("width", WHEEL_W);
    wheelL.setAttribute("height", WHEEL_H);
    wheelL.setAttribute("class", "snake-wheel snake-wheel-active");
    gSeg.appendChild(wheelL);

    // 右侧被动轮
    const wheelR = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    wheelR.setAttribute("x", SEG_WIDTH/2 + 1);
    wheelR.setAttribute("y", -WHEEL_H/2);
    wheelR.setAttribute("width", WHEEL_W);
    wheelR.setAttribute("height", WHEEL_H);
    wheelR.setAttribute("class", "snake-wheel snake-wheel-active");
    gSeg.appendChild(wheelR);

    // 关节电机 (除了最后一节)
    let joint = null;
    if (i < NUM_SEGMENTS - 1) {
        joint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        joint.setAttribute("r", 5);
        joint.setAttribute("class", "snake-joint");
        // 挂载在舱段底部位置
        joint.setAttribute("cx", 0);
        joint.setAttribute("cy", SEG_LENGTH/2);
        gSeg.appendChild(joint);
    }

    svgGroup.appendChild(gSeg);
    segments.push({ g: gSeg, joint: joint, theta: 0, x: 0, y: 0 });
}

// 力学分析箭头 (复用池)
const forceArrows = [];
for (let i=0; i<NUM_SEGMENTS; i+=2) { // 采样展示,避免视觉混乱
    const arrowLateral = document.createElementNS("http://www.w3.org/2000/svg", "line");
    arrowLateral.setAttribute("stroke", "#ff0055");
    arrowLateral.setAttribute("stroke-width", "2");
    arrowLateral.setAttribute("marker-end", "url(#arrow-red)");
    arrowLateral.style.opacity = "0.7";
    
    const arrowForward = document.createElementNS("http://www.w3.org/2000/svg", "line");
    arrowForward.setAttribute("stroke", "#00f3ff");
    arrowForward.setAttribute("stroke-width", "2");
    arrowForward.setAttribute("marker-end", "url(#arrow-cyan)");
    arrowForward.style.opacity = "0.8";

    analysisLayer.appendChild(arrowLateral);
    analysisLayer.appendChild(arrowForward);
    forceArrows.push({ lat: arrowLateral, fwd: arrowForward, idx: i });
}

// --- 动画主循环 ---
let startTime = Date.now();
let gridYOffset = 0;
let phaseHistory = [[], [], []];

function animate() {
    const t = (Date.now() - startTime) / 1000.0;
    
    // 1. 运动学解算:Serpenoid Curve (驻波形变)
    // 根据方程:theta(i, t) = alpha * sin(w*t - k*i)
    // 为了让机体总体朝上(-Y方向),基准角度为 -PI/2
    
    let totalX = 0, totalY = 0;
    const w = params.freq * Math.PI * 2;
    const k = params.phaseShift;
    const alpha = params.amp;

    // 先计算所有关节的绝对朝向角度
    for (let i = 0; i < NUM_SEGMENTS; i++) {
        // i=0 为头部。波从头部传向尾部,产生向前的推力
        segments[i].theta = -Math.PI/2 + alpha * Math.sin(w * t - k * i);
        
        // 记录前三个关节的相位历史用于UI展示
        if (i < 3) {
            phaseHistory[i].push(segments[i].theta + Math.PI/2);
            if (phaseHistory[i].length > phaseCanvas.width) phaseHistory[i].shift();
        }
    }

    // 2. 几何拓扑积分计算坐标
    // 设定尾部为初始计算起点,然后整体平移使其重心居中
    let currentX = 0, currentY = 0;
    
    // 以尾巴作为物理铰接链起点 (逆推)
    for (let i = NUM_SEGMENTS - 1; i >= 0; i--) {
        // 每节的质心
        segments[i].x = currentX - (SEG_LENGTH/2) * Math.cos(segments[i].theta);
        segments[i].y = currentY - (SEG_LENGTH/2) * Math.sin(segments[i].theta);
        
        totalX += segments[i].x;
        totalY += segments[i].y;
        
        // 更新连接点给下一节
        currentX = currentX - SEG_LENGTH * Math.cos(segments[i].theta);
        currentY = currentY - SEG_LENGTH * Math.sin(segments[i].theta);
    }

    // 计算重心偏差,用以修正将整条蛇定在画面中央偏下
    const cgX = totalX / NUM_SEGMENTS;
    const cgY = totalY / NUM_SEGMENTS;
    const offsetX = -cgX;
    const offsetY = -cgY + 100; // 偏下以便看到上方轨迹

    // 3. 更新 SVG 渲染与受力视觉分析
    for (let i = 0; i < NUM_SEGMENTS; i++) {
        const seg = segments[i];
        const finalX = seg.x + offsetX;
        const finalY = seg.y + offsetY;
        
        // SVG的旋转是以度为单位
        const deg = seg.theta * 180 / Math.PI + 90; // +90是因为rect默认朝上
        seg.g.setAttribute("transform", `translate(${finalX}, ${finalY}) rotate(${deg})`);

        // 电机峰值力矩视觉特效:根据相邻节角速度差异(即力矩大小)
        if (seg.joint && i < NUM_SEGMENTS - 1) {
            // 当前关节转速近似为波导数的绝对值
            const torque = Math.abs(alpha * w * Math.cos(w * t - k * i));
            if (torque > alpha * w * 0.7) { // 接近峰值时亮起警告色
                seg.joint.setAttribute("fill", "#ff0055");
                seg.joint.setAttribute("class", "snake-joint glow-magenta");
            } else {
                seg.joint.setAttribute("fill", "#060b13");
                seg.joint.setAttribute("class", "snake-joint glow-cyan");
            }
        }
    }

    // 更新力学箭头
    // 分析原理:当某节与整体前进方向( -Y )存在夹角时,产生横向滑动趋势。
    // 被动轮提供极大横向摩擦(红色箭头),并产生反作用的前向分力(蓝色箭头)
    forceArrows.forEach(fa => {
        const seg = segments[fa.idx];
        const finalX = seg.x + offsetX;
        const finalY = seg.y + offsetY;
        
        // 偏航角:相对正前方的偏角
        const yaw = seg.theta - (-Math.PI/2); 
        // 偏航角越大,说明该节正在横向扫动,受到的抗滑力越大
        const lateralForce = Math.sin(yaw) * 40; // 放大倍数用于视觉展示
        const forwardForce = Math.abs(lateralForce) * 0.6 + 10;

        // 横向抗滑阻力绘制 (垂直于舱体)
        // 舱体法向量: cos(theta + PI/2), sin(theta + PI/2)
        const nx = Math.cos(seg.theta + Math.PI/2);
        const ny = Math.sin(seg.theta + Math.PI/2);
        
        fa.lat.setAttribute("x1", finalX);
        fa.lat.setAttribute("y1", finalY);
        // 如果yaw为正,向左偏,阻力向右(法向正向);反之向左
        const latScale = -lateralForce; 
        fa.lat.setAttribute("x2", finalX + nx * latScale);
        fa.lat.setAttribute("y2", finalY + ny * latScale);
        // 力度小时隐藏
        fa.lat.style.opacity = Math.abs(latScale) > 10 ? 0.8 : 0;

        // 纵向推力绘制 (沿着机体向前)
        // 前向向量: cos(theta), sin(theta)
        const fx = Math.cos(seg.theta);
        const fy = Math.sin(seg.theta);
        fa.fwd.setAttribute("x1", finalX);
        fa.fwd.setAttribute("y1", finalY);
        fa.fwd.setAttribute("x2", finalX + fx * forwardForce);
        fa.fwd.setAttribute("y2", finalY + fy * forwardForce);
    });


    // 4. 背景网格相对运动 (体现宏观前进位移)
    // 前进速度近似与 (振幅^2 * 频率 * 驻波比) 成正比
    const forwardSpeed = params.amp * params.amp * params.freq * 20 * Math.sin(params.phaseShift);
    gridYOffset += forwardSpeed * 0.016; // 60fps delta
    grid.style.backgroundPosition = `center ${gridYOffset}px`;

    // 5. 渲染遥测波形
    drawTelemetry();

    requestAnimationFrame(animate);
}

// 绘制相位差遥测图
function drawTelemetry() {
    ctx.clearRect(0, 0, phaseCanvas.width, phaseCanvas.height);
    const colors = ["#00f3ff", "#ff0055", "#ffcc00"];
    
    for (let i = 0; i < 3; i++) {
        ctx.beginPath();
        ctx.strokeStyle = colors[i];
        ctx.lineWidth = 1.5;
        
        for (let x = 0; x < phaseHistory[i].length; x++) {
            // 归一化振幅显示
            const yOffset = phaseCanvas.height / 2;
            const val = phaseHistory[i][x];
            // alpha最大范围是0.8,映射到Canvas高度
            const y = yOffset - (val / 0.8) * (yOffset - 5); 
            
            if (x === 0) ctx.moveTo(x, y);
            else ctx.lineTo(x, y);
        }
        ctx.stroke();
    }
}

// 初始化启动 (确保无交互自动播放)
window.addEventListener('DOMContentLoaded', () => {
    requestAnimationFrame(animate);
});
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分