Files
spore-ledlab/presets/custom-preset.js
2025-10-12 13:52:22 +02:00

418 lines
11 KiB
JavaScript

// Custom Preset - A configurable preset based on JSON configuration
const BasePreset = require('./base-preset');
const { createFrame } = require('./frame-utils');
const { Shapes, Transforms, ColorGenerators, Animations, Patterns } = require('./building-blocks');
/**
* Custom Preset Configuration Schema:
*
* {
* "name": "My Custom Preset",
* "description": "Description of the preset",
* "layers": [
* {
* "type": "shape",
* "shape": "circle|rectangle|triangle|line|point|blob",
* "position": { "x": 8, "y": 8 },
* "size": { "width": 5, "height": 5, "radius": 3 },
* "color": {
* "type": "solid|gradient|palette|rainbow|radial",
* "value": "#ff0000",
* "stops": [...]
* },
* "animation": {
* "type": "move|rotate|scale|pulse|fade|bounce",
* "params": { ... }
* },
* "blendMode": "normal|add|multiply"
* },
* {
* "type": "pattern",
* "pattern": "trail|energyField|radial|spiral",
* "params": { ... }
* }
* ],
* "parameters": {
* "speed": { "type": "range", "min": 0.1, "max": 2.0, "default": 1.0 },
* ...
* }
* }
*/
class CustomPreset extends BasePreset {
constructor(width = 16, height = 16, configuration = null) {
super(width, height);
this.configuration = configuration || this.getDefaultConfiguration();
this.animationStates = new Map();
this.time = 0;
this.lastFrameTime = Date.now();
this.initializeParameters();
this.initializeAnimations();
}
getDefaultConfiguration() {
return {
name: 'Custom Preset',
description: 'A configurable preset',
layers: [],
parameters: {
speed: { type: 'range', min: 0.1, max: 2.0, step: 0.1, default: 1.0 },
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
},
};
}
setConfiguration(configuration) {
this.configuration = configuration;
this.initializeParameters();
this.initializeAnimations();
}
initializeParameters() {
this.defaultParameters = {};
if (this.configuration.parameters) {
Object.entries(this.configuration.parameters).forEach(([name, config]) => {
this.defaultParameters[name] = config.default;
});
}
this.resetToDefaults();
}
initializeAnimations() {
this.animationStates.clear();
this.configuration.layers?.forEach((layer, index) => {
if (layer.animation) {
const animation = this.createAnimation(layer.animation);
this.animationStates.set(index, animation);
}
});
}
createAnimation(animConfig) {
const type = animConfig.type;
const params = animConfig.params || {};
switch (type) {
case 'move':
return Animations.linearMove(
params.startX || 0,
params.startY || 0,
params.endX || this.width,
params.endY || this.height,
params.duration || 2.0
);
case 'rotate':
return Animations.rotation(params.speed || 1.0);
case 'pulse':
return Animations.pulse(
params.minScale || 0.5,
params.maxScale || 1.5,
params.frequency || 1.0
);
case 'fade':
return Animations.fade(
params.duration || 2.0,
params.fadeIn !== false
);
case 'oscillateX':
return Animations.oscillate(
params.center || this.width / 2,
params.amplitude || this.width / 4,
params.frequency || 0.5,
params.phase || 0
);
case 'oscillateY':
return Animations.oscillate(
params.center || this.height / 2,
params.amplitude || this.height / 4,
params.frequency || 0.5,
params.phase || 0
);
default:
return () => ({});
}
}
createColorGenerator(colorConfig) {
if (!colorConfig) {
return ColorGenerators.solid('ff0000');
}
const type = colorConfig.type || 'solid';
switch (type) {
case 'solid':
return ColorGenerators.solid(colorConfig.value || 'ff0000');
case 'gradient':
return ColorGenerators.gradient(
colorConfig.color1 || 'ff0000',
colorConfig.color2 || '0000ff'
);
case 'palette':
return ColorGenerators.palette(colorConfig.stops || [
{ position: 0, color: '000000' },
{ position: 1, color: 'ffffff' }
]);
case 'rainbow':
return ColorGenerators.rainbow();
case 'radial':
return ColorGenerators.radial(
colorConfig.color1 || 'ff0000',
colorConfig.color2 || '0000ff',
colorConfig.centerX || this.width / 2,
colorConfig.centerY || this.height / 2,
colorConfig.maxRadius || Math.hypot(this.width / 2, this.height / 2)
);
default:
return ColorGenerators.solid('ff0000');
}
}
renderFrame() {
const now = Date.now();
const deltaTime = (now - this.lastFrameTime) / 1000;
this.lastFrameTime = now;
const speed = this.getParameter('speed') || 1.0;
const brightness = this.getParameter('brightness') || 1.0;
this.time += deltaTime * speed;
let frame = createFrame(this.width, this.height, '000000');
// Render each layer
this.configuration.layers?.forEach((layer, index) => {
this.renderLayer(frame, layer, index, brightness);
});
return frame;
}
renderLayer(frame, layer, layerIndex, globalBrightness) {
const type = layer.type;
if (type === 'shape') {
this.renderShape(frame, layer, layerIndex, globalBrightness);
} else if (type === 'pattern') {
this.renderPattern(frame, layer, layerIndex, globalBrightness);
}
}
renderShape(frame, layer, layerIndex, globalBrightness) {
let position = layer.position || { x: this.width / 2, y: this.height / 2 };
let size = layer.size || { radius: 3 };
let rotation = 0;
let scale = 1.0;
let intensity = (layer.intensity || 1.0) * globalBrightness;
// Apply animation
if (this.animationStates.has(layerIndex)) {
const animation = this.animationStates.get(layerIndex);
const animState = animation();
if (animState.x !== undefined && animState.y !== undefined) {
position = { x: animState.x, y: animState.y };
}
if (animState.angle !== undefined) {
rotation = animState.angle;
}
if (animState.scale !== undefined) {
scale = animState.scale;
}
if (animState.intensity !== undefined) {
intensity *= animState.intensity;
}
if (animState.value !== undefined && layer.animation?.axis === 'x') {
position.x = animState.value;
} else if (animState.value !== undefined && layer.animation?.axis === 'y') {
position.y = animState.value;
}
}
// Create color generator
const colorGen = this.createColorGenerator(layer.color);
const color = typeof colorGen === 'function' ? colorGen(0.5) : colorGen;
// Apply scale to size
const scaledSize = {
radius: (size.radius || 3) * scale,
width: (size.width || 5) * scale,
height: (size.height || 5) * scale,
};
// Render shape
const shapeType = layer.shape || 'circle';
switch (shapeType) {
case 'circle':
Shapes.circle(
frame,
this.width,
this.height,
position.x,
position.y,
scaledSize.radius,
color,
intensity
);
break;
case 'rectangle':
Shapes.rectangle(
frame,
this.width,
this.height,
position.x - scaledSize.width / 2,
position.y - scaledSize.height / 2,
scaledSize.width,
scaledSize.height,
color,
intensity
);
break;
case 'blob':
Shapes.blob(
frame,
this.width,
this.height,
position.x,
position.y,
scaledSize.radius,
color,
layer.falloffPower || 2
);
break;
case 'point':
Shapes.point(
frame,
this.width,
this.height,
position.x,
position.y,
color,
intensity
);
break;
case 'triangle':
// Triangle with rotation
const triSize = scaledSize.radius || 3;
const points = [
{ x: 0, y: -triSize },
{ x: -triSize, y: triSize },
{ x: triSize, y: triSize }
];
const rotated = points.map(p =>
Transforms.rotate(p.x, p.y, 0, 0, rotation)
);
Shapes.triangle(
frame,
this.width,
this.height,
position.x + rotated[0].x,
position.y + rotated[0].y,
position.x + rotated[1].x,
position.y + rotated[1].y,
position.x + rotated[2].x,
position.y + rotated[2].y,
color,
intensity
);
break;
case 'line':
const lineParams = layer.lineParams || {};
Shapes.line(
frame,
this.width,
this.height,
lineParams.x1 || position.x,
lineParams.y1 || position.y,
lineParams.x2 || position.x + 5,
lineParams.y2 || position.y + 5,
color,
lineParams.thickness || 1,
intensity
);
break;
}
}
renderPattern(frame, layer, layerIndex, globalBrightness) {
const patternType = layer.pattern;
const params = layer.params || {};
const intensity = (layer.intensity || 1.0) * globalBrightness;
switch (patternType) {
case 'trail':
Patterns.trail(frame, params.decayFactor || 0.8);
break;
case 'radial':
const radialColorGen = this.createColorGenerator(layer.color);
Patterns.radial(
frame,
this.width,
this.height,
params.centerX || this.width / 2,
params.centerY || this.height / 2,
radialColorGen,
intensity
);
break;
case 'spiral':
const spiralColorGen = this.createColorGenerator(layer.color);
Patterns.spiral(
frame,
this.width,
this.height,
params.centerX || this.width / 2,
params.centerY || this.height / 2,
params.arms || 5,
this.time * (params.rotationSpeed || 1.0),
spiralColorGen,
intensity
);
break;
}
}
getMetadata() {
return {
name: this.configuration.name || 'Custom Preset',
description: this.configuration.description || 'A custom configurable preset',
parameters: this.configuration.parameters || {},
width: this.width,
height: this.height,
};
}
}
module.exports = CustomPreset;