// 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;