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> | |