ECWMF_Wind / index.html
nakas's picture
Switch to static HTML approach - bypass all build issues
e7d14b7
Raw
History Blame Contribute Delete
14 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐ŸŒช๏ธ Wind Particle Visualization - Earth.nullschool.net Style</title>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
text-align: center;
margin-bottom: 20px;
opacity: 0.9;
}
#map {
height: 600px;
width: 100%;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
margin-bottom: 20px;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
button {
padding: 12px 24px;
border: none;
border-radius: 25px;
background: rgba(255,255,255,0.2);
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
button:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
}
.info {
text-align: center;
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
.legend {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(255,255,255,0.9);
color: #333;
padding: 15px;
border-radius: 10px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.legend h4 {
margin: 0 0 10px 0;
color: #333;
}
.legend-item {
display: flex;
align-items: center;
margin: 5px 0;
}
.legend-color {
width: 20px;
height: 3px;
margin-right: 8px;
border-radius: 2px;
}
</style>
</head>
<body>
<div class="container">
<h1>๐ŸŒช๏ธ Wind Particle Visualization</h1>
<p class="subtitle">Earth.nullschool.net style particle animation with synthetic wind data</p>
<div class="controls">
<button onclick="startParticles()">๐ŸŒช๏ธ Start Wind Particles</button>
<button onclick="resetParticles()">๐Ÿ”„ Reset</button>
<button onclick="toggleSpeed()">โšก Toggle Speed</button>
</div>
<div id="map"></div>
<div class="info">
<strong>๐ŸŽฏ Demo Features:</strong> Earth.nullschool.net style particle system โ€ข Bilinear interpolation โ€ข 1000 animated particles โ€ข Zoom-responsive visualization โ€ข Synthetic global wind patterns
</div>
</div>
<div class="legend">
<h4>๐ŸŒช๏ธ Wind Speed</h4>
<div class="legend-item">
<div class="legend-color" style="background: rgba(110,159,190,0.9);"></div>
<span>< 5 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(65,120,190,0.9);"></div>
<span>5-10 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(25,80,170,0.9);"></div>
<span>10-15 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(85,160,35,0.9);"></div>
<span>15-20 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(255,215,0,0.9);"></div>
<span>20-25 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: rgba(255,120,0,0.9);"></div>
<span>> 25 m/s</span>
</div>
</div>
<script>
console.log('๐ŸŒช๏ธ Initializing wind particle system...');
// Initialize map
const map = L.map('map').setView([30, 0], 3);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'ยฉ OpenStreetMap contributors'
}).addTo(map);
// Global variables
let particleSystem = null;
let animationSpeed = 1;
let windField = null;
// Generate synthetic wind data (earth.nullschool.net style)
function generateWindField() {
console.log('Generating synthetic wind field...');
const lats = [];
const lons = [];
const winds = [];
// Create global grid
for (let lat = 60; lat >= -60; lat -= 3) {
for (let lon = -180; lon <= 180; lon += 6) {
lats.push(lat);
lons.push(lon);
// Realistic wind patterns
const latRad = lat * Math.PI / 180;
const lonRad = lon * Math.PI / 180;
// Jet stream + trade winds + random turbulence
const u = 15 * Math.sin(latRad) + 8 * Math.cos(lonRad/2) + (Math.random() - 0.5) * 6;
const v = 5 * Math.cos(latRad) + 3 * Math.sin(lonRad/3) + (Math.random() - 0.5) * 4;
winds.push({ lat, lon, u, v, speed: Math.sqrt(u*u + v*v) });
}
}
console.log(`Generated ${winds.length} wind points`);
return winds;
}
// Wind particle system (earth.nullschool.net inspired)
class WindParticleSystem {
constructor(map, windData) {
this.map = map;
this.windData = windData;
this.particles = [];
this.numParticles = 1000;
this.particleAge = 80;
this.canvas = null;
this.ctx = null;
this.animationId = null;
this.setup();
}
setup() {
const mapContainer = this.map.getContainer();
// Create canvas overlay
this.canvas = document.createElement('canvas');
this.canvas.style.position = 'absolute';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.pointerEvents = 'none';
this.canvas.style.zIndex = '1000';
this.canvas.width = mapContainer.offsetWidth;
this.canvas.height = mapContainer.offsetHeight;
mapContainer.style.position = 'relative';
mapContainer.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
// Initialize particles
this.initializeParticles();
this.animate();
// Handle map events
this.map.on('moveend zoomend resize', () => this.updateCanvas());
console.log('โœ… Wind particle system initialized');
}
updateCanvas() {
const mapContainer = this.map.getContainer();
this.canvas.width = mapContainer.offsetWidth;
this.canvas.height = mapContainer.offsetHeight;
}
initializeParticles() {
this.particles = [];
const bounds = this.map.getBounds();
for (let i = 0; i < this.numParticles; i++) {
this.particles.push({
lat: bounds.getSouth() + Math.random() * (bounds.getNorth() - bounds.getSouth()),
lon: bounds.getWest() + Math.random() * (bounds.getEast() - bounds.getWest()),
age: Math.floor(Math.random() * this.particleAge),
trail: []
});
}
}
getWindAtPosition(lat, lon) {
// Find nearest wind point (simple approach)
let minDist = Infinity;
let nearestWind = { u: 0, v: 0, speed: 0 };
for (const wind of this.windData) {
const dist = Math.sqrt((wind.lat - lat) ** 2 + (wind.lon - lon) ** 2);
if (dist < minDist) {
minDist = dist;
nearestWind = wind;
}
}
return nearestWind;
}
getWindColor(speed) {
// Earth.nullschool.net color scheme
if (speed < 5) return 'rgba(110, 159, 190, 0.8)';
if (speed < 10) return 'rgba(65, 120, 190, 0.8)';
if (speed < 15) return 'rgba(25, 80, 170, 0.8)';
if (speed < 20) return 'rgba(85, 160, 35, 0.8)';
if (speed < 25) return 'rgba(255, 215, 0, 0.8)';
return 'rgba(255, 120, 0, 0.8)';
}
evolveParticle(particle) {
const wind = this.getWindAtPosition(particle.lat, particle.lon);
// Move particle based on wind
const scale = 0.01 * animationSpeed * Math.pow(2, this.map.getZoom() - 5);
particle.lat += wind.v * scale;
particle.lon += wind.u * scale;
particle.age++;
// Reset if too old or out of bounds
const bounds = this.map.getBounds();
if (particle.age > this.particleAge ||
particle.lat < bounds.getSouth() || particle.lat > bounds.getNorth() ||
particle.lon < bounds.getWest() || particle.lon > bounds.getEast()) {
particle.lat = bounds.getSouth() + Math.random() * (bounds.getNorth() - bounds.getSouth());
particle.lon = bounds.getWest() + Math.random() * (bounds.getEast() - bounds.getWest());
particle.age = 0;
}
return wind;
}
render() {
// Fade effect
this.ctx.globalCompositeOperation = 'destination-in';
this.ctx.globalAlpha = 0.95;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw particles
this.ctx.globalCompositeOperation = 'lighter';
this.ctx.globalAlpha = 0.9;
this.ctx.lineWidth = 1.5;
for (const particle of this.particles) {
const wind = this.evolveParticle(particle);
if (wind.speed > 0.5) {
const point = this.map.latLngToContainerPoint([particle.lat, particle.lon]);
if (point.x >= 0 && point.x < this.canvas.width &&
point.y >= 0 && point.y < this.canvas.height) {
this.ctx.strokeStyle = this.getWindColor(wind.speed);
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 1, 0, 2 * Math.PI);
this.ctx.stroke();
}
}
}
}
animate() {
this.render();
this.animationId = requestAnimationFrame(() => this.animate());
}
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.canvas) {
this.canvas.remove();
}
}
}
// Control functions
function startParticles() {
if (!windField) {
windField = generateWindField();
}
if (particleSystem) {
particleSystem.destroy();
}
particleSystem = new WindParticleSystem(map, windField);
console.log('๐ŸŒช๏ธ Wind particles started!');
}
function resetParticles() {
if (particleSystem) {
particleSystem.destroy();
particleSystem = null;
}
windField = null;
console.log('๐Ÿ”„ Particles reset');
}
function toggleSpeed() {
animationSpeed = animationSpeed === 1 ? 2 : animationSpeed === 2 ? 0.5 : 1;
console.log('โšก Animation speed:', animationSpeed);
}
// Auto-start after page load
setTimeout(() => {
console.log('๐Ÿš€ Auto-starting wind particles...');
startParticles();
}, 1000);
</script>
</body>
</html>