分享图
动画工坊
引擎就绪
<!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>
    <!-- 引入非通用、具有科技感与工程感的字体 -->
    <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
    <style>
        :root {
            /* 核心调色板:赛博工程蓝图风格 */
            --bg-color: #050914;
            --grid-color: rgba(30, 58, 138, 0.4);
            --panel-bg: rgba(9, 14, 23, 0.75);
            --panel-border: #1e3a8a;
            
            --neon-blue: #00f0ff;
            --neon-pink: #ff0055;
            --neon-green: #00ff66;
            --neon-orange: #ff5500;
            --text-main: #e2e8f0;
            --text-muted: #94a3b8;

            --font-display: 'Rajdhani', sans-serif;
            --font-mono: 'Share Tech Mono', monospace;
        }

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

        body {
            background-color: var(--bg-color);
            color: var(--text-main);
            font-family: var(--font-display);
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            width: 100vw;
            position: relative;
        }

        /* 背景网格 - 模拟前进运动 */
        .grid-bg {
            position: absolute;
            top: 0;
            left: 0;
            width: 200vw;
            height: 200vh;
            background-image: 
                linear-gradient(to right, var(--grid-color) 1px, transparent 1px),
                linear-gradient(to bottom, var(--grid-color) 1px, transparent 1px);
            background-size: 50px 50px;
            transform: translate(-50px, -50px);
            animation: moveGrid 2s linear infinite;
            z-index: 0;
            opacity: 0.5;
        }

        @keyframes moveGrid {
            0% { transform: translateX(0); }
            100% { transform: translateX(-50px); } /* 网格向左移动,视觉上机器蛇向右前进 */
        }

        /* 动画主容器 */
        #animation-container {
            position: relative;
            width: 100%;
            height: 100%;
            z-index: 10;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        svg {
            width: 100%;
            height: 100%;
            display: block;
        }

        /* 数据仪表面板 */
        .dashboard {
            position: absolute;
            z-index: 20;
            background: var(--panel-bg);
            border: 1px solid var(--panel-border);
            border-radius: 4px;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            padding: 24px;
            box-shadow: 0 0 20px rgba(0, 240, 255, 0.1), inset 0 0 10px rgba(0, 240, 255, 0.05);
        }

        .panel-left {
            top: 40px;
            left: 40px;
            width: 380px;
            border-left: 4px solid var(--neon-blue);
        }

        .panel-right {
            bottom: 40px;
            right: 40px;
            width: 340px;
            border-right: 4px solid var(--neon-pink);
        }

        h1 {
            font-size: 24px;
            font-weight: 700;
            color: var(--neon-blue);
            margin-bottom: 12px;
            letter-spacing: 1px;
            text-transform: uppercase;
        }

        h2 {
            font-size: 16px;
            color: var(--neon-pink);
            margin-bottom: 16px;
            font-family: var(--font-mono);
            border-bottom: 1px solid rgba(255, 0, 85, 0.3);
            padding-bottom: 8px;
        }

        p, .triz-desc {
            font-size: 15px;
            line-height: 1.6;
            color: var(--text-main);
            margin-bottom: 12px;
        }

        .highlight {
            color: var(--neon-blue);
            font-weight: 600;
            text-shadow: 0 0 8px rgba(0, 240, 255, 0.4);
        }

        /* 数据监控台 */
        .data-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-family: var(--font-mono);
            font-size: 14px;
            background: rgba(0, 0, 0, 0.4);
            padding: 6px 12px;
            border-radius: 2px;
            border-left: 2px solid var(--neon-orange);
        }

        .data-label { color: var(--text-muted); }
        .data-val { color: var(--neon-green); }

        /* 交互控制器 */
        .control-group {
            margin-top: 20px;
        }
        
        .control-item {
            margin-bottom: 16px;
        }

        .control-item label {
            display: flex;
            justify-content: space-between;
            font-family: var(--font-mono);
            font-size: 13px;
            color: var(--neon-blue);
            margin-bottom: 6px;
        }

        input[type="range"] {
            -webkit-appearance: none;
            width: 100%;
            height: 4px;
            background: rgba(0, 240, 255, 0.2);
            border-radius: 2px;
            outline: none;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(--neon-blue);
            cursor: pointer;
            box-shadow: 0 0 10px var(--neon-blue);
        }

        /* SVG 样式 */
        .snake-segment {
            stroke: var(--text-main);
            stroke-width: 12;
            stroke-linecap: round;
            filter: drop-shadow(0 0 5px rgba(226, 232, 240, 0.5));
            transition: stroke 0.3s;
        }

        .snake-joint {
            fill: var(--bg-color);
            stroke: var(--neon-pink);
            stroke-width: 3;
            filter: drop-shadow(0 0 6px var(--neon-pink));
        }

        .wheel {
            fill: var(--bg-color);
            stroke: var(--neon-orange);
            stroke-width: 2;
        }

        .force-arrow {
            stroke-width: 3;
            stroke-linecap: round;
        }

        .force-lateral { stroke: var(--neon-orange); }
        .force-thrust { stroke: var(--neon-green); }
        .force-text {
            font-family: var(--font-mono);
            font-size: 14px;
            font-weight: bold;
            user-select: none;
        }

        /* 图例指示器 */
        .legend {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-top: 16px;
            font-size: 13px;
        }
        .legend-item { display: flex; align-items: center; gap: 6px; }
        .color-box { width: 12px; height: 12px; border-radius: 2px; }
        .bg-orange { background: var(--neon-orange); box-shadow: 0 0 8px var(--neon-orange); }
        .bg-green { background: var(--neon-green); box-shadow: 0 0 8px var(--neon-green); }

    </style>
</head>
<body>

    <div class="grid-bg"></div>

    <div class="dashboard panel-left">
        <h1>Serpenoid 动力学模型</h1>
        <h2>[ TRIZ 最终理想解 IFR ]</h2>
        <div class="triz-desc">
            <p><strong>矛盾破除:</strong>离散的刚性电机旋转 $\rightarrow$ 连续的生物形态蜿蜒推力。</p>
            <p><strong>资源利用:</strong>巧妙利用系统腹部结构,引入被动定向摩擦。无需额外主动推进轮,仅依靠姿态扭变即可将横向力转化为前进的 <span class="highlight">最终理想解</span>。</p>
        </div>
        
        <div class="data-row" style="margin-top: 20px;">
            <span class="data-label">单向轮正向滚动摩擦 (推进)</span>
            <span class="data-val">&mu; &le; 0.1</span>
        </div>
        <div class="data-row">
            <span class="data-label">单向轮横向滑动摩擦 (阻挡)</span>
            <span class="data-val">&mu; &ge; 0.8</span>
        </div>
        <div class="data-row">
            <span class="data-label">相邻关节相位差 &Delta;&phi;</span>
            <span class="data-val" id="val-phase">45&deg; (&pi;/4)</span>
        </div>

        <div class="legend">
            <div class="legend-item"><div class="color-box bg-orange"></div> 侧向静摩擦力 (被阻挡)</div>
            <div class="legend-item"><div class="color-box bg-green"></div> 前进推力 (被转化)</div>
        </div>
    </div>

    <div class="dashboard panel-right">
        <h2>系统运动控制台</h2>
        <div class="control-group">
            <div class="control-item">
                <label><span>正弦波传递频率 (ω)</span> <span id="label-speed">1.5 Hz</span></label>
                <input type="range" id="ctrl-speed" min="0.5" max="4.0" step="0.1" value="1.5">
            </div>
            <div class="control-item">
                <label><span>关节摆动幅度 (A)</span> <span id="label-amp">35&deg;</span></label>
                <input type="range" id="ctrl-amp" min="10" max="60" step="1" value="35">
            </div>
            <div class="control-item">
                <label><span>驻波相位差 (&Delta;&phi;)</span> <span id="label-phase">45&deg;</span></label>
                <input type="range" id="ctrl-phase" min="15" max="90" step="5" value="45">
            </div>
        </div>
        <div style="margin-top:20px; font-size:12px; color:var(--text-muted); font-family:var(--font-mono);">
            > STATUS: AUTO-RUNNING<br>
            > SENSOR: KINEMATICS ACTIVE<br>
            > ENVIRONMENT: RIGID SURFACE
        </div>
    </div>

    <div id="animation-container">
        <svg id="snakeCanvas" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid meet">
            <defs>
                <!-- 箭头标记 -->
                <marker id="arrow-lateral" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
                    <path d="M0,1 L8,5 L0,9" fill="none" stroke="#ff5500" stroke-width="2" />
                </marker>
                <marker id="arrow-thrust" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
                    <path d="M0,1 L8,5 L0,9" fill="none" stroke="#00ff66" stroke-width="2" />
                </marker>
                <!-- 发光滤镜 -->
                <filter id="glow-cyan" x="-50%" y="-50%" width="200%" height="200%">
                    <feGaussianBlur stdDeviation="4" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
            </defs>

            <!-- 动态渲染层 -->
            <g id="snake-layer"></g>
            <g id="force-layer"></g>
        </svg>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            // 系统参数
            const config = {
                segments: 14,          // 舱段数量 (10-15要求)
                segmentLength: 45,     // 每个舱段的长度
                speed: 1.5,            // 默认角速度 ω
                amplitude: 35,         // 默认摆幅角度 A
                phaseDiff: 45,         // 相邻相位差 Δφ
                centerX: 800,          // 视口中心X
                centerY: 450           // 视口中心Y
            };

            // DOM 元素
            const layerSnake = document.getElementById('snake-layer');
            const layerForce = document.getElementById('force-layer');
            const ctrlSpeed = document.getElementById('ctrl-speed');
            const ctrlAmp = document.getElementById('ctrl-amp');
            const ctrlPhase = document.getElementById('ctrl-phase');

            // 状态变量
            let time = 0;
            let lastTimestamp = 0;

            // 预构建SVG元素字典,避免每帧重复创建DOM
            const elements = {
                bodies: [],
                joints: [],
                wheelsL: [],
                wheelsR: [],
                forces: []
            };

            // 初始化构建 SVG 结构
            function initSVG() {
                layerSnake.innerHTML = '';
                layerForce.innerHTML = '';
                
                for (let i = 0; i < config.segments; i++) {
                    // 身体线段
                    const body = document.createElementNS("http://www.w3.org/2000/svg", "line");
                    body.setAttribute("class", "snake-segment");
                    layerSnake.appendChild(body);
                    elements.bodies.push(body);

                    // 腹部定向轮 (左/右)
                    const wheelL = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                    wheelL.setAttribute("class", "wheel");
                    wheelL.setAttribute("width", "16");
                    wheelL.setAttribute("height", "6");
                    wheelL.setAttribute("rx", "2");
                    layerSnake.appendChild(wheelL);
                    elements.wheelsL.push(wheelL);

                    const wheelR = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                    wheelR.setAttribute("class", "wheel");
                    wheelR.setAttribute("width", "16");
                    wheelR.setAttribute("height", "6");
                    wheelR.setAttribute("rx", "2");
                    layerSnake.appendChild(wheelR);
                    elements.wheelsR.push(wheelR);

                    // 关节圆点 (位于身体之后起连接作用,首段作为头部)
                    const joint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
                    joint.setAttribute("class", "snake-joint");
                    joint.setAttribute("r", "8");
                    layerSnake.appendChild(joint);
                    elements.joints.push(joint);
                    
                    // 力学矢量指示器 (仅部分段显示)
                    if (i % 3 === 1) { // 间隔显示力,避免画面杂乱
                        const forceGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
                        
                        const lineLat = document.createElementNS("http://www.w3.org/2000/svg", "line");
                        lineLat.setAttribute("class", "force-arrow force-lateral");
                        lineLat.setAttribute("marker-end", "url(#arrow-lateral)");
                        
                        const lineThrust = document.createElementNS("http://www.w3.org/2000/svg", "line");
                        lineThrust.setAttribute("class", "force-arrow force-thrust");
                        lineThrust.setAttribute("marker-end", "url(#arrow-thrust)");

                        const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
                        text.setAttribute("class", "force-text");
                        text.setAttribute("fill", "#00ff66");
                        text.setAttribute("dy", "-10");

                        forceGroup.appendChild(lineLat);
                        forceGroup.appendChild(lineThrust);
                        forceGroup.appendChild(text);
                        layerForce.appendChild(forceGroup);
                        
                        elements.forces.push({
                            index: i,
                            group: forceGroup,
                            lineLat: lineLat,
                            lineThrust: lineThrust,
                            text: text
                        });
                    }
                }
                
                // 突出头部
                elements.joints[0].setAttribute("r", "12");
                elements.joints[0].setAttribute("fill", "#00f0ff");
                elements.joints[0].setAttribute("stroke", "#fff");
            }

            // 更新交互参数
            function updateParams() {
                config.speed = parseFloat(ctrlSpeed.value);
                config.amplitude = parseFloat(ctrlAmp.value);
                config.phaseDiff = parseFloat(ctrlPhase.value);

                document.getElementById('label-speed').textContent = config.speed.toFixed(1) + ' Hz';
                document.getElementById('label-amp').textContent = config.amplitude + '°';
                document.getElementById('label-phase').textContent = config.phaseDiff + '°';
                document.getElementById('val-phase').textContent = `${config.phaseDiff}° (${(config.phaseDiff/180).toFixed(2)}π)`;
            }

            // 核心运动学计算与渲染
            function animate(timestamp) {
                if (!lastTimestamp) lastTimestamp = timestamp;
                const dt = (timestamp - lastTimestamp) / 1000;
                lastTimestamp = timestamp;
                
                // 更新时间系统
                time += dt * config.speed * 2; 

                // 1. 计算相对关节角度 (正弦波列)
                const relativeAngles = [];
                const ampRad = config.amplitude * Math.PI / 180;
                const phaseRad = config.phaseDiff * Math.PI / 180;
                
                for (let i = 0; i < config.segments; i++) {
                    // 时间延迟正弦波: α_i = A * sin(ωt - i * Δφ)
                    const angle = ampRad * Math.sin(time - i * phaseRad);
                    relativeAngles.push(angle);
                }

                // 2. 积分计算绝对角度
                const absoluteAngles = [];
                // 使用头部摆动来抵消整体偏航,使蛇身整体保持水平前进方向
                let headAngle = 0; 
                let sum = 0;
                for(let i=0; i<config.segments; i++) {
                    sum += relativeAngles[i];
                }
                // 简化的偏航平衡算法
                let baseAngle = -sum / config.segments; 
                
                absoluteAngles[0] = baseAngle + relativeAngles[0];
                for (let i = 1; i < config.segments; i++) {
                    absoluteAngles[i] = absoluteAngles[i-1] + relativeAngles[i];
                }

                // 3. 正向运动学计算各节点坐标
                const points = [{x: 0, y: 0}];
                for (let i = 0; i < config.segments; i++) {
                    const prev = points[i];
                    const nx = prev.x + config.segmentLength * Math.cos(absoluteAngles[i]);
                    const ny = prev.y + config.segmentLength * Math.sin(absoluteAngles[i]);
                    points.push({x: nx, y: ny});
                }

                // 4. 计算质心并平移,使机器蛇居中 (配合背景网格运动形成前进错觉)
                let cx = 0, cy = 0;
                for(let p of points) { cx += p.x; cy += p.y; }
                cx /= points.length;
                cy /= points.length;

                const offsetX = config.centerX - cx;
                const offsetY = config.centerY - cy;

                for(let p of points) {
                    p.x += offsetX;
                    p.y += offsetY;
                }

                // 5. 更新 SVG DOM
                for (let i = 0; i < config.segments; i++) {
                    const p1 = points[i];
                    const p2 = points[i+1];
                    const angle = absoluteAngles[i];
                    
                    // 身体线段
                    elements.bodies[i].setAttribute("x1", p1.x);
                    elements.bodies[i].setAttribute("y1", p1.y);
                    elements.bodies[i].setAttribute("x2", p2.x);
                    elements.bodies[i].setAttribute("y2", p2.y);

                    // 关节圆点
                    elements.joints[i].setAttribute("cx", p1.x);
                    elements.joints[i].setAttribute("cy", p1.y);

                    // 腹部单向轮定位 (计算法线向量平移)
                    const midX = (p1.x + p2.x) / 2;
                    const midY = (p1.y + p2.y) / 2;
                    const perpX = -Math.sin(angle);
                    const perpY = Math.cos(angle);
                    const wheelOffset = 12; // 轮子距离中心线的距离

                    // 通过 transform 进行中心旋转和平移
                    const deg = angle * 180 / Math.PI;
                    elements.wheelsL[i].setAttribute("transform", `translate(${midX + perpX * wheelOffset - 8}, ${midY + perpY * wheelOffset - 3}) rotate(${deg}, 8, 3)`);
                    elements.wheelsR[i].setAttribute("transform", `translate(${midX - perpX * wheelOffset - 8}, ${midY - perpY * wheelOffset - 3}) rotate(${deg}, 8, 3)`);
                }

                // 尾部收尾节点
                if(!elements.tailJoint) {
                    elements.tailJoint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
                    elements.tailJoint.setAttribute("class", "snake-joint");
                    elements.tailJoint.setAttribute("r", "5");
                    layerSnake.appendChild(elements.tailJoint);
                }
                const lastP = points[config.segments];
                elements.tailJoint.setAttribute("cx", lastP.x);
                elements.tailJoint.setAttribute("cy", lastP.y);

                // 6. IFR 核心演示:力学矢量场更新
                elements.forces.forEach(f => {
                    const i = f.index;
                    const p1 = points[i];
                    const p2 = points[i+1];
                    const midX = (p1.x + p2.x) / 2;
                    const midY = (p1.y + p2.y) / 2;
                    const angle = absoluteAngles[i];
                    
                    // 计算横向摆动速度 (即角度变化率造成的横向速度)
                    // 简略用导数估算:v_lat = A * ω * cos(ωt - i*Δφ)
                    const latVelocity = config.amplitude * config.speed * Math.cos(time - i * phaseRad);
                    
                    // 当横向速度较大时,显示受阻和推力
                    const forceMag = latVelocity * 0.8; 
                    
                    // 法向向量 (取决于横向速度方向)
                    const sign = latVelocity > 0 ? 1 : -1;
                    const perpX = -Math.sin(angle) * sign;
                    const perpY = Math.cos(angle) * sign;
                    
                    // 切向向量 (前进方向)
                    const tanX = Math.cos(angle);
                    const tanY = Math.sin(angle);

                    // 若横向运动幅度足够,绘制力箭头
                    if (Math.abs(forceMag) > 10) {
                        f.group.style.opacity = 1;
                        
                        // 侧向摩擦力 (抵抗滑动,指向反方向)
                        const fLatLen = Math.abs(forceMag) * 1.5;
                        f.lineLat.setAttribute("x1", midX + perpX * 20);
                        f.lineLat.setAttribute("y1", midY + perpY * 20);
                        f.lineLat.setAttribute("x2", midX + perpX * (20 + fLatLen));
                        f.lineLat.setAttribute("y2", midY + perpY * (20 + fLatLen));

                        // 转换出的前进推力
                        const fThrustLen = Math.abs(forceMag) * 1.2;
                        f.lineThrust.setAttribute("x1", midX);
                        f.lineThrust.setAttribute("y1", midY);
                        f.lineThrust.setAttribute("x2", midX + tanX * (20 + fThrustLen));
                        f.lineThrust.setAttribute("y2", midY + tanY * (20 + fThrustLen));

                        f.text.setAttribute("x", midX + tanX * (30 + fThrustLen));
                        f.text.setAttribute("y", midY + tanY * (30 + fThrustLen));
                        f.text.textContent = "推力";
                    } else {
                        f.group.style.opacity = 0; // 摆动到极值点时横向速度为0,不产生推力
                    }
                });

                // 循环动画
                requestAnimationFrame(animate);
            }

            // 绑定事件
            ctrlSpeed.addEventListener('input', updateParams);
            ctrlAmp.addEventListener('input', updateParams);
            ctrlPhase.addEventListener('input', updateParams);

            // 初始化与启动
            initSVG();
            updateParams();
            requestAnimationFrame(animate);
        });
    </script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分