Files
spore-ledlab/public/scripts/matrix-display.js
2025-10-11 17:46:32 +02:00

204 lines
7.0 KiB
JavaScript

// 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;
}