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

373 lines
12 KiB
JavaScript

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