// 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() { // FPS slider const fpsSlider = this.findElement('#fps-slider'); const fpsValue = this.findElement('#fps-value'); if (fpsSlider && fpsValue) { this.addEventListener(fpsSlider, 'input', (e) => { const fps = parseInt(e.target.value); fpsValue.textContent = fps; this.updateFrameRate(fps); }); } // Preset selection - immediate switching const presetSelect = this.findElement('#preset-select'); if (presetSelect) { this.addEventListener(presetSelect, 'change', (e) => { const presetName = e.target.value; this.selectPreset(presetName); // If currently streaming, automatically restart with new preset const toggleBtn = this.findElement('#toggle-stream-btn'); if (toggleBtn && toggleBtn.dataset.streaming === 'true' && presetName) { this.startStreaming(); } }); } // Apply matrix config button const applyMatrixBtn = this.findElement('#apply-matrix-btn'); if (applyMatrixBtn) { this.addEventListener(applyMatrixBtn, 'click', () => { this.applyMatrixConfig(); }); } // Toggle stream button const toggleStreamBtn = this.findElement('#toggle-stream-btn'); if (toggleStreamBtn) { this.addEventListener(toggleStreamBtn, 'click', () => { const isStreaming = toggleStreamBtn.dataset.streaming === 'true'; if (isStreaming) { this.stopStreaming(); } else { this.startStreaming(); } }); } // 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) => { // Update control display without triggering another update const control = this.presetControls.get(data.parameter); if (control) { if (control.type === 'range') { control.value = data.value; const valueDisplay = control.parentElement.querySelector('.preset-value'); if (valueDisplay) { valueDisplay.textContent = parseFloat(data.value).toFixed(2); } } else if (control.type === 'color') { control.value = this.hexToColorValue(data.value); } else { control.value = 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; const fps = data.data.fps; this.updateStreamingState(isStreaming, currentPreset ? { name: currentPreset } : null); // Update FPS display if (fps !== undefined) { const fpsSlider = this.findElement('#fps-slider'); const fpsValue = this.findElement('#fps-value'); if (fpsSlider && fpsValue) { fpsSlider.value = fps; fpsValue.textContent = fps; } } // Only select preset if it's different from current (avoid recreating controls) const presetSelect = this.findElement('#preset-select'); if (currentPreset && presetSelect && presetSelect.value !== currentPreset) { this.selectPreset(currentPreset); } if (presetParameters && this.currentPreset) { // Update parameter controls with current values without triggering events 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; } } }); } }); this.subscribeToEvent('frameRateUpdated', (data) => { const fpsSlider = this.findElement('#fps-slider'); const fpsValue = this.findElement('#fps-value'); if (fpsSlider && fpsValue && data.fps) { fpsSlider.value = data.fps; fpsValue.textContent = data.fps; } }); } 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) => { const value = parseFloat(e.target.value); valueDisplay.textContent = value.toFixed(2); this.updatePresetParameter(paramName, value); // Visual feedback for real-time update valueDisplay.style.color = 'var(--accent-primary)'; setTimeout(() => { valueDisplay.style.color = ''; }, 200); }); 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); // Visual feedback for real-time update colorInput.style.borderColor = 'var(--accent-primary)'; setTimeout(() => { colorInput.style.borderColor = ''; }, 200); }); 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); // Visual feedback for real-time update textInput.style.borderColor = 'var(--accent-primary)'; setTimeout(() => { textInput.style.borderColor = ''; }, 200); }); return textInput; } } updatePresetParameter(parameter, value) { // Send parameter update to server immediately (real-time) this.viewModel.publish('updatePresetParameter', { parameter, value }); console.log(`Parameter updated: ${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 toggleBtn = this.findElement('#toggle-stream-btn'); const btnIcon = toggleBtn?.querySelector('.btn-icon'); const btnText = toggleBtn?.querySelector('.btn-text'); if (toggleBtn) { toggleBtn.dataset.streaming = isStreaming ? 'true' : 'false'; if (isStreaming) { if (btnIcon) btnIcon.textContent = '⏸'; if (btnText) btnText.textContent = 'Stop Streaming'; toggleBtn.classList.remove('btn-primary'); toggleBtn.classList.add('btn-stop'); } else { if (btnIcon) btnIcon.textContent = '▶'; if (btnText) btnText.textContent = 'Start Streaming'; toggleBtn.classList.remove('btn-stop'); toggleBtn.classList.add('btn-primary'); } } } updateFrameRate(fps) { this.viewModel.publish('updateFrameRate', { fps }); } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = PresetControls; }