// Preset Controls Component class PresetControls extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); this.presets = {}; this.currentPreset = null; this.presetControls = new Map(); } mount() { super.mount(); this.setupEventListeners(); this.setupViewModelListeners(); this.loadPresets(); } setupEventListeners() { // Preset selection const presetSelect = this.findElement('#preset-select'); if (presetSelect) { this.addEventListener(presetSelect, 'change', (e) => { this.selectPreset(e.target.value); }); } // Apply matrix config button const applyMatrixBtn = this.findElement('#apply-matrix-btn'); if (applyMatrixBtn) { this.addEventListener(applyMatrixBtn, 'click', () => { this.applyMatrixConfig(); }); } // Start/Stop buttons const startBtn = this.findElement('#start-btn'); if (startBtn) { this.addEventListener(startBtn, 'click', () => { this.startStreaming(); }); } const stopBtn = this.findElement('#stop-btn'); if (stopBtn) { this.addEventListener(stopBtn, 'click', () => { this.stopStreaming(); }); } // Test and clear buttons const sendTestBtn = this.findElement('#send-test-btn'); if (sendTestBtn) { this.addEventListener(sendTestBtn, 'click', () => { this.sendTestFrame(); }); } const clearMatrixBtn = this.findElement('#clear-matrix-btn'); if (clearMatrixBtn) { this.addEventListener(clearMatrixBtn, 'click', () => { this.clearMatrix(); }); } } setupViewModelListeners() { this.subscribeToEvent('streamingStarted', (data) => { this.updateStreamingState(true, data.preset); }); this.subscribeToEvent('streamingStopped', () => { this.updateStreamingState(false); }); this.subscribeToEvent('presetParameterUpdated', (data) => { this.updatePresetParameter(data.parameter, data.value); }); this.subscribeToEvent('status', (data) => { // Update UI to reflect current server state const isStreaming = data.data.streaming; const currentPreset = data.data.currentPreset; const presetParameters = data.data.presetParameters; this.updateStreamingState(isStreaming, currentPreset ? { name: currentPreset } : null); if (currentPreset) { this.selectPreset(currentPreset.toLowerCase().replace('-preset', '')); } if (presetParameters && this.currentPreset) { // Update parameter controls with current values Object.entries(presetParameters).forEach(([param, value]) => { const control = this.presetControls.get(param); if (control) { if (control.type === 'range') { control.value = value; const valueDisplay = control.parentElement.querySelector('.preset-value'); if (valueDisplay) { valueDisplay.textContent = parseFloat(value).toFixed(2); } } else if (control.type === 'color') { control.value = this.hexToColorValue(value); } else { control.value = value; } } }); } }); } async loadPresets() { try { const response = await fetch('/api/presets'); const data = await response.json(); this.presets = data.presets; this.populatePresetSelect(); } catch (error) { console.error('Error loading presets:', error); } } populatePresetSelect() { const presetSelect = this.findElement('#preset-select'); if (!presetSelect) return; // Clear existing options (except the first one) while (presetSelect.children.length > 1) { presetSelect.removeChild(presetSelect.lastChild); } // Add preset options Object.entries(this.presets).forEach(([name, metadata]) => { const option = document.createElement('option'); option.value = name; option.textContent = metadata.name; presetSelect.appendChild(option); }); } selectPreset(presetName) { if (!presetName || !this.presets[presetName]) { this.currentPreset = null; this.clearPresetControls(); return; } this.currentPreset = this.presets[presetName]; this.createPresetControls(); } createPresetControls() { const controlsContainer = this.findElement('#preset-controls'); if (!controlsContainer) return; // Clear existing controls controlsContainer.innerHTML = ''; if (!this.currentPreset || !this.currentPreset.parameters) { return; } // Create controls for each parameter Object.entries(this.currentPreset.parameters).forEach(([paramName, paramConfig]) => { const controlDiv = document.createElement('div'); controlDiv.className = 'preset-control'; const label = document.createElement('label'); label.className = 'preset-label'; label.textContent = this.formatParameterName(paramName); controlDiv.appendChild(label); const input = this.createParameterInput(paramName, paramConfig); controlDiv.appendChild(input); controlsContainer.appendChild(controlDiv); this.presetControls.set(paramName, input); }); } createParameterInput(paramName, paramConfig) { const { type, min, max, step, default: defaultValue } = paramConfig; switch (type) { case 'range': const sliderInput = document.createElement('input'); sliderInput.type = 'range'; sliderInput.className = 'preset-slider'; sliderInput.min = min; sliderInput.max = max; sliderInput.step = step || 0.1; sliderInput.value = defaultValue; // Add value display const valueDisplay = document.createElement('span'); valueDisplay.className = 'preset-value'; valueDisplay.textContent = defaultValue; sliderInput.addEventListener('input', (e) => { valueDisplay.textContent = parseFloat(e.target.value).toFixed(2); this.updatePresetParameter(paramName, parseFloat(e.target.value)); }); const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.gap = '0.5rem'; container.appendChild(sliderInput); container.appendChild(valueDisplay); return container; case 'color': const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.className = 'preset-input'; colorInput.value = this.hexToColorValue(defaultValue); colorInput.addEventListener('input', (e) => { const hexValue = this.colorValueToHex(e.target.value); this.updatePresetParameter(paramName, hexValue); }); return colorInput; default: const textInput = document.createElement('input'); textInput.type = 'text'; textInput.className = 'preset-input'; textInput.value = defaultValue; textInput.addEventListener('input', (e) => { this.updatePresetParameter(paramName, e.target.value); }); return textInput; } } updatePresetParameter(parameter, value) { // Send parameter update to server this.viewModel.publish('updatePresetParameter', { parameter, value }); } clearPresetControls() { const controlsContainer = this.findElement('#preset-controls'); if (controlsContainer) { controlsContainer.innerHTML = ''; } this.presetControls.clear(); } formatParameterName(name) { return name .split(/(?=[A-Z])/) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } hexToColorValue(hex) { // Convert hex (rrggbb) to color value (#rrggbb) if (hex.startsWith('#')) return hex; return `#${hex}`; } colorValueToHex(colorValue) { // Convert color value (#rrggbb) to hex (rrggbb) return colorValue.replace('#', ''); } startStreaming() { const presetSelect = this.findElement('#preset-select'); if (!presetSelect || !presetSelect.value) { alert('Please select a preset first'); return; } const width = parseInt(this.findElement('#matrix-width')?.value) || 16; const height = parseInt(this.findElement('#matrix-height')?.value) || 16; this.viewModel.publish('startPreset', { presetName: presetSelect.value, width, height }); } stopStreaming() { this.viewModel.publish('stopStreaming', {}); } sendTestFrame() { // Create a test frame with a simple pattern in serpentine order const width = parseInt(this.findElement('#matrix-width')?.value) || 16; const height = parseInt(this.findElement('#matrix-height')?.value) || 16; let frameData = 'RAW:'; for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { // Calculate serpentine index manually const hardwareIndex = (row % 2 === 0) ? (row * width + col) : (row * width + (width - 1 - col)); // Create a checkerboard pattern if ((row + col) % 2 === 0) { frameData += '00ff00'; // Green } else { frameData += '000000'; // Black } } } this.viewModel.publish('broadcastToAll', { message: frameData }); } clearMatrix() { // Send a frame with all black pixels in serpentine order const width = parseInt(this.findElement('#matrix-width')?.value) || 16; const height = parseInt(this.findElement('#matrix-height')?.value) || 16; let frameData = 'RAW:'; for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { // Calculate serpentine index manually const hardwareIndex = (row % 2 === 0) ? (row * width + col) : (row * width + (width - 1 - col)); frameData += '000000'; } } this.viewModel.publish('broadcastToAll', { message: frameData }); } applyMatrixConfig() { const width = parseInt(this.findElement('#matrix-width')?.value); const height = parseInt(this.findElement('#matrix-height')?.value); if (width && height) { this.viewModel.publish('setMatrixSize', { width, height }); } } updateStreamingState(isStreaming, preset) { const startBtn = this.findElement('#start-btn'); const stopBtn = this.findElement('#stop-btn'); if (isStreaming) { startBtn.disabled = true; stopBtn.disabled = false; } else { startBtn.disabled = false; stopBtn.disabled = true; } } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = PresetControls; }