// Matrix Display Component class MatrixDisplay extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); this.canvas = null; this.ctx = null; this.pixelSize = 20; this.matrixWidth = 16; this.matrixHeight = 16; this.frameData = null; this.animationId = null; } mount() { super.mount(); this.setupCanvas(); this.setupEventListeners(); this.setupViewModelListeners(); } setupCanvas() { this.canvas = this.findElement('#matrix-canvas'); if (!this.canvas) { console.error('Matrix canvas element not found'); return; } this.ctx = this.canvas.getContext('2d'); this.updateCanvasSize(); } updateCanvasSize() { if (!this.canvas || !this.ctx) return; const container = this.canvas.parentElement; const containerWidth = container.clientWidth - 32; // Account for padding const containerHeight = container.clientHeight - 32; // Calculate pixel size to fit the matrix in the container const maxPixelWidth = Math.floor(containerWidth / this.matrixWidth); const maxPixelHeight = Math.floor(containerHeight / this.matrixHeight); this.pixelSize = Math.min(maxPixelWidth, maxPixelHeight, 40); // Cap at 40px this.canvas.width = this.matrixWidth * this.pixelSize; this.canvas.height = this.matrixHeight * this.pixelSize; // Center the canvas const canvasContainer = this.canvas.parentElement; canvasContainer.style.display = 'flex'; canvasContainer.style.alignItems = 'center'; canvasContainer.style.justifyContent = 'center'; } setupEventListeners() { // Handle window resize this.addEventListener(window, 'resize', () => { this.updateCanvasSize(); this.renderFrame(); }); } setupViewModelListeners() { this.subscribeToEvent('frame', (data) => { this.frameData = data.data; this.renderFrame(); }); this.subscribeToEvent('matrixSizeChanged', (data) => { this.matrixWidth = data.size.width; this.matrixHeight = data.size.height; this.updateCanvasSize(); this.updateMatrixSize(data.size.width, data.size.height); this.renderFrame(); }); this.subscribeToEvent('streamingStarted', (data) => { this.updateStreamingStatus('streaming'); }); this.subscribeToEvent('streamingStopped', () => { this.updateStreamingStatus('stopped'); }); this.subscribeToEvent('status', (data) => { // Update UI to reflect current server state this.updateStreamingStatus(data.data.streaming ? 'streaming' : 'stopped'); if (data.data.matrixSize) { this.matrixWidth = data.data.matrixSize.width; this.matrixHeight = data.data.matrixSize.height; this.updateCanvasSize(); this.updateMatrixSize(data.data.matrixSize.width, data.data.matrixSize.height); } }); } updateStreamingStatus(status) { const statusElement = this.findElement('#streaming-status'); if (statusElement) { statusElement.className = `status-indicator status-${status}`; statusElement.textContent = status === 'streaming' ? 'Streaming' : 'Stopped'; } } renderFrame() { if (!this.ctx || !this.canvas) return; // Clear canvas this.ctx.fillStyle = 'var(--matrix-bg)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Render pixels if we have frame data if (this.frameData && this.frameData.startsWith('RAW:')) { const pixelData = this.frameData.substring(4); // Remove 'RAW:' prefix // Render pixels in serpentine order to match hardware layout for (let row = 0; row < this.matrixHeight; row++) { for (let col = 0; col < this.matrixWidth; col++) { // Calculate serpentine index manually const hardwareIndex = (row % 2 === 0) ? (row * this.matrixWidth + col) : (row * this.matrixWidth + (this.matrixWidth - 1 - col)); const pixelStart = hardwareIndex * 6; if (pixelStart + 5 < pixelData.length) { const hexColor = pixelData.substring(pixelStart, pixelStart + 6); this.renderPixel(col, row, hexColor); } } } } } renderPixel(col, row, hexColor) { const x = col * this.pixelSize; const y = row * this.pixelSize; // Convert hex to RGB const r = parseInt(hexColor.substring(0, 2), 16); const g = parseInt(hexColor.substring(2, 4), 16); const b = parseInt(hexColor.substring(4, 6), 16); // Skip rendering if pixel is black (optimization) if (r === 0 && g === 0 && b === 0) { return; } // Draw pixel with a subtle border for better visibility this.ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; this.ctx.fillRect(x + 1, y + 1, this.pixelSize - 2, this.pixelSize - 2); // Add a subtle glow effect for brighter pixels if (r > 128 || g > 128 || b > 128) { this.ctx.shadowColor = `rgb(${r}, ${g}, ${b})`; this.ctx.shadowBlur = 2; this.ctx.fillRect(x + 1, y + 1, this.pixelSize - 2, this.pixelSize - 2); this.ctx.shadowBlur = 0; } } // Method to render a test pattern renderTestPattern() { this.frameData = 'RAW:'; for (let row = 0; row < this.matrixHeight; row++) { for (let col = 0; col < this.matrixWidth; col++) { // Calculate serpentine index manually const hardwareIndex = (row % 2 === 0) ? (row * this.matrixWidth + col) : (row * this.matrixWidth + (this.matrixWidth - 1 - col)); // Create a checkerboard pattern if ((row + col) % 2 === 0) { this.frameData += '00ff00'; // Green } else { this.frameData += '000000'; // Black } } } this.renderFrame(); } // Method to clear the matrix clearMatrix() { if (this.ctx && this.canvas) { this.ctx.fillStyle = 'var(--matrix-bg)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } this.frameData = null; } // Update matrix size display updateMatrixSize(width, height) { const sizeElement = this.findElement('#matrix-size'); if (sizeElement) { sizeElement.textContent = `${width}x${height}`; } } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = MatrixDisplay; }