Add min brightness threshold to LED targets

New per-target property: when effective output brightness
(max pixel value × device/source brightness) falls below
the threshold, LEDs turn off completely. Useful for cutting
dim flicker in audio-reactive and ambient setups.

Threshold slider (0–254) in target editor, badge on card,
hot-swap to running processors, persisted in storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 15:03:53 +03:00
parent c2deef214e
commit a164abe774
11 changed files with 75 additions and 1 deletions

View File

@@ -140,6 +140,7 @@ class TargetEditorModal extends Modal {
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,
brightness_threshold: document.getElementById('target-editor-brightness-threshold').value,
fps: document.getElementById('target-editor-fps').value,
keepalive_interval: document.getElementById('target-editor-keepalive-interval').value,
};
@@ -198,6 +199,11 @@ function _updateKeepaliveVisibility() {
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
}
function _updateBrightnessThresholdVisibility() {
// Always visible — threshold considers both brightness source and pixel content
document.getElementById('target-editor-brightness-threshold-group').style.display = '';
}
function _populateCssDropdown(selectedId = '') {
const select = document.getElementById('target-editor-css-source');
select.innerHTML = _editorCssSources.map(s =>
@@ -262,6 +268,10 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-keepalive-interval-value').textContent = target.keepalive_interval ?? 1.0;
document.getElementById('target-editor-title').textContent = t('targets.edit');
const thresh = target.min_brightness_threshold ?? 0;
document.getElementById('target-editor-brightness-threshold').value = thresh;
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
_populateCssDropdown(target.color_strip_source_id || '');
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
} else if (cloneData) {
@@ -276,6 +286,10 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-keepalive-interval-value').textContent = cloneData.keepalive_interval ?? 1.0;
document.getElementById('target-editor-title').textContent = t('targets.add');
const cloneThresh = cloneData.min_brightness_threshold ?? 0;
document.getElementById('target-editor-brightness-threshold').value = cloneThresh;
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
_populateCssDropdown(cloneData.color_strip_source_id || '');
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
} else {
@@ -288,6 +302,9 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-keepalive-interval-value').textContent = '1.0';
document.getElementById('target-editor-title').textContent = t('targets.add');
document.getElementById('target-editor-brightness-threshold').value = 0;
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
_populateCssDropdown('');
_populateBrightnessVsDropdown('');
}
@@ -298,12 +315,14 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
window._targetAutoName = _autoGenerateTargetName;
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
document.getElementById('target-editor-css-source').onchange = () => { _autoGenerateTargetName(); };
document.getElementById('target-editor-brightness-vs').onchange = () => { _updateBrightnessThresholdVisibility(); };
if (!targetId && !cloneData) _autoGenerateTargetName();
// Show/hide standby interval based on selected device capabilities
// Show/hide conditional fields
_updateDeviceInfo();
_updateKeepaliveVisibility();
_updateFpsRecommendation();
_updateBrightnessThresholdVisibility();
targetEditorModal.snapshot();
targetEditorModal.open();
@@ -343,12 +362,14 @@ export async function saveTargetEditor() {
const colorStripSourceId = document.getElementById('target-editor-css-source').value;
const brightnessVsId = document.getElementById('target-editor-brightness-vs').value;
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
const payload = {
name,
device_id: deviceId,
color_strip_source_id: colorStripSourceId,
brightness_value_source_id: brightnessVsId,
min_brightness_threshold: minBrightnessThreshold,
fps,
keepalive_interval: standbyInterval,
};
@@ -808,6 +829,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
${target.min_brightness_threshold > 0 ? `<span class="stream-card-prop" title="${t('targets.min_brightness_threshold')}">🔅 &lt;${target.min_brightness_threshold} → off</span>` : ''}
</div>
<div class="card-content">
${isProcessing ? `