独立渲染引擎就绪引擎就绪
<!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分
等待动画代码生成...
