// Lava Lamp preset for LEDLab const BasePreset = require('./base-preset'); const { createFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp } = require('./frame-utils'); class LavaLampPreset extends BasePreset { constructor(width = 16, height = 16) { super(width, height); this.blobs = []; this.defaultParameters = { blobCount: 6, blobSpeed: 0.18, minBlobRadius: 0.18, maxBlobRadius: 0.38, intensity: 1.0, color1: '050319', color2: '2a0c4f', color3: '8f1f73', color4: 'ff4a22', color5: 'ff9333', color6: 'fff7b0', }; } init() { super.init(); this.blobs = this.createBlobs(this.getParameter('blobCount') || 6); } createBlobs(count) { const blobList = []; const maxAxis = Math.max(this.width, this.height); const minBlobRadius = Math.max(3, maxAxis * (this.getParameter('minBlobRadius') || 0.18)); const maxBlobRadius = Math.max(minBlobRadius + 1, maxAxis * (this.getParameter('maxBlobRadius') || 0.38)); for (let index = 0; index < count; ++index) { const angle = Math.random() * Math.PI * 2; const speed = (this.getParameter('blobSpeed') || 0.18) * (0.6 + Math.random() * 0.8); blobList.push({ x: Math.random() * Math.max(1, this.width - 1), y: Math.random() * Math.max(1, this.height - 1), vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, minRadius: minBlobRadius * (0.6 + Math.random() * 0.3), maxRadius: maxBlobRadius * (0.8 + Math.random() * 0.4), intensity: (this.getParameter('intensity') || 1.0) * (0.8 + Math.random() * 0.7), phase: Math.random() * Math.PI * 2, phaseVelocity: 0.6 + Math.random() * 0.6, }); } return blobList; } updateBlobs(deltaSeconds) { const maxX = Math.max(0, this.width - 1); const maxY = Math.max(0, this.height - 1); this.blobs.forEach((blob) => { blob.x += blob.vx * deltaSeconds; blob.y += blob.vy * deltaSeconds; if (blob.x < 0) { blob.x = -blob.x; blob.vx = Math.abs(blob.vx); } else if (blob.x > maxX) { blob.x = 2 * maxX - blob.x; blob.vx = -Math.abs(blob.vx); } if (blob.y < 0) { blob.y = -blob.y; blob.vy = Math.abs(blob.vy); } else if (blob.y > maxY) { blob.y = 2 * maxY - blob.y; blob.vy = -Math.abs(blob.vy); } blob.phase += blob.phaseVelocity * deltaSeconds; }); } renderFrame() { this.updateBlobs(0.016); // Assume 60 FPS const frame = createFrame(this.width, this.height); const paletteStops = [ { stop: 0.0, color: hexToRgb(this.getParameter('color1') || '050319') }, { stop: 0.28, color: hexToRgb(this.getParameter('color2') || '2a0c4f') }, { stop: 0.55, color: hexToRgb(this.getParameter('color3') || '8f1f73') }, { stop: 0.75, color: hexToRgb(this.getParameter('color4') || 'ff4a22') }, { stop: 0.9, color: hexToRgb(this.getParameter('color5') || 'ff9333') }, { stop: 1.0, color: hexToRgb(this.getParameter('color6') || 'fff7b0') }, ]; for (let row = 0; row < this.height; ++row) { for (let col = 0; col < this.width; ++col) { const energy = this.calculateEnergyAt(col, row); const color = samplePalette(paletteStops, energy); frame[toIndex(col, row, this.width)] = color; } } return frame; } calculateEnergyAt(col, row) { let energy = 0; this.blobs.forEach((blob) => { const radius = this.getBlobRadius(blob); const dx = col - blob.x; const dy = row - blob.y; const distance = Math.hypot(dx, dy); const falloff = Math.max(0, 1 - distance / radius); energy += blob.intensity * falloff * falloff; }); return clamp(energy / this.blobs.length, 0, 1); } getBlobRadius(blob) { const oscillation = (Math.sin(blob.phase) + 1) * 0.5; return blob.minRadius + (blob.maxRadius - blob.minRadius) * oscillation; } setParameter(name, value) { super.setParameter(name, value); // Recreate blobs if blob-related parameters change if (name === 'blobCount' || name === 'blobSpeed' || name === 'minBlobRadius' || name === 'maxBlobRadius' || name === 'intensity') { this.blobs = this.createBlobs(this.getParameter('blobCount') || 6); } } getMetadata() { return { name: 'Lava Lamp', description: 'Floating blobs creating organic color gradients', parameters: { blobCount: { type: 'range', min: 3, max: 12, step: 1, default: 6 }, blobSpeed: { type: 'range', min: 0.1, max: 0.5, step: 0.05, default: 0.18 }, minBlobRadius: { type: 'range', min: 0.1, max: 0.3, step: 0.02, default: 0.18 }, maxBlobRadius: { type: 'range', min: 0.2, max: 0.5, step: 0.02, default: 0.38 }, intensity: { type: 'range', min: 0.5, max: 2.0, step: 0.1, default: 1.0 }, color1: { type: 'color', default: '050319' }, color2: { type: 'color', default: '2a0c4f' }, color3: { type: 'color', default: '8f1f73' }, color4: { type: 'color', default: 'ff4a22' }, color5: { type: 'color', default: 'ff9333' }, color6: { type: 'color', default: 'fff7b0' }, }, width: this.width, height: this.height, }; } } module.exports = LavaLampPreset;