分享图
动画工坊
引擎就绪
<!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: #050810;
            --panel-bg: rgba(13, 20, 36, 0.75);
            --border-color: #1E2D4A;
            --text-main: #E2E8F0;
            --text-dim: #94A3B8;
            
            /* 状态颜色:柔性(青绿) -> 刚性(亮珀) */
            --soft-main: #10B981;
            --soft-glow: rgba(16, 185, 129, 0.3);
            --rigid-main: #F59E0B;
            --rigid-glow: rgba(245, 158, 11, 0.4);
            
            --rock-color: #1A233A;
            --rock-stroke: #3B82F6;
            
            --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        }

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

        body {
            background-color: var(--bg-color);
            color: var(--text-main);
            font-family: var(--font-ui);
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
            background-image: 
                radial-gradient(circle at 50% 50%, #0d1424 0%, #050810 100%),
                linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
                linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
            background-size: 100% 100%, 40px 40px, 40px 40px;
        }

        /* 主容器,约束最大尺寸,保持核心区域主角地位 */
        #app-container {
            position: relative;
            width: 100%;
            max-width: 1200px;
            aspect-ratio: 16 / 9;
            background: rgba(0, 0, 0, 0.2);
            border: 1px solid var(--border-color);
            box-shadow: 0 0 60px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(13, 20, 36, 0.8);
            border-radius: 8px;
            overflow: hidden;
        }

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

        /* 绝对定位的 UI 悬浮面板,字号极小,靠边放置以免遮挡 */
        .hud-panel {
            position: absolute;
            background: var(--panel-bg);
            border: 1px solid var(--border-color);
            backdrop-filter: blur(8px);
            border-radius: 4px;
            padding: 12px 16px;
            display: flex;
            flex-direction: column;
            gap: 6px;
            pointer-events: none;
            z-index: 10;
        }

        .hud-top-left { top: 20px; left: 20px; }
        .hud-top-right { top: 20px; right: 20px; text-align: right; }
        .hud-bottom-right { bottom: 80px; right: 20px; width: 220px; }

        .title-primary {
            font-size: 14px;
            font-weight: 700;
            letter-spacing: 1px;
            color: #fff;
            text-transform: uppercase;
        }

        .title-secondary {
            font-size: 11px;
            color: var(--soft-main);
            letter-spacing: 0.5px;
        }

        .data-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 12px;
            font-family: monospace;
            border-bottom: 1px solid rgba(255,255,255,0.05);
            padding-bottom: 4px;
        }

        .data-label { color: var(--text-dim); }
        .data-value { font-weight: 600; transition: color 0.3s; }

        .value-soft { color: var(--soft-main); }
        .value-rigid { color: var(--rigid-main); text-shadow: 0 0 8px var(--rigid-glow); }

        /* 底部交互控制栏 */
        .control-bar {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            width: 60%;
            height: 40px;
            background: var(--panel-bg);
            border: 1px solid var(--border-color);
            border-radius: 20px;
            display: flex;
            align-items: center;
            padding: 0 20px;
            gap: 15px;
            backdrop-filter: blur(8px);
            z-index: 20;
            pointer-events: auto;
        }

        .play-btn {
            background: none;
            border: none;
            color: var(--text-main);
            cursor: pointer;
            width: 24px;
            height: 24px;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: color 0.2s;
        }
        .play-btn:hover { color: var(--soft-main); }

        .timeline-container {
            flex-grow: 1;
            height: 100%;
            display: flex;
            align-items: center;
            position: relative;
        }

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

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 14px;
            height: 14px;
            background: var(--text-main);
            border-radius: 50%;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
            transition: background 0.2s, transform 0.1s;
        }
        input[type="range"]:active::-webkit-slider-thumb {
            transform: scale(1.3);
            background: var(--soft-main);
        }

        .phase-indicator {
            position: absolute;
            top: -25px;
            left: 0;
            font-size: 11px;
            color: var(--rigid-main);
            font-weight: 600;
            white-space: nowrap;
            pointer-events: none;
            transition: left 0.1s linear;
        }

        /* SVG 动画与特效类 */
        .glow-pulse { animation: pulse 2s infinite; }
        @keyframes pulse {
            0% { filter: drop-shadow(0 0 2px var(--soft-main)); }
            50% { filter: drop-shadow(0 0 12px var(--soft-main)); }
            100% { filter: drop-shadow(0 0 2px var(--soft-main)); }
        }

        .dash-anim {
            stroke-dasharray: 6 6;
            animation: dashMove 1s linear infinite;
        }
        @keyframes dashMove { to { stroke-dashoffset: -12; } }

    </style>
</head>
<body>

<div id="app-container">
    <!-- UI 面板 -->
    <div class="hud-panel hud-top-left">
        <div class="title-primary">颗粒阻塞自适应履带系统</div>
        <div class="title-secondary">TRIZ 最终理想解: 柔性接触 · 刚性咬合</div>
    </div>

    <div class="hud-panel hud-top-right">
        <div class="title-primary" style="font-size: 12px; color: var(--text-dim);">真空负压泵监控</div>
        <div style="font-family: monospace; font-size: 24px; font-weight: 700; margin-top: 4px;" id="hud-pressure">
            0.0 <span style="font-size: 12px; color: var(--text-dim);">kPa</span>
        </div>
    </div>

    <div class="hud-panel hud-bottom-right">
        <div class="data-row">
            <span class="data-label">当前阶段</span>
            <span class="data-value" id="hud-phase">复位准备</span>
        </div>
        <div class="data-row">
            <span class="data-label">介质状态</span>
            <span class="data-value value-soft" id="hud-state">常压 / 柔软流动</span>
        </div>
        <div class="data-row">
            <span class="data-label">形变包裹深度</span>
            <span class="data-value" id="hud-depth">0 mm</span>
        </div>
        <div class="data-row">
            <span class="data-label">扭矩滑移率</span>
            <span class="data-value" id="hud-slip">0.0 %</span>
        </div>
    </div>

    <!-- 底部控制栏 -->
    <div class="control-bar">
        <button class="play-btn" id="btn-play">
            <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                <!-- 初始显示暂停图标(因为自动播放) -->
                <path d="M6 4h4v16H6zm8 0h4v16h-4z" id="icon-play-state"/>
            </svg>
        </button>
        <div class="timeline-container">
            <div class="phase-indicator" id="timeline-indicator"></div>
            <input type="range" id="timeline-slider" min="0" max="1000" value="0">
        </div>
    </div>

    <!-- 核心可视化 SVG 容器 -->
    <svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" id="main-canvas">
        <defs>
            <!-- 滤镜:用于刚性状态的发光效果 -->
            <filter id="glow-rigid" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur stdDeviation="8" result="blur" />
                <feComposite in="SourceGraphic" in2="blur" operator="over" />
            </filter>
            <filter id="glow-soft" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur stdDeviation="4" result="blur" />
                <feComposite in="SourceGraphic" in2="blur" operator="over" />
            </filter>
            
            <!-- 台阶截面纹理 -->
            <pattern id="rock-pattern" width="20" height="20" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
                <line x1="0" y1="0" x2="0" y2="20" stroke="#1E293B" stroke-width="2" />
            </pattern>
        </defs>

        <!-- 背景刻度网格线 -->
        <g stroke="rgba(255,255,255,0.05)" stroke-width="1">
            <line x1="100" y1="0" x2="100" y2="600" /><line x1="200" y1="0" x2="200" y2="600" />
            <line x1="300" y1="0" x2="300" y2="600" /><line x1="400" y1="0" x2="400" y2="600" />
            <line x1="500" y1="0" x2="500" y2="600" /><line x1="600" y1="0" x2="600" y2="600" />
            <line x1="700" y1="0" x2="700" y2="600" /><line x1="800" y1="0" x2="800" y2="600" />
            <line x1="900" y1="0" x2="900" y2="600" />
            
            <line x1="0" y1="100" x2="1000" y2="100" /><line x1="0" y1="200" x2="1000" y2="200" />
            <line x1="0" y1="300" x2="1000" y2="300" /><line x1="0" y1="400" x2="1000" y2="400" />
            <line x1="0" y1="500" x2="1000" y2="500" />
        </g>

        <!-- 复杂不规则台阶轮廓 -->
        <g id="ground-system">
            <path id="rock-surface" fill="url(#rock-pattern)" stroke="var(--rock-stroke)" stroke-width="3" filter="url(#glow-soft)" />
        </g>

        <!-- 履带底盘机构与颗粒包 -->
        <g id="chassis-system">
            <!-- 履带基板/气室通道 -->
            <path d="M 320 80 L 680 80 L 700 120 L 300 120 Z" fill="#1E293B" stroke="#334155" stroke-width="2"/>
            <!-- 气阀与管路 -->
            <rect x="480" y="30" width="40" height="50" fill="#0F172A" stroke="#475569" rx="4"/>
            <path id="vacuum-flow-line" d="M 500 80 L 500 120" stroke="var(--soft-main)" stroke-width="4" stroke-linecap="round" class="dash-anim"/>
            <text x="500" y="20" fill="var(--text-dim)" font-size="10" text-anchor="middle" font-family="monospace">VACUUM PUMP</text>

            <!-- 弹性阻塞包外壳 (路径由 JS 动态生成控制) -->
            <path id="bag-membrane" stroke-width="3" fill-opacity="0.1" />

            <!-- 颗粒网络系统:连接线 (仅刚性状态显示) -->
            <g id="particle-network" opacity="0"></g>

            <!-- 内部颗粒物群体 -->
            <g id="particles-group"></g>
        </g>

        <!-- 物理受力指示箭头 -->
        <g id="force-vectors" opacity="0" transform="translate(0, 0)">
            <!-- 推进力 -->
            <path d="M 400 100 L 600 100" stroke="#EF4444" stroke-width="6" marker-end="url(#arrow-red)"/>
            <polygon points="600,90 620,100 600,110" fill="#EF4444"/>
            <text x="500" y="85" fill="#EF4444" font-size="14" font-weight="bold" text-anchor="middle">履带驱动力向导</text>
            
            <!-- 反作用力/咬合受力 -->
            <path id="reaction-arrow" d="M 500 450 L 350 450" stroke="#3B82F6" stroke-width="4"/>
            <polygon points="350,442 335,450 350,458" fill="#3B82F6"/>
            <text x="420" y="440" fill="#3B82F6" font-size="12" text-anchor="middle">无滑移刚性支点</text>
        </g>

    </svg>
</div>

<script>
    /**
     * 核心逻辑控制:动画与状态机
     * 遵循最终理想解 (IFR):去除繁冗中间件,直接展示 柔软包裹 -> 瞬间固化 的核心破局点。
     */
    
    // --- 配置参数 ---
    const DURATION = 10000; // 完整周期 10 秒
    const NUM_PARTICLES = 160; // 内部阻塞颗粒数量
    const BAG_WIDTH = 400;
    const BAG_MAX_DEPTH = 220;
    const CENTER_X = 500;
    
    // 台阶表面生成:使用一段无限循环的不规则波纹代表复杂地形
    const rockHeights = [
        450, 430, 480, 400, 390, 460, 490, 420, 380, 450, 
        410, 470, 480, 420, 400, 440, 460, 430, 450, 430,
        // 循环补偿
        450, 430, 480, 400, 390, 460, 490, 420, 380, 450
    ];
    const ROCK_SEG_WIDTH = 60; // 每段控制点的水平间距

    // 状态元素引用
    const dom = {
        slider: document.getElementById('timeline-slider'),
        btnPlay: document.getElementById('btn-play'),
        iconPlay: document.getElementById('icon-play-state'),
        indicator: document.getElementById('timeline-indicator'),
        membrane: document.getElementById('bag-membrane'),
        particles: document.getElementById('particles-group'),
        network: document.getElementById('particle-network'),
        flowLine: document.getElementById('vacuum-flow-line'),
        forceVectors: document.getElementById('force-vectors'),
        rockSurface: document.getElementById('rock-surface'),
        chassis: document.getElementById('chassis-system'),
        // HUD
        phase: document.getElementById('hud-phase'),
        state: document.getElementById('hud-state'),
        pressure: document.getElementById('hud-pressure'),
        depth: document.getElementById('hud-depth'),
        slip: document.getElementById('hud-slip')
    };

    // --- 数据初始化 ---
    let isPlaying = true;
    let manualTime = 0;
    let lastFrameTime = performance.now();
    let animTime = 0; // 0 到 DURATION

    // 生成颗粒物初始坐标 (相对于包裹器中心点的位置)
    const particleData = [];
    for (let i = 0; i < NUM_PARTICLES; i++) {
        // 在半圆范围内随机生成点
        let rx, ry, isValid = false;
        while (!isValid) {
            rx = (Math.random() - 0.5) * (BAG_WIDTH - 20);
            ry = Math.random() * BAG_MAX_DEPTH;
            // 判断是否在椭圆边界内
            let maxRy = Math.sqrt(1 - Math.pow(rx / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
            if (ry < maxRy - 10) {
                isValid = true;
            }
        }
        particleData.push({
            rx, ry, 
            el: null, // DOM 引用
            jitterX: Math.random() * 2000, // 柏林噪声相位
            jitterY: Math.random() * 2000,
            size: 3 + Math.random() * 4
        });
    }

    // 预计算颗粒间的连接线(用于刚化状态的网络可视化)
    const networkLinks = [];
    for (let i = 0; i < NUM_PARTICLES; i++) {
        for (let j = i + 1; j < NUM_PARTICLES; j++) {
            let dx = particleData[i].rx - particleData[j].rx;
            let dy = particleData[i].ry - particleData[j].ry;
            if (dx*dx + dy*dy < 1200) { // 距离小于 34 像素则连接
                networkLinks.push({i, j, el: null});
            }
        }
    }

    // --- DOM 初始化 ---
    function initSVG() {
        // 创建颗粒物
        particleData.forEach(p => {
            const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
            circle.setAttribute('r', p.size);
            dom.particles.appendChild(circle);
            p.el = circle;
        });

        // 创建网络连接线
        networkLinks.forEach(link => {
            const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            line.setAttribute('stroke-width', '1');
            dom.network.appendChild(line);
            link.el = line;
        });
    }

    // --- 核心数学与运动学函数 ---
    
    // 获取台阶在指定 x 坐标处的高度
    function getRockY(x, offset) {
        let globalX = x - offset;
        // 映射到无限循环的波纹数组
        let maxIndex = (rockHeights.length - 10) * ROCK_SEG_WIDTH;
        while (globalX < 0) globalX += maxIndex;
        globalX = globalX % maxIndex;
        
        let index = Math.floor(globalX / ROCK_SEG_WIDTH);
        let t = (globalX % ROCK_SEG_WIDTH) / ROCK_SEG_WIDTH;
        
        // 缓动插值,使山峰圆滑
        let smoothT = t * t * (3 - 2 * t); 
        return rockHeights[index] * (1 - smoothT) + rockHeights[index + 1] * smoothT;
    }

    // 渲染帧
    function render(time) {
        // 时间归一化到 0~1 周期
        const progress = (time % DURATION) / DURATION;
        
        // --- 阶段定义与时间线插值 ---
        // 0.00-0.15 : 底盘下压接触 (BaseY 增加)
        // 0.15-0.25 : 台阶挤压,完美共形 (包裹形变)
        // 0.25-0.30 : 抽真空,瞬间固化 (属性变化)
        // 0.30-0.65 : 电机发力,驱动车体 (台阶后移,显示抓地力)
        // 0.65-0.70 : 泄压解除固化
        // 0.70-0.85 : 底盘抬起复位
        // 0.85-1.00 : 台阶滚动刷新地形

        let phaseText = "复位准备";
        let stateText = "常压 / 柔软流动";
        let isRigid = false;
        let rigidRatio = 0; // 0 到 1 的过渡
        
        // 运动学变量
        let baseY = 80;        // 底盘基准高度
        let rockOffset = 0;    // 台阶相对移动量
        let displayPressure = 0;
        let displayDepth = 0;

        if (progress < 0.15) {
            phaseText = "柔顺接触地形";
            baseY = 80 + (progress / 0.15) * 120; // 80 -> 200
        } else if (progress < 0.25) {
            phaseText = "自适应不规则轮廓";
            baseY = 200;
            displayDepth = ((progress - 0.15) / 0.1) * 50; // 形变深度 0->50
        } else if (progress < 0.30) {
            phaseText = "负压瞬间固化";
            baseY = 200;
            rigidRatio = (progress - 0.25) / 0.05;
            isRigid = true;
            displayDepth = 50;
            displayPressure = -80 * rigidRatio; // 压力 0 -> -80
        } else if (progress < 0.65) {
            phaseText = "刚性咬合传递大扭矩";
            baseY = 200;
            rigidRatio = 1;
            isRigid = true;
            displayDepth = 50;
            displayPressure = -80;
            // 驱动地形后移模拟履带前进
            rockOffset = - ((progress - 0.30) / 0.35) * 400; 
        } else if (progress < 0.70) {
            phaseText = "气阀泄压复位";
            baseY = 200;
            rigidRatio = 1 - ((progress - 0.65) / 0.05);
            displayDepth = 50;
            displayPressure = -80 * rigidRatio;
            rockOffset = -400;
        } else if (progress < 0.85) {
            phaseText = "脱离台阶障碍";
            baseY = 200 - ((progress - 0.70) / 0.15) * 120; // 200 -> 80
            displayDepth = 50 * (1 - (progress - 0.70) / 0.15);
            rockOffset = -400;
        } else {
            phaseText = "寻找下一支点";
            baseY = 80;
            // 刷新地形到下一个位置(无缝连接)
            rockOffset = -400 - ((progress - 0.85) / 0.15) * 200;
        }

        // --- 渲染视觉更新 ---

        // 1. 生成包裹器外壳路径
        let pathD = `M ${CENTER_X - BAG_WIDTH/2} ${baseY + 40}`; // 起点左上
        let rightBound = CENTER_X + BAG_WIDTH/2;
        
        // 分段计算底部曲线以贴合地形
        const segmentCount = 30;
        const bagBottomPoints = [];
        
        for (let i = 0; i <= segmentCount; i++) {
            let cx = (CENTER_X - BAG_WIDTH/2) + (BAG_WIDTH / segmentCount) * i;
            // 自由悬垂状态下的理论高度 (半椭圆)
            let freeY = (baseY + 40) + Math.sqrt(1 - Math.pow((cx - CENTER_X) / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
            // 地形限制高度
            let rY = getRockY(cx, rockOffset);
            
            // IFR核心:包裹器完美被动顺应地形限制
            let actualY = Math.min(freeY, rY - 5); 
            
            bagBottomPoints.push({x: cx, y: actualY, freeY: freeY, rockY: rY});
            pathD += ` L ${cx} ${actualY}`;
        }
        pathD += ` L ${rightBound} ${baseY + 40} Z`; // 闭合回到右上
        dom.membrane.setAttribute('d', pathD);

        // 2. 更新地形面 SVG 路径
        let rockPathD = `M 0 600 `;
        for (let x = 0; x <= 1000; x += 20) {
            rockPathD += `L ${x} ${getRockY(x, rockOffset)} `;
        }
        rockPathD += `L 1000 600 Z`;
        dom.rockSurface.setAttribute('d', rockPathD);

        // 3. 渲染内部颗粒动态
        // 软态颜色:深青;刚态颜色:亮珀
        const rC = isRigid ? [245, 158, 11] : [16, 185, 129];
        // 混合颜色计算
        const currentR = 16 + (245 - 16) * rigidRatio;
        const currentG = 185 + (158 - 185) * rigidRatio;
        const currentB = 129 + (11 - 129) * rigidRatio;
        const rgbStr = `rgb(${currentR}, ${currentG}, ${currentB})`;

        dom.membrane.setAttribute('fill', `rgba(${currentR}, ${currentG}, ${currentB}, ${0.1 + rigidRatio*0.3})`);
        dom.membrane.setAttribute('stroke', rgbStr);
        if (rigidRatio < 0.5) {
            dom.membrane.classList.add('dash-anim');
            dom.membrane.setAttribute('filter', 'none');
        } else {
            dom.membrane.classList.remove('dash-anim');
            dom.membrane.setAttribute('stroke-dasharray', 'none');
            dom.membrane.setAttribute('filter', 'url(#glow-rigid)');
        }

        // 时间变量用于布朗运动 (软态才运动)
        let tSlow = isRigid ? 0 : time * 0.002;

        particleData.forEach((p, idx) => {
            let px = CENTER_X + p.rx;
            let theoryMaxY = Math.sqrt(1 - Math.pow(p.rx / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
            let py_free = baseY + 40 + p.ry;
            
            // 空间挤压计算 (体积等比压缩)
            let rockLimY = getRockY(px, rockOffset) - 10;
            let actualY = py_free;
            
            // 当前 x 处可用最大深度
            let availDepth = rockLimY - (baseY + 40);
            if (availDepth < theoryMaxY && availDepth > 0) {
                // 等比例压缩颗粒位置
                let compressRatio = availDepth / theoryMaxY;
                actualY = baseY + 40 + (p.ry * compressRatio);
            } else if (availDepth <= 0) {
                actualY = baseY + 40;
            }

            // 添加柔软状态下的流体颤动
            let jitterX = isRigid ? 0 : Math.sin(p.jitterX + tSlow) * 2;
            let jitterY = isRigid ? 0 : Math.cos(p.jitterY + tSlow) * 2;

            p.currentPx = px + jitterX;
            p.currentPy = actualY + jitterY;

            p.el.setAttribute('cx', p.currentPx);
            p.el.setAttribute('cy', p.currentPy);
            p.el.setAttribute('fill', isRigid ? rgbStr : 'var(--soft-main)');
            p.el.setAttribute('opacity', 0.6 + rigidRatio*0.4);
        });

        // 4. 更新刚化网络 (IFR: 微观力链形成)
        if (rigidRatio > 0.1) {
            dom.network.setAttribute('opacity', rigidRatio * 0.8);
            dom.network.setAttribute('stroke', rgbStr);
            networkLinks.forEach(link => {
                let p1 = particleData[link.i];
                let p2 = particleData[link.j];
                link.el.setAttribute('x1', p1.currentPx);
                link.el.setAttribute('y1', p1.currentPy);
                link.el.setAttribute('x2', p2.currentPx);
                link.el.setAttribute('y2', p2.currentPy);
            });
        } else {
            dom.network.setAttribute('opacity', 0);
        }

        // 5. 驱动阶段附加效果
        if (progress > 0.30 && progress < 0.65) {
            dom.forceVectors.setAttribute('opacity', 1);
            // 箭头轻微震动表现强劲扭矩
            let forceJitter = Math.sin(time * 0.05) * 2;
            dom.forceVectors.setAttribute('transform', `translate(${forceJitter}, 0)`);
        } else {
            dom.forceVectors.setAttribute('opacity', 0);
        }

        // 真空管颜色变化
        dom.flowLine.setAttribute('stroke', isRigid ? varCol('rigid-main') : varCol('soft-main'));

        // 6. 更新 UI 数据与指示器
        dom.phase.innerText = phaseText;
        dom.state.innerText = isRigid ? "负压 / 刚性咬合" : "常压 / 柔软流动";
        dom.state.className = isRigid ? "data-value value-rigid" : "data-value value-soft";
        dom.pressure.innerHTML = `${displayPressure.toFixed(1)} <span style="font-size: 12px; color: var(--text-dim);">kPa</span>`;
        dom.pressure.style.color = isRigid ? varCol('rigid-main') : '#fff';
        dom.depth.innerText = `${displayDepth.toFixed(1)} mm`;
        dom.slip.innerText = isRigid ? "0.0 % (无打滑)" : "0.0 %";
        dom.slip.style.color = isRigid ? varCol('soft-main') : varCol('text-dim');

        // 更新进度条位置
        if (isPlaying) {
            dom.slider.value = progress * 1000;
        }
        dom.indicator.innerText = `${(progress * 10).toFixed(1)}s`;
        dom.indicator.style.left = `${progress * 100}%`;
    }

    // 辅助获取 CSS 变量
    function varCol(name) {
        return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim();
    }

    // --- 主循环 ---
    function loop(timestamp) {
        if (isPlaying) {
            // 计算流逝时间并累加
            let delta = timestamp - lastFrameTime;
            // 限制最大 delta 防止休眠唤醒跳跃过大
            if (delta > 100) delta = 16; 
            animTime += delta;
        }
        lastFrameTime = timestamp;

        render(animTime);
        requestAnimationFrame(loop);
    }

    // --- 交互事件绑定 ---
    dom.btnPlay.addEventListener('click', () => {
        isPlaying = !isPlaying;
        lastFrameTime = performance.now();
        // 切换图标 (播放/暂停)
        dom.iconPlay.setAttribute('d', isPlaying 
            ? "M6 4h4v16H6zm8 0h4v16h-4z" // 暂停图标
            : "M8 5v14l11-7z"             // 播放图标
        );
    });

    dom.slider.addEventListener('input', (e) => {
        isPlaying = false;
        animTime = (e.target.value / 1000) * DURATION;
        render(animTime);
        dom.iconPlay.setAttribute('d', "M8 5v14l11-7z");
    });

    dom.slider.addEventListener('change', () => {
        // 如果需要拖动松开后自动播放,可取消下方注释
        // isPlaying = true;
        // lastFrameTime = performance.now();
        // dom.iconPlay.setAttribute('d', "M6 4h4v16H6zm8 0h4v16h-4z");
    });

    // 页面加载完成后立即初始化并运行
    window.addEventListener('DOMContentLoaded', () => {
        initSVG();
        lastFrameTime = performance.now();
        requestAnimationFrame(loop);
    });

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