Add color_cycle as standalone source type; UI polish

- color_cycle is now a top-level source type (alongside picture/static/gradient)
  with a configurable color list and cycle_speed; defaults to full rainbow spectrum
- ColorCycleColorStripSource + ColorCycleColorStripStream: smooth 30 fps interpolation
  between user-defined colors, one full cycle every 20s at speed=1.0
- Removed color_cycle animation sub-type from StaticColorStripStream
- Color cycle editor: compact horizontal swatch layout, proper module-scope fix
  (colorCycleAdd/Remove now exposed on window, DOM-synced before mutations)
- Animation enabled + Frame interpolation checkboxes use toggle-switch style
- Removed Potential FPS metric from targets and KC targets metric grids

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 22:14:42 +03:00
parent 872949a7e1
commit c31818a20d
14 changed files with 674 additions and 40 deletions

View File

@@ -24,6 +24,7 @@
<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>
</select>
</div>
@@ -84,7 +85,10 @@
<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>
<input type="checkbox" id="css-editor-frame-interpolation">
<label class="settings-toggle">
<input type="checkbox" id="css-editor-frame-interpolation">
<span class="settings-toggle-slider"></span>
</label>
</div>
<details class="form-collapse">
@@ -141,6 +145,31 @@
</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 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">
@@ -165,6 +194,48 @@
</div>
</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-enabled" data-i18n="color_strip.animation.enabled">Enable Animation:</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.enabled.hint">Enables procedural animation. The LEDs will update at 30 fps driven by the selected effect.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-animation-enabled">
<span class="settings-toggle-slider"></span>
</label>
</div>
<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.</small>
<select id="css-editor-animation-type">
<!-- populated by onCSSTypeChange() -->
</select>
</div>
<div 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>
<!-- LED count — picture type only (auto-sized from device for static/gradient) -->
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">