分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TRIZ IFR: 高保真反向伞原理可视化</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Outfit:wght@300;500;700&display=swap" rel="stylesheet">
    <style>
        :root {
            --bg-base: #060913;
            --bg-panel: rgba(14, 20, 36, 0.7);
            --border-glow: rgba(0, 240, 255, 0.3);
            --text-main: #e2e8f0;
            --text-muted: #94a3b8;
            --accent-cyan: #00f0ff;
            --accent-cyan-dim: rgba(0, 240, 255, 0.15);
            --accent-orange: #ff9d00;
            --accent-orange-dim: rgba(255, 157, 0, 0.15);
            --font-ui: 'Outfit', system-ui, sans-serif;
            --font-mono: 'JetBrains Mono', monospace;
        }

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

        body {
            background-color: var(--bg-base);
            color: var(--text-main);
            font-family: var(--font-ui);
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            background-image: 
                radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.05) 0%, transparent 60%),
                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;
        }

        .container {
            width: 95vw;
            height: 90vh;
            max-width: 1400px;
            display: grid;
            grid-template-columns: 350px 1fr;
            gap: 24px;
            padding: 24px;
            backdrop-filter: blur(10px);
            border-radius: 20px;
            border: 1px solid rgba(255, 255, 255, 0.05);
            box-shadow: 0 0 40px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 240, 255, 0.05);
        }

        /* 左侧信息面板 */
        .panel {
            background: var(--bg-panel);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 16px;
            padding: 24px;
            display: flex;
            flex-direction: column;
            gap: 20px;
            position: relative;
            overflow: hidden;
        }

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

        .header h1 {
            font-size: 1.5rem;
            font-weight: 700;
            letter-spacing: 1px;
            color: #fff;
            margin-bottom: 8px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .header h1::before {
            content: '';
            display: block;
            width: 12px;
            height: 12px;
            background: var(--accent-cyan);
            border-radius: 2px;
            box-shadow: 0 0 10px var(--accent-cyan);
        }

        .desc {
            font-size: 0.9rem;
            color: var(--text-muted);
            line-height: 1.6;
        }

        .ifr-box {
            background: rgba(0, 240, 255, 0.03);
            border-left: 3px solid var(--accent-cyan);
            padding: 16px;
            border-radius: 0 8px 8px 0;
        }

        .ifr-title {
            font-family: var(--font-mono);
            font-size: 0.75rem;
            color: var(--accent-cyan);
            text-transform: uppercase;
            letter-spacing: 1.5px;
            margin-bottom: 6px;
        }

        .ifr-content {
            font-size: 0.95rem;
            font-weight: 500;
        }

        /* 交互控制区 */
        .controls {
            margin-top: auto;
            padding-top: 20px;
            border-top: 1px dashed rgba(255, 255, 255, 0.1);
        }

        .slider-label {
            display: flex;
            justify-content: space-between;
            font-family: var(--font-mono);
            font-size: 0.8rem;
            color: var(--text-muted);
            margin-bottom: 12px;
        }

        input[type=range] {
            -webkit-appearance: none;
            width: 100%;
            background: transparent;
            cursor: pointer;
        }

        input[type=range]:focus {
            outline: none;
        }

        input[type=range]::-webkit-slider-runnable-track {
            width: 100%;
            height: 6px;
            background: #1e293b;
            border-radius: 3px;
            border: 1px solid #334155;
        }

        input[type=range]::-webkit-slider-thumb {
            height: 18px;
            width: 18px;
            border-radius: 50%;
            background: var(--accent-cyan);
            -webkit-appearance: none;
            margin-top: -7px;
            box-shadow: 0 0 15px var(--accent-cyan);
            transition: transform 0.1s;
        }

        input[type=range]::-webkit-slider-thumb:hover {
            transform: scale(1.2);
        }

        .control-btns {
            display: flex;
            gap: 10px;
            margin-top: 16px;
        }

        button {
            flex: 1;
            background: rgba(255, 255, 255, 0.05);
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: #fff;
            padding: 10px;
            border-radius: 6px;
            font-family: var(--font-mono);
            font-size: 0.8rem;
            cursor: pointer;
            transition: all 0.2s;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

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

        button.active {
            background: var(--accent-cyan-dim);
            color: var(--accent-cyan);
            border-color: var(--accent-cyan);
        }

        /* 右侧渲染区 */
        .canvas-container {
            position: relative;
            background: radial-gradient(circle at center, #111827 0%, #030712 100%);
            border-radius: 16px;
            border: 1px solid rgba(255, 255, 255, 0.05);
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: inset 0 0 50px rgba(0,0,0,0.8);
        }

        svg {
            width: 100%;
            height: 100%;
            max-height: 800px;
        }

        /* 遥测数据悬浮窗 */
        .telemetry {
            position: absolute;
            top: 24px;
            right: 24px;
            background: rgba(3, 7, 18, 0.8);
            backdrop-filter: blur(5px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 8px;
            padding: 16px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            display: flex;
            flex-direction: column;
            gap: 12px;
            min-width: 200px;
        }

        .data-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .data-label { color: var(--text-muted); }
        .data-value { 
            color: #fff;
            font-weight: 700;
            text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
        }
        .data-value.cyan { color: var(--accent-cyan); text-shadow: 0 0 8px var(--accent-cyan); }
        .data-value.orange { color: var(--accent-orange); text-shadow: 0 0 8px var(--accent-orange); }

        /* 图例 */
        .legend {
            position: absolute;
            bottom: 24px;
            left: 24px;
            display: flex;
            gap: 20px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
        }
        
        .legend-item { display: flex; align-items: center; gap: 8px; color: var(--text-muted); }
        .legend-color { width: 12px; height: 12px; border-radius: 3px; }
        .lc-wet { background: var(--accent-cyan); box-shadow: 0 0 8px var(--accent-cyan); }
        .lc-dry { background: var(--accent-orange); box-shadow: 0 0 8px var(--accent-orange); }

    </style>
</head>
<body>

<div class="container">
    <!-- 左侧控制面板 -->
    <div class="panel">
        <div class="header">
            <h1>反向伞原理中枢</h1>
            <div class="desc">
                解决传统雨伞收拢时湿面朝外、雨水滴落的环境污染矛盾。通过机构拓扑反转,重构收展时序。
            </div>
        </div>

        <div class="ifr-box">
            <div class="ifr-title">TRIZ - 最终理想解 (IFR)</div>
            <div class="ifr-content">
                系统利用原有的开合动力源,在不增加外部组件的情况下,自主将有害的“淋湿面”包裹于内部,同时将安全的“干燥面”暴露于外部环境。
            </div>
        </div>

        <div class="controls">
            <div class="slider-label">
                <span>展开状态 (撑伞)</span>
                <span>收拢状态 (闭合)</span>
            </div>
            <input type="range" id="state-slider" min="0" max="100" value="0">
            
            <div class="control-btns">
                <button id="btn-auto" class="active">AUTO PLAY</button>
                <button id="btn-manual">MANUAL</button>
            </div>
        </div>
    </div>

    <!-- 右侧可视化画布 -->
    <div class="canvas-container">
        <!-- 遥测数据 -->
        <div class="telemetry">
            <div class="data-row">
                <span class="data-label">SYS_STATE</span>
                <span class="data-value" id="val-state">OPEN (RAIN)</span>
            </div>
            <div class="data-row">
                <span class="data-label">RIB_ANGLE</span>
                <span class="data-value" id="val-angle">25.0°</span>
            </div>
            <div class="data-row">
                <span class="data-label">LAYER_GAP</span>
                <span class="data-value" id="val-gap">30.0 mm</span>
            </div>
            <div class="data-row" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1);">
                <span class="data-label">WET_SURFACE</span>
                <span class="data-value cyan" id="val-wet">EXPOSED</span>
            </div>
            <div class="data-row">
                <span class="data-label">DRY_SURFACE</span>
                <span class="data-value orange" id="val-dry">SHIELDED</span>
            </div>
        </div>

        <!-- 图例 -->
        <div class="legend">
            <div class="legend-item"><div class="legend-color lc-wet"></div>外层防雨层 (湿)</div>
            <div class="legend-item"><div class="legend-color lc-dry"></div>内层透气层 (干)</div>
        </div>

        <!-- SVG 核心动画区 -->
        <svg id="umbrella-stage" viewBox="0 0 800 800" preserveAspectRatio="xMidYMid meet">
            <defs>
                <!-- 滤镜与发光效果 -->
                <filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="6" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                <filter id="glow-orange" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="5" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                
                <!-- 渐变定义 -->
                <linearGradient id="grad-wet" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#06b6d4" stop-opacity="0.8"/>
                    <stop offset="100%" stop-color="#0891b2" stop-opacity="0.2"/>
                </linearGradient>
                <linearGradient id="grad-dry" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#fbbf24" stop-opacity="0.8"/>
                    <stop offset="100%" stop-color="#f59e0b" stop-opacity="0.2"/>
                </linearGradient>

                <!-- 雨滴模板 -->
                <path id="drop" d="M0,0 C2,4 4,8 0,12 C-4,8 -2,4 0,0 Z" fill="#00f0ff" opacity="0.6"/>
            </defs>

            <!-- 坐标网格背景 -->
            <g id="grid" stroke="rgba(255,255,255,0.05)" stroke-width="1">
                <line x1="400" y1="0" x2="400" y2="800" stroke-dasharray="4 4" />
                <line x1="0" y1="400" x2="800" y2="400" stroke-dasharray="4 4" />
                <circle cx="400" cy="400" r="150" fill="none" stroke="rgba(255,255,255,0.02)"/>
                <circle cx="400" cy="400" r="300" fill="none" stroke="rgba(255,255,255,0.02)"/>
            </g>

            <!-- 动态雨水层 (由JS生成控制) -->
            <g id="rain-layer"></g>

            <!-- 伞体主结构组 -->
            <g id="umbrella-rig">
                <!-- 中轴杆 -->
                <rect x="394" y="200" width="12" height="450" fill="#334155" rx="4"/>
                <rect x="396" y="200" width="4" height="450" fill="#94a3b8" rx="2"/>
                
                <!-- 底部伞把 -->
                <rect x="385" y="650" width="30" height="60" fill="#1e293b" rx="8" stroke="#475569" stroke-width="2"/>
                
                <!-- 顶部固定巢 (Top Hub) -->
                <path d="M 380 200 L 420 200 L 410 230 L 390 230 Z" fill="#475569" />
                <circle cx="400" cy="215" r="4" fill="#00f0ff" filter="url(#glow-cyan)"/>

                <!-- 伞面路径 (动态生成) -->
                <!-- 内层干面 (橘色) -->
                <path id="canopy-inner" d="" fill="none" stroke="var(--accent-orange)" stroke-width="4" stroke-linecap="round" filter="url(#glow-orange)"/>
                <path id="canopy-inner-fill" d="" fill="url(#grad-dry)" />
                
                <!-- 外层湿面 (青色) -->
                <path id="canopy-outer" d="" fill="none" stroke="var(--accent-cyan)" stroke-width="5" stroke-linecap="round" filter="url(#glow-cyan)"/>
                <path id="canopy-outer-fill" d="" fill="url(#grad-wet)" />

                <!-- 伞骨联动机构 (动态更新) -->
                <g id="ribs" stroke="#cbd5e1" stroke-width="3" stroke-linecap="round">
                    <!-- 左侧主骨 -->
                    <line id="rib-l" x1="390" y1="215" x2="200" y2="350" />
                    <!-- 右侧主骨 -->
                    <line id="rib-r" x1="410" y1="215" x2="600" y2="350" />
                    
                    <!-- 支撑杆 (Stretcher) -->
                    <line id="stretcher-l" x1="390" y1="350" x2="295" y2="282.5" stroke="#64748b" stroke-width="2"/>
                    <line id="stretcher-r" x1="410" y1="350" x2="505" y2="282.5" stroke="#64748b" stroke-width="2"/>
                </g>

                <!-- 活动滑块 (Slider) -->
                <g id="slider-group" transform="translate(0, 350)">
                    <rect x="382" y="-15" width="36" height="30" fill="#3b82f6" rx="4" filter="url(#glow-cyan)"/>
                    <line x1="385" y1="0" x2="415" y2="0" stroke="#fff" stroke-width="2"/>
                </g>
                
                <!-- 内部截留水滴指示 (收拢时显示) -->
                <g id="trapped-water" opacity="0" transition="opacity 0.3s">
                    <circle cx="390" cy="260" r="3" fill="#00f0ff" filter="url(#glow-cyan)"/>
                    <circle cx="410" cy="275" r="2.5" fill="#00f0ff" filter="url(#glow-cyan)"/>
                    <circle cx="400" cy="285" r="4" fill="#00f0ff" filter="url(#glow-cyan)"/>
                    <path d="M 390 240 Q 400 290 410 240 Z" fill="#00f0ff" opacity="0.3" filter="url(#glow-cyan)"/>
                </g>

            </g>
        </svg>
    </div>
</div>

<script>
/**
 * 高保真反向伞核心物理与渲染引擎
 */
class InverseUmbrella {
    constructor() {
        // DOM Elements
        this.slider = document.getElementById('state-slider');
        this.btnAuto = document.getElementById('btn-auto');
        this.btnManual = document.getElementById('btn-manual');
        
        // SVG Elements
        this.ribL = document.getElementById('rib-l');
        this.ribR = document.getElementById('rib-r');
        this.stretcherL = document.getElementById('stretcher-l');
        this.stretcherR = document.getElementById('stretcher-r');
        this.sliderGroup = document.getElementById('slider-group');
        this.canopyOuter = document.getElementById('canopy-outer');
        this.canopyOuterFill = document.getElementById('canopy-outer-fill');
        this.canopyInner = document.getElementById('canopy-inner');
        this.canopyInnerFill = document.getElementById('canopy-inner-fill');
        this.trappedWater = document.getElementById('trapped-water');
        this.rainLayer = document.getElementById('rain-layer');
        
        // Telemetry Elements
        this.valState = document.getElementById('val-state');
        this.valAngle = document.getElementById('val-angle');
        this.valGap = document.getElementById('val-gap');
        this.valWet = document.getElementById('val-wet');
        this.valDry = document.getElementById('val-dry');

        // Physical Constants
        this.hubTop = { x: 400, y: 215 };
        this.ribLength = 260;
        this.angleOpen = 25;   // 展开时,伞骨朝下 25度
        this.angleClosed = -82; // 收拢时,伞骨反向朝上 82度 (接近垂直)
        
        this.sliderYOpen = 300;
        this.sliderYClosed = 580; // 滑块向下滑动收伞
        
        // State
        this.progress = 0; // 0 = Open, 1 = Closed
        this.isAuto = true;
        this.animPhase = 0; // 0: WaitOpen, 1: Closing, 2: WaitClosed, 3: Opening
        this.lastTime = 0;
        this.phaseTimer = 0;
        
        // Rain particles
        this.particles = [];
        this.initRain();

        // Bindings
        this.bindEvents();
        
        // Start loop
        requestAnimationFrame(this.loop.bind(this));
    }

    initRain() {
        for(let i=0; i<40; i++) {
            const el = document.createElementNS("http://www.w3.org/2000/svg", "use");
            el.setAttribute("href", "#drop");
            this.rainLayer.appendChild(el);
            this.particles.push({
                el: el,
                x: 150 + Math.random() * 500,
                y: -50 - Math.random() * 800,
                speed: 15 + Math.random() * 10
            });
        }
    }

    bindEvents() {
        this.slider.addEventListener('input', (e) => {
            this.setManual();
            this.progress = e.target.value / 100;
            this.updateRender();
        });

        this.btnAuto.addEventListener('click', () => {
            this.isAuto = true;
            this.btnAuto.classList.add('active');
            this.btnManual.classList.remove('active');
            // Sync auto state with current slider pos
            this.progress = this.slider.value / 100;
            if(this.progress > 0.5) this.animPhase = 2; else this.animPhase = 0;
            this.phaseTimer = 0;
        });

        this.btnManual.addEventListener('click', () => this.setManual());
    }

    setManual() {
        this.isAuto = false;
        this.btnAuto.classList.remove('active');
        this.btnManual.classList.add('active');
    }

    // 核心缓动函数 (Ease In Out Cubic)
    easeInOut(t) {
        return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    }

    loop(timestamp) {
        if (!this.lastTime) this.lastTime = timestamp;
        const dt = timestamp - this.lastTime;
        this.lastTime = timestamp;

        if (this.isAuto) {
            this.phaseTimer += dt;
            
            // State Machine for Auto Play
            const holdTime = 2000;
            const moveTime = 1800;

            switch(this.animPhase) {
                case 0: // Holding Open
                    this.progress = 0;
                    if(this.phaseTimer > holdTime) { this.animPhase = 1; this.phaseTimer = 0; }
                    break;
                case 1: // Closing (Progress 0 -> 1)
                    let pC = this.phaseTimer / moveTime;
                    if (pC >= 1) { pC = 1; this.animPhase = 2; this.phaseTimer = 0; }
                    this.progress = this.easeInOut(pC);
                    break;
                case 2: // Holding Closed
                    this.progress = 1;
                    if(this.phaseTimer > holdTime) { this.animPhase = 3; this.phaseTimer = 0; }
                    break;
                case 3: // Opening (Progress 1 -> 0)
                    let pO = this.phaseTimer / moveTime;
                    if (pO >= 1) { pO = 1; this.animPhase = 0; this.phaseTimer = 0; }
                    this.progress = 1 - this.easeInOut(pO);
                    break;
            }
            
            // Sync slider UI
            this.slider.value = this.progress * 100;
        }

        this.updateRender();
        this.updateRain(dt);
        
        requestAnimationFrame(this.loop.bind(this));
    }

    updateRender() {
        const p = this.progress;
        
        // 1. Calculate main geometric parameters based on progress
        const currentAngleDeg = this.angleOpen + (this.angleClosed - this.angleOpen) * p;
        const currentAngleRad = currentAngleDeg * Math.PI / 180;
        
        const sliderY = this.sliderYOpen + (this.sliderYClosed - this.sliderYOpen) * p;
        
        // Right Rib endpoint
        const rx = this.hubTop.x + Math.sin(currentAngleRad) * this.ribLength;
        const ry = this.hubTop.y + Math.cos(currentAngleRad) * this.ribLength;
        
        // Left Rib endpoint (Symmetrical)
        const lx = this.hubTop.x - Math.sin(currentAngleRad) * this.ribLength;
        const ly = ry;

        // 2. Update SVG DOM Attributes
        // Slider
        this.sliderGroup.setAttribute('transform', `translate(0, ${sliderY})`);
        
        // Ribs
        this.ribR.setAttribute('x2', rx); this.ribR.setAttribute('y2', ry);
        this.ribL.setAttribute('x2', lx); this.ribL.setAttribute('y2', ly);
        
        // Stretchers (approximate midpoint to slider)
        const midRX = this.hubTop.x + Math.sin(currentAngleRad) * (this.ribLength * 0.4);
        const midRY = this.hubTop.y + Math.cos(currentAngleRad) * (this.ribLength * 0.4);
        const midLX = this.hubTop.x - Math.sin(currentAngleRad) * (this.ribLength * 0.4);
        const midLY = midRY;
        
        this.stretcherR.setAttribute('x1', 410); this.stretcherR.setAttribute('y1', sliderY);
        this.stretcherR.setAttribute('x2', midRX); this.stretcherR.setAttribute('y2', midRY);
        
        this.stretcherL.setAttribute('x1', 390); this.stretcherL.setAttribute('y1', sliderY);
        this.stretcherL.setAttribute('x2', midLX); this.stretcherL.setAttribute('y2', midLY);

        // 3. Generate High-Fidelity Canopy Paths
        // 外层(湿面)连接 TopHub 到 Rib 端点
        // 内层(干面)连接 Slider 到 Rib 端点
        
        // 控制点计算以模拟布料垂坠与张力
        // 展开时受重力略微下垂,收拢时受挤压变形
        const sagForceOuter = 40 * (1 - p); // 展开时下垂,收拢时拉直/挤压
        const sagForceInner = 60 * (1 - p); // 内层下垂更明显
        
        // 右侧外层控制点
        let cpOutRX = (this.hubTop.x + rx) / 2 + (p * 30); // 收拢时向外挤出一点弧度
        let cpOutRY = (this.hubTop.y + ry) / 2 + sagForceOuter;
        
        // 右侧内层控制点 (连接滑块)
        let cpInRX = (400 + rx) / 2 + (p * 50); 
        let cpInRY = (sliderY + ry) / 2 + sagForceInner;

        // 对称左侧
        let cpOutLX = 800 - cpOutRX; let cpOutLY = cpOutRY;
        let cpInLX = 800 - cpInRX;   let cpInLY = cpInRY;

        // Path Strings
        // Outer Path (Cyan) - 从左边边缘经过顶部中心到右边边缘
        const pathOuter = `M ${lx} ${ly} Q ${cpOutLX} ${cpOutLY} 400 ${this.hubTop.y} Q ${cpOutRX} ${cpOutRY} ${rx} ${ry}`;
        const fillOuter = `${pathOuter} L 400 ${this.hubTop.y} Z`; // Fill area
        
        // Inner Path (Orange) - 从左边边缘经过滑块到右边边缘
        const pathInner = `M ${lx} ${ly} Q ${cpInLX} ${cpInLY} 400 ${sliderY - 15} Q ${cpInRX} ${cpInRY} ${rx} ${ry}`;
        const fillInner = `${pathInner} L 400 ${sliderY} Z`;

        this.canopyOuter.setAttribute('d', pathOuter);
        this.canopyOuterFill.setAttribute('d', fillOuter);
        
        this.canopyInner.setAttribute('d', pathInner);
        this.canopyInnerFill.setAttribute('d', fillInner);

        // 4. Update Telemetry UI
        const currentAngleDisp = (currentAngleDeg).toFixed(1);
        this.valAngle.textContent = `${currentAngleDisp}°`;
        
        // Gap calculation: rough estimate based on Slider vs Hub distance when folded
        const gap = (30 + p * 120).toFixed(1); 
        this.valGap.textContent = `${gap} mm`;

        if (p < 0.1) {
            this.valState.textContent = "OPEN (RAIN)";
            this.valState.style.color = "#fff";
            this.valWet.textContent = "EXPOSED";
            this.valWet.style.color = "var(--accent-cyan)";
            this.valDry.textContent = "SHIELDED";
            this.valDry.style.color = "var(--text-muted)";
            this.trappedWater.style.opacity = 0;
        } else if (p > 0.9) {
            this.valState.textContent = "CLOSED (IFR REACHED)";
            this.valState.style.color = "var(--accent-orange)";
            this.valWet.textContent = "ISOLATED (INSIDE)";
            this.valWet.style.color = "var(--text-muted)";
            this.valDry.textContent = "EXPOSED (OUTSIDE)";
            this.valDry.style.color = "var(--accent-orange)";
            this.trappedWater.style.opacity = 1;
        } else {
            this.valState.textContent = "TRANSITIONING...";
            this.valState.style.color = "#fff";
            this.trappedWater.style.opacity = p * p; // 渐显
        }
        
        // Store collision data for rain
        this.rainCollider = {
            ly: ly, ry: ry,
            cpOutRY: cpOutRY,
            topY: this.hubTop.y
        };
    }

    updateRain(dt) {
        const p = this.progress;
        
        this.particles.forEach(pt => {
            pt.y += pt.speed * (dt/16);
            
            // Check collision with outer canopy roughly
            let hit = false;
            if (p < 0.5) {
                // Open state: bounce off top
                if (pt.y > this.rainCollider.topY && pt.y < this.rainCollider.ry) {
                    // Simple bounding curve check
                    const dx = Math.abs(pt.x - 400);
                    const maxY = this.rainCollider.topY + (dx / 300) * (this.rainCollider.ry - this.rainCollider.topY);
                    if (pt.y > maxY) hit = true;
                }
            }

            // Reset logic
            if (pt.y > 800 || hit || (p > 0.8 && Math.random() < 0.1)) {
                pt.y = -50 - Math.random() * 200;
                pt.x = 150 + Math.random() * 500;
                // If completely closed, stop rain visually to emphasize the "inside/safe" feeling
                if (p > 0.9) pt.y = -1000; 
            }

            pt.el.setAttribute("transform", `translate(${pt.x}, ${pt.y})`);
        });
    }
}

// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
    new InverseUmbrella();
});
</script>

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