// 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.defaultParameters = { meteorCount: 12, minSpeed: 4, maxSpeed: 10, trailDecay: 0.76, color1: '0a0126', color2: '123d8b', color3: '21c7d9', color4: 'f7ffff', }; } 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, paletteStops) { 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(paletteStops, energy); } updateMeteors(deltaSeconds, paletteStops) { this.meteors.forEach((meteor, index) => { meteor.x += meteor.vx * deltaSeconds; meteor.y += meteor.vy * deltaSeconds; this.drawMeteor(meteor, paletteStops); 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; const paletteStops = [ { stop: 0.0, color: hexToRgb(this.getParameter('color1') || '0a0126') }, { stop: 0.3, color: hexToRgb(this.getParameter('color2') || '123d8b') }, { stop: 0.7, color: hexToRgb(this.getParameter('color3') || '21c7d9') }, { stop: 1.0, color: hexToRgb(this.getParameter('color4') || 'f7ffff') }, ]; 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, paletteStops); // 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 }, color1: { type: 'color', default: '0a0126' }, color2: { type: 'color', default: '123d8b' }, color3: { type: 'color', default: '21c7d9' }, color4: { type: 'color', default: 'f7ffff' }, }, width: this.width, height: this.height, }; } } module.exports = MeteorRainPreset;