const dgram = require('dgram'); const { clamp, createFrame, fadeFrame, frameToPayload, hexToRgb, samplePalette, toIndex, } = require('./shared-frame-utils'); const DEFAULT_PORT = 4210; const DEFAULT_WIDTH = 16; const DEFAULT_HEIGHT = 16; const DEFAULT_INTERVAL_MS = 45; const DEFAULT_METEOR_COUNT = 12; const BASE_SPEED_MIN = 4; const BASE_SPEED_MAX = 10; const TRAIL_DECAY = 0.76; const 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') }, ]; const host = process.argv[2]; const port = parseInt(process.argv[3] || String(DEFAULT_PORT), 10); const width = parseInt(process.argv[4] || String(DEFAULT_WIDTH), 10); const height = parseInt(process.argv[5] || String(DEFAULT_HEIGHT), 10); const intervalMs = parseInt(process.argv[6] || String(DEFAULT_INTERVAL_MS), 10); const meteorCount = parseInt(process.argv[7] || String(DEFAULT_METEOR_COUNT), 10); if (!host) { console.error('Usage: node meteor-rain.js [port] [width] [height] [interval-ms] [meteor-count]'); process.exit(1); } if (Number.isNaN(port) || Number.isNaN(width) || Number.isNaN(height) || Number.isNaN(intervalMs) || Number.isNaN(meteorCount)) { console.error('Invalid numeric argument. Expected integers for port, width, height, interval-ms, and meteor-count.'); process.exit(1); } if (width <= 0 || height <= 0) { console.error('Matrix dimensions must be positive integers.'); process.exit(1); } if (meteorCount <= 0) { console.error('Meteor count must be a positive integer.'); process.exit(1); } const socket = dgram.createSocket('udp4'); const isBroadcast = host === '255.255.255.255' || host.endsWith('.255'); const frame = createFrame(width, height); const meteors = createMeteors(meteorCount, width, height); const frameTimeSeconds = intervalMs / 1000; if (isBroadcast) { socket.bind(() => { socket.setBroadcast(true); }); } socket.on('error', (error) => { console.error('Socket error:', error.message); }); function createMeteors(count, matrixWidth, matrixHeight) { const meteorList = []; for (let index = 0; index < count; ++index) { meteorList.push(spawnMeteor(matrixWidth, matrixHeight)); } return meteorList; } function spawnMeteor(matrixWidth, matrixHeight) { const angle = (Math.PI / 4) * (0.6 + Math.random() * 0.8); const speed = BASE_SPEED_MIN + Math.random() * (BASE_SPEED_MAX - BASE_SPEED_MIN); return { x: Math.random() * matrixWidth, y: -Math.random() * matrixHeight, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, }; } function drawMeteor(meteor) { const col = Math.round(meteor.x); const row = Math.round(meteor.y); if (col < 0 || col >= width || row < 0 || row >= height) { return; } const energy = clamp(1.2 - Math.random() * 0.2, 0, 1); frame[toIndex(col, row, width)] = samplePalette(paletteStops, energy); } function updateMeteors(deltaSeconds) { meteors.forEach((meteor, index) => { meteor.x += meteor.vx * deltaSeconds; meteor.y += meteor.vy * deltaSeconds; drawMeteor(meteor); if (meteor.x > width + 1 || meteor.y > height + 1) { meteors[index] = spawnMeteor(width, height); } }); } function generateFrame() { fadeFrame(frame, TRAIL_DECAY); updateMeteors(frameTimeSeconds); return frameToPayload(frame); } function sendFrame() { const payload = generateFrame(); const message = Buffer.from(payload, 'utf8'); socket.send(message, port, host); } setInterval(sendFrame, intervalMs); console.log( `Streaming meteor rain to ${host}:${port} (${width}x${height}, interval=${intervalMs}ms, meteors=${meteorCount})`, );