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