Add value sources for dynamic brightness control on LED targets

Introduces a new Value Source entity that produces a scalar float (0.0-1.0)
for dynamic brightness modulation. Three subtypes: Static (constant),
Animated (sine/triangle/square/sawtooth waveform), and Audio-reactive
(RMS/peak/beat from mono audio source). Value sources can be optionally
attached to LED targets to control brightness each frame.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:19:40 +03:00
parent 27720e51aa
commit ef474fe275
26 changed files with 1704 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ import {
_targetEditorDevices, set_targetEditorDevices,
_deviceBrightnessCache,
kcWebSockets,
_cachedValueSources, set_cachedValueSources,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js';
@@ -112,6 +113,7 @@ class TargetEditorModal extends Modal {
name: document.getElementById('target-editor-name').value,
device: document.getElementById('target-editor-device').value,
css_source: document.getElementById('target-editor-css-source').value,
brightness_vs: document.getElementById('target-editor-brightness-vs').value,
fps: document.getElementById('target-editor-fps').value,
keepalive_interval: document.getElementById('target-editor-keepalive-interval').value,
};
@@ -177,16 +179,32 @@ function _populateCssDropdown(selectedId = '') {
).join('');
}
function _populateBrightnessVsDropdown(selectedId = '') {
const select = document.getElementById('target-editor-brightness-vs');
let html = `<option value="">${t('targets.brightness_vs.none')}</option>`;
_cachedValueSources.forEach(vs => {
const typeIcons = { static: '📊', animated: '🔄', audio: '🎵' };
const icon = typeIcons[vs.source_type] || '🎚️';
html += `<option value="${vs.id}"${vs.id === selectedId ? ' selected' : ''}>${icon} ${escapeHtml(vs.name)}</option>`;
});
select.innerHTML = html;
}
export async function showTargetEditor(targetId = null, cloneData = null) {
try {
// Load devices and CSS sources for dropdowns
const [devicesResp, cssResp] = await Promise.all([
// Load devices, CSS sources, and value sources for dropdowns
const [devicesResp, cssResp, vsResp] = await Promise.all([
fetch(`${API_BASE}/devices`, { headers: getHeaders() }),
fetchWithAuth('/color-strip-sources'),
fetchWithAuth('/value-sources'),
]);
const devices = devicesResp.ok ? (await devicesResp.json()).devices || [] : [];
const cssSources = cssResp.ok ? (await cssResp.json()).sources || [] : [];
if (vsResp.ok) {
const vsData = await vsResp.json();
set_cachedValueSources(vsData.sources || []);
}
set_targetEditorDevices(devices);
_editorCssSources = cssSources;
@@ -220,6 +238,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-title').textContent = t('targets.edit');
_populateCssDropdown(target.color_strip_source_id || '');
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
} else if (cloneData) {
// Cloning — create mode but pre-filled from clone data
document.getElementById('target-editor-id').value = '';
@@ -233,6 +252,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-title').textContent = t('targets.add');
_populateCssDropdown(cloneData.color_strip_source_id || '');
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
} else {
// Creating new target
document.getElementById('target-editor-id').value = '';
@@ -244,6 +264,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-title').textContent = t('targets.add');
_populateCssDropdown('');
_populateBrightnessVsDropdown('');
}
// Auto-name generation
@@ -296,10 +317,13 @@ export async function saveTargetEditor() {
const fps = parseInt(document.getElementById('target-editor-fps').value) || 30;
const colorStripSourceId = document.getElementById('target-editor-css-source').value;
const brightnessVsId = document.getElementById('target-editor-brightness-vs').value;
const payload = {
name,
device_id: deviceId,
color_strip_source_id: colorStripSourceId,
brightness_value_source_id: brightnessVsId,
fps,
keepalive_interval: standbyInterval,
};