Files
spore-ledlab/presets/lava-lamp-preset.js
2025-10-11 17:46:32 +02:00

147 lines
4.6 KiB
JavaScript

// 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.paletteStops = [
{ stop: 0.0, color: hexToRgb('050319') },
{ stop: 0.28, color: hexToRgb('2a0c4f') },
{ stop: 0.55, color: hexToRgb('8f1f73') },
{ stop: 0.75, color: hexToRgb('ff4a22') },
{ stop: 0.9, color: hexToRgb('ff9333') },
{ stop: 1.0, color: hexToRgb('fff7b0') },
];
this.defaultParameters = {
blobCount: 6,
blobSpeed: 0.18,
minBlobRadius: 0.18,
maxBlobRadius: 0.38,
intensity: 1.0,
};
}
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);
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(this.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 },
},
width: this.width,
height: this.height,
};
}
}
module.exports = LavaLampPreset;