feat: ledlab
This commit is contained in:
111
presets/aurora-curtains-preset.js
Normal file
111
presets/aurora-curtains-preset.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// Aurora Curtains preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, toIndex, samplePalette, hexToRgb } = require('./frame-utils');
|
||||
|
||||
class AuroraCurtainsPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.bands = [];
|
||||
this.defaultParameters = {
|
||||
bandCount: 5,
|
||||
waveSpeed: 0.35,
|
||||
horizontalSway: 0.45,
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.createBands();
|
||||
}
|
||||
|
||||
createBands() {
|
||||
const bandCount = this.getParameter('bandCount') || 5;
|
||||
this.bands = [];
|
||||
|
||||
for (let index = 0; index < bandCount; ++index) {
|
||||
this.bands.push({
|
||||
center: Math.random() * (this.width - 1),
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
width: 1.2 + Math.random() * 1.8,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const waveSpeed = this.getParameter('waveSpeed') || 0.35;
|
||||
const horizontalSway = this.getParameter('horizontalSway') || 0.45;
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
const timeSeconds = this.frameCount * 0.05; // Convert frame count to time
|
||||
|
||||
const paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('01010a') },
|
||||
{ stop: 0.2, color: hexToRgb('041332') },
|
||||
{ stop: 0.4, color: hexToRgb('0c3857') },
|
||||
{ stop: 0.65, color: hexToRgb('1aa07a') },
|
||||
{ stop: 0.85, color: hexToRgb('68d284') },
|
||||
{ stop: 1.0, color: hexToRgb('f4f5c6') },
|
||||
];
|
||||
|
||||
for (let row = 0; row < this.height; ++row) {
|
||||
const verticalRatio = row / Math.max(1, this.height - 1);
|
||||
|
||||
for (let col = 0; col < this.width; ++col) {
|
||||
let intensity = 0;
|
||||
|
||||
this.bands.forEach((band, index) => {
|
||||
const sway = Math.sin(timeSeconds * waveSpeed + band.phase + verticalRatio * Math.PI * 2) * horizontalSway;
|
||||
const center = band.center + sway * (index % 2 === 0 ? 1 : -1);
|
||||
const distance = Math.abs(col - center);
|
||||
const blurred = Math.exp(-(distance * distance) / (2 * band.width * band.width));
|
||||
intensity += blurred * (0.8 + Math.sin(timeSeconds * 0.4 + index) * 0.2);
|
||||
});
|
||||
|
||||
const normalized = Math.min(intensity / this.bands.length, 1);
|
||||
const gradientBlend = Math.min((normalized * 0.7 + verticalRatio * 0.3), 1);
|
||||
|
||||
let colorHex = samplePalette(paletteStops, gradientBlend);
|
||||
|
||||
// Apply brightness
|
||||
if (brightness < 1.0) {
|
||||
const rgb = hexToRgb(colorHex);
|
||||
colorHex = Math.round(rgb.r * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.g * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.b * brightness).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
frame[toIndex(col, row, this.width)] = colorHex;
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
setParameter(name, value) {
|
||||
super.setParameter(name, value);
|
||||
|
||||
// Recreate bands if band-related parameters change
|
||||
if (name === 'bandCount') {
|
||||
this.createBands();
|
||||
}
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Aurora Curtains',
|
||||
description: 'Flowing aurora-like curtains with wave motion',
|
||||
parameters: {
|
||||
bandCount: { type: 'range', min: 3, max: 10, step: 1, default: 5 },
|
||||
waveSpeed: { type: 'range', min: 0.1, max: 2.0, step: 0.05, default: 0.35 },
|
||||
horizontalSway: { type: 'range', min: 0.1, max: 1.0, step: 0.05, default: 0.45 },
|
||||
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuroraCurtainsPreset;
|
||||
86
presets/base-preset.js
Normal file
86
presets/base-preset.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// Base preset class for LEDLab
|
||||
|
||||
const { createFrame, frameToPayload } = require('./frame-utils');
|
||||
|
||||
class BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.frameCount = 0;
|
||||
this.isActive = false;
|
||||
this.parameters = {};
|
||||
this.defaultParameters = {};
|
||||
}
|
||||
|
||||
// Initialize the preset with default parameters
|
||||
init() {
|
||||
this.frameCount = 0;
|
||||
this.resetToDefaults();
|
||||
}
|
||||
|
||||
// Reset parameters to their default values
|
||||
resetToDefaults() {
|
||||
this.parameters = { ...this.defaultParameters };
|
||||
}
|
||||
|
||||
// Set a parameter value
|
||||
setParameter(name, value) {
|
||||
this.parameters[name] = value;
|
||||
}
|
||||
|
||||
// Get a parameter value
|
||||
getParameter(name) {
|
||||
return this.parameters[name];
|
||||
}
|
||||
|
||||
// Get all parameters
|
||||
getParameters() {
|
||||
return { ...this.parameters };
|
||||
}
|
||||
|
||||
// Start the preset
|
||||
start() {
|
||||
this.isActive = true;
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Stop the preset
|
||||
stop() {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// Generate the next frame
|
||||
generateFrame() {
|
||||
if (!this.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frame = this.renderFrame();
|
||||
this.frameCount++;
|
||||
|
||||
if (frame) {
|
||||
return frameToPayload(frame);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Override in subclasses to render a frame
|
||||
renderFrame() {
|
||||
// Default implementation returns a blank frame
|
||||
return createFrame(this.width, this.height, '000000');
|
||||
}
|
||||
|
||||
// Get preset metadata
|
||||
getMetadata() {
|
||||
return {
|
||||
name: this.constructor.name,
|
||||
description: 'Base preset class',
|
||||
parameters: this.defaultParameters,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BasePreset;
|
||||
113
presets/bouncing-ball-preset.js
Normal file
113
presets/bouncing-ball-preset.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// Bouncing Ball preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, toIndex } = require('./frame-utils');
|
||||
|
||||
class BouncingBallPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.position = 0;
|
||||
this.velocity = 0;
|
||||
this.defaultParameters = {
|
||||
speed: 1.0,
|
||||
ballSize: 1,
|
||||
trailLength: 5,
|
||||
color: 'ff8000',
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.position = Math.random() * (this.width * this.height - 1);
|
||||
this.velocity = this.randomVelocity();
|
||||
}
|
||||
|
||||
randomVelocity() {
|
||||
const min = 0.15;
|
||||
const max = 0.4;
|
||||
const sign = Math.random() < 0.5 ? -1 : 1;
|
||||
return (min + Math.random() * (max - min)) * sign;
|
||||
}
|
||||
|
||||
rebound(sign) {
|
||||
this.velocity = this.randomVelocity() * sign;
|
||||
}
|
||||
|
||||
mix(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const speed = this.getParameter('speed') || 1.0;
|
||||
const ballSize = this.getParameter('ballSize') || 1;
|
||||
const trailLength = this.getParameter('trailLength') || 5;
|
||||
const color = this.getParameter('color') || 'ff8000';
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
|
||||
const dt = 0.016; // Assume 60 FPS
|
||||
this.position += this.velocity * speed * dt * 60;
|
||||
|
||||
// Handle boundary collisions
|
||||
if (this.position < 0) {
|
||||
this.position = -this.position;
|
||||
this.rebound(1);
|
||||
} else if (this.position > this.width * this.height - 1) {
|
||||
this.position = (this.width * this.height - 1) - (this.position - (this.width * this.height - 1));
|
||||
this.rebound(-1);
|
||||
}
|
||||
|
||||
const activeIndex = Math.max(0, Math.min(this.width * this.height - 1, Math.round(this.position)));
|
||||
|
||||
// Render ball and trail
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
if (i === activeIndex) {
|
||||
// Main ball
|
||||
frame[i] = color;
|
||||
} else {
|
||||
// Trail effect
|
||||
const distance = Math.abs(i - this.position);
|
||||
if (distance <= trailLength) {
|
||||
const intensity = Math.max(0, 1 - distance / trailLength);
|
||||
const trailColor = this.hexToRgb(color);
|
||||
|
||||
const r = Math.round(this.mix(0, trailColor.r, intensity * brightness));
|
||||
const g = Math.round(this.mix(20, trailColor.g, intensity * brightness));
|
||||
const b = Math.round(this.mix(40, trailColor.b, intensity * brightness));
|
||||
|
||||
frame[i] = r.toString(16).padStart(2, '0') +
|
||||
g.toString(16).padStart(2, '0') +
|
||||
b.toString(16).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
hexToRgb(hex) {
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Bouncing Ball',
|
||||
description: 'A ball that bounces around the matrix with a trailing effect',
|
||||
parameters: {
|
||||
speed: { type: 'range', min: 0.1, max: 3.0, step: 0.1, default: 1.0 },
|
||||
ballSize: { type: 'range', min: 1, max: 5, step: 1, default: 1 },
|
||||
trailLength: { type: 'range', min: 1, max: 10, step: 1, default: 5 },
|
||||
color: { type: 'color', default: 'ff8000' },
|
||||
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BouncingBallPreset;
|
||||
152
presets/circuit-pulse-preset.js
Normal file
152
presets/circuit-pulse-preset.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// Circuit Pulse preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, fadeFrame, frameToPayload, hexToRgb, samplePalette, toIndex, addHexColor } = require('./frame-utils');
|
||||
|
||||
class CircuitPulsePreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.paths = [];
|
||||
this.pulses = [];
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('020209') },
|
||||
{ stop: 0.3, color: hexToRgb('023047') },
|
||||
{ stop: 0.6, color: hexToRgb('115173') },
|
||||
{ stop: 0.8, color: hexToRgb('1ca78f') },
|
||||
{ stop: 1.0, color: hexToRgb('94fdf3') },
|
||||
];
|
||||
this.accentColors = ['14f5ff', 'a7ff4d', 'ffcc3f'];
|
||||
this.defaultParameters = {
|
||||
pathFade: 0.85,
|
||||
pulseLength: 6,
|
||||
pulseSpeed: 5.0,
|
||||
pulseCount: 3,
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.paths = this.createPaths(this.width, this.height);
|
||||
this.pulses = this.createPulses(this.paths.length);
|
||||
}
|
||||
|
||||
createPaths(matrixWidth, matrixHeight) {
|
||||
const horizontalStep = Math.max(2, Math.floor(matrixHeight / 4));
|
||||
const verticalStep = Math.max(2, Math.floor(matrixWidth / 4));
|
||||
const generatedPaths = [];
|
||||
|
||||
// Horizontal paths
|
||||
for (let y = 1; y < matrixHeight; y += horizontalStep) {
|
||||
const path = [];
|
||||
for (let x = 0; x < matrixWidth; ++x) {
|
||||
path.push({ x, y });
|
||||
}
|
||||
generatedPaths.push(path);
|
||||
}
|
||||
|
||||
// Vertical paths
|
||||
for (let x = 2; x < matrixWidth; x += verticalStep) {
|
||||
const path = [];
|
||||
for (let y = 0; y < matrixHeight; ++y) {
|
||||
path.push({ x, y });
|
||||
}
|
||||
generatedPaths.push(path);
|
||||
}
|
||||
|
||||
return generatedPaths;
|
||||
}
|
||||
|
||||
createPulses(count) {
|
||||
const pulseList = [];
|
||||
for (let index = 0; index < count; ++index) {
|
||||
pulseList.push(this.spawnPulse(index));
|
||||
}
|
||||
return pulseList;
|
||||
}
|
||||
|
||||
spawnPulse(pathIndex) {
|
||||
const color = this.accentColors[pathIndex % this.accentColors.length];
|
||||
return {
|
||||
pathIndex,
|
||||
position: 0,
|
||||
speed: 3 + Math.random() * 2,
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
updatePulse(pulse, deltaSeconds) {
|
||||
pulse.position += pulse.speed * deltaSeconds;
|
||||
const path = this.paths[pulse.pathIndex];
|
||||
|
||||
if (!path || path.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pulse.position >= path.length + this.getParameter('pulseLength')) {
|
||||
Object.assign(pulse, this.spawnPulse(pulse.pathIndex));
|
||||
pulse.position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
renderPulse(pulse) {
|
||||
const path = this.paths[pulse.pathIndex];
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pulseLength = this.getParameter('pulseLength');
|
||||
|
||||
for (let offset = 0; offset < pulseLength; ++offset) {
|
||||
const index = Math.floor(pulse.position) - offset;
|
||||
if (index < 0 || index >= path.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { x, y } = path[index];
|
||||
const intensity = Math.max(0, 1 - offset / pulseLength);
|
||||
const baseColor = samplePalette(this.paletteStops, intensity);
|
||||
this.frame[toIndex(x, y, this.width)] = baseColor;
|
||||
addHexColor(this.frame, toIndex(x, y, this.width), pulse.color, intensity * 1.4);
|
||||
}
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.frame = createFrame(this.width, this.height);
|
||||
const pathFade = this.getParameter('pathFade') || 0.85;
|
||||
const pulseCount = this.getParameter('pulseCount') || 3;
|
||||
|
||||
fadeFrame(this.frame, pathFade);
|
||||
|
||||
// Update pulse count if it changed
|
||||
while (this.pulses.length < pulseCount) {
|
||||
this.pulses.push(this.spawnPulse(this.pulses.length));
|
||||
}
|
||||
while (this.pulses.length > pulseCount) {
|
||||
this.pulses.pop();
|
||||
}
|
||||
|
||||
this.pulses.forEach((pulse) => {
|
||||
this.updatePulse(pulse, 0.016); // Assume 60 FPS
|
||||
this.renderPulse(pulse);
|
||||
});
|
||||
|
||||
return this.frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Circuit Pulse',
|
||||
description: 'Animated circuit board with pulsing paths',
|
||||
parameters: {
|
||||
pathFade: { type: 'range', min: 0.7, max: 0.95, step: 0.05, default: 0.85 },
|
||||
pulseLength: { type: 'range', min: 3, max: 12, step: 1, default: 6 },
|
||||
pulseSpeed: { type: 'range', min: 2.0, max: 8.0, step: 0.5, default: 5.0 },
|
||||
pulseCount: { type: 'range', min: 1, max: 6, step: 1, default: 3 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CircuitPulsePreset;
|
||||
51
presets/fade-green-blue-preset.js
Normal file
51
presets/fade-green-blue-preset.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Fade Green Blue preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, frameToPayload } = require('./frame-utils');
|
||||
|
||||
class FadeGreenBluePreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.tick = 0;
|
||||
this.defaultParameters = {
|
||||
speed: 0.5, // cycles per second
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const timeSeconds = (this.tick * 0.016); // Assume 60 FPS
|
||||
const phase = timeSeconds * this.getParameter('speed') * Math.PI * 2;
|
||||
const blend = (Math.sin(phase) + 1) * 0.5; // 0..1
|
||||
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
const green = Math.round(255 * (1 - blend) * brightness);
|
||||
const blue = Math.round(255 * blend * brightness);
|
||||
|
||||
const gHex = green.toString(16).padStart(2, '0');
|
||||
const bHex = blue.toString(16).padStart(2, '0');
|
||||
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
frame[i] = '00' + gHex + bHex;
|
||||
}
|
||||
|
||||
this.tick++;
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Fade Green Blue',
|
||||
description: 'Smooth fade between green and blue colors',
|
||||
parameters: {
|
||||
speed: { type: 'range', min: 0.1, max: 2.0, step: 0.1, default: 0.5 },
|
||||
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FadeGreenBluePreset;
|
||||
133
presets/frame-utils.js
Normal file
133
presets/frame-utils.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// Frame utilities for LEDLab presets
|
||||
|
||||
const SERPENTINE_WIRING = true;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const normalizedHex = hex.trim().toLowerCase();
|
||||
const value = normalizedHex.startsWith('#') ? normalizedHex.slice(1) : normalizedHex;
|
||||
return {
|
||||
r: parseInt(value.slice(0, 2), 16),
|
||||
g: parseInt(value.slice(2, 4), 16),
|
||||
b: parseInt(value.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
return toHex(rgb.r) + toHex(rgb.g) + toHex(rgb.b);
|
||||
}
|
||||
|
||||
function toHex(value) {
|
||||
const boundedValue = clamp(Math.round(value), 0, 255);
|
||||
const hex = boundedValue.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
function lerpRgb(lhs, rhs, t) {
|
||||
return {
|
||||
r: Math.round(lhs.r + (rhs.r - lhs.r) * t),
|
||||
g: Math.round(lhs.g + (rhs.g - lhs.g) * t),
|
||||
b: Math.round(lhs.b + (rhs.b - lhs.b) * t),
|
||||
};
|
||||
}
|
||||
|
||||
function samplePalette(paletteStops, value) {
|
||||
const clampedValue = clamp(value, 0, 1);
|
||||
|
||||
for (let index = 0; index < paletteStops.length - 1; ++index) {
|
||||
const left = paletteStops[index];
|
||||
const right = paletteStops[index + 1];
|
||||
if (clampedValue <= right.stop) {
|
||||
const span = right.stop - left.stop || 1;
|
||||
const t = clamp((clampedValue - left.stop) / span, 0, 1);
|
||||
const interpolatedColor = lerpRgb(left.color, right.color, t);
|
||||
return rgbToHex(interpolatedColor);
|
||||
}
|
||||
}
|
||||
|
||||
return rgbToHex(paletteStops[paletteStops.length - 1].color);
|
||||
}
|
||||
|
||||
function toIndex(col, row, width, serpentine = SERPENTINE_WIRING) {
|
||||
if (!serpentine || row % 2 === 0) {
|
||||
return row * width + col;
|
||||
}
|
||||
return row * width + (width - 1 - col);
|
||||
}
|
||||
|
||||
function createFrame(width, height, fill = '000000') {
|
||||
return new Array(width * height).fill(fill);
|
||||
}
|
||||
|
||||
function frameToPayload(frame) {
|
||||
return 'RAW:' + frame.join('');
|
||||
}
|
||||
|
||||
function fadeFrame(frame, factor) {
|
||||
for (let index = 0; index < frame.length; ++index) {
|
||||
const hex = frame[index];
|
||||
const r = parseInt(hex.slice(0, 2), 16) * factor;
|
||||
const g = parseInt(hex.slice(2, 4), 16) * factor;
|
||||
const b = parseInt(hex.slice(4, 6), 16) * factor;
|
||||
frame[index] = toHex(r) + toHex(g) + toHex(b);
|
||||
}
|
||||
}
|
||||
|
||||
function addRgbToFrame(frame, index, rgb) {
|
||||
if (index < 0 || index >= frame.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = hexToRgb(frame[index]);
|
||||
const updated = {
|
||||
r: clamp(current.r + rgb.r, 0, 255),
|
||||
g: clamp(current.g + rgb.g, 0, 255),
|
||||
b: clamp(current.b + rgb.b, 0, 255),
|
||||
};
|
||||
frame[index] = rgbToHex(updated);
|
||||
}
|
||||
|
||||
function addHexColor(frame, index, hexColor, intensity = 1) {
|
||||
if (intensity <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base = hexToRgb(hexColor);
|
||||
addRgbToFrame(frame, index, {
|
||||
r: base.r * intensity,
|
||||
g: base.g * intensity,
|
||||
b: base.b * intensity,
|
||||
});
|
||||
}
|
||||
|
||||
// Color wheel function for rainbow effects
|
||||
function wheel(pos) {
|
||||
pos = 255 - pos;
|
||||
if (pos < 85) {
|
||||
return [255 - pos * 3, 0, pos * 3];
|
||||
}
|
||||
if (pos < 170) {
|
||||
pos -= 85;
|
||||
return [0, pos * 3, 255 - pos * 3];
|
||||
}
|
||||
pos -= 170;
|
||||
return [pos * 3, 255 - pos * 3, 0];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clamp,
|
||||
hexToRgb,
|
||||
rgbToHex,
|
||||
lerpRgb,
|
||||
samplePalette,
|
||||
toIndex,
|
||||
createFrame,
|
||||
frameToPayload,
|
||||
fadeFrame,
|
||||
addRgbToFrame,
|
||||
addHexColor,
|
||||
wheel,
|
||||
};
|
||||
146
presets/lava-lamp-preset.js
Normal file
146
presets/lava-lamp-preset.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// 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;
|
||||
108
presets/meteor-rain-preset.js
Normal file
108
presets/meteor-rain-preset.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// Meteor Rain preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, fadeFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp } = require('./frame-utils');
|
||||
|
||||
class MeteorRainPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.meteors = [];
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('0a0126') },
|
||||
{ stop: 0.3, color: hexToRgb('123d8b') },
|
||||
{ stop: 0.7, color: hexToRgb('21c7d9') },
|
||||
{ stop: 1.0, color: hexToRgb('f7ffff') },
|
||||
];
|
||||
this.defaultParameters = {
|
||||
meteorCount: 12,
|
||||
minSpeed: 4,
|
||||
maxSpeed: 10,
|
||||
trailDecay: 0.76,
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.meteors = this.createMeteors(this.getParameter('meteorCount') || 12, this.width, this.height);
|
||||
}
|
||||
|
||||
createMeteors(count, matrixWidth, matrixHeight) {
|
||||
const meteorList = [];
|
||||
for (let index = 0; index < count; ++index) {
|
||||
meteorList.push(this.spawnMeteor(matrixWidth, matrixHeight));
|
||||
}
|
||||
return meteorList;
|
||||
}
|
||||
|
||||
spawnMeteor(matrixWidth, matrixHeight) {
|
||||
const angle = (Math.PI / 4) * (0.6 + Math.random() * 0.8);
|
||||
const speed = (this.getParameter('minSpeed') || 4) + Math.random() * ((this.getParameter('maxSpeed') || 10) - (this.getParameter('minSpeed') || 4));
|
||||
return {
|
||||
x: Math.random() * matrixWidth,
|
||||
y: -Math.random() * matrixHeight,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
};
|
||||
}
|
||||
|
||||
drawMeteor(meteor) {
|
||||
const col = Math.round(meteor.x);
|
||||
const row = Math.round(meteor.y);
|
||||
if (col < 0 || col >= this.width || row < 0 || row >= this.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const energy = clamp(1.2 - Math.random() * 0.2, 0, 1);
|
||||
this.frame[toIndex(col, row, this.width)] = samplePalette(this.paletteStops, energy);
|
||||
}
|
||||
|
||||
updateMeteors(deltaSeconds) {
|
||||
this.meteors.forEach((meteor, index) => {
|
||||
meteor.x += meteor.vx * deltaSeconds;
|
||||
meteor.y += meteor.vy * deltaSeconds;
|
||||
|
||||
this.drawMeteor(meteor);
|
||||
|
||||
if (meteor.x > this.width + 1 || meteor.y > this.height + 1) {
|
||||
this.meteors[index] = this.spawnMeteor(this.width, this.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.frame = createFrame(this.width, this.height);
|
||||
const trailDecay = this.getParameter('trailDecay') || 0.76;
|
||||
const meteorCount = this.getParameter('meteorCount') || 12;
|
||||
|
||||
fadeFrame(this.frame, trailDecay);
|
||||
|
||||
// Update meteor count if it changed
|
||||
while (this.meteors.length < meteorCount) {
|
||||
this.meteors.push(this.spawnMeteor(this.width, this.height));
|
||||
}
|
||||
while (this.meteors.length > meteorCount) {
|
||||
this.meteors.pop();
|
||||
}
|
||||
|
||||
this.updateMeteors(0.016); // Assume 60 FPS
|
||||
|
||||
return this.frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Meteor Rain',
|
||||
description: 'Falling meteors with trailing effects',
|
||||
parameters: {
|
||||
meteorCount: { type: 'range', min: 5, max: 20, step: 1, default: 12 },
|
||||
minSpeed: { type: 'range', min: 2, max: 8, step: 1, default: 4 },
|
||||
maxSpeed: { type: 'range', min: 6, max: 15, step: 1, default: 10 },
|
||||
trailDecay: { type: 'range', min: 0.6, max: 0.9, step: 0.02, default: 0.76 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MeteorRainPreset;
|
||||
81
presets/nebula-drift-preset.js
Normal file
81
presets/nebula-drift-preset.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Nebula Drift preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp } = require('./frame-utils');
|
||||
|
||||
class NebulaDriftPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.timeSeconds = 0;
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('100406') },
|
||||
{ stop: 0.25, color: hexToRgb('2e0f1f') },
|
||||
{ stop: 0.5, color: hexToRgb('6a1731') },
|
||||
{ stop: 0.7, color: hexToRgb('b63b32') },
|
||||
{ stop: 0.85, color: hexToRgb('f48b2a') },
|
||||
{ stop: 1.0, color: hexToRgb('ffe9b0') },
|
||||
];
|
||||
this.defaultParameters = {
|
||||
primarySpeed: 0.15,
|
||||
secondarySpeed: 0.32,
|
||||
waveScale: 0.75,
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
layeredWave(u, v, speed, offset) {
|
||||
return Math.sin((u * 3 + v * 2) * Math.PI * this.getParameter('waveScale') + this.timeSeconds * speed + offset);
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.timeSeconds += 0.016; // Assume 60 FPS
|
||||
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
|
||||
for (let row = 0; row < this.height; ++row) {
|
||||
const v = row / Math.max(1, this.height - 1);
|
||||
for (let col = 0; col < this.width; ++col) {
|
||||
const u = col / Math.max(1, this.width - 1);
|
||||
const primary = this.layeredWave(u, v, this.getParameter('primarySpeed') || 0.15, 0);
|
||||
const secondary = this.layeredWave(v, u, this.getParameter('secondarySpeed') || 0.32, Math.PI / 4);
|
||||
const tertiary = Math.sin((u + v) * Math.PI * 1.5 + this.timeSeconds * 0.18);
|
||||
|
||||
const combined = 0.45 * primary + 0.35 * secondary + 0.2 * tertiary;
|
||||
const envelope = Math.sin((u * v) * Math.PI * 2 + this.timeSeconds * 0.1) * 0.25 + 0.75;
|
||||
const value = clamp((combined * 0.5 + 0.5) * envelope, 0, 1);
|
||||
|
||||
let color = samplePalette(this.paletteStops, value);
|
||||
|
||||
// Apply brightness
|
||||
if (brightness < 1.0) {
|
||||
const rgb = hexToRgb(color);
|
||||
color = Math.round(rgb.r * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.g * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.b * brightness).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
frame[toIndex(col, row, this.width)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Nebula Drift',
|
||||
description: 'Organic drifting nebula with layered wave patterns',
|
||||
parameters: {
|
||||
primarySpeed: { type: 'range', min: 0.05, max: 0.3, step: 0.05, default: 0.15 },
|
||||
secondarySpeed: { type: 'range', min: 0.2, max: 0.5, step: 0.02, default: 0.32 },
|
||||
waveScale: { type: 'range', min: 0.5, max: 1.0, step: 0.05, default: 0.75 },
|
||||
brightness: { type: 'range', min: 0.3, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NebulaDriftPreset;
|
||||
79
presets/ocean-glimmer-preset.js
Normal file
79
presets/ocean-glimmer-preset.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// Ocean Glimmer preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp } = require('./frame-utils');
|
||||
|
||||
class OceanGlimmerPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.timeSeconds = 0;
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('031521') },
|
||||
{ stop: 0.35, color: hexToRgb('024f6d') },
|
||||
{ stop: 0.65, color: hexToRgb('13a4a1') },
|
||||
{ stop: 0.85, color: hexToRgb('67dcd0') },
|
||||
{ stop: 1.0, color: hexToRgb('fcdba4') },
|
||||
];
|
||||
this.defaultParameters = {
|
||||
shimmer: 0.08,
|
||||
waveSpeed1: 1.2,
|
||||
waveSpeed2: 0.9,
|
||||
waveSpeed3: 0.5,
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.timeSeconds += 0.016; // Assume 60 FPS
|
||||
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const shimmer = this.getParameter('shimmer') || 0.08;
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
|
||||
for (let row = 0; row < this.height; ++row) {
|
||||
const v = row / Math.max(1, this.height - 1);
|
||||
for (let col = 0; col < this.width; ++col) {
|
||||
const u = col / Math.max(1, this.width - 1);
|
||||
const base =
|
||||
0.33 +
|
||||
0.26 * Math.sin(u * Math.PI * 2 + this.timeSeconds * (this.getParameter('waveSpeed1') || 1.2)) +
|
||||
0.26 * Math.sin(v * Math.PI * 2 - this.timeSeconds * (this.getParameter('waveSpeed2') || 0.9)) +
|
||||
0.26 * Math.sin((u + v) * Math.PI * 2 + this.timeSeconds * (this.getParameter('waveSpeed3') || 0.5));
|
||||
const noise = (Math.random() - 0.5) * shimmer;
|
||||
const value = clamp(base + noise, 0, 1);
|
||||
|
||||
let color = samplePalette(this.paletteStops, value);
|
||||
|
||||
// Apply brightness
|
||||
if (brightness < 1.0) {
|
||||
const rgb = hexToRgb(color);
|
||||
color = Math.round(rgb.r * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.g * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.b * brightness).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
frame[toIndex(col, row, this.width)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Ocean Glimmer',
|
||||
description: 'Ocean-like waves with shimmer effects',
|
||||
parameters: {
|
||||
shimmer: { type: 'range', min: 0.02, max: 0.15, step: 0.01, default: 0.08 },
|
||||
waveSpeed1: { type: 'range', min: 0.5, max: 2.0, step: 0.1, default: 1.2 },
|
||||
waveSpeed2: { type: 'range', min: 0.5, max: 2.0, step: 0.1, default: 0.9 },
|
||||
waveSpeed3: { type: 'range', min: 0.2, max: 1.0, step: 0.1, default: 0.5 },
|
||||
brightness: { type: 'range', min: 0.3, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OceanGlimmerPreset;
|
||||
74
presets/preset-registry.js
Normal file
74
presets/preset-registry.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Preset registry for LEDLab
|
||||
|
||||
const RainbowPreset = require('./rainbow-preset');
|
||||
const AuroraCurtainsPreset = require('./aurora-curtains-preset');
|
||||
const BouncingBallPreset = require('./bouncing-ball-preset');
|
||||
const CircuitPulsePreset = require('./circuit-pulse-preset');
|
||||
const FadeGreenBluePreset = require('./fade-green-blue-preset');
|
||||
const LavaLampPreset = require('./lava-lamp-preset');
|
||||
const MeteorRainPreset = require('./meteor-rain-preset');
|
||||
const NebulaDriftPreset = require('./nebula-drift-preset');
|
||||
const OceanGlimmerPreset = require('./ocean-glimmer-preset');
|
||||
const SpiralBloomPreset = require('./spiral-bloom-preset');
|
||||
const VoxelFirefliesPreset = require('./voxel-fireflies-preset');
|
||||
const WormholeTunnelPreset = require('./wormhole-tunnel-preset');
|
||||
|
||||
class PresetRegistry {
|
||||
constructor() {
|
||||
this.presets = new Map();
|
||||
this.registerDefaults();
|
||||
}
|
||||
|
||||
registerDefaults() {
|
||||
this.register('rainbow', RainbowPreset);
|
||||
this.register('aurora-curtains', AuroraCurtainsPreset);
|
||||
this.register('bouncing-ball', BouncingBallPreset);
|
||||
this.register('circuit-pulse', CircuitPulsePreset);
|
||||
this.register('fade-green-blue', FadeGreenBluePreset);
|
||||
this.register('lava-lamp', LavaLampPreset);
|
||||
this.register('meteor-rain', MeteorRainPreset);
|
||||
this.register('nebula-drift', NebulaDriftPreset);
|
||||
this.register('ocean-glimmer', OceanGlimmerPreset);
|
||||
this.register('spiral-bloom', SpiralBloomPreset);
|
||||
this.register('voxel-fireflies', VoxelFirefliesPreset);
|
||||
this.register('wormhole-tunnel', WormholeTunnelPreset);
|
||||
}
|
||||
|
||||
register(name, presetClass) {
|
||||
this.presets.set(name, presetClass);
|
||||
}
|
||||
|
||||
createPreset(name, width = 16, height = 16) {
|
||||
const presetClass = this.presets.get(name);
|
||||
if (!presetClass) {
|
||||
throw new Error(`Preset '${name}' not found`);
|
||||
}
|
||||
|
||||
return new presetClass(width, height);
|
||||
}
|
||||
|
||||
getPresetNames() {
|
||||
return Array.from(this.presets.keys());
|
||||
}
|
||||
|
||||
getPresetMetadata(name) {
|
||||
try {
|
||||
const preset = this.createPreset(name);
|
||||
return preset.getMetadata();
|
||||
} catch (error) {
|
||||
console.error(`Error getting metadata for preset '${name}':`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getAllPresetMetadata() {
|
||||
const metadata = {};
|
||||
for (const name of this.getPresetNames()) {
|
||||
metadata[name] = this.getPresetMetadata(name);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the constructor class
|
||||
module.exports = PresetRegistry;
|
||||
53
presets/rainbow-preset.js
Normal file
53
presets/rainbow-preset.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Rainbow preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, wheel } = require('./frame-utils');
|
||||
|
||||
class RainbowPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.defaultParameters = {
|
||||
speed: 1,
|
||||
brightness: 1.0,
|
||||
offset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const speed = this.getParameter('speed') || 1;
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
const offset = (this.getParameter('offset') || 0) + this.frameCount * speed;
|
||||
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const colorIndex = (i * 256 / frame.length + offset) & 255;
|
||||
const [r, g, b] = wheel(colorIndex);
|
||||
|
||||
// Apply brightness
|
||||
const adjustedR = Math.round(r * brightness);
|
||||
const adjustedG = Math.round(g * brightness);
|
||||
const adjustedB = Math.round(b * brightness);
|
||||
|
||||
frame[i] = adjustedR.toString(16).padStart(2, '0') +
|
||||
adjustedG.toString(16).padStart(2, '0') +
|
||||
adjustedB.toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Rainbow',
|
||||
description: 'Classic rainbow pattern that cycles through colors',
|
||||
parameters: {
|
||||
speed: { type: 'range', min: 0.1, max: 5.0, step: 0.1, default: 1.0 },
|
||||
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RainbowPreset;
|
||||
81
presets/spiral-bloom-preset.js
Normal file
81
presets/spiral-bloom-preset.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Spiral Bloom preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, frameToPayload, hexToRgb, samplePalette, toIndex } = require('./frame-utils');
|
||||
|
||||
class SpiralBloomPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.rotation = 0;
|
||||
this.hueShift = 0;
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('051923') },
|
||||
{ stop: 0.2, color: hexToRgb('0c4057') },
|
||||
{ stop: 0.45, color: hexToRgb('1d7a70') },
|
||||
{ stop: 0.7, color: hexToRgb('39b15f') },
|
||||
{ stop: 0.88, color: hexToRgb('9dd54c') },
|
||||
{ stop: 1.0, color: hexToRgb('f7f5bc') },
|
||||
];
|
||||
this.defaultParameters = {
|
||||
rotationSpeed: 0.7,
|
||||
hueSpeed: 0.2,
|
||||
spiralArms: 5,
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.rotation += 0.016 * (this.getParameter('rotationSpeed') || 0.7);
|
||||
this.hueShift += 0.016 * (this.getParameter('hueSpeed') || 0.2);
|
||||
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
const spiralArms = this.getParameter('spiralArms') || 5;
|
||||
|
||||
const cx = (this.width - 1) / 2;
|
||||
const cy = (this.height - 1) / 2;
|
||||
const radiusNorm = Math.hypot(cx, cy) || 1;
|
||||
|
||||
for (let row = 0; row < this.height; ++row) {
|
||||
for (let col = 0; col < this.width; ++col) {
|
||||
const dx = col - cx;
|
||||
const dy = row - cy;
|
||||
const radius = Math.hypot(dx, dy) / radiusNorm;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const arm = 0.5 + 0.5 * Math.sin(spiralArms * (angle + this.rotation) + this.hueShift * Math.PI * 2);
|
||||
const value = Math.min(1, radius * 0.8 + arm * 0.4);
|
||||
|
||||
let color = samplePalette(this.paletteStops, value);
|
||||
|
||||
// Apply brightness
|
||||
if (brightness < 1.0) {
|
||||
const rgb = hexToRgb(color);
|
||||
color = Math.round(rgb.r * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.g * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.b * brightness).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
frame[toIndex(col, row, this.width)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Spiral Bloom',
|
||||
description: 'Rotating spiral patterns blooming outward',
|
||||
parameters: {
|
||||
rotationSpeed: { type: 'range', min: 0.3, max: 1.5, step: 0.1, default: 0.7 },
|
||||
hueSpeed: { type: 'range', min: 0.1, max: 0.5, step: 0.05, default: 0.2 },
|
||||
spiralArms: { type: 'range', min: 3, max: 8, step: 1, default: 5 },
|
||||
brightness: { type: 'range', min: 0.3, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SpiralBloomPreset;
|
||||
141
presets/voxel-fireflies-preset.js
Normal file
141
presets/voxel-fireflies-preset.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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;
|
||||
90
presets/wormhole-tunnel-preset.js
Normal file
90
presets/wormhole-tunnel-preset.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// Wormhole Tunnel preset for LEDLab
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame, frameToPayload, hexToRgb, samplePalette, toIndex, clamp } = require('./frame-utils');
|
||||
|
||||
class WormholeTunnelPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16) {
|
||||
super(width, height);
|
||||
this.timeSeconds = 0;
|
||||
this.paletteStops = [
|
||||
{ stop: 0.0, color: hexToRgb('010005') },
|
||||
{ stop: 0.2, color: hexToRgb('07204f') },
|
||||
{ stop: 0.45, color: hexToRgb('124aa0') },
|
||||
{ stop: 0.7, color: hexToRgb('36a5ff') },
|
||||
{ stop: 0.87, color: hexToRgb('99e6ff') },
|
||||
{ stop: 1.0, color: hexToRgb('f1fbff') },
|
||||
];
|
||||
this.defaultParameters = {
|
||||
ringDensity: 8,
|
||||
ringSpeed: 1.4,
|
||||
ringSharpness: 7.5,
|
||||
twistIntensity: 2.2,
|
||||
twistSpeed: 0.9,
|
||||
coreExponent: 1.6,
|
||||
brightness: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
this.timeSeconds += 0.016; // Assume 60 FPS
|
||||
|
||||
const frame = createFrame(this.width, this.height);
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
|
||||
const cx = (this.width - 1) / 2;
|
||||
const cy = (this.height - 1) / 2;
|
||||
const radiusNorm = Math.hypot(cx, cy) || 1;
|
||||
|
||||
for (let row = 0; row < this.height; ++row) {
|
||||
for (let col = 0; col < this.width; ++col) {
|
||||
const dx = col - cx;
|
||||
const dy = row - cy;
|
||||
const radius = Math.hypot(dx, dy) / radiusNorm;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
|
||||
const radialPhase = radius * (this.getParameter('ringDensity') || 8) - this.timeSeconds * (this.getParameter('ringSpeed') || 1.4);
|
||||
const ring = Math.exp(-Math.pow(Math.sin(radialPhase * Math.PI), 2) * (this.getParameter('ringSharpness') || 7.5));
|
||||
|
||||
const twist = Math.sin(angle * (this.getParameter('twistIntensity') || 2.2) + this.timeSeconds * (this.getParameter('twistSpeed') || 0.9)) * 0.35 + 0.65;
|
||||
const depth = Math.pow(clamp(1 - radius, 0, 1), this.getParameter('coreExponent') || 1.6);
|
||||
|
||||
const value = clamp(ring * 0.6 + depth * 0.3 + twist * 0.1, 0, 1);
|
||||
|
||||
let color = samplePalette(this.paletteStops, value);
|
||||
|
||||
// Apply brightness
|
||||
if (brightness < 1.0) {
|
||||
const rgb = hexToRgb(color);
|
||||
color = Math.round(rgb.r * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.g * brightness).toString(16).padStart(2, '0') +
|
||||
Math.round(rgb.b * brightness).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
frame[toIndex(col, row, this.width)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Wormhole Tunnel',
|
||||
description: 'Hypnotic tunnel effect with rotating rings',
|
||||
parameters: {
|
||||
ringDensity: { type: 'range', min: 4, max: 12, step: 1, default: 8 },
|
||||
ringSpeed: { type: 'range', min: 0.5, max: 2.5, step: 0.1, default: 1.4 },
|
||||
ringSharpness: { type: 'range', min: 3, max: 12, step: 0.5, default: 7.5 },
|
||||
twistIntensity: { type: 'range', min: 1.0, max: 4.0, step: 0.2, default: 2.2 },
|
||||
twistSpeed: { type: 'range', min: 0.3, max: 1.5, step: 0.1, default: 0.9 },
|
||||
coreExponent: { type: 'range', min: 1.0, max: 2.5, step: 0.1, default: 1.6 },
|
||||
brightness: { type: 'range', min: 0.3, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WormholeTunnelPreset;
|
||||
Reference in New Issue
Block a user