const dgram = require('dgram'); const host = process.argv[2]; const port = parseInt(process.argv[3] || '4210', 10); const pixels = parseInt(process.argv[4] || '64', 10); const intervalMs = parseInt(process.argv[5] || '30', 10); if (!host) { console.error('Usage: node bouncing-ball.js [port] [pixels] [interval-ms]'); process.exit(1); } const socket = dgram.createSocket('udp4'); const isBroadcast = host === '255.255.255.255' || host.endsWith('.255'); let position = Math.random() * (pixels - 1); let velocity = randomVelocity(); function randomVelocity() { const min = 0.15; const max = 0.4; const sign = Math.random() < 0.5 ? -1 : 1; return (min + Math.random() * (max - min)) * sign; } function rebound(sign) { velocity = randomVelocity() * sign; } function mix(a, b, t) { return a + (b - a) * t; } function generateFrame() { const dt = intervalMs / 1000; position += velocity * dt * 60; // scale velocity to 60 FPS reference if (position < 0) { position = -position; rebound(1); } else if (position > pixels - 1) { position = (pixels - 1) - (position - (pixels - 1)); rebound(-1); } const activeIndex = Math.max(0, Math.min(pixels - 1, Math.round(position))); let payload = 'RAW:'; for (let i = 0; i < pixels; i++) { if (i === activeIndex) { payload += 'ff8000'; continue; } const distance = Math.abs(i - position); const intensity = Math.max(0, 1 - distance); const green = Math.round(mix(20, 200, intensity)).toString(16).padStart(2, '0'); const blue = Math.round(mix(40, 255, intensity)).toString(16).padStart(2, '0'); payload += '00' + green + blue; } return payload; } function sendFrame() { const payload = generateFrame(); const message = Buffer.from(payload, 'utf8'); socket.send(message, port, host); } setInterval(sendFrame, intervalMs); if (isBroadcast) { socket.bind(() => { socket.setBroadcast(true); }); } console.log(`Streaming bouncing ball pattern to ${host}:${port} with ${pixels} pixels (interval=${intervalMs}ms)`);