Introduces Synchronization Clocks — shared, controllable time bases that CSS sources can optionally reference for synchronized animation. Backend: - New SyncClock dataclass, JSON store, Pydantic schemas, REST API - Runtime clock with thread-safe pause/resume/reset and speed control - Ref-counted runtime pool with eager creation for API control - clock_id field on all ColorStripSource types - Stream integration: clock time/speed replaces source-local values - Paused clock skips rendering (saves CPU + stops frame pushes) - Included in backup/restore via STORE_MAP Frontend: - Sync Clocks tab in Streams section with cards and controls - Clock dropdown in CSS editor (hidden speed slider when clock set) - Clock crosslink badge on CSS source cards (replaces speed badge) - Targets tab uses DataCache for picture/audio sources and sync clocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
532 lines
42 KiB
HTML
532 lines
42 KiB
HTML
<!-- Color Strip Source Editor Modal -->
|
||
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2 id="css-editor-title"><svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></svg> <span data-i18n="color_strip.add">Add Color Strip Source</span></h2>
|
||
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="css-editor-form">
|
||
<input type="hidden" id="css-editor-id">
|
||
|
||
<div class="form-group">
|
||
<label for="css-editor-name" data-i18n="color_strip.name">Name:</label>
|
||
<input type="text" id="css-editor-name" data-i18n-placeholder="color_strip.name.placeholder" placeholder="Wall Strip" required>
|
||
</div>
|
||
|
||
<div id="css-editor-type-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-type" data-i18n="color_strip.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="color_strip.type.hint">Picture Source derives colors from a screen capture. Static Color fills all LEDs with one constant color. Gradient distributes a color gradient across all LEDs.</small>
|
||
<select id="css-editor-type" onchange="onCSSTypeChange()">
|
||
<option value="picture" data-i18n="color_strip.type.picture">Picture Source</option>
|
||
<option value="static" data-i18n="color_strip.type.static">Static Color</option>
|
||
<option value="gradient" data-i18n="color_strip.type.gradient">Gradient</option>
|
||
<option value="color_cycle" data-i18n="color_strip.type.color_cycle">Color Cycle</option>
|
||
<option value="effect" data-i18n="color_strip.type.effect">Procedural Effect</option>
|
||
<option value="composite" data-i18n="color_strip.type.composite">Composite</option>
|
||
<option value="mapped" data-i18n="color_strip.type.mapped">Mapped</option>
|
||
<option value="audio" data-i18n="color_strip.type.audio">Audio Reactive</option>
|
||
<option value="api_input" data-i18n="color_strip.type.api_input">API Input</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Picture-source-specific fields -->
|
||
<div id="css-editor-picture-section">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-picture-source" data-i18n="color_strip.picture_source">Picture 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="color_strip.picture_source.hint">Which screen capture source to use as input for LED color calculation</small>
|
||
<select id="css-editor-picture-source"></select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-interpolation" data-i18n="color_strip.interpolation">Color 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="color_strip.interpolation.hint">How to calculate LED color from sampled border pixels</small>
|
||
<select id="css-editor-interpolation">
|
||
<option value="average" data-i18n="color_strip.interpolation.average">Average</option>
|
||
<option value="median" data-i18n="color_strip.interpolation.median">Median</option>
|
||
<option value="dominant" data-i18n="color_strip.interpolation.dominant">Dominant</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-smoothing">
|
||
<span data-i18n="color_strip.smoothing">Smoothing:</span>
|
||
<span id="css-editor-smoothing-value">0.30</span>
|
||
</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="color_strip.smoothing.hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
|
||
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-frame-interpolation" data-i18n="color_strip.frame_interpolation">Frame Interpolation:</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="color_strip.frame_interpolation.hint">Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.</small>
|
||
<label class="settings-toggle">
|
||
<input type="checkbox" id="css-editor-frame-interpolation">
|
||
<span class="settings-toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<details class="form-collapse">
|
||
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
|
||
<div class="form-collapse-body">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-brightness">
|
||
<span data-i18n="color_strip.brightness">Brightness:</span>
|
||
<span id="css-editor-brightness-value">1.00</span>
|
||
</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="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
|
||
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-saturation">
|
||
<span data-i18n="color_strip.saturation">Saturation:</span>
|
||
<span id="css-editor-saturation-value">1.00</span>
|
||
</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="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
|
||
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-gamma">
|
||
<span data-i18n="color_strip.gamma">Gamma:</span>
|
||
<span id="css-editor-gamma-value">1.00</span>
|
||
</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="color_strip.gamma.hint">Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)</small>
|
||
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Static-color-specific fields -->
|
||
<div id="css-editor-static-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-color" data-i18n="color_strip.static_color">Color:</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="color_strip.static_color.hint">The solid color that will be sent to all LEDs on the strip.</small>
|
||
<input type="color" id="css-editor-color" value="#ffffff">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Color-cycle-specific fields -->
|
||
<div id="css-editor-color-cycle-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.color_cycle.colors">Colors:</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="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
|
||
<div id="color-cycle-colors-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="colorCycleAddColor()" data-i18n="color_strip.color_cycle.add_color">+ Add Color</button>
|
||
</div>
|
||
<div id="css-editor-cycle-speed-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-cycle-speed">
|
||
<span data-i18n="color_strip.color_cycle.speed">Speed:</span>
|
||
<span id="css-editor-cycle-speed-val">1.0</span>×
|
||
</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="color_strip.color_cycle.speed.hint">Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds.</small>
|
||
<input type="range" id="css-editor-cycle-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-cycle-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gradient-specific fields -->
|
||
<div id="css-editor-gradient-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.preset">Preset:</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="color_strip.gradient.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small>
|
||
<select id="css-editor-gradient-preset" onchange="applyGradientPreset(this.value)">
|
||
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
|
||
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
|
||
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
|
||
<option value="ocean" data-i18n="color_strip.gradient.preset.ocean">Ocean</option>
|
||
<option value="forest" data-i18n="color_strip.gradient.preset.forest">Forest</option>
|
||
<option value="fire" data-i18n="color_strip.gradient.preset.fire">Fire</option>
|
||
<option value="lava" data-i18n="color_strip.gradient.preset.lava">Lava</option>
|
||
<option value="aurora" data-i18n="color_strip.gradient.preset.aurora">Aurora</option>
|
||
<option value="ice" data-i18n="color_strip.gradient.preset.ice">Ice</option>
|
||
<option value="warm" data-i18n="color_strip.gradient.preset.warm">Warm</option>
|
||
<option value="cool" data-i18n="color_strip.gradient.preset.cool">Cool</option>
|
||
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
|
||
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.gradient.preview">Gradient:</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="color_strip.gradient.preview.hint">Visual preview. Click the marker track below to add a stop. Drag markers to reposition.</small>
|
||
<div class="gradient-editor">
|
||
<canvas id="gradient-canvas" height="44"></canvas>
|
||
<div id="gradient-markers-track" class="gradient-markers-track"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.gradient.stops">Color Stops:</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="color_strip.gradient.stops.hint">Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.</small>
|
||
<div id="gradient-stops-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Procedural effect fields -->
|
||
<div id="css-editor-effect-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-type" data-i18n="color_strip.effect.type">Effect:</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="color_strip.effect.type.hint">The procedural effect algorithm to use.</small>
|
||
<select id="css-editor-effect-type" onchange="onEffectTypeChange()">
|
||
<option value="fire" data-i18n="color_strip.effect.fire">Fire</option>
|
||
<option value="meteor" data-i18n="color_strip.effect.meteor">Meteor</option>
|
||
<option value="plasma" data-i18n="color_strip.effect.plasma">Plasma</option>
|
||
<option value="noise" data-i18n="color_strip.effect.noise">Perlin Noise</option>
|
||
<option value="aurora" data-i18n="color_strip.effect.aurora">Aurora</option>
|
||
</select>
|
||
<small id="css-editor-effect-type-desc" class="field-desc"></small>
|
||
<div id="css-editor-effect-preview" class="effect-palette-preview"></div>
|
||
</div>
|
||
|
||
<div id="css-editor-effect-speed-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-speed">
|
||
<span data-i18n="color_strip.effect.speed">Speed:</span>
|
||
<span id="css-editor-effect-speed-val">1.0</span>x
|
||
</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="color_strip.effect.speed.hint">How fast the effect animates. 1.0 = default speed.</small>
|
||
<input type="range" id="css-editor-effect-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
|
||
<div id="css-editor-effect-palette-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-palette" data-i18n="color_strip.effect.palette">Palette:</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="color_strip.effect.palette.hint">Color palette used by the effect.</small>
|
||
<select id="css-editor-effect-palette" onchange="updateEffectPreview()">
|
||
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
|
||
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
|
||
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
|
||
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
|
||
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
|
||
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
|
||
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
|
||
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="css-editor-effect-color-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-color" data-i18n="color_strip.effect.color">Color:</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="color_strip.effect.color.hint">Head color for the meteor effect.</small>
|
||
<input type="color" id="css-editor-effect-color" value="#ff5000" oninput="updateEffectPreview()">
|
||
</div>
|
||
|
||
<div id="css-editor-effect-intensity-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-intensity">
|
||
<span data-i18n="color_strip.effect.intensity">Intensity:</span>
|
||
<span id="css-editor-effect-intensity-val">1.0</span>
|
||
</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="color_strip.effect.intensity.hint">Effect-specific intensity.</small>
|
||
<input type="range" id="css-editor-effect-intensity" min="0.1" max="2.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
|
||
<div id="css-editor-effect-scale-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-scale">
|
||
<span data-i18n="color_strip.effect.scale">Scale:</span>
|
||
<span id="css-editor-effect-scale-val">1.0</span>
|
||
</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="color_strip.effect.scale.hint">Spatial zoom level.</small>
|
||
<input type="range" id="css-editor-effect-scale" min="0.5" max="5.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
|
||
<div id="css-editor-effect-mirror-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-effect-mirror" data-i18n="color_strip.effect.mirror">Mirror:</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="color_strip.effect.mirror.hint">Bounce back and forth instead of wrapping around.</small>
|
||
<label class="settings-toggle">
|
||
<input type="checkbox" id="css-editor-effect-mirror">
|
||
<span class="settings-toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Composite-specific fields -->
|
||
<div id="css-editor-composite-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.composite.layers">Layers:</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="color_strip.composite.layers.hint">Stack multiple color strip sources. First layer is the bottom, last is the top.</small>
|
||
<div id="composite-layers-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="compositeAddLayer()" data-i18n="color_strip.composite.add_layer">+ Add Layer</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mapped-specific fields -->
|
||
<div id="css-editor-mapped-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.mapped.zones">Zones:</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="color_strip.mapped.zones.hint">Each zone maps a color strip source to a specific LED range. Zones are placed side-by-side.</small>
|
||
<div id="mapped-zones-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="mappedAddZone()" data-i18n="color_strip.mapped.add_zone">+ Add Zone</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio-reactive fields -->
|
||
<div id="css-editor-audio-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-viz" data-i18n="color_strip.audio.visualization">Visualization:</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="color_strip.audio.visualization.hint">How audio data is rendered to LEDs.</small>
|
||
<select id="css-editor-audio-viz" onchange="onAudioVizChange()">
|
||
<option value="spectrum" data-i18n="color_strip.audio.viz.spectrum">Spectrum Analyzer</option>
|
||
<option value="beat_pulse" data-i18n="color_strip.audio.viz.beat_pulse">Beat Pulse</option>
|
||
<option value="vu_meter" data-i18n="color_strip.audio.viz.vu_meter">VU Meter</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-source" data-i18n="color_strip.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="color_strip.audio.source.hint">Mono audio source that provides audio data for this visualization. Create and manage audio sources in the Sources tab.</small>
|
||
<select id="css-editor-audio-source">
|
||
<!-- populated dynamically from /api/v1/audio-sources?source_type=mono -->
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-sensitivity">
|
||
<span data-i18n="color_strip.audio.sensitivity">Sensitivity:</span>
|
||
<span id="css-editor-audio-sensitivity-val">1.0</span>
|
||
</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="color_strip.audio.sensitivity.hint">Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.</small>
|
||
<input type="range" id="css-editor-audio-sensitivity" min="0.1" max="5.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-audio-sensitivity-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-smoothing">
|
||
<span data-i18n="color_strip.audio.smoothing">Smoothing:</span>
|
||
<span id="css-editor-audio-smoothing-val">0.30</span>
|
||
</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="color_strip.audio.smoothing.hint">Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.</small>
|
||
<input type="range" id="css-editor-audio-smoothing" min="0.0" max="1.0" step="0.05" value="0.3"
|
||
oninput="document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(this.value).toFixed(2)">
|
||
</div>
|
||
|
||
<div id="css-editor-audio-palette-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-palette" data-i18n="color_strip.audio.palette">Palette:</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="color_strip.audio.palette.hint">Color palette used for spectrum bars or beat pulse coloring.</small>
|
||
<select id="css-editor-audio-palette">
|
||
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
|
||
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
|
||
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
|
||
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
|
||
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
|
||
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
|
||
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
|
||
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="css-editor-audio-color-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-color" data-i18n="color_strip.audio.color">Base Color:</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="color_strip.audio.color.hint">Low-level color for VU meter bar.</small>
|
||
<input type="color" id="css-editor-audio-color" value="#00ff00">
|
||
</div>
|
||
|
||
<div id="css-editor-audio-color-peak-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-color-peak" data-i18n="color_strip.audio.color_peak">Peak Color:</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="color_strip.audio.color_peak.hint">High-level color at the top of the VU meter bar.</small>
|
||
<input type="color" id="css-editor-audio-color-peak" value="#ff0000">
|
||
</div>
|
||
|
||
<div id="css-editor-audio-mirror-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-audio-mirror" data-i18n="color_strip.audio.mirror">Mirror:</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="color_strip.audio.mirror.hint">Mirror spectrum from center outward: bass in the middle, treble at the edges.</small>
|
||
<label class="settings-toggle">
|
||
<input type="checkbox" id="css-editor-audio-mirror">
|
||
<span class="settings-toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- API Input fields -->
|
||
<div id="css-editor-api-input-section" style="display:none">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-api-input-fallback-color" data-i18n="color_strip.api_input.fallback_color">Fallback Color:</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="color_strip.api_input.fallback_color.hint">Color to display when no data has been received within the timeout period.</small>
|
||
<input type="color" id="css-editor-api-input-fallback-color" value="#000000">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-api-input-timeout">
|
||
<span data-i18n="color_strip.api_input.timeout">Timeout (seconds):</span>
|
||
<span id="css-editor-api-input-timeout-val">5.0</span>
|
||
</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="color_strip.api_input.timeout.hint">How long to wait for new color data before reverting to the fallback color.</small>
|
||
<input type="range" id="css-editor-api-input-timeout" min="0" max="60" step="0.5" value="5.0"
|
||
oninput="document.getElementById('css-editor-api-input-timeout-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
|
||
<div class="form-group" id="css-editor-api-input-endpoints-group">
|
||
<div class="label-row">
|
||
<label data-i18n="color_strip.api_input.endpoints">Push Endpoints:</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="color_strip.api_input.endpoints.hint">Use these URLs to push LED color data from your external application.</small>
|
||
<div id="css-editor-api-input-endpoints" class="template-config" style="font-family:monospace; font-size:0.85em; word-break:break-all;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Shared LED count field -->
|
||
<div id="css-editor-led-count-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</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="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to auto-detect from calibration or device.</small>
|
||
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
|
||
</div>
|
||
|
||
<!-- Animation — shown for static/gradient, hidden for picture -->
|
||
<div id="css-editor-animation-section" style="display:none">
|
||
<details class="form-collapse">
|
||
<summary><span data-i18n="color_strip.animation">Animation</span></summary>
|
||
<div class="form-collapse-body">
|
||
<div class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-animation-type" data-i18n="color_strip.animation.type">Effect:</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="color_strip.animation.type.hint">The animation effect to apply. Available effects depend on source type. Select None to disable animation.</small>
|
||
<select id="css-editor-animation-type" onchange="onAnimationTypeChange()">
|
||
<!-- populated by onCSSTypeChange() -->
|
||
</select>
|
||
<small id="css-editor-animation-type-desc" class="field-desc"></small>
|
||
</div>
|
||
<div id="css-editor-animation-speed-group" class="form-group">
|
||
<div class="label-row">
|
||
<label for="css-editor-animation-speed">
|
||
<span data-i18n="color_strip.animation.speed">Speed:</span>
|
||
<span id="css-editor-animation-speed-val">1.0</span>×
|
||
</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="color_strip.animation.speed.hint">Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.</small>
|
||
<input type="range" id="css-editor-animation-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||
oninput="document.getElementById('css-editor-animation-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Sync Clock (shown for animated types: static, gradient, color_cycle, effect) -->
|
||
<div id="css-editor-clock-group" class="form-group" style="display:none">
|
||
<div class="label-row">
|
||
<label for="css-editor-clock" data-i18n="color_strip.clock">Sync Clock:</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="color_strip.clock.hint">Optionally link to a sync clock to synchronize animation timing and speed with other sources</small>
|
||
<select id="css-editor-clock" onchange="onCSSClockChange()">
|
||
<option value="" data-i18n="common.none">None</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="css-editor-error" class="error-message" style="display: none;"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-icon btn-secondary" onclick="closeCSSEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
||
<button class="btn btn-icon btn-primary" onclick="saveCSSEditor()" title="Save" data-i18n-aria-label="aria.save">✓</button>
|
||
</div>
|
||
</div>
|
||
</div>
|