分享图
动画工坊
引擎就绪
<!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: #050914;
            --grid-color: rgba(30, 50, 100, 0.15);
            --text-main: #e2e8f0;
            --text-muted: #64748b;
            --accent-cyan: #00f0ff;
            --accent-amber: #ff9d00;
            --stair-color: #1e293b;
            --stair-edge: #38bdf8;
            --belt-color: #111827;
            --chassis-color: #334155;
            --panel-bg: rgba(5, 9, 20, 0.7);
        }

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

        body, html {
            width: 100vw;
            height: 100vh;
            background-color: var(--bg-color);
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 30px 30px;
            font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            color: var(--text-main);
        }

        /* 核心动画容器,确保居中且尺寸适中 */
        .animation-container {
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
        }

        svg {
            width: 100%;
            height: 100%;
            max-width: 1200px;
            max-height: 800px;
            filter: drop-shadow(0 0 20px rgba(0, 240, 255, 0.05));
        }

        /* 绝对定位的说明面板,严格控制大小和位置,避免遮挡核心区域 */
        .info-panel {
            position: absolute;
            background: var(--panel-bg);
            backdrop-filter: blur(8px);
            border: 1px solid rgba(0, 240, 255, 0.2);
            padding: 12px 16px;
            border-radius: 4px;
            font-size: 12px;
            line-height: 1.5;
            pointer-events: none; /* 防止干扰 */
            z-index: 10;
        }

        .panel-top-left { top: 24px; left: 24px; max-width: 300px; }
        .panel-bottom-left { bottom: 24px; left: 24px; max-width: 250px; }
        .panel-bottom-right { bottom: 24px; right: 24px; width: 220px; }
        .panel-top-right { top: 24px; right: 24px; max-width: 200px; text-align: right; }

        .title {
            color: var(--accent-cyan);
            font-size: 14px;
            font-weight: bold;
            margin-bottom: 8px;
            letter-spacing: 1px;
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .title::before {
            content: '';
            display: block;
            width: 8px;
            height: 8px;
            background: var(--accent-cyan);
            box-shadow: 0 0 8px var(--accent-cyan);
        }

        h3 {
            color: var(--accent-amber);
            font-size: 12px;
            margin: 8px 0 4px 0;
            border-bottom: 1px dashed rgba(255, 157, 0, 0.3);
            padding-bottom: 2px;
        }

        ul {
            list-style: none;
            color: var(--text-muted);
        }

        li {
            margin-bottom: 4px;
            display: flex;
            align-items: flex-start;
            gap: 6px;
        }

        li::before {
            content: '>';
            color: var(--accent-cyan);
            opacity: 0.7;
        }

        .highlight { color: var(--text-main); }
        .cyan-text { color: var(--accent-cyan); }
        
        /* 遥测数据动态样式 */
        .telemetry-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 4px;
            font-variant-numeric: tabular-nums;
        }
        
        .value { color: var(--accent-amber); }

        /* SVG 内部样式 */
        .stair-path {
            fill: var(--stair-color);
            stroke: var(--stair-edge);
            stroke-width: 2;
            stroke-linejoin: round;
        }
        
        .stair-damage {
            fill: #151d2a;
            stroke: #ef4444;
            stroke-width: 1.5;
            stroke-dasharray: 4 2;
        }

        .belt-core {
            fill: none;
            stroke: var(--belt-color);
            stroke-width: 24;
            stroke-linejoin: round;
            stroke-linecap: round;
        }

        .belt-teeth {
            fill: none;
            stroke: var(--accent-cyan);
            stroke-width: 28;
            stroke-dasharray: 4 8;
            stroke-linejoin: round;
            stroke-linecap: round;
            opacity: 0.8;
            /* JS 将驱动 dashoffset 以产生运动感 */
        }

        .chassis-segment {
            fill: var(--chassis-color);
            stroke: rgba(255,255,255,0.1);
            stroke-width: 1;
        }

        .hinge-joint {
            fill: var(--bg-color);
            stroke: var(--accent-amber);
            stroke-width: 2;
        }

        .spring-arc {
            fill: none;
            stroke: var(--accent-amber);
            stroke-width: 2;
            opacity: 0.3;
            transition: opacity 0.2s, stroke 0.2s;
        }
        
        .spring-active {
            stroke: var(--accent-cyan);
            opacity: 1;
            filter: drop-shadow(0 0 4px var(--accent-cyan));
        }

        .main-beam {
            fill: none;
            stroke: rgba(255, 255, 255, 0.15);
            stroke-width: 4;
            stroke-dasharray: 10 5;
        }

        .force-vector {
            stroke: var(--accent-amber);
            stroke-width: 1.5;
            marker-end: url(#arrowhead);
            opacity: 0;
            transition: opacity 0.1s;
        }

    </style>
</head>
<body>

    <div class="animation-container">
        <!-- 信息面板 (绝对定位,小字号,避免遮挡) -->
        <div class="info-panel panel-top-left">
            <div class="title">地形自适应履带系统</div>
            <h3>TRIZ: 最终理想解 (IFR)</h3>
            <ul>
                <li><span class="highlight">理想状态:</span> 系统自身主动消除台阶形状带来的干涉,履带始终保持线接触推进。</li>
                <li><span class="highlight">资源利用:</span> 利用<span class="cyan-text">重力与弹簧被动顺应</span>,无需增加复杂的主动执行机构,即解决“连续爬升”与“悬空打滑”的物理矛盾。</li>
            </ul>
        </div>

        <div class="info-panel panel-bottom-left">
            <h3>系统规格参数</h3>
            <ul>
                <li>模块结构: 4段铝合金铰接</li>
                <li>单段长度: 150mm (总600mm)</li>
                <li>铰链限位: 上折10° / 下折30°</li>
                <li>履带带宽: 160mm (齿高4mm)</li>
                <li>工况带速: 0.3m/s</li>
            </ul>
        </div>

        <div class="info-panel panel-bottom-right">
            <h3>实时形态遥测</h3>
            <div class="telemetry-row"><span>带面速度:</span> <span class="value" id="t-speed">0.30 m/s</span></div>
            <div class="telemetry-row"><span>J1 铰接角:</span> <span class="value" id="t-a1">0.0°</span></div>
            <div class="telemetry-row"><span>J2 铰接角:</span> <span class="value" id="t-a2">0.0°</span></div>
            <div class="telemetry-row"><span>J3 铰接角:</span> <span class="value" id="t-a3">0.0°</span></div>
            <div class="telemetry-row"><span>地形贴合率:</span> <span class="value cyan-text" id="t-contact">98%</span></div>
        </div>
        
        <div class="info-panel panel-top-right">
            <div class="title" style="justify-content: flex-end; color: var(--text-muted);">状态: 自动运行中</div>
            <div style="color: var(--text-muted); font-size: 10px; margin-top: 4px;">连续爬升工况模拟</div>
        </div>

        <!-- SVG 核心动画区域 -->
        <svg viewBox="0 0 1200 800" id="main-svg">
            <defs>
                <marker id="arrowhead" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
                    <polygon points="0 0, 6 2, 0 4" fill="var(--accent-amber)" />
                </marker>
                <filter id="glow">
                    <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
                    <feMerge>
                        <feMergeNode in="coloredBlur"/>
                        <feMergeNode in="SourceGraphic"/>
                    </feMerge>
                </filter>
                <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                    <path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
                </pattern>
            </defs>

            <!-- 背景网格 -->
            <rect width="100%" height="100%" fill="url(#grid)" />

            <!-- 环境: 破损不规则楼梯 -->
            <g id="environment">
                <!-- 静态楼梯几何体将由 JS 生成以匹配数学模型 -->
                <path id="stair-surface" class="stair-path" d="" />
                <!-- 标示破损缺角区域 -->
                <path id="stair-broken-area" class="stair-damage" d="" />
            </g>

            <!-- 主体: 履带系统 -->
            <g id="vehicle">
                <!-- 主刚性梁 (虚线示意) -->
                <line id="main-beam-line" class="main-beam" x1="0" y1="0" x2="0" y2="0" />
                
                <!-- 宽幅同步带 (外层齿纹) -->
                <path id="track-belt-teeth" class="belt-teeth" d="" />
                <!-- 宽幅同步带 (内层基体) -->
                <path id="track-belt-core" class="belt-core" d="" />

                <!-- 4段铰接底盘模块 -->
                <g id="segments-group">
                    <!-- JS 动态注入 -->
                </g>

                <!-- 接触点法向力指示 -->
                <g id="force-vectors">
                    <!-- JS 动态注入 -->
                </g>
            </g>
        </svg>
    </div>

    <script>
        /**
         * 物理模型与动画引擎
         */
        document.addEventListener("DOMContentLoaded", () => {
            // 系统参数
            const SEGMENT_COUNT = 4;
            const SEGMENT_LENGTH = 75; // 缩放后的像素长度 (对应真实150mm)
            const MAX_DOWN_ANGLE = 30 * (Math.PI / 180); // 铰链下折极限
            const MAX_UP_ANGLE = 10 * (Math.PI / 180);   // 铰链上折极限
            const TRACK_THICKNESS = 14;
            const SPEED = 1.2; // 动画推进速度

            // 楼梯地形数据定义 (x, y) - 包含不规则和破损段
            const stairProfile = [
                [-200, 700],
                [100, 700],   // 平地结束
                [100, 580], [250, 580], // 台阶1
                [250, 460], [380, 460], // 台阶2
                [380, 360], [420, 375], // 台阶3 - 破损缺角
                [420, 340], [530, 340], // 台阶3 剩余部分
                [530, 220], [680, 220], // 台阶4
                [680, 100], [830, 100], // 台阶5
                [830, -20], [1200, -20] // 顶部平台
            ];

            // 破损区域可视化坐标
            const brokenArea = "M 380 360 L 420 375 L 420 340 Z";

            // 获取地形Y坐标 (简单的线段插值)
            function getTerrainY(x) {
                if (x <= stairProfile[0][0]) return stairProfile[0][1];
                if (x >= stairProfile[stairProfile.length-1][0]) return stairProfile[stairProfile.length-1][1];
                
                for (let i = 0; i < stairProfile.length - 1; i++) {
                    let p1 = stairProfile[i];
                    let p2 = stairProfile[i+1];
                    if (x >= p1[0] && x <= p2[0]) {
                        // 垂直面处理:如果在同一个X上有两个点,取较高的(较小的Y)
                        if (p1[0] === p2[0]) {
                            return Math.min(p1[1], p2[1]);
                        }
                        let t = (x - p1[0]) / (p2[0] - p1[0]);
                        return p1[1] + t * (p2[1] - p1[1]);
                    }
                }
                return 0;
            }

            // 初始化 DOM 元素
            const stairPathEl = document.getElementById('stair-surface');
            const stairBrokenEl = document.getElementById('stair-broken-area');
            const segmentsGroup = document.getElementById('segments-group');
            const forceGroup = document.getElementById('force-vectors');
            const beltCoreEl = document.getElementById('track-belt-core');
            const beltTeethEl = document.getElementById('track-belt-teeth');
            const mainBeamEl = document.getElementById('main-beam-line');

            // 绘制地形
            let stairPathData = `M ${stairProfile[0][0]} 800 L ${stairProfile[0][0]} ${stairProfile[0][1]} `;
            for(let i=1; i<stairProfile.length; i++) {
                stairPathData += `L ${stairProfile[i][0]} ${stairProfile[i][1]} `;
            }
            stairPathData += `L ${stairProfile[stairProfile.length-1][0]} 800 Z`;
            stairPathEl.setAttribute('d', stairPathData);
            stairBrokenEl.setAttribute('d', brokenArea);

            // 创建模块DOM
            const segments = [];
            for(let i=0; i<SEGMENT_COUNT; i++) {
                const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                
                // 车体块
                const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
                rect.setAttribute('x', '0');
                rect.setAttribute('y', '-10');
                rect.setAttribute('width', SEGMENT_LENGTH);
                rect.setAttribute('height', '20');
                rect.setAttribute('rx', '4');
                rect.setAttribute('class', 'chassis-segment');
                
                // 铰接点 (除了最后一个)
                let hinge = null;
                let spring = null;
                if (i < SEGMENT_COUNT - 1) {
                    hinge = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    hinge.setAttribute('cx', SEGMENT_LENGTH);
                    hinge.setAttribute('cy', '0');
                    hinge.setAttribute('r', '5');
                    hinge.setAttribute('class', 'hinge-joint');

                    // 限位扭簧可视化
                    spring = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                    spring.setAttribute('d', `M ${SEGMENT_LENGTH-8} -8 A 12 12 0 0 1 ${SEGMENT_LENGTH+8} -8`);
                    spring.setAttribute('class', 'spring-arc');
                }

                g.appendChild(rect);
                if(hinge) {
                    g.appendChild(spring);
                    g.appendChild(hinge);
                }
                
                segmentsGroup.appendChild(g);
                segments.push({ el: g, springEl: spring, angle: 0 });
            }

            // 状态变量
            let trackPositionX = -150; 
            let beltDashOffset = 0;
            
            // 遥测UI元素
            const telA1 = document.getElementById('t-a1');
            const telA2 = document.getElementById('t-a2');
            const telA3 = document.getElementById('t-a3');

            /**
             * 核心运动学计算:模拟被动贴合
             * 从后向前计算每个铰接点的位置。受重力影响,铰接点倾向于贴地;
             * 受扭簧限位影响,相邻模块夹角不能超过物理限制。
             */
            function updateKinematics() {
                // 节点坐标 [x, y]
                const nodes = [];
                // 尾部基准点 (沿X轴匀速推进)
                nodes[0] = [trackPositionX, getTerrainY(trackPositionX) - TRACK_THICKNESS];

                // 迭代计算其余节点
                for(let i=0; i<SEGMENT_COUNT; i++) {
                    const prevNode = nodes[i];
                    
                    // 假设下一个节点沿当前阶梯斜率延伸,然后进行限位修正
                    let nextX = prevNode[0] + SEGMENT_LENGTH;
                    let targetY = getTerrainY(nextX) - TRACK_THICKNESS;
                    
                    // 计算自然下垂/上抬角度
                    let angle = Math.atan2(targetY - prevNode[1], nextX - prevNode[0]);
                    
                    // 施加铰链限位约束 (相对于前一段的绝对角度差)
                    if (i > 0) {
                        const prevAngle = segments[i-1].angle;
                        let relativeAngle = angle - prevAngle;
                        
                        // 修正角度超出范围 (下折为正,上折为负)
                        if (relativeAngle > MAX_DOWN_ANGLE) {
                            angle = prevAngle + MAX_DOWN_ANGLE;
                        } else if (relativeAngle < -MAX_UP_ANGLE) {
                            angle = prevAngle - MAX_UP_ANGLE;
                        }
                        
                        // 重新计算受约束后的XY
                        nextX = prevNode[0] + SEGMENT_LENGTH * Math.cos(angle);
                        let constrainedY = prevNode[1] + SEGMENT_LENGTH * Math.sin(angle);
                        
                        // 防穿模检测:如果约束后的Y仍然低于地形,强行抬高 (主梁牵引效果)
                        const groundY = getTerrainY(nextX) - TRACK_THICKNESS;
                        if (constrainedY > groundY) {
                            constrainedY = groundY;
                            angle = Math.atan2(constrainedY - prevNode[1], nextX - prevNode[0]);
                        }
                        targetY = constrainedY;
                    }

                    nodes[i+1] = [nextX, targetY];
                    segments[i].angle = angle;
                }

                // 更新 DOM 呈现
                let beltPathStr = "";
                let topBeltPathStr = "";
                
                // 生成包覆带的外边缘轮廓
                const topNodes = [];
                const bottomNodes = [];
                
                for(let i=0; i<SEGMENT_COUNT; i++) {
                    const p1 = nodes[i];
                    const p2 = nodes[i+1];
                    const angle = segments[i].angle;
                    
                    // 绘制底盘模块
                    segments[i].el.setAttribute('transform', `translate(${p1[0]}, ${p1[1]}) rotate(${angle * 180 / Math.PI})`);
                    
                    // 更新扭簧状态UI (如果处于受力极限)
                    if (i < SEGMENT_COUNT - 1 && segments[i].springEl) {
                        const relAngle = segments[i+1].angle - angle;
                        if (relAngle > MAX_DOWN_ANGLE * 0.9) {
                            segments[i].springEl.classList.add('spring-active');
                        } else {
                            segments[i].springEl.classList.remove('spring-active');
                        }
                    }

                    // 计算法线向量用于绘制履带外壳
                    const nx = -Math.sin(angle) * (TRACK_THICKNESS + 10);
                    const ny = Math.cos(angle) * (TRACK_THICKNESS + 10);
                    
                    topNodes.push([p1[0] - nx, p1[1] - ny]);
                    bottomNodes.push([p1[0] + nx, p1[1] + ny]);
                    if(i === SEGMENT_COUNT - 1) {
                        topNodes.push([p2[0] - nx, p2[1] - ny]);
                        bottomNodes.push([p2[0] + nx, p2[1] + ny]);
                    }
                }

                // 构建环形履带路径
                beltPathStr += `M ${bottomNodes[0][0]} ${bottomNodes[0][1]} `;
                for(let i=1; i<bottomNodes.length; i++) beltPathStr += `L ${bottomNodes[i][0]} ${bottomNodes[i][1]} `;
                // 头部半圆转折
                beltPathStr += `A 18 18 0 0 0 ${topNodes[topNodes.length-1][0]} ${topNodes[topNodes.length-1][1]} `;
                for(let i=topNodes.length-2; i>=0; i--) beltPathStr += `L ${topNodes[i][0]} ${topNodes[i][1]} `;
                // 尾部半圆转折
                beltPathStr += `A 18 18 0 0 0 ${bottomNodes[0][0]} ${bottomNodes[0][1]} `;

                beltCoreEl.setAttribute('d', beltPathStr);
                beltTeethEl.setAttribute('d', beltPathStr);
                
                // 履带滚动动画
                beltDashOffset -= SPEED * 2;
                beltTeethEl.style.strokeDashoffset = beltDashOffset;

                // 主梁位置 (连接头尾的抽象直线)
                mainBeamEl.setAttribute('x1', nodes[0][0]);
                mainBeamEl.setAttribute('y1', nodes[0][1]-20);
                mainBeamEl.setAttribute('x2', nodes[nodes.length-1][0]);
                mainBeamEl.setAttribute('y2', nodes[nodes.length-1][1]-20);

                // 更新遥测数据
                const relA1 = (segments[1].angle - segments[0].angle) * 180 / Math.PI;
                const relA2 = (segments[2].angle - segments[1].angle) * 180 / Math.PI;
                const relA3 = (segments[3].angle - segments[2].angle) * 180 / Math.PI;
                
                telA1.textContent = relA1.toFixed(1) + '°';
                telA2.textContent = relA2.toFixed(1) + '°';
                telA3.textContent = relA3.toFixed(1) + '°';
                
                // 高亮受力点颜色的逻辑
                telA1.style.color = relA1 > 28 ? 'var(--accent-cyan)' : 'var(--accent-amber)';
                telA2.style.color = relA2 > 28 ? 'var(--accent-cyan)' : 'var(--accent-amber)';
                telA3.style.color = relA3 > 28 ? 'var(--accent-cyan)' : 'var(--accent-amber)';
            }

            // 动画主循环
            function animate() {
                trackPositionX += SPEED;
                
                // 重置循环
                if (trackPositionX > 900) {
                    trackPositionX = -200;
                }
                
                updateKinematics();
                requestAnimationFrame(animate);
            }

            // 启动动画 (自动播放)
            animate();
        });
    </script>
</body>
</html>
积分规则:每轮对话扣10分