Animation None option, FPS min 1, serial COM lifecycle fixes

- Replace animation Enable checkbox with None option in effect selector;
  show effect description tooltip; disable speed slider when None selected
- Allow target FPS range 1-90 (was 10-90) across UI and backend validation
- Scope serial COM connections to target lifetime (no idle caching);
  use temporary connections for power-off/test mode
- Fix serial black frame on stop: flush after write, delay after task
  cancel to prevent race with in-flight thread pool write

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 04:33:56 +03:00
parent 8a0730d91b
commit ee52e2d98f
15 changed files with 126 additions and 71 deletions

View File

@@ -89,7 +89,7 @@ import {
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, colorCycleAddColor, colorCycleRemoveColor,
onCSSTypeChange, onAnimationTypeChange, colorCycleAddColor, colorCycleRemoveColor,
} from './features/color-strips.js';
// Layer 5: calibration
@@ -274,6 +274,7 @@ Object.assign(window, {
saveCSSEditor,
deleteColorStrip,
onCSSTypeChange,
onAnimationTypeChange,
colorCycleAddColor,
colorCycleRemoveColor,

View File

@@ -27,7 +27,6 @@ class CSSEditorModal extends Modal {
frame_interpolation: document.getElementById('css-editor-frame-interpolation').checked,
led_count: document.getElementById('css-editor-led-count').value,
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
animation_enabled: document.getElementById('css-editor-animation-enabled').checked,
animation_type: document.getElementById('css-editor-animation-type').value,
animation_speed: document.getElementById('css-editor-animation-speed').value,
cycle_speed: document.getElementById('css-editor-cycle-speed').value,
@@ -50,9 +49,10 @@ export function onCSSTypeChange() {
// Animation section — shown for static/gradient only (color_cycle is always animating)
const animSection = document.getElementById('css-editor-animation-section');
const animTypeSelect = document.getElementById('css-editor-animation-type');
const noneOpt = `<option value="none">${t('color_strip.animation.type.none')}</option>`;
if (type === 'static') {
animSection.style.display = '';
animTypeSelect.innerHTML =
animTypeSelect.innerHTML = noneOpt +
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
`<option value="strobe">${t('color_strip.animation.type.strobe')}</option>` +
`<option value="sparkle">${t('color_strip.animation.type.sparkle')}</option>` +
@@ -61,7 +61,7 @@ export function onCSSTypeChange() {
`<option value="rainbow_fade">${t('color_strip.animation.type.rainbow_fade')}</option>`;
} else if (type === 'gradient') {
animSection.style.display = '';
animTypeSelect.innerHTML =
animTypeSelect.innerHTML = noneOpt +
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
`<option value="gradient_shift">${t('color_strip.animation.type.gradient_shift')}</option>` +
`<option value="wave">${t('color_strip.animation.type.wave')}</option>` +
@@ -73,6 +73,7 @@ export function onCSSTypeChange() {
} else {
animSection.style.display = 'none';
}
_syncAnimationSpeedState();
if (type === 'gradient') {
requestAnimationFrame(() => gradientRenderAll());
@@ -80,22 +81,41 @@ export function onCSSTypeChange() {
}
function _getAnimationPayload() {
const type = document.getElementById('css-editor-animation-type').value;
return {
enabled: document.getElementById('css-editor-animation-enabled').checked,
type: document.getElementById('css-editor-animation-type').value,
enabled: type !== 'none',
type: type !== 'none' ? type : 'breathing',
speed: parseFloat(document.getElementById('css-editor-animation-speed').value),
};
}
function _loadAnimationState(anim) {
document.getElementById('css-editor-animation-enabled').checked = !!(anim && anim.enabled);
const speedEl = document.getElementById('css-editor-animation-speed');
speedEl.value = (anim && anim.speed != null) ? anim.speed : 1.0;
document.getElementById('css-editor-animation-speed-val').textContent =
parseFloat(speedEl.value).toFixed(1);
// Set type after onCSSTypeChange() has populated the dropdown
if (anim && anim.type) {
if (anim && anim.enabled && anim.type) {
document.getElementById('css-editor-animation-type').value = anim.type;
} else {
document.getElementById('css-editor-animation-type').value = 'none';
}
_syncAnimationSpeedState();
}
export function onAnimationTypeChange() {
_syncAnimationSpeedState();
}
function _syncAnimationSpeedState() {
const type = document.getElementById('css-editor-animation-type').value;
const isNone = type === 'none';
document.getElementById('css-editor-animation-speed').disabled = isNone;
const descEl = document.getElementById('css-editor-animation-type-desc');
if (descEl) {
const desc = t('color_strip.animation.type.' + type + '.desc') || '';
descEl.textContent = desc;
descEl.style.display = desc ? '' : 'none';
}
}