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

@@ -0,0 +1,179 @@
<!-- Value Source Editor Modal -->
<div id="value-source-modal" class="modal" role="dialog" aria-labelledby="value-source-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="value-source-modal-title" data-i18n="value_source.add">Add Value Source</h2>
<button class="modal-close-btn" onclick="closeValueSourceModal()" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<form id="value-source-form" onsubmit="return false;">
<input type="hidden" id="value-source-id">
<div id="value-source-error" class="error-message" style="display: none;"></div>
<!-- Name -->
<div class="form-group">
<div class="label-row">
<label for="value-source-name" data-i18n="value_source.name">Name:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.name.hint">A descriptive name for this value source</small>
<input type="text" id="value-source-name" data-i18n-placeholder="value_source.name.placeholder" placeholder="Brightness Pulse" required>
</div>
<!-- Type -->
<div class="form-group">
<div class="label-row">
<label for="value-source-type" data-i18n="value_source.type">Type:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.type.hint">Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input.</small>
<select id="value-source-type" onchange="onValueSourceTypeChange()">
<option value="static" data-i18n="value_source.type.static">Static</option>
<option value="animated" data-i18n="value_source.type.animated">Animated</option>
<option value="audio" data-i18n="value_source.type.audio">Audio</option>
</select>
</div>
<!-- Static fields -->
<div id="value-source-static-section">
<div class="form-group">
<div class="label-row">
<label for="value-source-value" data-i18n="value_source.value">Value:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.value.hint">Constant output value (0.0 = off, 1.0 = full brightness)</small>
<div class="range-with-value">
<input type="range" id="value-source-value" min="0" max="1" step="0.01" value="1.0"
oninput="document.getElementById('value-source-value-display').textContent = this.value">
<span id="value-source-value-display">1.0</span>
</div>
</div>
</div>
<!-- Animated fields -->
<div id="value-source-animated-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-waveform" data-i18n="value_source.waveform">Waveform:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.waveform.hint">Shape of the brightness animation cycle</small>
<select id="value-source-waveform">
<option value="sine" data-i18n="value_source.waveform.sine">Sine</option>
<option value="triangle" data-i18n="value_source.waveform.triangle">Triangle</option>
<option value="square" data-i18n="value_source.waveform.square">Square</option>
<option value="sawtooth" data-i18n="value_source.waveform.sawtooth">Sawtooth</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-speed" data-i18n="value_source.speed">Speed (cpm):</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.speed.hint">Cycles per minute — how fast the waveform repeats (1 = very slow, 120 = very fast)</small>
<div class="range-with-value">
<input type="range" id="value-source-speed" min="1" max="120" step="1" value="10"
oninput="document.getElementById('value-source-speed-display').textContent = this.value">
<span id="value-source-speed-display">10</span>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-min-value" data-i18n="value_source.min_value">Min Value:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.min_value.hint">Minimum output of the waveform cycle</small>
<div class="range-with-value">
<input type="range" id="value-source-min-value" min="0" max="1" step="0.01" value="0"
oninput="document.getElementById('value-source-min-value-display').textContent = this.value">
<span id="value-source-min-value-display">0</span>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-max-value" data-i18n="value_source.max_value">Max Value:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.max_value.hint">Maximum output of the waveform cycle</small>
<div class="range-with-value">
<input type="range" id="value-source-max-value" min="0" max="1" step="0.01" value="1"
oninput="document.getElementById('value-source-max-value-display').textContent = this.value">
<span id="value-source-max-value-display">1</span>
</div>
</div>
</div>
<!-- Audio fields -->
<div id="value-source-audio-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-audio-source" data-i18n="value_source.audio_source">Audio Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.audio_source.hint">Mono audio source to read audio levels from</small>
<select id="value-source-audio-source">
<!-- populated dynamically with mono audio sources -->
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-mode" data-i18n="value_source.mode">Mode:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.mode.hint">RMS measures average volume. Peak tracks loudest moments. Beat triggers on rhythm.</small>
<select id="value-source-mode">
<option value="rms" data-i18n="value_source.mode.rms">RMS (Volume)</option>
<option value="peak" data-i18n="value_source.mode.peak">Peak</option>
<option value="beat" data-i18n="value_source.mode.beat">Beat</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-sensitivity" data-i18n="value_source.sensitivity">Sensitivity:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.sensitivity.hint">Gain multiplier for the audio signal (higher = more reactive)</small>
<div class="range-with-value">
<input type="range" id="value-source-sensitivity" min="0.1" max="5" step="0.1" value="1.0"
oninput="document.getElementById('value-source-sensitivity-display').textContent = this.value">
<span id="value-source-sensitivity-display">1.0</span>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="value-source-smoothing" data-i18n="value_source.smoothing">Smoothing:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.smoothing.hint">Temporal smoothing (0 = instant response, 1 = very smooth/slow)</small>
<div class="range-with-value">
<input type="range" id="value-source-smoothing" min="0" max="1" step="0.05" value="0.3"
oninput="document.getElementById('value-source-smoothing-display').textContent = this.value">
<span id="value-source-smoothing-display">0.3</span>
</div>
</div>
</div>
<!-- Description -->
<div class="form-group">
<div class="label-row">
<label for="value-source-description" data-i18n="value_source.description">Description (optional):</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.description.hint">Optional notes about this value source</small>
<input type="text" id="value-source-description" data-i18n-placeholder="value_source.description.placeholder" placeholder="Describe this value source...">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeValueSourceModal()" data-i18n="settings.button.cancel">&times; Cancel</button>
<button class="btn btn-primary" onclick="saveValueSource()" data-i18n="settings.button.save">&check; Save</button>
</div>
</div>
</div>