Add sync clock entity for synchronized animation timing

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>
This commit is contained in:
2026-03-01 21:46:55 +03:00
parent 52ee4bdeb6
commit aa1e4a6afc
32 changed files with 1255 additions and 58 deletions

View File

@@ -169,6 +169,7 @@
{% include 'modals/test-audio-template.html' %}
{% include 'modals/value-source-editor.html' %}
{% include 'modals/test-value-source.html' %}
{% include 'modals/sync-clock-editor.html' %}
{% include 'modals/settings.html' %}
{% include 'partials/tutorial-overlay.html' %}

View File

@@ -146,7 +146,7 @@
<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 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>
@@ -226,7 +226,7 @@
<div id="css-editor-effect-preview" class="effect-palette-preview"></div>
</div>
<div class="form-group">
<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>
@@ -492,7 +492,7 @@
</select>
<small id="css-editor-animation-type-desc" class="field-desc"></small>
</div>
<div class="form-group">
<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>
@@ -508,6 +508,18 @@
</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>

View File

@@ -0,0 +1,51 @@
<!-- Sync Clock Editor Modal -->
<div id="sync-clock-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="sync-clock-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="sync-clock-modal-title" data-i18n="sync_clock.add">Add Sync Clock</h2>
<button class="modal-close-btn" onclick="closeSyncClockModal()" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="sync-clock-form" onsubmit="return false;">
<input type="hidden" id="sync-clock-id">
<div id="sync-clock-error" class="error-message" style="display: none;"></div>
<!-- Name -->
<div class="form-group">
<div class="label-row">
<label for="sync-clock-name" data-i18n="sync_clock.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="sync_clock.name.hint">A descriptive name for this synchronization clock</small>
<input type="text" id="sync-clock-name" data-i18n-placeholder="sync_clock.name.placeholder" placeholder="Main Clock" required>
</div>
<!-- Speed -->
<div class="form-group">
<div class="label-row">
<label for="sync-clock-speed"><span data-i18n="sync_clock.speed">Speed:</span> <span id="sync-clock-speed-display">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="sync_clock.speed.hint">Animation speed multiplier for all linked sources (0.1x slow - 10x fast)</small>
<input type="range" id="sync-clock-speed" min="0.1" max="10" step="0.1" value="1.0"
oninput="document.getElementById('sync-clock-speed-display').textContent = this.value">
</div>
<!-- Description -->
<div class="form-group">
<div class="label-row">
<label for="sync-clock-description" data-i18n="sync_clock.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="sync_clock.description.hint">Optional notes about this clock's purpose</small>
<textarea id="sync-clock-description" rows="2" data-i18n-placeholder="sync_clock.description.placeholder" placeholder=""></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeSyncClockModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveSyncClock()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>