// 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.defaultParameters = { fireflyCount: 18, hoverSpeed: 0.6, glowSpeed: 1.8, trailDecay: 0.8, brightness: 1.0, color1: '02030a', color2: '031c2d', color3: '053d4a', color4: '107b68', color5: '14c491', color6: 'f2ffd2', accentColor: 'ffd966', }; } 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, paletteStops) { const baseGlow = (Math.sin(firefly.glowPhase) + 1) * 0.5; const col = Math.round(firefly.x); const row = Math.round(firefly.y); const accentColor = this.getParameter('accentColor') || 'ffd966'; 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(paletteStops, intensity); if (distance === 0) { addHexColor(this.frame, toIndex(sampleX, sampleY, this.width), accentColor, 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; const paletteStops = [ { stop: 0.0, color: hexToRgb(this.getParameter('color1') || '02030a') }, { stop: 0.2, color: hexToRgb(this.getParameter('color2') || '031c2d') }, { stop: 0.4, color: hexToRgb(this.getParameter('color3') || '053d4a') }, { stop: 0.6, color: hexToRgb(this.getParameter('color4') || '107b68') }, { stop: 0.8, color: hexToRgb(this.getParameter('color5') || '14c491') }, { stop: 1.0, color: hexToRgb(this.getParameter('color6') || 'f2ffd2') }, ]; 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, paletteStops); }); 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 }, color1: { type: 'color', default: '02030a' }, color2: { type: 'color', default: '031c2d' }, color3: { type: 'color', default: '053d4a' }, color4: { type: 'color', default: '107b68' }, color5: { type: 'color', default: '14c491' }, color6: { type: 'color', default: 'f2ffd2' }, accentColor: { type: 'color', default: 'ffd966' }, }, width: this.width, height: this.height, }; } } module.exports = VoxelFirefliesPreset;