// Voxel Fireflies preset for LEDLab const BasePreset = require('./base-preset'); const { createFrame, fadeFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp, addHexColor } = require('./frame-utils'); class VoxelFirefliesPreset extends BasePreset { constructor(width = 16, height = 16) { super(width, height); this.fireflies = []; this.paletteStops = [ { stop: 0.0, color: hexToRgb('02030a') }, { stop: 0.2, color: hexToRgb('031c2d') }, { stop: 0.4, color: hexToRgb('053d4a') }, { stop: 0.6, color: hexToRgb('107b68') }, { stop: 0.8, color: hexToRgb('14c491') }, { stop: 1.0, color: hexToRgb('f2ffd2') }, ]; this.defaultParameters = { fireflyCount: 18, hoverSpeed: 0.6, glowSpeed: 1.8, trailDecay: 0.8, brightness: 1.0, }; } init() { super.init(); this.fireflies = this.createFireflies(this.getParameter('fireflyCount') || 18, this.width, this.height); } createFireflies(count, matrixWidth, matrixHeight) { const list = []; for (let index = 0; index < count; ++index) { list.push(this.spawnFirefly(matrixWidth, matrixHeight)); } return list; } spawnFirefly(matrixWidth, matrixHeight) { return { x: Math.random() * (matrixWidth - 1), y: Math.random() * (matrixHeight - 1), targetX: Math.random() * (matrixWidth - 1), targetY: Math.random() * (matrixHeight - 1), glowPhase: Math.random() * Math.PI * 2, dwell: 1 + Math.random() * 2, }; } updateFirefly(firefly, deltaSeconds) { const dx = firefly.targetX - firefly.x; const dy = firefly.targetY - firefly.y; const distance = Math.hypot(dx, dy); if (distance < 0.2) { firefly.dwell -= deltaSeconds; if (firefly.dwell <= 0) { firefly.targetX = Math.random() * (this.width - 1); firefly.targetY = Math.random() * (this.height - 1); firefly.dwell = 1 + Math.random() * 2; } } else { const speed = (this.getParameter('hoverSpeed') || 0.6) * (0.8 + Math.random() * 0.4); firefly.x += (dx / distance) * speed * deltaSeconds; firefly.y += (dy / distance) * speed * deltaSeconds; } firefly.glowPhase += deltaSeconds * (this.getParameter('glowSpeed') || 1.8) * (0.7 + Math.random() * 0.6); } drawFirefly(firefly) { const baseGlow = (Math.sin(firefly.glowPhase) + 1) * 0.5; const col = Math.round(firefly.x); const row = Math.round(firefly.y); for (let dy = -1; dy <= 1; ++dy) { for (let dx = -1; dx <= 1; ++dx) { const sampleX = col + dx; const sampleY = row + dy; if (sampleX < 0 || sampleX >= this.width || sampleY < 0 || sampleY >= this.height) { continue; } const distance = Math.hypot(dx, dy); const falloff = clamp(1 - distance * 0.7, 0, 1); const intensity = baseGlow * falloff; if (intensity <= 0) { continue; } this.frame[toIndex(sampleX, sampleY, this.width)] = samplePalette(this.paletteStops, intensity); if (distance === 0) { addHexColor(this.frame, toIndex(sampleX, sampleY, this.width), 'ffd966', intensity * 1.6); } } } } renderFrame() { this.frame = createFrame(this.width, this.height); const trailDecay = this.getParameter('trailDecay') || 0.8; const fireflyCount = this.getParameter('fireflyCount') || 18; const brightness = this.getParameter('brightness') || 1.0; fadeFrame(this.frame, trailDecay); // Update firefly count if it changed while (this.fireflies.length < fireflyCount) { this.fireflies.push(this.spawnFirefly(this.width, this.height)); } while (this.fireflies.length > fireflyCount) { this.fireflies.pop(); } this.fireflies.forEach((firefly) => { this.updateFirefly(firefly, 0.016); // Assume 60 FPS this.drawFirefly(firefly); }); return this.frame; } getMetadata() { return { name: 'Voxel Fireflies', description: 'Glowing fireflies that hover and move around', parameters: { fireflyCount: { type: 'range', min: 8, max: 30, step: 2, default: 18 }, hoverSpeed: { type: 'range', min: 0.3, max: 1.2, step: 0.1, default: 0.6 }, glowSpeed: { type: 'range', min: 1.0, max: 3.0, step: 0.2, default: 1.8 }, trailDecay: { type: 'range', min: 0.7, max: 0.95, step: 0.05, default: 0.8 }, brightness: { type: 'range', min: 0.5, max: 1.5, step: 0.1, default: 1.0 }, }, width: this.width, height: this.height, }; } } module.exports = VoxelFirefliesPreset;