Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Interactive Water Cycle!</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script> | |
| <script type="module" src="https://unpkg.com/lucide@latest"></script> | |
| <style> | |
| /* Use Inter font */ | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| overscroll-behavior: none; /* Prevent pull-to-refresh */ | |
| background: linear-gradient(to bottom, #87CEEB 0%, #ADD8E6 100%); /* Sky gradient */ | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| overflow: hidden; /* Hide overflow to prevent scrollbars from animations */ | |
| } | |
| /* Scene container */ | |
| .scene { | |
| position: relative; | |
| width: 90vw; | |
| max-width: 800px; | |
| height: 500px; /* Fixed height for scene elements */ | |
| background: linear-gradient(to bottom, #ADD8E6 0%, #F0F8FF 60%, #90EE90 60%, #3CB371 100%); /* Sky/Land gradient */ | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.2); | |
| cursor: default; /* Default cursor for the scene */ | |
| } | |
| /* Sun element */ | |
| .sun { | |
| position: absolute; | |
| top: 30px; | |
| left: 50px; | |
| width: 80px; | |
| height: 80px; | |
| background-color: #FFD700; /* Gold */ | |
| border-radius: 50%; | |
| box-shadow: 0 0 20px #FFD700; | |
| cursor: pointer; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| z-index: 10; | |
| } | |
| .sun:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 0 30px #FFA500; /* Orange glow */ | |
| } | |
| /* Ocean element */ | |
| .ocean { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 60%; | |
| height: 100px; /* Approx 20% of scene height */ | |
| background: linear-gradient(to bottom, #1E90FF, #00008B); /* Blue gradient */ | |
| border-top-left-radius: 50% 20px; /* Wavy top */ | |
| border-top-right-radius: 50% 20px; | |
| cursor: pointer; | |
| z-index: 5; | |
| } | |
| .ocean:hover .wave { /* Subtle wave animation on hover */ | |
| transform: scaleY(1.1); | |
| } | |
| /* Simple wave effect */ | |
| .wave { | |
| position: absolute; | |
| top: -5px; | |
| left: 0; | |
| width: 100%; | |
| height: 10px; | |
| background: rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| transform-origin: bottom; | |
| transition: transform 0.5s ease-in-out; | |
| } | |
| /* Land element */ | |
| .land { | |
| position: absolute; | |
| bottom: 0; | |
| right: 0; | |
| width: 40%; | |
| height: 200px; /* Taller than ocean */ | |
| background: #8FBC8F; /* Dark Sea Green */ | |
| border-top-left-radius: 30% 50px; | |
| z-index: 1; | |
| } | |
| /* Tree element */ | |
| .tree { | |
| position: absolute; | |
| bottom: 100px; /* Base on the land */ | |
| right: 100px; | |
| cursor: pointer; | |
| z-index: 6; | |
| } | |
| .trunk { | |
| width: 15px; | |
| height: 50px; | |
| background-color: #8B4513; /* Saddle Brown */ | |
| margin: 0 auto; | |
| } | |
| .leaves { | |
| width: 50px; | |
| height: 50px; | |
| background-color: #228B22; /* Forest Green */ | |
| border-radius: 50%; | |
| position: relative; | |
| bottom: 10px; /* Overlap trunk slightly */ | |
| transition: transform 0.3s ease; | |
| } | |
| .tree:hover .leaves { | |
| transform: scale(1.1); | |
| } | |
| /* Cloud element */ | |
| .cloud { | |
| position: absolute; | |
| top: 60px; | |
| width: 100px; | |
| height: 40px; | |
| background-color: white; | |
| border-radius: 50px; | |
| opacity: 0.8; | |
| transition: background-color 1s ease, transform 0.5s ease, opacity 1s ease; | |
| cursor: pointer; | |
| z-index: 15; | |
| filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); | |
| } | |
| .cloud::before, .cloud::after { /* Cloud puffs */ | |
| content: ''; | |
| position: absolute; | |
| background-color: white; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| top: -20px; | |
| transition: background-color 1s ease; | |
| } | |
| .cloud::before { left: 15px; } | |
| .cloud::after { right: 15px; top: -15px; width: 60px; height: 45px;} | |
| .cloud.condensing { | |
| background-color: #D3D3D3; /* Light grey */ | |
| } | |
| .cloud.condensing::before, .cloud.condensing::after { | |
| background-color: #D3D3D3; | |
| } | |
| .cloud.ready { | |
| background-color: #778899; /* Light Slate Gray */ | |
| transform: scale(1.1); | |
| opacity: 1; | |
| } | |
| .cloud.ready::before, .cloud.ready::after { | |
| background-color: #778899; | |
| } | |
| .cloud.ready:hover { | |
| background-color: #708090; /* Slate Gray */ | |
| } | |
| .cloud.ready:hover::before, .cloud.ready:hover::after { | |
| background-color: #708090; | |
| } | |
| /* Evaporation animation */ | |
| @keyframes evaporate { | |
| 0% { transform: translateY(0) scale(1); opacity: 0.6; } | |
| 100% { transform: translateY(-100px) scale(0.5); opacity: 0; } | |
| } | |
| .vapor { | |
| position: absolute; | |
| width: 5px; | |
| height: 20px; | |
| background: linear-gradient(to top, rgba(255,255,255,0.1), rgba(255,255,255,0.6)); | |
| border-radius: 50%; | |
| animation: evaporate 2s ease-out infinite; | |
| opacity: 0; /* Start hidden */ | |
| pointer-events: none; /* Don't interfere with clicks */ | |
| z-index: 7; | |
| } | |
| /* Precipitation animation */ | |
| @keyframes fall { | |
| 0% { transform: translateY(0); opacity: 1; } | |
| 100% { transform: translateY(150px); opacity: 0; } /* Fall towards ocean/land */ | |
| } | |
| .raindrop { | |
| position: absolute; | |
| width: 3px; | |
| height: 10px; | |
| background: linear-gradient(to bottom, rgba(173, 216, 230, 0.1), rgba(70, 130, 180, 1)); /* Light to Steel Blue */ | |
| border-radius: 50%; | |
| animation: fall 1s linear infinite; | |
| opacity: 0; /* Start hidden */ | |
| pointer-events: none; | |
| z-index: 14; | |
| } | |
| /* Info text */ | |
| #info-text { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: rgba(0, 0, 0, 0.6); | |
| color: white; | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| z-index: 20; | |
| text-align: center; | |
| opacity: 0; | |
| transition: opacity 0.5s ease; | |
| pointer-events: none; /* Don't block clicks */ | |
| } | |
| #info-text.visible { | |
| opacity: 1; | |
| } | |
| /* Control buttons */ | |
| .controls { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| display: flex; | |
| gap: 10px; | |
| z-index: 25; | |
| } | |
| .control-btn { | |
| background-color: rgba(255, 255, 255, 0.8); | |
| border: none; | |
| border-radius: 50%; | |
| padding: 8px; | |
| cursor: pointer; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| transition: background-color 0.2s ease, transform 0.2s ease; | |
| } | |
| .control-btn:hover { | |
| background-color: white; | |
| transform: scale(1.1); | |
| } | |
| .control-btn svg { | |
| width: 20px; | |
| height: 20px; | |
| stroke: #333; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="scene"> | |
| <div id="sun" class="sun" title="Click the Sun to start Evaporation"></div> | |
| <div id="ocean" class="ocean" title="Click the Ocean for Evaporation"> | |
| <div class="wave"></div> | |
| </div> | |
| <div class="land"></div> | |
| <div id="tree" class="tree" title="Click the Tree for Transpiration"> | |
| <div class="leaves"></div> | |
| <div class="trunk"></div> | |
| </div> | |
| <div id="cloud-container"></div> | |
| <div id="rain-container"></div> | |
| <div id="info-text">Water Cycle Stage</div> | |
| <div class="controls"> | |
| <button id="sound-toggle" class="control-btn" title="Toggle Sound"> | |
| <i data-lucide="volume-2"></i> </button> | |
| <button id="share-twitter" class="control-btn" title="Share on Twitter"> | |
| <i data-lucide="twitter"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| // --- DOM Elements --- | |
| const sun = document.getElementById('sun'); | |
| const ocean = document.getElementById('ocean'); | |
| const tree = document.getElementById('tree'); | |
| const cloudContainer = document.getElementById('cloud-container'); | |
| const rainContainer = document.getElementById('rain-container'); | |
| const infoText = document.getElementById('info-text'); | |
| const soundToggleButton = document.getElementById('sound-toggle'); | |
| const shareTwitterButton = document.getElementById('share-twitter'); | |
| const scene = document.querySelector('.scene'); // Get scene dimensions | |
| // --- State Variables --- | |
| let evaporationRate = 0; // How much water vapor is generated | |
| let condensationLevel = 0; // How much vapor is in clouds | |
| let clouds = []; // Array to hold cloud elements | |
| let isRaining = false; | |
| let soundEnabled = true; | |
| let rainSynth; // Tone.js synth for rain sound | |
| // --- Sound Setup (Tone.js) --- | |
| function setupSound() { | |
| // Simple rain sound using noise synth | |
| rainSynth = new Tone.NoiseSynth({ | |
| noise: { type: 'pink' }, | |
| envelope: { attack: 0.1, decay: 0.1, sustain: 0.5, release: 0.2 } | |
| }).toDestination(); | |
| // Adjust volume | |
| rainSynth.volume.value = -20; // Quieter volume in dB | |
| } | |
| function playRainSound() { | |
| if (soundEnabled && rainSynth && !rainSynth.triggered) { | |
| // Start the noise envelope - simulates continuous rain drops | |
| rainSynth.triggerAttack(); | |
| // We need to manually stop it when rain stops | |
| } | |
| } | |
| function stopRainSound() { | |
| if (rainSynth && rainSynth.signal.value > 0) { // Check if it's playing | |
| rainSynth.triggerRelease(); | |
| } | |
| } | |
| // Initialize sound on first interaction (required by browsers) | |
| document.body.addEventListener('click', () => { | |
| if (Tone.context.state !== 'running') { | |
| Tone.start(); | |
| console.log('AudioContext started'); | |
| } | |
| if (!rainSynth) { | |
| setupSound(); // Setup synth after user interaction | |
| } | |
| }, { once: true }); // Run only once | |
| // --- Helper Functions --- | |
| function showInfo(text, duration = 3000) { | |
| infoText.textContent = text; | |
| infoText.classList.add('visible'); | |
| setTimeout(() => { | |
| infoText.classList.remove('visible'); | |
| }, duration); | |
| } | |
| function getRandomPosition(containerWidth, containerHeight, elementWidth, elementHeight) { | |
| const x = Math.random() * (containerWidth - elementWidth); | |
| const y = Math.random() * (containerHeight - elementHeight); | |
| return { x, y }; | |
| } | |
| // --- Simulation Logic --- | |
| // 1. Evaporation / Transpiration | |
| function triggerEvaporation(sourceElement, intensity = 1) { | |
| if (isRaining) return; // Don't evaporate while raining | |
| showInfo('Evaporation: Water turns into vapor and rises!'); | |
| evaporationRate += intensity; | |
| // Create vapor particles rising from the source | |
| const sourceRect = sourceElement.getBoundingClientRect(); | |
| const sceneRect = scene.getBoundingClientRect(); // Use scene for positioning | |
| const startX = sourceRect.left - sceneRect.left + (sourceRect.width / 2); | |
| let startY = sourceRect.top - sceneRect.top + 5; // Start slightly above surface | |
| // Adjust startY for tree transpiration to come from leaves | |
| if (sourceElement.id === 'tree') { | |
| const leaves = sourceElement.querySelector('.leaves'); | |
| const leavesRect = leaves.getBoundingClientRect(); | |
| startY = leavesRect.top - sceneRect.top; | |
| } | |
| for (let i = 0; i < intensity * 2; i++) { | |
| const vapor = document.createElement('div'); | |
| vapor.classList.add('vapor'); | |
| vapor.style.left = `${startX + (Math.random() * 40 - 20)}px`; // Spread horizontally | |
| vapor.style.top = `${startY}px`; | |
| vapor.style.animationDelay = `${Math.random() * 0.5}s`; // Stagger animation | |
| scene.appendChild(vapor); | |
| // Remove vapor after animation ends | |
| vapor.addEventListener('animationiteration', () => { | |
| vapor.remove(); | |
| }, { once: true }); | |
| } | |
| // Increase condensation level (with a cap) | |
| condensationLevel = Math.min(condensationLevel + intensity, 100); // Cap at 100 | |
| updateClouds(); | |
| } | |
| // 2. Condensation (Updating Clouds) | |
| function updateClouds() { | |
| // Create initial clouds if none exist | |
| if (clouds.length === 0 && condensationLevel > 5) { | |
| createCloud(); | |
| createCloud(); // Start with a couple | |
| } | |
| // Update existing clouds based on condensation level | |
| clouds.forEach(cloud => { | |
| const cloudElement = document.getElementById(cloud.id); | |
| if (!cloudElement) return; // Skip if somehow removed | |
| if (condensationLevel > 70 && !cloudElement.classList.contains('ready')) { | |
| cloudElement.classList.remove('condensing'); | |
| cloudElement.classList.add('ready'); | |
| cloudElement.title = "Click the Cloud to make it Rain!"; | |
| } else if (condensationLevel > 30 && !cloudElement.classList.contains('condensing') && !cloudElement.classList.contains('ready')) { | |
| cloudElement.classList.add('condensing'); | |
| cloudElement.title = "Cloud is forming..."; | |
| } else if (condensationLevel <= 30) { | |
| cloudElement.classList.remove('condensing', 'ready'); | |
| cloudElement.title = "Just a little cloud"; | |
| } | |
| }); | |
| } | |
| function createCloud() { | |
| const cloudId = `cloud-${Date.now()}-${Math.random()}`; | |
| const cloudElement = document.createElement('div'); | |
| cloudElement.id = cloudId; | |
| cloudElement.classList.add('cloud'); | |
| cloudElement.title = "Just a little cloud"; | |
| // Position randomly in the upper part of the scene | |
| const sceneWidth = scene.offsetWidth; | |
| const cloudWidth = 100; | |
| const cloudHeight = 40; // Base height | |
| const maxCloudY = 150; // Don't go too low initially | |
| const { x, y } = getRandomPosition(sceneWidth, maxCloudY, cloudWidth, cloudHeight); | |
| cloudElement.style.left = `${x}px`; | |
| cloudElement.style.top = `${y + 20}px`; // Add offset from top | |
| cloudElement.addEventListener('click', () => triggerPrecipitation(cloudElement)); | |
| cloudContainer.appendChild(cloudElement); | |
| clouds.push({ id: cloudId, element: cloudElement }); | |
| updateClouds(); // Apply initial state based on condensationLevel | |
| } | |
| // 3. Precipitation | |
| function triggerPrecipitation(cloudElement) { | |
| if (!cloudElement.classList.contains('ready') || isRaining) { | |
| showInfo(cloudElement.classList.contains('ready') ? 'It\'s already raining!' : 'Cloud isn\'t full enough for rain yet!'); | |
| return; | |
| } | |
| showInfo('Precipitation: Water falls back to Earth as rain!'); | |
| isRaining = true; | |
| playRainSound(); // Start rain sound | |
| // Make it rain from under the cloud | |
| const cloudRect = cloudElement.getBoundingClientRect(); | |
| const sceneRect = scene.getBoundingClientRect(); | |
| const startX = cloudRect.left - sceneRect.left; | |
| const startY = cloudRect.bottom - sceneRect.top - 10; // Start below cloud puffs | |
| const cloudWidth = cloudRect.width; | |
| const rainDuration = 3000; // Rain for 3 seconds | |
| const intervalId = setInterval(() => { | |
| for (let i = 0; i < 5; i++) { // Number of drops per interval | |
| const drop = document.createElement('div'); | |
| drop.classList.add('raindrop'); | |
| drop.style.left = `${startX + Math.random() * cloudWidth}px`; | |
| drop.style.top = `${startY + Math.random() * 10}px`; // Slight vertical spread | |
| drop.style.animationDuration = `${0.5 + Math.random() * 0.5}s`; // Vary fall speed | |
| rainContainer.appendChild(drop); | |
| // Remove drop after animation | |
| drop.addEventListener('animationiteration', () => { | |
| drop.remove(); | |
| }, { once: true }); | |
| } | |
| }, 100); // Create drops every 100ms | |
| // Stop raining after duration | |
| setTimeout(() => { | |
| clearInterval(intervalId); | |
| isRaining = false; | |
| stopRainSound(); // Stop rain sound | |
| condensationLevel = 0; // Reset condensation | |
| evaporationRate = 0; // Reset evaporation | |
| updateClouds(); // Reset cloud appearance | |
| // Optionally remove clouds or make them fade | |
| cloudElement.classList.remove('ready', 'condensing'); | |
| cloudElement.title = "Just a little cloud"; | |
| showInfo('Collection: Water gathers in oceans and lakes.'); | |
| }, rainDuration); | |
| } | |
| // --- Event Listeners --- | |
| sun.addEventListener('click', () => triggerEvaporation(ocean, 5)); // More intense from sun | |
| ocean.addEventListener('click', () => triggerEvaporation(ocean, 2)); | |
| tree.addEventListener('click', () => triggerEvaporation(tree, 1)); // Transpiration is less intense | |
| // Sound Toggle Button | |
| soundToggleButton.addEventListener('click', () => { | |
| soundEnabled = !soundEnabled; | |
| const icon = soundToggleButton.querySelector('i'); | |
| if (soundEnabled) { | |
| icon.setAttribute('data-lucide', 'volume-2'); | |
| showInfo('Sound On', 1000); | |
| // If it was raining and sound was off, start sound now | |
| if (isRaining && !rainSynth.triggered) playRainSound(); | |
| } else { | |
| icon.setAttribute('data-lucide', 'volume-x'); | |
| showInfo('Sound Off', 1000); | |
| stopRainSound(); // Stop sound immediately if disabled | |
| } | |
| // Re-render the icon using Lucide's library function | |
| lucide.createIcons(); | |
| }); | |
| // Twitter Share Button | |
| shareTwitterButton.addEventListener('click', () => { | |
| const text = encodeURIComponent("Look! I made it rain 🌧️ exploring the water cycle with this cool simulation! #WaterCycle #ScienceForKids #EdTech"); | |
| // Get current URL - replace with actual deployed URL if needed | |
| const url = encodeURIComponent(window.location.href || "https://huggingface.co/spaces/pp/WaterCycle"); // Replace placeholder | |
| window.open(`https://twitter.com/intent/tweet?text=${text}&url=${url}`, '_blank'); | |
| }); | |
| // --- Initial Setup --- | |
| // Create a couple of initial placeholder clouds (optional) | |
| // createCloud(); | |
| // createCloud(); | |
| showInfo("Click the Sun, Ocean, or Tree to start the water cycle!", 5000); | |
| // Ensure Lucide icons render initially | |
| lucide.createIcons(); | |
| </script> | |
| </body> | |
| </html> | |