418 lines
11 KiB
JavaScript
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;
|
|
|