分享图
动画工坊
引擎就绪
<!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>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Space+Grotesk:wght@500;700&display=swap');

        :root {
            --bg-base: #060913;
            --grid-line: rgba(74, 102, 160, 0.15);
            --pipe-stroke: #2E4057;
            --pipe-fill: rgba(46, 64, 87, 0.3);
            --accent-cyan: #00F0FF;
            --accent-orange: #FF4D00;
            --accent-yellow: #FFD600;
            --text-main: #E0E6ED;
            --text-dim: #8A99A8;
            --hud-bg: rgba(6, 9, 19, 0.85);
            --hud-border: rgba(0, 240, 255, 0.3);
            
            --glow-cyan: drop-shadow(0 0 8px rgba(0, 240, 255, 0.6));
            --glow-orange: drop-shadow(0 0 12px rgba(255, 77, 0, 0.8));
        }

        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: var(--bg-base);
            font-family: 'Space Grotesk', system-ui, sans-serif;
            color: var(--text-main);
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .workspace {
            position: relative;
            width: 100vw;
            height: 100vh;
            max-width: 1600px;
            max-height: 900px;
            display: flex;
            justify-content: center;
            align-items: center;
            background: 
                linear-gradient(rgba(6,9,19,0.9), rgba(6,9,19,0.9)),
                radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.05) 0%, transparent 60%);
        }

        svg {
            width: 100%;
            height: 100%;
            filter: drop-shadow(0 0 30px rgba(0,0,0,0.5));
        }

        /* Technical Grid Pattern */
        .grid-pattern {
            stroke: var(--grid-line);
            stroke-width: 1;
        }

        /* SVG Element Styles */
        .pipe-wall {
            fill: var(--pipe-fill);
            stroke: var(--pipe-stroke);
            stroke-width: 4;
            stroke-linecap: round;
        }
        
        .centerline {
            stroke: var(--text-dim);
            stroke-width: 1;
            stroke-dasharray: 10, 5, 2, 5;
            opacity: 0.5;
        }

        .valve-seat {
            fill: #1A2235;
            stroke: var(--accent-cyan);
            stroke-width: 2;
        }

        .valve-plate {
            fill: rgba(0, 240, 255, 0.1);
            stroke: var(--accent-cyan);
            stroke-width: 3;
            transition: stroke 0.3s ease;
        }
        
        .valve-plate.high-torque {
            stroke: var(--accent-orange);
            fill: rgba(255, 77, 0, 0.15);
            filter: var(--glow-orange);
        }

        .shaft {
            fill: var(--bg-base);
            stroke: #FFF;
            stroke-width: 3;
        }

        .linkage-crank {
            stroke: var(--accent-orange);
            stroke-width: 8;
            stroke-linecap: round;
            stroke-linejoin: round;
        }

        .linkage-rod {
            stroke: #8A99A8;
            stroke-width: 6;
            stroke-linecap: round;
            stroke-linejoin: round;
        }

        .linkage-rocker {
            stroke: var(--accent-orange);
            stroke-width: 8;
            stroke-linecap: round;
            stroke-linejoin: round;
        }

        .pivot {
            fill: #FFF;
            stroke: var(--bg-base);
            stroke-width: 2;
        }

        .j-curve {
            fill: none;
            stroke: var(--accent-yellow);
            stroke-width: 2;
            stroke-dasharray: 4 4;
            opacity: 0.4;
        }
        
        .j-curve-active {
            fill: none;
            stroke: var(--accent-yellow);
            stroke-width: 3;
            filter: drop-shadow(0 0 5px var(--accent-yellow));
        }

        /* HUD UI */
        .hud-panel {
            position: absolute;
            background: var(--hud-bg);
            border: 1px solid var(--hud-border);
            padding: 24px;
            border-radius: 8px;
            backdrop-filter: blur(10px);
            pointer-events: none;
        }

        .hud-top-left { top: 40px; left: 40px; }
        .hud-bottom-right { bottom: 40px; right: 40px; }
        
        .hud-title {
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 2px;
            color: var(--accent-cyan);
            margin: 0 0 12px 0;
            font-weight: 700;
        }

        .hud-data {
            font-family: 'JetBrains Mono', monospace;
            font-size: 28px;
            font-weight: 800;
            color: #FFF;
            margin: 0;
            display: flex;
            align-items: baseline;
            gap: 8px;
        }

        .hud-label {
            font-family: 'Space Grotesk', sans-serif;
            font-size: 12px;
            color: var(--text-dim);
            text-transform: uppercase;
        }

        .status-indicator {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: var(--accent-cyan);
            margin-right: 10px;
            box-shadow: 0 0 10px var(--accent-cyan);
        }

        .status-indicator.alert {
            background: var(--accent-orange);
            box-shadow: 0 0 10px var(--accent-orange);
        }

        .data-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 16px;
            border-top: 1px solid rgba(255,255,255,0.1);
            padding-top: 12px;
            gap: 40px;
        }

        .phase-text {
            font-family: 'JetBrains Mono', monospace;
            font-size: 14px;
            color: var(--accent-yellow);
            height: 20px;
        }

        /* Dynamic Visual Indicators */
        .force-arrow {
            fill: none;
            stroke: var(--accent-orange);
            stroke-width: 3;
            marker-end: url(#arrowhead-orange);
            opacity: 0;
            transition: opacity 0.2s;
        }
        
        .detach-arrow {
            fill: none;
            stroke: var(--accent-cyan);
            stroke-width: 3;
            marker-end: url(#arrowhead-cyan);
            opacity: 0;
            transition: opacity 0.2s;
        }

        .visible { opacity: 1; }

    </style>
</head>
<body>

<div class="workspace">
    <!-- Main SV Canvas -->
    <svg id="simulation" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid meet">
        <defs>
            <!-- Grid Pattern -->
            <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                <path d="M 40 0 L 0 0 0 40" fill="none" class="grid-pattern" />
                <circle cx="0" cy="0" r="1" fill="rgba(255,255,255,0.2)"/>
            </pattern>

            <!-- Arrow Markers -->
            <marker id="arrowhead-orange" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                <polygon points="0 0, 10 3.5, 0 7" fill="var(--accent-orange)" />
            </marker>
            <marker id="arrowhead-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                <polygon points="0 0, 10 3.5, 0 7" fill="var(--accent-cyan)" />
            </marker>
        </defs>

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

        <!-- Reference Lines & Pipe Structure -->
        <g id="static-structure">
            <!-- Pipe Centerline -->
            <line x1="100" y1="400" x2="1100" y2="400" class="centerline" />
            <text x="120" y="390" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">PIPE AXIS (Cp)</text>

            <!-- Pipe Walls -->
            <path d="M 100 200 L 1100 200 M 100 600 L 1100 600" class="pipe-wall" />
            
            <!-- Flow Direction Hint -->
            <path d="M 150 300 L 250 300 M 230 280 L 250 300 L 230 320" stroke="rgba(255,255,255,0.1)" stroke-width="2" fill="none"/>
            <path d="M 150 500 L 250 500 M 230 480 L 250 500 L 230 520" stroke="rgba(255,255,255,0.1)" stroke-width="2" fill="none"/>

            <!-- Valve Seat Rings (Cross Section) -->
            <rect x="730" y="180" width="20" height="40" rx="4" class="valve-seat" />
            <rect x="730" y="580" width="20" height="40" rx="4" class="valve-seat" />
            
            <!-- Seat Sealing Plane Line -->
            <line x1="750" y1="150" x2="750" y2="650" class="centerline" stroke="var(--accent-cyan)" stroke-opacity="0.3" />
            <text x="760" y="170" fill="var(--accent-cyan)" font-family="JetBrains Mono" font-size="12" opacity="0.6">SEALING PLANE</text>
            
            <!-- Shaft Centerline (Vertical offset e2) -->
            <line x1="830" y1="300" x2="830" y2="550" class="centerline" stroke-dasharray="4,4" />
        </g>

        <!-- Mechanism & Kinematics -->
        <g id="kinematics">
            <!-- Full Trajectory Paths (Calculated via JS) -->
            <path id="j-curve-full" class="j-curve" d="" />
            <!-- Active Trajectory Trace -->
            <path id="j-curve-trace" class="j-curve-active" d="" />

            <!-- The Valve Assembly (Rotates around O2) -->
            <g id="valve-group">
                <!-- Valve Plate Profile (Triple Eccentric Section) -->
                <!-- Coordinates are relative to Shaft Center O2 -->
                <path id="valve-body" class="valve-plate" d="
                    M -80 -270 
                    C -60 -270, -20 -150, -20 0 
                    C -20 150, -60 210, -80 210 
                    L -120 210 
                    C -100 150, -80 50, -80 0 
                    C -80 -50, -100 -200, -120 -270 
                    Z" 
                />
                
                <!-- Center point of valve plate (showing offset) -->
                <circle cx="-80" cy="0" r="4" fill="var(--accent-cyan)" />
                <line x1="0" y1="0" x2="-80" y2="0" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="2,2"/>
                <line x1="0" y1="0" x2="0" y2="-70" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="2,2"/>
                
                <!-- Dynamic Force Vectors -->
                <!-- Compressive Force (Closing) -->
                <g id="force-compressive" class="force-arrow">
                    <line x1="-30" y1="-270" x2="-100" y2="-270" />
                    <text x="-40" y="-285" fill="var(--accent-orange)" font-family="Space Grotesk" font-size="14" stroke="none" font-weight="bold">HIGH TORQUE SEALING</text>
                </g>
                <!-- Detachment Velocity (Opening) -->
                <g id="force-detach" class="detach-arrow">
                    <line x1="-80" y1="-270" x2="20" y2="-270" />
                    <text x="-30" y="-285" fill="var(--accent-cyan)" font-family="Space Grotesk" font-size="14" stroke="none" font-weight="bold">INSTANT DETACHMENT</text>
                </g>
                
                <!-- Tip tracking point -->
                <circle id="valve-tip" cx="-80" cy="-270" r="5" fill="#FFF" filter="drop-shadow(0 0 5px #FFF)" />
            </g>

            <!-- Shaft O2 -->
            <circle cx="830" cy="470" r="18" class="shaft" />
            <circle cx="830" cy="470" r="6" fill="var(--bg-base)" />
            <text x="850" y="485" fill="#FFF" font-family="JetBrains Mono" font-size="14" font-weight="bold">O₂</text>

            <!-- Motor Shaft O1 -->
            <circle cx="280" cy="550" r="22" class="shaft" />
            <circle cx="280" cy="550" r="8" fill="var(--bg-base)" />
            <text x="240" y="580" fill="#FFF" font-family="JetBrains Mono" font-size="14" font-weight="bold">O₁</text>

            <!-- Linkages -->
            <line id="link-crank" class="linkage-crank" x1="280" y1="550" x2="400" y2="550" />
            <line id="link-rocker" class="linkage-rocker" x1="830" y1="470" x2="700" y2="350" />
            <line id="link-rod" class="linkage-rod" x1="400" y1="550" x2="700" y2="350" />

            <!-- Joint Pivots -->
            <circle id="joint-A" cx="400" cy="550" r="8" class="pivot" />
            <circle id="joint-B" cx="700" cy="350" r="8" class="pivot" />
            <text id="label-A" x="400" y="530" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">A</text>
            <text id="label-B" x="700" y="330" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">B</text>
        </g>
        
        <!-- Eccentricity Dimension Lines -->
        <g stroke="var(--text-dim)" stroke-width="1" font-family="JetBrains Mono" font-size="12" fill="var(--text-dim)">
            <!-- Radial Eccentricity e1 -->
            <line x1="850" y1="400" x2="850" y2="470" />
            <line x1="845" y1="400" x2="855" y2="400" />
            <line x1="845" y1="470" x2="855" y2="470" />
            <text x="860" y="440">e₁: 70mm (Radial)</text>

            <!-- Axial Eccentricity e2 -->
            <line x1="750" y1="670" x2="830" y2="670" />
            <line x1="750" y1="665" x2="750" y2="675" />
            <line x1="830" y1="665" x2="830" y2="675" />
            <text x="760" y="690">e₂: 80mm (Axial)</text>
        </g>
    </svg>

    <!-- Overlay UI (HUD) -->
    <div class="hud-panel hud-top-left">
        <h2 class="hud-title">System Telemetry</h2>
        <div class="hud-data">
            <span id="ui-status-dot" class="status-indicator"></span>
            <span id="ui-mode">INITIALIZING</span>
        </div>
        <div class="phase-text" id="ui-phase-desc">System booting...</div>
        
        <div class="data-row">
            <div>
                <div class="hud-label">Mechanism State</div>
                <div class="hud-data" style="font-size: 20px;"><span id="ui-angle-crank">0</span>°</div>
            </div>
            <div>
                <div class="hud-label">Valve Angle</div>
                <div class="hud-data" style="font-size: 20px;"><span id="ui-angle-valve">0</span>°</div>
            </div>
        </div>
    </div>

    <div class="hud-panel hud-bottom-right">
        <h2 class="hud-title">IFR: Resource Utilization</h2>
        <div style="max-width: 300px; color: var(--text-dim); font-size: 13px; line-height: 1.5;">
            By integrating an <strong>external crank-rocker mechanism</strong> with a <strong>triple-eccentric geometric core</strong>, uniform actuation is converted into a non-linear <span style="color:var(--accent-yellow)">'J' shaped trajectory</span>.
            <br><br>
            <span style="color:var(--accent-cyan)">■ Instant Detachment</span><br>
            <span style="color:var(--accent-orange)">■ High-Torque Perpendicular Sealing</span>
        </div>
        <div class="data-row" style="margin-top: 20px;">
            <div>
                <div class="hud-label">Torque Multiplier (Est.)</div>
                <div class="hud-data" style="color: var(--accent-orange);"><span id="ui-torque">1.0</span>x</div>
            </div>
        </div>
    </div>
</div>

<script>
    /**
     * Kinematic Simulation of Crank-Rocker + Triple Eccentric Butterfly Valve
     * Solves exact geometry frame-by-frame for high-fidelity visualization.
     */
    
    // --- Configuration & Parameters ---
    const O1 = { x: 280, y: 550 }; // Crank Motor Axis
    const O2 = { x: 830, y: 470 }; // Valve Shaft Axis
    
    // Mechanism Link Lengths
    const L1 = 140; // Crank length
    const L3 = 180; // Rocker length
    const L2 = Math.hypot(O2.x - O1.x, O2.y - O1.y) + 20; // Coupler length (calculated for specific geometry)
    
    // Valve Local Geometry (Relative to O2)
    // Sealing edge is global x=750. O2 is at x=830. Local x = -80.
    const tipLocal = { x: -80, y: -270 }; // Top sealing tip relative to O2
    
    // Animation Timing
    const CYCLE_DURATION = 8000; // ms per full open-close cycle
    const ANGLE_OPEN = -30 * (Math.PI / 180);  // Crank angle when fully open
    const ANGLE_CLOSE = 175 * (Math.PI / 180); // Crank angle when fully closed (near dead center)
    
    // Reference state for valve rotation mapping
    // We want the valve to be vertically aligned (sealing) when mechanism is closed.
    let theta3_closed = 0; 
    
    // --- DOM Elements ---
    const elCrank = document.getElementById('link-crank');
    const elRod = document.getElementById('link-rod');
    const elRocker = document.getElementById('link-rocker');
    const elJointA = document.getElementById('joint-A');
    const elJointB = document.getElementById('joint-B');
    const elLabelA = document.getElementById('label-A');
    const elLabelB = document.getElementById('label-B');
    const elValveGroup = document.getElementById('valve-group');
    const elValveBody = document.getElementById('valve-body');
    const elJCurveFull = document.getElementById('j-curve-full');
    const elJCurveTrace = document.getElementById('j-curve-trace');
    
    // HUD Elements
    const uiMode = document.getElementById('ui-mode');
    const uiPhaseDesc = document.getElementById('ui-phase-desc');
    const uiStatusDot = document.getElementById('ui-status-dot');
    const uiAngleCrank = document.getElementById('ui-angle-crank');
    const uiAngleValve = document.getElementById('ui-angle-valve');
    const uiTorque = document.getElementById('ui-torque');
    
    const forceCompressive = document.getElementById('force-compressive');
    const forceDetach = document.getElementById('force-detach');
    
    // --- Core Kinematic Solver ---
    function solveKinematics(theta1) {
        // Point A (End of Crank)
        // SVG coordinate system: y increases downwards. Standard math puts y upwards.
        // We use SVG logic directly: x = r*cos, y = -r*sin (for visually standard rotation)
        const xA = O1.x + L1 * Math.cos(theta1);
        const yA = O1.y - L1 * Math.sin(theta1);
        
        // Distance from A to O2
        const d = Math.hypot(O2.x - xA, O2.y - yA);
        
        // Check if mechanism binds (d must be <= L2+L3 and >= |L2-L3|)
        if (d > L2 + L3 || d < Math.abs(L2 - L3)) {
            console.warn("Mechanism bound/invalid geometry");
            return null; // Should not happen with current hardcoded parameters
        }
        
        // Law of Cosines to find angle of rocker (theta3)
        // Triangle A-O2-B. Sides are d, L2, L3.
        let cosGamma = (L3*L3 + d*d - L2*L2) / (2 * L3 * d);
        // Prevent precision errors causing NaN
        cosGamma = Math.max(-1, Math.min(1, cosGamma));
        const gamma = Math.acos(cosGamma);
        
        // Angle of line A-O2
        const angleAO2 = Math.atan2(yA - O2.y, xA - O2.x);
        
        // Assembly mode: We choose one of the two intersections.
        // Given our layout, we want the "upper" assembly.
        let theta3 = angleAO2 - Math.PI + gamma; 
        
        // Point B (End of Rocker)
        const xB = O2.x + L3 * Math.cos(theta3);
        const yB = O2.y - L3 * Math.sin(theta3);
        
        return {
            A: { x: xA, y: yA },
            B: { x: xB, y: yB },
            theta3: theta3
        };
    }
    
    // Calculate reference theta3 for closed state
    const closedState = solveKinematics(ANGLE_CLOSE);
    if(closedState) {
        theta3_closed = closedState.theta3;
    }
    
    // --- Pre-compute Trajectory Path ---
    function generateTrajectoryPath() {
        let pathD = "";
        let steps = 100;
        let p = [];
        
        for(let i=0; i<=steps; i++) {
            let t = i / steps;
            let theta1 = ANGLE_OPEN + (ANGLE_CLOSE - ANGLE_OPEN) * t;
            let state = solveKinematics(theta1);
            if(!state) continue;
            
            // Map rocker rotation to valve rotation
            let dTheta = state.theta3 - theta3_closed;
            
            // Calculate global position of tip
            // Rotate local tip coordinate by -dTheta (because SVG rotation is clockwise for positive angles, but we defined math y-up. Let's align with SVG rotate transform)
            // Local to global rotation: x' = x*cos(a) - y*sin(a), y' = x*sin(a) + y*cos(a)
            // Since we use SVG `transform="rotate(-deg)"`, we apply it here to find points
            let angleRad = -dTheta; // Negate because SVG transform rotate is clockwise
            let tipX_rot = tipLocal.x * Math.cos(angleRad) - tipLocal.y * Math.sin(angleRad);
            let tipY_rot = tipLocal.x * Math.sin(angleRad) + tipLocal.y * Math.cos(angleRad);
            
            let tipGlobalX = O2.x + tipX_rot;
            let tipGlobalY = O2.y + tipY_rot;
            
            p.push({x: tipGlobalX, y: tipGlobalY});
        }
        
        pathD += `M ${p[0].x} ${p[0].y} `;
        for(let i=1; i<p.length; i++) {
            pathD += `L ${p[i].x} ${p[i].y} `;
        }
        elJCurveFull.setAttribute('d', pathD);
        return p;
    }
    
    const tracePoints = generateTrajectoryPath();
    
    // --- Animation Loop ---
    function updateSimulation(time) {
        // Calculate normalized progress (0 to 1 back to 0)
        let phase = (time % CYCLE_DURATION) / CYCLE_DURATION;
        
        // Easing function to make motion look natural (motor speeds up/slows down slightly, pauses at ends)
        // Custom easing: smoothstep with dwells
        let t = 0;
        if (phase < 0.4) {
            // Closing (0 to 0.4)
            t = phase / 0.4;
            t = t * t * (3 - 2 * t); // Smoothstep
        } else if (phase < 0.5) {
            // Dwell Closed (0.4 to 0.5)
            t = 1;
        } else if (phase < 0.9) {
            // Opening (0.5 to 0.9)
            t = 1 - ((phase - 0.5) / 0.4);
            t = t * t * (3 - 2 * t); // Smoothstep
        } else {
            // Dwell Open (0.9 to 1.0)
            t = 0;
        }
        
        // Current Crank Angle
        const currentTheta1 = ANGLE_OPEN + (ANGLE_CLOSE - ANGLE_OPEN) * t;
        
        // Solve kinematics
        const state = solveKinematics(currentTheta1);
        if(!state) return;
        
        // Update Linkage SVGs
        elCrank.setAttribute('x2', state.A.x);
        elCrank.setAttribute('y2', state.A.y);
        
        elRocker.setAttribute('x2', state.B.x);
        elRocker.setAttribute('y2', state.B.y);
        
        elRod.setAttribute('x1', state.A.x);
        elRod.setAttribute('y1', state.A.y);
        elRod.setAttribute('x2', state.B.x);
        elRod.setAttribute('y2', state.B.y);
        
        elJointA.setAttribute('cx', state.A.x);
        elJointA.setAttribute('cy', state.A.y);
        elLabelA.setAttribute('x', state.A.x + 10);
        elLabelA.setAttribute('y', state.A.y - 10);
        
        elJointB.setAttribute('cx', state.B.x);
        elJointB.setAttribute('cy', state.B.y);
        elLabelB.setAttribute('x', state.B.x + 10);
        elLabelB.setAttribute('y', state.B.y - 10);
        
        // Update Valve Assembly Rotation
        // Calculate required rotation in degrees.
        // state.theta3 is in radians. SVG rotate expects degrees clockwise.
        let dThetaRad = state.theta3 - theta3_closed;
        let dThetaDeg = -(dThetaRad * 180 / Math.PI); // Negative because math y is up, SVG y is down
        
        elValveGroup.setAttribute('transform', `translate(${O2.x}, ${O2.y}) rotate(${dThetaDeg}) translate(${-O2.x}, ${-O2.y})`);
        
        // Update Trace Curve
        // Find closest point index based on t
        let traceIdx = Math.floor(t * (tracePoints.length - 1));
        if (traceIdx > 0) {
            let activePath = `M ${tracePoints[0].x} ${tracePoints[0].y} `;
            for(let i=1; i<=traceIdx; i++) {
                activePath += `L ${tracePoints[i].x} ${tracePoints[i].y} `;
            }
            elJCurveTrace.setAttribute('d', activePath);
        } else {
            elJCurveTrace.setAttribute('d', "");
        }
        
        // --- Calculate Torque & UI State Logic ---
        
        // Determine instantaneous transmission ratio (kinematic advantage)
        // Derivative dt3/dt1 approximated numerically
        let delta = 0.01;
        let s2 = solveKinematics(currentTheta1 + delta);
        let ratio = 1.0;
        if(s2) {
            ratio = Math.abs(delta / (s2.theta3 - state.theta3));
        }
        // Format ratio
        let displayTorque = ratio > 50 ? "MAX" : ratio.toFixed(2);
        uiTorque.innerText = displayTorque;
        
        // State Machine for UI and Visuals
        let crankDeg = (currentTheta1 * 180 / Math.PI).toFixed(0);
        let valveDeg = Math.abs(dThetaDeg).toFixed(1);
        
        uiAngleCrank.innerText = crankDeg;
        uiAngleValve.innerText = valveDeg;
        
        if (t > 0.95 && phase < 0.5) {
            // SEALS ENGAGED (High Torque)
            uiMode.innerText = "SEALS ENGAGED";
            uiMode.style.color = "var(--accent-orange)";
            uiPhaseDesc.innerText = "Non-linear leverage maximizing pressure";
            uiStatusDot.className = "status-indicator alert";
            
            elValveBody.classList.add("high-torque");
            forceCompressive.classList.add("visible");
            forceDetach.classList.remove("visible");
            
        } else if (t < 0.05 && phase > 0.5) {
            // FULLY OPEN
            uiMode.innerText = "FLOW CLEARED";
            uiMode.style.color = "var(--accent-cyan)";
            uiPhaseDesc.innerText = "Valve parked parallel to flow";
            uiStatusDot.className = "status-indicator";
            
            elValveBody.classList.remove("high-torque");
            forceCompressive.classList.remove("visible");
            forceDetach.classList.remove("visible");
            
        } else if (phase >= 0.5 && phase <= 0.6 && t < 0.9) {
            // INITIAL OPENING (Detachment phase)
            uiMode.innerText = "OPENING";
            uiMode.style.color = "var(--text-main)";
            uiPhaseDesc.innerText = "J-Curve execution: Pulling away from seat";
            uiStatusDot.className = "status-indicator";
            
            elValveBody.classList.remove("high-torque");
            forceCompressive.classList.remove("visible");
            forceDetach.classList.add("visible");
            
        } else if (phase < 0.4) {
            // CLOSING
            uiMode.innerText = "CLOSING";
            uiMode.style.color = "var(--text-main)";
            uiPhaseDesc.innerText = "High-speed sweep approach";
            uiStatusDot.className = "status-indicator";
            
            elValveBody.classList.remove("high-torque");
            forceCompressive.classList.remove("visible");
            forceDetach.classList.remove("visible");
        } else {
             // Default transit
             elValveBody.classList.remove("high-torque");
             forceCompressive.classList.remove("visible");
             forceDetach.classList.remove("visible");
        }

        requestAnimationFrame(updateSimulation);
    }
    
    // Start simulation immediately
    requestAnimationFrame(updateSimulation);

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