分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Magnetic Kinematic Coupling - IFR Demonstration</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');

        :root {
            --bg-base: #050914;
            --bg-panel: rgba(15, 23, 42, 0.75);
            --grid-color: rgba(0, 240, 255, 0.05);
            --cyan-glow: #00f0ff;
            --cyan-dark: #0891b2;
            --alert-red: #ef4444;
            --alert-red-glow: rgba(239, 68, 68, 0.5);
            --metal-light: #94a3b8;
            --metal-dark: #334155;
            --text-muted: #64748b;
        }

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

        body {
            margin: 0;
            padding: 0;
            background-color: var(--bg-base);
            color: #fff;
            font-family: 'Space Mono', monospace;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            overflow: hidden;
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 30px 30px;
            background-position: center center;
        }

        /* Scanline effect */
        body::after {
            content: "";
            position: absolute;
            inset: 0;
            background: linear-gradient(
                to bottom,
                rgba(255,255,255,0),
                rgba(255,255,255,0) 50%,
                rgba(0,0,0,0.1) 50%,
                rgba(0,0,0,0.1)
            );
            background-size: 100% 4px;
            pointer-events: none;
            z-index: 999;
        }

        #canvas-container {
            position: relative;
            width: 100%;
            max-width: 1200px;
            aspect-ratio: 16 / 9;
            background: radial-gradient(circle at 50% 50%, #0f172a 0%, #050914 100%);
            border: 1px solid #1e293b;
            box-shadow: 0 0 80px rgba(0, 240, 255, 0.05), inset 0 0 40px rgba(0,0,0,0.8);
            border-radius: 8px;
            overflow: hidden;
        }

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

        /* SVG Element Styles */
        .metal-fill { fill: url(#metal-gradient); stroke: #475569; stroke-width: 2; }
        .metal-dark-fill { fill: url(#metal-dark-gradient); stroke: #1e293b; stroke-width: 2; }
        .v-groove { fill: #0f172a; stroke: var(--cyan-dark); stroke-width: 1.5; }
        
        .magnet { fill: var(--cyan-dark); stroke: var(--cyan-glow); stroke-width: 1; filter: url(#glow-cyan-subtle); transition: all 0.2s; }
        .magnet.active { fill: var(--cyan-glow); filter: url(#glow-cyan); }
        
        .steel-ball { fill: url(#ball-gradient); filter: drop-shadow(0 8px 6px rgba(0,0,0,0.6)); }
        
        .bellows-base { fill: none; stroke: #020617; stroke-width: 50; stroke-linecap: round; }
        .bellows-ridges { fill: none; stroke: #1e293b; stroke-width: 52; stroke-dasharray: 4 6; stroke-linecap: round; }
        
        .flux-line { fill: none; stroke: var(--cyan-glow); stroke-width: 2; stroke-dasharray: 4 4; opacity: 0; transition: opacity 0.1s; filter: url(#glow-cyan-subtle); }
        .flux-line.active { opacity: 0.8; animation: flux-flow 0.5s linear infinite; }
        .flux-line.broken { stroke: var(--alert-red); stroke-dasharray: 2 8; opacity: 0.4; animation: none; filter: none; }
        
        @keyframes flux-flow {
            to { stroke-dashoffset: -8; }
        }

        .impact-arrow { fill: var(--alert-red); filter: url(#glow-red); opacity: 0; }
        
        /* HUD Texts */
        .hud-panel { fill: var(--bg-panel); stroke: var(--cyan-dark); stroke-width: 1; rx: 6; }
        .hud-title { fill: var(--cyan-glow); font-size: 16px; font-weight: bold; letter-spacing: 2px; }
        .hud-label { fill: var(--text-muted); font-size: 13px; }
        .hud-value { fill: #f8fafc; font-size: 14px; font-weight: bold; }
        .hud-value.cyan { fill: var(--cyan-glow); }
        .hud-value.red { fill: var(--alert-red); }
        .hud-line { stroke: #334155; stroke-width: 1; }

        .triz-box { fill: rgba(0, 240, 255, 0.05); stroke: var(--cyan-dark); stroke-width: 1; rx: 4; transition: all 0.3s; }
        .triz-box.highlight { fill: rgba(0, 240, 255, 0.2); stroke: var(--cyan-glow); filter: url(#glow-cyan-subtle); }
        .triz-title { fill: var(--cyan-glow); font-size: 12px; font-weight: bold; }
        .triz-text { fill: var(--metal-light); font-size: 10px; }

        /* Controls */
        .controls {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 15px;
            background: var(--bg-panel);
            padding: 10px 20px;
            border-radius: 30px;
            border: 1px solid #1e293b;
            backdrop-filter: blur(10px);
            z-index: 10;
        }

        button {
            background: transparent;
            border: 1px solid var(--cyan-dark);
            color: var(--cyan-glow);
            font-family: 'Space Mono', monospace;
            padding: 8px 16px;
            border-radius: 20px;
            cursor: pointer;
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: 1px;
            transition: all 0.2s;
        }

        button:hover {
            background: rgba(0, 240, 255, 0.1);
            box-shadow: 0 0 15px rgba(0, 240, 255, 0.2);
        }

        button.active {
            background: var(--cyan-dark);
            color: #fff;
            border-color: var(--cyan-glow);
            box-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
        }
    </style>
</head>
<body>

    <div id="canvas-container">
        <svg viewBox="0 0 1000 600" id="main-svg">
            <defs>
                <!-- Gradients -->
                <linearGradient id="metal-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#475569" />
                    <stop offset="50%" stop-color="#334155" />
                    <stop offset="100%" stop-color="#1e293b" />
                </linearGradient>
                
                <linearGradient id="metal-dark-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#334155" />
                    <stop offset="100%" stop-color="#0f172a" />
                </linearGradient>

                <radialGradient id="ball-gradient" cx="30%" cy="30%" r="70%">
                    <stop offset="0%" stop-color="#ffffff" />
                    <stop offset="30%" stop-color="#94a3b8" />
                    <stop offset="80%" stop-color="#334155" />
                    <stop offset="100%" stop-color="#0f172a" />
                </radialGradient>

                <!-- Filters for Glows -->
                <filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="8" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                
                <filter id="glow-cyan-subtle" x="-10%" y="-10%" width="120%" height="120%">
                    <feGaussianBlur stdDeviation="3" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>

                <filter id="glow-red" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="10" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>

                <!-- Hazard Stripes Pattern -->
                <pattern id="hazard-stripes" width="20" height="20" patternTransform="rotate(45)">
                    <rect width="10" height="20" fill="#fbbf24" opacity="0.7"/>
                    <rect x="10" width="10" height="20" fill="#1e293b" opacity="0.9"/>
                </pattern>
            </defs>

            <!-- Background Decor -->
            <g opacity="0.3">
                <line x1="500" y1="0" x2="500" y2="600" stroke="#334155" stroke-dasharray="10 10"/>
                <circle cx="500" cy="205" r="350" fill="none" stroke="#1e293b" stroke-width="1"/>
                <circle cx="500" cy="205" r="250" fill="none" stroke="#1e293b" stroke-width="1"/>
            </g>

            <!-- Magnetic Flux Lines -->
            <g id="flux-layer">
                <!-- Left Flux -->
                <path id="flux-l1" class="flux-line active" d="" />
                <path id="flux-l2" class="flux-line active" d="" />
                <path id="flux-l3" class="flux-line active" d="" />
                <!-- Right Flux -->
                <path id="flux-r1" class="flux-line active" d="" />
                <path id="flux-r2" class="flux-line active" d="" />
                <path id="flux-r3" class="flux-line active" d="" />
            </g>

            <!-- UPPER FLANGE (Robot Arm Side - Fixed/Shakes) -->
            <g id="upper-assembly">
                <!-- Main block with V-groove cutouts -->
                <path class="metal-fill" d="
                    M 250 20 
                    L 750 20 
                    L 750 180 
                    L 700 180
                    L 650 130
                    L 600 180
                    L 540 180
                    L 540 100
                    L 460 100
                    L 460 180
                    L 400 180
                    L 350 130
                    L 300 180
                    L 250 180 
                    Z" />
                
                <!-- Robot arm attachment hint -->
                <rect x="400" y="0" width="200" height="20" fill="url(#metal-dark-gradient)"/>
                
                <!-- Magnets -->
                <rect id="mag-left" class="magnet active" x="310" y="105" width="80" height="20" rx="2" />
                <rect id="mag-right" class="magnet active" x="610" y="105" width="80" height="20" rx="2" />

                <!-- V-Groove highlights -->
                <path class="v-groove" d="M 300 180 L 350 130 L 400 180" />
                <path class="v-groove" d="M 600 180 L 650 130 L 700 180" />

                <!-- Central Vacuum Pipe (Upper part) -->
                <rect x="475" y="100" width="50" height="80" fill="#0f172a" stroke="#334155"/>
            </g>

            <!-- FLEXIBLE BELLOWS TUBE -->
            <!-- We draw it via JS to connect upper (500,180) to lower flange center -->
            <path id="bellows-base" class="bellows-base" d="M 500 180 L 500 230" />
            <path id="bellows-ridges" class="bellows-ridges" d="M 500 180 L 500 230" />

            <!-- LOWER FLANGE (Tool Side - Kinematic) -->
            <!-- Center of origin for this group is set to (500, 205) via JS -->
            <g id="lower-assembly">
                <!-- Tool attachment (Vacuum Cup) -->
                <path fill="#0f172a" stroke="#334155" stroke-width="2" d="M -30 25 L 30 25 L 50 120 L -50 120 Z"/>
                <rect x="-60" y="120" width="120" height="15" fill="#1e293b" rx="5"/>
                <!-- Hazard decal -->
                <rect x="-50" y="25" width="100" height="10" fill="url(#hazard-stripes)"/>

                <!-- Lower Plate -->
                <path class="metal-dark-fill" d="M -220 0 L 220 0 L 200 25 L -200 25 Z" />
                
                <!-- Central Vacuum Pipe (Lower part) -->
                <rect x="-25" y="-10" width="50" height="35" fill="#0f172a" stroke="#334155"/>

                <!-- Steel Balls (Centers at local X: -150, 150. Y: -25 to fit into upper groove at Y=155) -->
                <circle id="ball-left" class="steel-ball" cx="-150" cy="-25" r="28" />
                <circle id="ball-right" class="steel-ball" cx="150" cy="-25" r="28" />

                <!-- Ball Socket Mounts -->
                <path fill="url(#metal-gradient)" d="M -170 0 L -130 0 L -130 -10 C -130 -20, -140 -25, -150 -25 C -160 -25, -170 -20, -170 -10 Z"/>
                <path fill="url(#metal-gradient)" d="M 130 0 L 170 0 L 170 -10 C 170 -20, 160 -25, 150 -25 C 140 -25, 130 -20, 130 -10 Z"/>
            </g>

            <!-- IMPACT ANIMATION ELEMENT -->
            <g id="impact-arrow" class="impact-arrow" transform="translate(850, 215)">
                <path d="M 0 -20 L -60 -20 L -60 -40 L -120 0 L -60 40 L -60 20 L 0 20 Z" />
                <text x="-90" y="-45" fill="var(--alert-red)" font-size="16" font-weight="bold" filter="url(#glow-red)">SHEAR OVERLOAD</text>
            </g>

            <!-- UI HUD OVERLAY (Right Side) -->
            <g id="hud-layer" transform="translate(730, 40)">
                <rect class="hud-panel" width="240" height="340" />
                
                <!-- Title -->
                <rect x="0" y="15" width="4" height="20" fill="var(--cyan-glow)" />
                <text x="15" y="30" class="hud-title">SYSTEM TELEMETRY</text>
                <line x1="15" y1="45" x2="225" y2="45" class="hud-line" />

                <!-- Data Rows -->
                <text x="15" y="75" class="hud-label">Mech Status:</text>
                <text x="115" y="75" id="val-status" class="hud-value cyan">COUPLED</text>

                <text x="15" y="105" class="hud-label">Hold Force:</text>
                <text x="115" y="105" id="val-force" class="hud-value cyan">200.0 N</text>

                <text x="15" y="135" class="hud-label">Deflection:</text>
                <text x="115" y="135" id="val-angle" class="hud-value cyan">0.00°</text>

                <text x="15" y="165" class="hud-label">Lat. Shear:</text>
                <text x="115" y="165" id="val-shear" class="hud-value cyan">15.0 N</text>

                <!-- TRIZ IFR Highlight Box -->
                <rect id="triz-box" class="triz-box" x="15" y="195" width="210" height="125" />
                <rect x="15" y="195" width="4" height="125" fill="var(--cyan-dark)" />
                <text x="28" y="215" class="triz-title">TRIZ: IDEAL FINAL RESULT</text>
                <text x="28" y="235" class="triz-text">矛盾:刚性固定 vs 侧向过载断裂</text>
                <text x="28" y="255" class="triz-text">解法:引入磁力运动学耦合</text>
                <text x="28" y="275" class="triz-text">状态:平常提供高精度刚性定位,</text>
                <text x="28" y="290" class="triz-text">      过载时自动断开充当保险丝,</text>
                <text x="28" y="305" class="triz-text">      事后自动复原,消除破坏。</text>
            </g>
        </svg>
    </div>

    <!-- Interactive Controls -->
    <div class="controls">
        <button id="btn-auto" class="active">Auto Loop Sequence</button>
        <button id="btn-trigger">Trigger Impact</button>
    </div>

    <script>
        // DOM Elements
        const lowerAssembly = document.getElementById('lower-assembly');
        const upperAssembly = document.getElementById('upper-assembly');
        const bellowsBase = document.getElementById('bellows-base');
        const bellowsRidges = document.getElementById('bellows-ridges');
        const impactArrow = document.getElementById('impact-arrow');
        const magLeft = document.getElementById('mag-left');
        const magRight = document.getElementById('mag-right');
        const trizBox = document.getElementById('triz-box');

        const valStatus = document.getElementById('val-status');
        const valForce = document.getElementById('val-force');
        const valAngle = document.getElementById('val-angle');
        const valShear = document.getElementById('val-shear');

        // Flux paths
        const fluxes = [
            document.getElementById('flux-l1'), document.getElementById('flux-l2'), document.getElementById('flux-l3'),
            document.getElementById('flux-r1'), document.getElementById('flux-r2'), document.getElementById('flux-r3')
        ];

        // Easing functions
        const easeOutQuint = t => 1 - Math.pow(1 - t, 5);
        const easeInOutCubic = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
        const easeInExpo = t => t === 0 ? 0 : Math.pow(2, 10 * t - 10);
        const easeOutBack = t => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); };

        // State Variables
        let startTime = Date.now();
        let isAutoPlay = true;
        let manualTriggerTime = 0;
        
        // Physics constants
        const PIVOT_L_X = -150;
        const PIVOT_L_Y = -25;
        const CRITICAL_ANGLE = 3.0; // degrees

        // Helper: Calculate global position of a point inside the transformed lower assembly
        function getGlobalPos(localX, localY, tx, ty, angleDeg) {
            let rad = angleDeg * Math.PI / 180;
            // The transform rotates around pivot (PIVOT_L_X, PIVOT_L_Y)
            let x1 = localX - PIVOT_L_X;
            let y1 = localY - PIVOT_L_Y;
            
            let x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
            let y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
            
            return {
                x: x2 + PIVOT_L_X + tx,
                y: y2 + PIVOT_L_Y + ty
            };
        }

        // Draw magnetic flux lines
        function updateFlux(tx, ty, angleDeg, isBroken) {
            // Magnet centers
            const mLeft = { x: 350, y: 125 };
            const mRight = { x: 650, y: 125 };
            
            // Ball global centers
            const bLeft = getGlobalPos(-150, -25, tx, ty, angleDeg);
            const bRight = getGlobalPos(150, -25, tx, ty, angleDeg);

            // Left fluxes
            for(let i=0; i<3; i++) {
                let offset = (i-1) * 15;
                let d = `M ${mLeft.x + offset} ${mLeft.y} Q ${mLeft.x + offset/2} ${(mLeft.y + bLeft.y)/2} ${bLeft.x} ${bLeft.y}`;
                fluxes[i].setAttribute('d', d);
                if (isBroken) {
                    fluxes[i].classList.remove('active');
                    fluxes[i].classList.add('broken');
                } else {
                    fluxes[i].classList.add('active');
                    fluxes[i].classList.remove('broken');
                }
            }
            
            // Right fluxes
            for(let i=0; i<3; i++) {
                let offset = (i-1) * 15;
                let d = `M ${mRight.x + offset} ${mRight.y} Q ${mRight.x + offset/2} ${(mRight.y + bRight.y)/2} ${bRight.x} ${bRight.y}`;
                fluxes[i+3].setAttribute('d', d);
                if (isBroken) {
                    fluxes[i+3].classList.remove('active');
                    fluxes[i+3].classList.add('broken');
                } else {
                    fluxes[i+3].classList.add('active');
                    fluxes[i+3].classList.remove('broken');
                }
            }
            
            // Fade out completely if too far
            const dist = ty - 205;
            fluxes.forEach(f => {
                if (dist > 50 && isBroken) f.style.opacity = Math.max(0, 0.4 - (dist-50)/100);
                else f.style.opacity = '';
            });
        }

        // Update central bellows tube
        function updateBellows(tx, ty, angleDeg) {
            // Upper anchor
            const upX = 500, upY = 180;
            // Lower anchor (center of lower plate local (0,-10))
            const lowPt = getGlobalPos(0, -10, tx, ty, angleDeg);
            
            // Draw smooth S-curve
            const cpY = upY + (lowPt.y - upY) * 0.5;
            const d = `M ${upX} ${upY} C ${upX} ${cpY}, ${lowPt.x} ${cpY}, ${lowPt.x} ${lowPt.y}`;
            
            bellowsBase.setAttribute('d', d);
            bellowsRidges.setAttribute('d', d);
        }

        // Main Animation Loop
        function render() {
            let now = Date.now();
            let elapsed = now - startTime;
            
            // Timeline (Total 8000ms per cycle)
            let cycleTime = isAutoPlay ? (elapsed % 8000) : (now - manualTriggerTime);
            if (!isAutoPlay && cycleTime > 8000) cycleTime = 8000; // Hold end state

            let tx = 500;
            let ty = 205;
            let angle = 0;
            
            let force = 200;
            let shear = 10;
            let statusText = "COUPLED";
            let statusClass = "cyan";
            let isBroken = false;
            let showImpact = false;
            let upperShakeX = 0;

            // Phase logic
            if (cycleTime < 2000) {
                // 1. Idle Coupled (0 - 2s)
                shear = 10 + Math.sin(cycleTime/150)*5;
                trizBox.classList.remove('highlight');
            } 
            else if (cycleTime < 2100) {
                // 2. Impact Hit (2.0 - 2.1s)
                let p = (cycleTime - 2000) / 100;
                shear = 10 + p * 800; // Spike
                angle = easeInExpo(p) * -CRITICAL_ANGLE;
                showImpact = true;
                upperShakeX = (Math.random() - 0.5) * 6; // Shake
                statusText = "IMPACT DETECTED";
                statusClass = "red";
                trizBox.classList.add('highlight');
            }
            else if (cycleTime < 2600) {
                // 3. Breakaway & Drop (2.1 - 2.6s)
                let p = (cycleTime - 2100) / 500;
                isBroken = true;
                shear = 0;
                force = 200 * (1 - easeOutQuint(p));
                statusText = "FUSE BLOWN - SEPARATED";
                statusClass = "red";
                
                if (p < 0.3) {
                    // Pivot fast
                    let p2 = p / 0.3;
                    angle = -CRITICAL_ANGLE + easeOutQuint(p2) * -12;
                } else {
                    // Drop down and swing
                    let p2 = (p - 0.3) / 0.7;
                    angle = -15 + p2 * 7; // Swing back slightly to -8
                    ty = 205 + easeOutQuint(p2) * 85;
                    tx = 500 - easeOutQuint(p2) * 25;
                }
            }
            else if (cycleTime < 4500) {
                // 4. Hanging Safe (2.6 - 4.5s)
                isBroken = true;
                shear = 0;
                force = 0;
                angle = -8 + Math.sin(cycleTime/300)*1.5; // Dangling swing
                ty = 290;
                tx = 475;
                statusText = "ENERGY RELEASED (SAFE)";
                statusClass = "cyan";
            }
            else if (cycleTime < 6000) {
                // 5. Recovery Approach (4.5 - 6.0s)
                let p = (cycleTime - 4500) / 1500;
                isBroken = true; // still technically broken but approaching
                shear = 0;
                statusText = "AUTO-RECOVERY...";
                
                ty = 290 - easeInOutCubic(p) * 75; // approach 215
                tx = 475 + easeInOutCubic(p) * 25; // approach 500
                angle = -8 + easeInOutCubic(p) * 8; // level out to 0
                
                if (p > 0.8) {
                    force = ((p - 0.8) / 0.2) * 100; // Magnets start pulling
                    if (cycleTime % 200 < 100) statusText = "MAGNETIC CAPTURE";
                }
                trizBox.classList.remove('highlight');
            }
            else if (cycleTime < 6200) {
                // 6. Magnetic Snap (6.0 - 6.2s)
                let p = (cycleTime - 6000) / 200;
                isBroken = false;
                shear = 0;
                force = 200;
                statusText = "LOCKED";
                
                ty = 215 - easeOutBack(p) * 10; // snap to 205 with slight overshoot
                tx = 500;
                angle = 0;
                
                // Visual snap flash
                magLeft.classList.add('active');
                magRight.classList.add('active');
            }
            else {
                // 7. Stabilize (6.2 - 8.0s)
                shear = 10;
                force = 200;
            }

            // Apply transforms
            lowerAssembly.setAttribute('transform', `translate(${tx}, ${ty}) rotate(${angle}, ${PIVOT_L_X}, ${PIVOT_L_Y})`);
            upperAssembly.setAttribute('transform', `translate(${upperShakeX}, 0)`);
            
            // Impact Arrow
            if (showImpact) {
                impactArrow.style.opacity = 1;
                impactArrow.setAttribute('transform', `translate(${850 - (cycleTime-2000)}, 215)`);
            } else {
                impactArrow.style.opacity = 0;
                impactArrow.setAttribute('transform', `translate(850, 215)`);
            }

            // Magnets appearance
            if (isBroken) {
                magLeft.classList.remove('active');
                magRight.classList.remove('active');
            } else if (cycleTime < 6000 || cycleTime > 6300) {
                // subtle pulse when locked
                if (Math.sin(cycleTime/200) > 0.8) {
                    magLeft.classList.add('active');
                    magRight.classList.add('active');
                } else {
                    magLeft.classList.remove('active');
                    magRight.classList.remove('active');
                }
            }

            // Dynamic drawn elements
            updateFlux(tx, ty, angle, isBroken);
            updateBellows(tx, ty, angle);

            // Update HUD
            valStatus.textContent = statusText;
            valStatus.className = `hud-value ${statusClass}`;
            valForce.textContent = force.toFixed(1) + " N";
            valForce.className = force < 100 ? "hud-value red" : "hud-value cyan";
            valAngle.textContent = Math.abs(angle).toFixed(2) + "°";
            valAngle.className = Math.abs(angle) >= CRITICAL_ANGLE ? "hud-value red" : "hud-value cyan";
            valShear.textContent = shear.toFixed(1) + " N";
            valShear.className = shear > 50 ? "hud-value red" : "hud-value cyan";

            requestAnimationFrame(render);
        }

        // Controls Logic
        document.getElementById('btn-auto').addEventListener('click', (e) => {
            isAutoPlay = true;
            e.target.classList.add('active');
            document.getElementById('btn-trigger').classList.remove('active');
        });

        document.getElementById('btn-trigger').addEventListener('click', (e) => {
            isAutoPlay = false;
            manualTriggerTime = Date.now() - 1900; // Jump right before impact
            e.target.classList.add('active');
            document.getElementById('btn-auto').classList.remove('active');
        });

        // Start animation immediately on load
        requestAnimationFrame(render);
    </script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分