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