Firefly Garden - Day 27
Advanced LevelEach firefly is a self-contained particle with its own position, velocity, visual properties, and behavior state. The particle system manages creation, updates, and rendering of all fireflies.
class Firefly {
constructor(x, y) {
// Position and velocity
this.x = x || Math.random() * width;
this.y = y || Math.random() * height * 0.8;
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2;
this.baseSpeed = Math.random() * 0.5 + 0.3;
// Glow properties - each firefly pulses independently
this.phase = Math.random() * Math.PI * 2;
this.pulseSpeed = Math.random() * 0.05 + 0.02;
this.brightness = 0;
// Color variation (yellow to green spectrum)
this.hue = Math.random() * 60 + 60; // 60-120
this.saturation = Math.random() * 30 + 70;
// Wandering behavior
this.wanderAngle = Math.random() * Math.PI * 2;
this.curiosity = Math.random() * 0.5 + 0.5;
// Trail history
this.trail = [];
}
}
Each firefly has randomized properties to create natural variation. The phase offset ensures fireflies don't all blink together (unless in sync mode), while curiosity determines how strongly each firefly is attracted to the cursor.
Real fireflies produce a soft, organic glow through bioluminescence. We simulate this using multiple layered radial gradients with varying opacity and size.
draw() {
const intensity = settings.glowIntensity;
const alpha = this.brightness * intensity;
// Outer glow - size pulses with brightness
const glowSize = this.size * 8 * (0.5 + this.brightness * 0.5);
const gradient = ctx.createRadialGradient(
this.x, this.y, 0,
this.x, this.y, glowSize
);
// Multi-stop gradient for soft falloff
gradient.addColorStop(0, `hsla(${hue}, ${sat}%, 80%, ${alpha * 0.6})`);
gradient.addColorStop(0.3, `hsla(${hue}, ${sat}%, 60%, ${alpha * 0.3})`);
gradient.addColorStop(0.6, `hsla(${hue}, ${sat}%, 50%, ${alpha * 0.1})`);
gradient.addColorStop(1, `hsla(${hue}, ${sat}%, 40%, 0)`);
ctx.fillStyle = gradient;
ctx.fill();
// Core bright spot
ctx.arc(this.x, this.y, this.size * 0.8, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue}, 100%, 95%, ${alpha})`;
ctx.fill();
}
Firefly brightness follows a sine wave, creating the characteristic slow pulse. The brightness value (0-1) controls both the glow opacity and size, making the entire effect breathe naturally.
// Individual pulse timing
this.targetBrightness = (Math.sin(
time * this.pulseSpeed + this.phase
) + 1) / 2;
// Smooth transition (easing)
this.brightness += (this.targetBrightness - this.brightness) * 0.1;
Fireflies support three behavior modes that change how they move and interact. Each mode creates a distinct visual atmosphere.
// WANDER MODE - Natural, random flight
if (settings.mode === 'wander') {
// Perlin-like wandering using angle accumulation
this.wanderAngle += (Math.random() - 0.5) * this.wanderSpeed;
this.vx += Math.cos(this.wanderAngle) * 0.02;
this.vy += Math.sin(this.wanderAngle) * 0.02;
}
// SYNC MODE - Synchronized flashing
if (settings.mode === 'sync') {
// All fireflies share a global phase
this.targetBrightness = (Math.sin(
syncPhase + this.phase * 0.2 // Slight variation
) + 1) / 2;
}
// SWARM MODE - Flock toward cursor
if (settings.mode === 'swarm' && mouse.active) {
const dx = mouse.x - this.x;
const dy = mouse.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 50) {
// Attract toward cursor
this.vx += (dx / dist) * 0.1 * this.curiosity;
this.vy += (dy / dist) * 0.1 * this.curiosity;
} else {
// Orbit around cursor when close
const angle = Math.atan2(dy, dx) + Math.PI / 2;
this.vx += Math.cos(angle) * 0.05;
this.vy += Math.sin(angle) * 0.05;
}
}
In nature, some firefly species actually synchronize their flashing. The "Sync" mode simulates this phenomenon, where a global phase variable creates waves of light across the swarm.
Light trails add motion blur and enhance the magical atmosphere. Each firefly maintains a history of recent positions that fade out over time.
// Store position history
if (settings.trailLength > 0) {
this.trail.push({
x: this.x,
y: this.y,
brightness: this.brightness
});
// Limit trail length based on setting
const maxLen = Math.floor(this.maxTrail * settings.trailLength);
while (this.trail.length > maxLen) {
this.trail.shift();
}
}
// Render trail with fading opacity
for (let i = 0; i < this.trail.length - 1; i++) {
const t = i / this.trail.length; // 0 = oldest, 1 = newest
const point = this.trail[i];
const trailAlpha = t * alpha * 0.3;
ctx.arc(point.x, point.y, this.size * 0.5 * t, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue}, ${sat}%, 70%, ${trailAlpha})`;
ctx.fill();
}
Trail length is dynamically adjustable via the UI. Setting trail to 0 completely disables trail storage and rendering, improving performance for slower devices.
Fireflies respond to cursor movement with attraction, avoidance, or orbiting behavior depending on distance and mode. This creates an engaging, interactive experience.
// Gentle attraction when nearby (Wander mode)
if (mouse.active) {
const dx = mouse.x - this.x;
const dy = mouse.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200 && dist > 30) {
// Attract gently
this.vx += (dx / dist) * 0.02 * this.curiosity;
this.vy += (dy / dist) * 0.02 * this.curiosity;
} else if (dist <= 30) {
// Scatter when too close
this.vx -= (dx / dist) * 0.05;
this.vy -= (dy / dist) * 0.05;
}
}
// Custom cursor glow
if (mouse.active) {
const cursorGlow = ctx.createRadialGradient(
mouse.x, mouse.y, 0,
mouse.x, mouse.y, 30
);
cursorGlow.addColorStop(0, 'rgba(255, 255, 200, 0.3)');
cursorGlow.addColorStop(1, 'rgba(255, 255, 100, 0)');
ctx.fillStyle = cursorGlow;
ctx.fill();
}
The simulation supports both mouse and touch input. Touch events update the same mouse coordinates, allowing mobile users to interact by dragging their finger across the garden.
Clicking anywhere spawns 3 new fireflies at that location, up to a maximum of 200 total. This lets users add fireflies to specific areas.
The garden environment creates an immersive nocturnal setting with a gradient sky, twinkling stars, moon with atmospheric glow, and animated grass in the foreground.
// Night sky gradient
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, '#0a0f0a'); // Deep night at top
gradient.addColorStop(0.6, '#0f1a12'); // Slightly lighter
gradient.addColorStop(1, '#111f14'); // Ground level
// Deterministic star positions (same every frame)
for (let i = 0; i < 100; i++) {
const x = (Math.sin(i * 567.8) * 0.5 + 0.5) * width;
const y = (Math.cos(i * 234.5) * 0.5 + 0.5) * height * 0.5;
const twinkle = Math.sin(time * 0.02 + i) * 0.5 + 0.5;
// Draw star with twinkling alpha
}
// Moon with atmospheric glow
const moonGradient = ctx.createRadialGradient(
moonX - 5, moonY - 5, 0, // Offset for 3D effect
moonX, moonY, 35
);
moonGradient.addColorStop(0, 'rgba(255, 255, 240, 0.9)');
moonGradient.addColorStop(1, 'rgba(255, 255, 200, 0)');
Grass blades in the foreground add depth and movement. Each blade sways independently using sine waves with randomized phase and speed.
// Initialize grass with random properties
grassBlades.push({
x: (i / bladeCount) * width + (Math.random() - 0.5) * 15,
height: Math.random() * 60 + 40,
swayOffset: Math.random() * Math.PI * 2,
swaySpeed: Math.random() * 0.02 + 0.01,
hue: Math.random() * 30 + 100 // Green variations
});
// Draw with quadratic curve for natural bend
const sway = Math.sin(time * blade.swaySpeed + blade.swayOffset) * 10;
ctx.quadraticCurveTo(
blade.x + sway, // Control point with sway
height - blade.height * 0.6,
blade.x + sway * 1.5, // End point with more sway
height - blade.height
);
Rendering order matters: background -> stars -> moon -> fireflies -> grass. This creates proper depth with fireflies appearing to fly among the grass blades.