WaveVisualizer / src_old /particle_animation.js
nakas's picture
Remove old src directory and add clear logging for new app
3c385aa
Raw
History Blame Contribute Delete
13.6 kB
/**
* Wave Particle Animation System for Leaflet
* Based on wind particle systems but adapted for ocean wave data
*/
class WaveParticleSystem {
constructor(map, options = {}) {
this.map = map;
this.particles = [];
this.activeParticles = [];
this.animationId = null;
this.isAnimating = false;
// Configuration
this.config = {
particleCount: options.particleCount || 1500,
particleLife: options.particleLife || 50, // frames
particleSpeed: options.particleSpeed || 1.0,
particleSize: options.particleSize || 1.5,
fadeOpacity: options.fadeOpacity || 0.98,
colorScheme: options.colorScheme || 'ocean',
trailLength: options.trailLength || 10,
...options
};
// Canvas setup
this.canvas = null;
this.ctx = null;
this.setupCanvas();
// Wave data
this.waveData = null;
this.bounds = null;
}
setupCanvas() {
// 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.ctx = this.canvas.getContext('2d');
// Add to map
this.map.getContainer().appendChild(this.canvas);
// Resize handling
this.map.on('resize', () => this.resizeCanvas());
this.map.on('zoom', () => this.handleMapChange());
this.map.on('move', () => this.handleMapChange());
this.resizeCanvas();
}
resizeCanvas() {
const container = this.map.getContainer();
this.canvas.width = container.offsetWidth;
this.canvas.height = container.offsetHeight;
this.canvas.style.width = container.offsetWidth + 'px';
this.canvas.style.height = container.offsetHeight + 'px';
}
handleMapChange() {
// Recalculate particle positions when map changes
this.updateParticleScreenPositions();
}
loadWaveData(waveData) {
this.waveData = waveData;
this.calculateBounds();
this.initializeParticles();
console.log(`Loaded ${waveData.particles ? waveData.particles.length : 0} wave data points for particle animation`);
}
calculateBounds() {
if (!this.waveData || !this.waveData.particles) return;
const lats = this.waveData.particles.map(p => p.lat);
const lons = this.waveData.particles.map(p => p.lon);
this.bounds = {
north: Math.max(...lats),
south: Math.min(...lats),
east: Math.max(...lons),
west: Math.min(...lons)
};
}
initializeParticles() {
this.particles = [];
this.activeParticles = [];
if (!this.waveData || !this.waveData.particles) return;
// Create particle pool from wave data
for (let i = 0; i < Math.min(this.config.particleCount, this.waveData.particles.length); i++) {
const wavePoint = this.waveData.particles[i];
const particle = this.createParticle(wavePoint);
this.particles.push(particle);
}
// Initialize some active particles
const initialActiveCount = Math.min(200, this.particles.length);
for (let i = 0; i < initialActiveCount; i++) {
this.activateParticle();
}
}
createParticle(wavePoint) {
return {
// Geographic position
lat: wavePoint.lat,
lon: wavePoint.lon,
// Screen position (calculated dynamically)
x: 0,
y: 0,
// Wave properties
waveHeight: wavePoint.wave_height,
waveDirection: wavePoint.wave_direction,
wavePeriod: wavePoint.wave_period,
// Velocity components (adjusted for screen space)
vx: wavePoint.u_velocity || 0,
vy: wavePoint.v_velocity || 0,
// Particle state
life: 0,
maxLife: this.config.particleLife + Math.random() * 20,
size: wavePoint.particle_size || this.config.particleSize,
opacity: 1.0,
active: false,
// Visual properties
colorIntensity: wavePoint.color_intensity || 0.5,
trail: [] // For particle trails
};
}
activateParticle() {
// Find inactive particle
const inactive = this.particles.find(p => !p.active);
if (!inactive) return;
// Reset particle properties
inactive.active = true;
inactive.life = 0;
inactive.opacity = 1.0;
inactive.trail = [];
// Random starting position within bounds
const randomWavePoint = this.waveData.particles[Math.floor(Math.random() * this.waveData.particles.length)];
inactive.lat = randomWavePoint.lat + (Math.random() - 0.5) * 2; // Small random offset
inactive.lon = randomWavePoint.lon + (Math.random() - 0.5) * 2;
// Update screen position
this.updateParticleScreenPosition(inactive);
this.activeParticles.push(inactive);
}
updateParticleScreenPosition(particle) {
const point = this.map.latLngToContainerPoint([particle.lat, particle.lon]);
particle.x = point.x;
particle.y = point.y;
}
updateParticleScreenPositions() {
this.activeParticles.forEach(particle => {
this.updateParticleScreenPosition(particle);
});
}
interpolateWaveField(lat, lon) {
if (!this.waveData || !this.waveData.particles) return { u: 0, v: 0, height: 0 };
// Simple nearest neighbor for now (could be improved with bilinear interpolation)\n let nearest = null;
let minDistance = Infinity;
\n for (const point of this.waveData.particles) {
const distance = Math.sqrt(\n Math.pow(point.lat - lat, 2) + Math.pow(point.lon - lon, 2)\n );
if (distance < minDistance) {\n minDistance = distance;\n nearest = point;\n }\n }\n \n if (nearest && minDistance < 2.0) { // Within 2 degrees\n return {\n u: nearest.u_velocity || 0,\n v: nearest.v_velocity || 0,\n height: nearest.wave_height || 0,\n direction: nearest.wave_direction || 0\n };\n }\n \n return { u: 0, v: 0, height: 0, direction: 0 };\n }\n \n updateParticles() {\n const zoom = this.map.getZoom();\n const speedScale = Math.pow(2, zoom - 2) * this.config.particleSpeed;\n \n this.activeParticles = this.activeParticles.filter(particle => {\n // Age the particle\n particle.life++;\n \n // Check if particle should die\n if (particle.life > particle.maxLife) {\n particle.active = false;\n return false;\n }\n \n // Get wave field at current position\n const field = this.interpolateWaveField(particle.lat, particle.lon);\n \n // Update velocity based on local wave field\n particle.vx = field.u * speedScale * 100; // Scale for screen pixels\n particle.vy = -field.v * speedScale * 100; // Negative because screen Y increases downward\n \n // Update position\n particle.x += particle.vx;\n particle.y += particle.vy;\n \n // Convert back to geographic coordinates\n const latlng = this.map.containerPointToLatLng([particle.x, particle.y]);\n particle.lat = latlng.lat;\n particle.lon = latlng.lng;\n \n // Update trail\n particle.trail.push({ x: particle.x, y: particle.y });\n if (particle.trail.length > this.config.trailLength) {\n particle.trail.shift();\n }\n \n // Update opacity (fade with age)\n particle.opacity = 1.0 - (particle.life / particle.maxLife);\n \n // Check bounds\n const bounds = this.map.getBounds();\n const margin = 100; // pixels\n if (particle.x < -margin || particle.x > this.canvas.width + margin ||\n particle.y < -margin || particle.y > this.canvas.height + margin) {\n // Particle is off screen, deactivate\n particle.active = false;\n return false;\n }\n \n return true;\n });\n \n // Maintain particle count by activating new ones\n const targetActive = Math.min(300, this.particles.length);\n while (this.activeParticles.length < targetActive) {\n this.activateParticle();\n }\n }\n \n getParticleColor(particle) {\n const intensity = particle.colorIntensity;\n const opacity = particle.opacity;\n \n switch (this.config.colorScheme) {\n case 'ocean':\n // Blue to white based on wave height\n const r = Math.floor(intensity * 255);\n const g = Math.floor(100 + intensity * 155);\n const b = 255;\n return `rgba(${r}, ${g}, ${b}, ${opacity})`;\n \n case 'heatmap':\n // Heat map colors\n if (intensity < 0.3) {\n return `rgba(0, 0, ${Math.floor(255 * intensity / 0.3)}, ${opacity})`;\n } else if (intensity < 0.7) {\n const green = Math.floor(255 * (intensity - 0.3) / 0.4);\n return `rgba(0, ${green}, 255, ${opacity})`;\n } else {\n const red = Math.floor(255 * (intensity - 0.7) / 0.3);\n return `rgba(${red}, 255, 255, ${opacity})`;\n }\n \n default:\n return `rgba(255, 255, 255, ${opacity})`;\n }\n }\n \n drawParticles() {\n // Clear canvas with fade effect\n this.ctx.fillStyle = `rgba(0, 0, 0, ${1 - this.config.fadeOpacity})`;\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n \n // Draw particles\n this.activeParticles.forEach(particle => {\n this.drawParticle(particle);\n });\n }\n \n drawParticle(particle) {\n const color = this.getParticleColor(particle);\n \n // Draw particle trail\n if (particle.trail.length > 1) {\n this.ctx.beginPath();\n this.ctx.strokeStyle = color;\n this.ctx.lineWidth = 1;\n this.ctx.globalAlpha = particle.opacity * 0.3;\n \n this.ctx.moveTo(particle.trail[0].x, particle.trail[0].y);\n for (let i = 1; i < particle.trail.length; i++) {\n this.ctx.lineTo(particle.trail[i].x, particle.trail[i].y);\n }\n this.ctx.stroke();\n }\n \n // Draw particle\n this.ctx.beginPath();\n this.ctx.fillStyle = color;\n this.ctx.globalAlpha = particle.opacity;\n this.ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);\n this.ctx.fill();\n \n // Reset alpha\n this.ctx.globalAlpha = 1.0;\n }\n \n animate() {\n if (!this.isAnimating) return;\n \n this.updateParticles();\n this.drawParticles();\n \n this.animationId = requestAnimationFrame(() => this.animate());\n }\n \n start() {\n if (this.isAnimating) return;\n \n this.isAnimating = true;\n console.log('Starting wave particle animation');\n this.animate();\n }\n \n stop() {\n if (!this.isAnimating) return;\n \n this.isAnimating = false;\n if (this.animationId) {\n cancelAnimationFrame(this.animationId);\n this.animationId = null;\n }\n console.log('Stopped wave particle animation');\n }\n \n setColorScheme(scheme) {\n this.config.colorScheme = scheme;\n }\n \n setParticleSpeed(speed) {\n this.config.particleSpeed = speed;\n }\n \n setParticleCount(count) {\n this.config.particleCount = count;\n // Reinitialize if needed\n if (this.waveData) {\n this.initializeParticles();\n }\n }\n \n destroy() {\n this.stop();\n \n if (this.canvas && this.canvas.parentNode) {\n this.canvas.parentNode.removeChild(this.canvas);\n }\n \n this.map.off('resize');\n this.map.off('zoom');\n this.map.off('move');\n \n this.particles = [];\n this.activeParticles = [];\n this.waveData = null;\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = WaveParticleSystem;\n} else if (typeof window !== 'undefined') {\n window.WaveParticleSystem = WaveParticleSystem;\n}