分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>自适应变径轮(Whegs)机械原理演示</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Teko:wght@400;600&display=swap');

        :root {
            --bg-color: #060913;
            --grid-color: #111a33;
            --cyan: #00f3ff;
            --cyan-dim: #007b80;
            --red: #ff2a55;
            --orange: #ff9d00;
            --text-main: #e0f7fa;
            --text-dim: #7097a8;
        }

        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: var(--bg-color);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Share Tech Mono', monospace;
            overflow: hidden;
            color: var(--text-main);
        }

        #animation-container {
            position: relative;
            width: 100%;
            max-width: 1200px;
            aspect-ratio: 16 / 9;
            background: radial-gradient(circle at 50% 50%, #0a1124 0%, #060913 100%);
            box-shadow: 0 0 50px rgba(0, 243, 255, 0.05) inset;
            border: 1px solid rgba(0, 243, 255, 0.1);
            overflow: hidden;
        }

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

        /* Overlay UI Styling */
        .hud-panel {
            position: absolute;
            background: rgba(6, 9, 19, 0.8);
            border: 1px solid var(--cyan-dim);
            backdrop-filter: blur(4px);
            padding: 15px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
            z-index: 10;
        }

        .hud-panel::before {
            content: '';
            position: absolute;
            top: 0; left: 0; width: 100%; height: 2px;
            background: linear-gradient(90deg, var(--cyan), transparent);
        }

        #data-panel {
            top: 30px;
            left: 30px;
            width: 280px;
        }

        #triz-panel {
            top: 30px;
            right: 30px;
            width: 320px;
        }

        .hud-title {
            font-family: 'Teko', sans-serif;
            font-size: 24px;
            letter-spacing: 1px;
            color: var(--cyan);
            margin-bottom: 10px;
            text-transform: uppercase;
        }

        .hud-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 14px;
        }

        .hud-label { color: var(--text-dim); }
        .hud-value { font-weight: bold; color: var(--text-main); text-shadow: 0 0 5px rgba(255,255,255,0.3); }
        
        .progress-bar-bg {
            width: 100%;
            height: 6px;
            background: #1a2640;
            margin-top: 4px;
            position: relative;
            overflow: hidden;
        }

        .progress-bar-fill {
            height: 100%;
            background: var(--cyan);
            width: 0%;
            transition: background-color 0.2s;
        }

        .triz-tag {
            display: inline-block;
            background: rgba(0, 243, 255, 0.1);
            color: var(--cyan);
            padding: 2px 6px;
            font-size: 12px;
            border: 1px solid var(--cyan-dim);
            margin-bottom: 8px;
        }

        .triz-desc {
            font-size: 13px;
            line-height: 1.5;
            color: var(--text-dim);
        }
        
        .highlight-text { color: var(--orange); font-weight: bold; }

        /* SVG Specific Styling */
        .glow-cyan { filter: drop-shadow(0 0 8px rgba(0, 243, 255, 0.6)); }
        .glow-red { filter: drop-shadow(0 0 10px rgba(255, 42, 85, 0.8)); }
        
        .stair-path {
            fill: #0c1424;
            stroke: var(--cyan-dim);
            stroke-width: 2;
        }
        
        .stair-top-edge {
            stroke: var(--cyan);
            stroke-width: 3;
            stroke-linecap: round;
        }

    </style>
</head>
<body>

<div id="animation-container">
    <!-- Overlay UI -->
    <div class="hud-panel" id="data-panel">
        <div class="hud-title">SYS: ADAPTIVE_WHEGS</div>
        <div class="hud-row">
            <span class="hud-label">MODE:</span>
            <span class="hud-value" id="val-mode">ROLLING</span>
        </div>
        <div class="hud-row">
            <span class="hud-label">DIAMETER:</span>
            <span class="hud-value" id="val-diameter">20.0 cm</span>
        </div>
        <div class="hud-row" style="margin-top: 15px;">
            <span class="hud-label">RESISTANCE TORQUE:</span>
            <span class="hud-value" id="val-torque">12 N·m</span>
        </div>
        <div class="progress-bar-bg">
            <div class="progress-bar-fill" id="bar-torque"></div>
        </div>
        <div class="hud-row" style="margin-top: 15px;">
            <span class="hud-label">LATCH STATUS:</span>
            <span class="hud-value" id="val-latch" style="color: var(--cyan)">LOCKED</span>
        </div>
    </div>

    <div class="hud-panel" id="triz-panel">
        <div class="hud-title">IFR ANALYSIS</div>
        <div class="triz-tag">最终理想解 (IFR)</div>
        <div class="triz-desc">
            系统在不需要复杂电控和额外驱动的情况下,<span class="highlight-text">依靠自身受到的阻力(有害因素)作为触发源</span>,自动适应地形变化。
        </div>
        <div class="triz-tag" style="margin-top:10px;">空间与时间的物理矛盾分离</div>
        <div class="triz-desc">
            <br/>- 平地移动时:结构必须是连续圆轮(高效率)。
            <br/>- 越障爬坡时:结构必须是分散辐条(强抓地)。
            <br/>→ 解决方案:由内部<span class="highlight-text">纯机械弹簧锁扣</span>根据实时扭矩进行瞬态构型切换。
        </div>
    </div>

    <!-- Main SVG Canvas -->
    <svg id="canvas" viewBox="0 0 1200 675" preserveAspectRatio="xMidYMid slice">
        <defs>
            <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                <path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--grid-color)" stroke-width="1"/>
            </pattern>
            
            <filter id="neon-cyan" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur stdDeviation="4" result="blur" />
                <feMerge>
                    <feMergeNode in="blur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>

            <filter id="neon-red" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur stdDeviation="5" result="blur" />
                <feComponentTransfer in="blur" result="glow">
                    <feFuncA type="linear" slope="2"/>
                </feComponentTransfer>
                <feMerge>
                    <feMergeNode in="glow"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>

            <linearGradient id="metal-grad" x1="0%" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" stop-color="#2a3b5c" />
                <stop offset="50%" stop-color="#141e33" />
                <stop offset="100%" stop-color="#0a1124" />
            </linearGradient>

            <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
                <path d="M 0 1 L 10 5 L 0 9 z" fill="var(--orange)" />
            </marker>
        </defs>

        <!-- Background Grid -->
        <rect width="100%" height="100%" fill="url(#grid)" />

        <!-- Terrain Context Group -->
        <g id="terrain" transform="translate(0, 0)">
            <!-- Flat Ground -->
            <path d="M -100 500 L 600 500 L 600 700 L -100 700 Z" class="stair-path" />
            <line x1="-100" y1="500" x2="600" y2="500" class="stair-top-edge" />
            
            <!-- Stair Step -->
            <path d="M 600 500 L 600 320 L 1300 320 L 1300 700 L 600 700 Z" class="stair-path" />
            <line x1="600" y1="320" x2="1300" y2="320" class="stair-top-edge" filter="url(#neon-cyan)" />
            
            <!-- Dimensions/Guidelines -->
            <line x1="600" y1="500" x2="600" y2="540" stroke="var(--text-dim)" stroke-width="1" stroke-dasharray="4" />
            <line x1="600" y1="320" x2="560" y2="320" stroke="var(--text-dim)" stroke-width="1" stroke-dasharray="4" />
            <path d="M 580 320 L 580 500" stroke="var(--text-dim)" stroke-width="1" marker-start="url(#arrow)" marker-end="url(#arrow)" />
            <text x="560" y="415" fill="var(--text-dim)" font-size="14" transform="rotate(-90, 560, 415)" text-anchor="middle">OBSTACLE ΔH</text>
        </g>

        <!-- Dynamic Vectors Group -->
        <g id="force-vectors" opacity="0">
            <!-- Pushing Force -->
            <path d="M 0 0 L -60 60" stroke="var(--orange)" stroke-width="4" marker-end="url(#arrow)" id="vec-push" filter="url(#neon-red)"/>
            <!-- Hooking Force -->
            <path d="M 0 0 L 40 40" stroke="var(--cyan)" stroke-width="4" marker-end="url(#arrow)" id="vec-hook" filter="url(#neon-cyan)"/>
        </g>

        <!-- The Wheg Assembly -->
        <!-- Will be dynamically transformed via JS -->
        <g id="wheel-assembly" transform="translate(200, 400)">
            
            <!-- Ideal/Reference Circle (Faint) -->
            <circle cx="0" cy="0" r="100" fill="none" stroke="rgba(0, 243, 255, 0.15)" stroke-width="1" stroke-dasharray="5 5" />
            <circle cx="0" cy="0" r="175" fill="none" stroke="rgba(255, 157, 0, 0.1)" stroke-width="1" stroke-dasharray="5 5" />

            <!-- Rotating Component -->
            <g id="wheel-rotator">
                <!-- Inner Hub -->
                <circle cx="0" cy="0" r="40" fill="url(#metal-grad)" stroke="var(--cyan-dim)" stroke-width="2" />
                <circle cx="0" cy="0" r="15" fill="#060913" stroke="var(--cyan)" stroke-width="3" filter="url(#neon-cyan)" />
                <path d="M 0 -15 L 0 15 M -15 0 L 15 0" stroke="var(--cyan)" stroke-width="2" />

                <!-- Legs Container (Generated via JS) -->
                <g id="legs-container"></g>
            </g>
        </g>
        
        <!-- Fading Overlay for Loop Reset -->
        <rect id="fade-overlay" width="100%" height="100%" fill="#060913" opacity="0" pointer-events="none" />
    </svg>
</div>

<script>
/**
 * 核心参数配置
 */
const CONFIG = {
    R_MIN: 100,        // 收缩时半径 (px, 对应物理模型20cm)
    R_MAX: 175,        // 展开时腿长 (px, 对应物理模型35cm)
    GROUND_Y: 500,     // 平地Y坐标
    STEP_X: 600,       // 台阶边缘X坐标
    STEP_Y: 320,       // 台阶顶面Y坐标
    TORQUE_THRESH: 50, // 触发阈值
    LEG_COUNT: 3       // 轮腿数量
};

// DOM 元素引用
const elWheelAssy = document.getElementById('wheel-assembly');
const elWheelRotator = document.getElementById('wheel-rotator');
const elLegsContainer = document.getElementById('legs-container');
const elForceVectors = document.getElementById('force-vectors');
const elVecPush = document.getElementById('vec-push');
const elVecHook = document.getElementById('vec-hook');
const elFade = document.getElementById('fade-overlay');

// UI 元素引用
const uiMode = document.getElementById('val-mode');
const uiDia = document.getElementById('val-diameter');
const uiTorque = document.getElementById('val-torque');
const uiTorqueBar = document.getElementById('bar-torque');
const uiLatch = document.getElementById('val-latch');

/**
 * 初始化生成轮腿SVG结构
 */
function buildWheelGeometry() {
    elLegsContainer.innerHTML = '';
    const angleStep = 360 / CONFIG.LEG_COUNT;

    for (let i = 0; i < CONFIG.LEG_COUNT; i++) {
        const rot = i * angleStep;
        
        // 创建单个腿的组
        const legGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
        legGroup.setAttribute("transform", `rotate(${rot})`);
        legGroup.setAttribute("class", "leg-group");
        legGroup.dataset.index = i;

        // 1. 固定导轨 (滑轨)
        const track = document.createElementNS("http://www.w3.org/2000/svg", "line");
        track.setAttribute("x1", "40"); track.setAttribute("y1", "0");
        track.setAttribute("x2", `${CONFIG.R_MAX - 20}`); track.setAttribute("y2", "0");
        track.setAttribute("stroke", "#141e33"); track.setAttribute("stroke-width", "12");
        track.setAttribute("stroke-linecap", "round");
        legGroup.appendChild(track);

        // 2. 滑动连杆组 (包含弹簧、外轮缘、锁扣)
        const sliderGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
        sliderGroup.setAttribute("class", "slider-group");
        
        // 弹簧视觉线 (简化为波浪线或多边形,这里用虚线代表拉伸)
        const spring = document.createElementNS("http://www.w3.org/2000/svg", "line");
        spring.setAttribute("x1", "40"); spring.setAttribute("y1", "0");
        spring.setAttribute("x2", `${CONFIG.R_MIN - 10}`); spring.setAttribute("y2", "0");
        spring.setAttribute("stroke", "var(--cyan-dim)");
        spring.setAttribute("stroke-width", "6");
        spring.setAttribute("stroke-dasharray", "4 4");
        spring.setAttribute("class", "leg-spring");
        sliderGroup.appendChild(spring);

        // 外轮缘弧线段 (120度)
        const halfAngle = (angleStep / 2) * (Math.PI / 180);
        // 调整弧线两端留一点间隙,避免完全重合,体现机械拼接感
        const gap = 0.05; 
        const x1 = CONFIG.R_MIN * Math.cos(-halfAngle + gap);
        const y1 = CONFIG.R_MIN * Math.sin(-halfAngle + gap);
        const x2 = CONFIG.R_MIN * Math.cos(halfAngle - gap);
        const y2 = CONFIG.R_MIN * Math.sin(halfAngle - gap);

        const rim = document.createElementNS("http://www.w3.org/2000/svg", "path");
        const d = `M ${x1} ${y1} A ${CONFIG.R_MIN} ${CONFIG.R_MIN} 0 0 1 ${x2} ${y2}`;
        rim.setAttribute("d", d);
        rim.setAttribute("fill", "none");
        rim.setAttribute("stroke", "url(#metal-grad)");
        rim.setAttribute("stroke-width", "16");
        rim.setAttribute("stroke-linecap", "round");
        sliderGroup.appendChild(rim);

        // 轮缘外侧高亮纹理
        const rimOuter = document.createElementNS("http://www.w3.org/2000/svg", "path");
        rimOuter.setAttribute("d", d);
        rimOuter.setAttribute("fill", "none");
        rimOuter.setAttribute("stroke", "var(--cyan)");
        rimOuter.setAttribute("stroke-width", "3");
        rimOuter.setAttribute("stroke-dasharray", "10 15");
        sliderGroup.appendChild(rimOuter);
        
        // 主支撑杆
        const strut = document.createElementNS("http://www.w3.org/2000/svg", "line");
        strut.setAttribute("x1", "40"); strut.setAttribute("y1", "0");
        strut.setAttribute("x2", `${CONFIG.R_MIN}`); strut.setAttribute("y2", "0");
        strut.setAttribute("stroke", "#2a3b5c");
        strut.setAttribute("stroke-width", "8");
        sliderGroup.appendChild(strut);

        // 离心/扭矩弹簧锁扣 (触发销)
        const latch = document.createElementNS("http://www.w3.org/2000/svg", "rect");
        latch.setAttribute("x", "50"); latch.setAttribute("y", "-8");
        latch.setAttribute("width", "10"); latch.setAttribute("height", "16");
        latch.setAttribute("fill", "var(--cyan)");
        latch.setAttribute("class", "leg-latch");
        sliderGroup.appendChild(latch);

        legGroup.appendChild(sliderGroup);
        elLegsContainer.appendChild(legGroup);
    }
}

/**
 * 动画状态机与时间线控制器
 */
const TIMELINE = {
    DURATION: 8000, // 总周期 8 秒
    PHASES: [
        { id: 'ROLL',    start: 0.00, end: 0.25 }, // 0 - 2s: 平地滚动
        { id: 'IMPACT',  start: 0.25, end: 0.35 }, // 2 - 2.8s: 撞击台阶,扭矩激增
        { id: 'UNLOCK',  start: 0.35, end: 0.45 }, // 2.8 - 3.6s: 锁扣释放,变径展开
        { id: 'CLIMB',   start: 0.45, end: 0.75 }, // 3.6 - 6s: 跨越台阶,像桨一样爬升
        { id: 'RECOVER', start: 0.75, end: 0.85 }, // 6 - 6.8s: 越障完毕,阻力消失,收缩复原
        { id: 'ROLL_UP', start: 0.85, end: 0.95 }, // 6.8 - 7.6s: 在高台上继续滚动
        { id: 'FADE',    start: 0.95, end: 1.00 }  // 7.6 - 8s: 淡出重置
    ]
};

// 缓动函数
function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
function easeOutElastic(t) {
    const c4 = (2 * Math.PI) / 3;
    return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}
function lerp(a, b, t) { return a + (b - a) * t; }

let startTime = null;

function updateAnimation(timestamp) {
    if (!startTime) startTime = timestamp;
    let elapsed = timestamp - startTime;
    let progress = (elapsed % TIMELINE.DURATION) / TIMELINE.DURATION;

    // 当前状态变量
    let x = 0, y = 0, angle = 0;
    let expansion = 0; // 0 (收缩 R_MIN) 到 1 (展开 R_MAX)
    let torquePct = 0.1; // 10% 基础扭矩
    let modeText = "ROLLING";
    let isRedAlert = false;
    let showVectors = false;
    let vecData = { pX:0, pY:0, hX:0, hY:0 }; // 力向量数据
    let opacity = 1;

    // --- 状态计算 ---
    if (progress < 0.25) {
        // 阶段 1:平地滚动
        let p = progress / 0.25;
        let startX = 100;
        let endX = CONFIG.STEP_X - CONFIG.R_MIN - 2; // 刚好接触台阶
        x = lerp(startX, endX, p);
        y = CONFIG.GROUND_Y - CONFIG.R_MIN;
        angle = (x - startX) / CONFIG.R_MIN * (180 / Math.PI); 
        torquePct = 0.1;
        modeText = "CRUISE (ROUND TIRE)";

    } else if (progress < 0.35) {
        // 阶段 2:撞击台阶,扭矩激增
        let p = (progress - 0.25) / 0.10;
        x = CONFIG.STEP_X - CONFIG.R_MIN - 2;
        y = CONFIG.GROUND_Y - CONFIG.R_MIN;
        
        // 模拟电机受阻时的微小抖动和扭矩攀升
        angle = ((x - 100) / CONFIG.R_MIN * (180 / Math.PI)) + Math.sin(p * Math.PI * 10) * 2 * p;
        torquePct = lerp(0.1, 1.0, p);
        modeText = "OBSTACLE DETECTED";
        if (p > 0.5) isRedAlert = true;

    } else if (progress < 0.45) {
        // 阶段 3:锁扣释放,纯机械变径展开
        let p = (progress - 0.35) / 0.10;
        let easeP = easeOutElastic(p);
        
        x = CONFIG.STEP_X - CONFIG.R_MIN - 2;
        // 展开时,半径变大,支撑点在平地,所以轴心必须被抬高
        expansion = easeP;
        let currentR = lerp(CONFIG.R_MIN, CONFIG.R_MAX, expansion);
        y = CONFIG.GROUND_Y - currentR;
        
        // 保持之前累积的角度,不再滚动
        angle = ((x - 100) / CONFIG.R_MIN * (180 / Math.PI)); 
        
        // 扭矩释放
        torquePct = lerp(1.0, 0.4, p);
        modeText = "MECHANICAL TRIGGER";
        isRedAlert = true;

    } else if (progress < 0.75) {
        // 阶段 4:像风车桨一样“刨”上台阶
        let p = (progress - 0.45) / 0.30;
        let easeP = easeInOutQuad(p);
        expansion = 1.0;
        torquePct = 0.8; // 爬坡需较大扭矩但未超限
        modeText = "WHEGS ENGAGED";
        isRedAlert = false;
        showVectors = true;

        // 复杂的运动学模拟:轴心绕台阶边缘划出一个弧线
        // 起点 (x0, y0)
        let x0 = CONFIG.STEP_X - CONFIG.R_MIN - 2;
        let y0 = CONFIG.GROUND_Y - CONFIG.R_MAX;
        
        // 终点 (x1, y1) 在台阶上方
        let x1 = CONFIG.STEP_X + CONFIG.R_MAX;
        let y1 = CONFIG.STEP_Y - CONFIG.R_MAX;

        // 简单使用二阶贝塞尔曲线模拟跨越轨迹
        let cx = CONFIG.STEP_X - 20; 
        let cy = CONFIG.STEP_Y - CONFIG.R_MAX - 50;

        x = (1-easeP)*(1-easeP)*x0 + 2*(1-easeP)*easeP*cx + easeP*easeP*x1;
        y = (1-easeP)*(1-easeP)*y0 + 2*(1-easeP)*easeP*cy + easeP*easeP*y1;

        // 腿的旋转:整个爬坡过程刚好转过 120 度 (一根腿的夹角),实现交替步态
        let startAngle = ((x0 - 100) / CONFIG.R_MIN * (180 / Math.PI));
        angle = startAngle + easeP * 120;

        // 计算力向量位置 (大致在轮的后下方和前上方)
        vecData.pX = x - CONFIG.R_MAX * 0.5; vecData.pY = y + CONFIG.R_MAX * 0.8;
        vecData.hX = x + CONFIG.R_MAX * 0.8; vecData.hY = y - CONFIG.R_MAX * 0.2;

    } else if (progress < 0.85) {
        // 阶段 5:越障完毕,阻力消失,拉簧收缩复原
        let p = (progress - 0.75) / 0.10;
        let easeP = easeInOutQuad(p);
        
        // 终点X
        let xStart = CONFIG.STEP_X + CONFIG.R_MAX;
        let xEnd = xStart + 80;
        x = lerp(xStart, xEnd, p);
        
        expansion = 1.0 - easeP; // 收缩
        let currentR = lerp(CONFIG.R_MIN, CONFIG.R_MAX, expansion);
        y = CONFIG.STEP_Y - currentR;
        
        // 同步旋转补偿
        let baseAngle = ((xStart - 100) / CONFIG.R_MIN * (180 / Math.PI)) + 120;
        angle = baseAngle + (x - xStart) / currentR * (180 / Math.PI);
        
        torquePct = lerp(0.8, 0.1, p);
        modeText = "RECOVERY (SPRING RETRACT)";
        
    } else if (progress < 0.95) {
        // 阶段 6:高台继续滚动
        let p = (progress - 0.85) / 0.10;
        let xStart = CONFIG.STEP_X + CONFIG.R_MAX + 80;
        let xEnd = 1200;
        x = lerp(xStart, xEnd, p);
        y = CONFIG.STEP_Y - CONFIG.R_MIN;
        expansion = 0;
        
        let baseAngle = ((CONFIG.STEP_X + CONFIG.R_MAX - 100) / CONFIG.R_MIN * (180 / Math.PI)) + 120 + (80 / CONFIG.R_MIN * (180/Math.PI));
        angle = baseAngle + (x - xStart) / CONFIG.R_MIN * (180 / Math.PI);
        
        torquePct = 0.1;
        modeText = "CRUISE (ROUND TIRE)";
    } else {
        // 阶段 7:淡出重置
        opacity = 1.0 - (progress - 0.95) / 0.05;
        x = 1200;
        y = CONFIG.STEP_Y - CONFIG.R_MIN;
        expansion = 0;
        torquePct = 0;
        modeText = "RESTARTING...";
    }

    // --- 应用到 DOM ---
    
    // 主容器变换
    elWheelAssy.setAttribute("transform", `translate(${x}, ${y})`);
    elWheelRotator.setAttribute("transform", `rotate(${angle})`);
    elFade.setAttribute("opacity", 1 - opacity);

    // 内部轮腿展开变换
    const sliders = document.querySelectorAll('.slider-group');
    const springs = document.querySelectorAll('.leg-spring');
    const latches = document.querySelectorAll('.leg-latch');
    
    let expandDist = expansion * (CONFIG.R_MAX - CONFIG.R_MIN);
    
    sliders.forEach((slider, idx) => {
        // 轮缘和腿向外平移
        slider.setAttribute("transform", `translate(${expandDist}, 0)`);
        
        // 弹簧随之拉长视觉效果
        let springLine = springs[idx];
        springLine.setAttribute("x2", `${CONFIG.R_MIN - 10 + expandDist}`);
        
        // 弹簧受力变色
        if (expansion > 0.1) {
            springLine.setAttribute("stroke", "var(--orange)");
        } else {
            springLine.setAttribute("stroke", "var(--cyan-dim)");
        }

        // 锁扣状态更新
        let latch = latches[idx];
        if (progress >= 0.25 && progress < 0.35) {
            // 预备断开,闪红光
            latch.setAttribute("fill", isRedAlert ? "var(--red)" : "var(--cyan)");
            latch.setAttribute("filter", "url(#neon-red)");
        } else if (progress >= 0.35 && progress < 0.75) {
            // 缩回释放状态
            latch.setAttribute("transform", "translate(0, 10)"); // 机械销收起
            latch.setAttribute("fill", "var(--red)");
            latch.setAttribute("filter", "none");
        } else {
            // 正常锁定
            latch.setAttribute("transform", "translate(0, 0)");
            latch.setAttribute("fill", "var(--cyan)");
            latch.setAttribute("filter", "url(#neon-cyan)");
        }
    });

    // 力向量显示控制
    if (showVectors) {
        elForceVectors.setAttribute("opacity", "1");
        // 将向量箭头放置在受力点附件,做简单的反向推力动画
        let pulse = (Math.sin(timestamp / 50) + 1) * 10;
        elVecPush.setAttribute("d", `M ${vecData.pX} ${vecData.pY} L ${vecData.pX - 40 - pulse} ${vecData.pY + 40 + pulse}`);
        elVecHook.setAttribute("d", `M ${vecData.hX} ${vecData.hY} L ${vecData.hX + 30 + pulse} ${vecData.hY - 30 - pulse}`);
    } else {
        elForceVectors.setAttribute("opacity", "0");
    }

    // --- UI 数据面板更新 ---
    let actualDia = 20.0 + (expansion * 15.0); // 20cm to 35cm
    uiDia.innerText = actualDia.toFixed(1) + " cm";
    uiMode.innerText = modeText;
    
    let torqueVal = torquePct * 70; // 最大模拟 70 N.m
    uiTorque.innerText = torqueVal.toFixed(1) + " N·m";
    uiTorqueBar.style.width = (torquePct * 100) + "%";
    
    if (isRedAlert) {
        uiTorqueBar.style.backgroundColor = "var(--red)";
        uiTorqueBar.style.boxShadow = "0 0 10px var(--red)";
        uiTorque.style.color = "var(--red)";
    } else if (torquePct > 0.5) {
        uiTorqueBar.style.backgroundColor = "var(--orange)";
        uiTorqueBar.style.boxShadow = "0 0 10px var(--orange)";
        uiTorque.style.color = "var(--orange)";
    } else {
        uiTorqueBar.style.backgroundColor = "var(--cyan)";
        uiTorqueBar.style.boxShadow = "none";
        uiTorque.style.color = "var(--text-main)";
    }

    if (progress >= 0.35 && progress < 0.75) {
        uiLatch.innerText = "RELEASED (MECHANICAL)";
        uiLatch.style.color = "var(--red)";
    } else if (progress >= 0.75 && progress < 0.85) {
        uiLatch.innerText = "RE-ENGAGING";
        uiLatch.style.color = "var(--orange)";
    } else {
        uiLatch.innerText = "LOCKED";
        uiLatch.style.color = "var(--cyan)";
    }

    // 继续下一帧
    requestAnimationFrame(updateAnimation);
}

// 初始化并自动播放
window.addEventListener('DOMContentLoaded', () => {
    buildWheelGeometry();
    requestAnimationFrame(updateAnimation);
});

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