feat: add chase and gradient flash notification effects with priority queue
Some checks failed
Lint & Test / test (push) Failing after 28s

New notification effects:
- Chase: light bounces across strip with Gaussian glow tail
- Gradient flash: bright center fades to edges with exponential decay

Queue priority: notifications with color_override get high priority and
interrupt the current effect.

Also fixes transient preview for notification sources — adds WebSocket
"fire" command so inline preview works without a saved source, plus
auto-fires on preview open so the effect is visible immediately.
This commit is contained in:
2026-03-24 00:00:49 +03:00
parent 9b80076b5b
commit 2c3f08344c
6 changed files with 120 additions and 9 deletions

View File

@@ -31,12 +31,14 @@ export function ensureNotificationEffectIconSelect() {
const sel = document.getElementById('css-editor-notification-effect') as HTMLSelectElement | null;
if (!sel) return;
const items = [
{ value: 'flash', icon: _icon(P.zap), label: t('color_strip.notification.effect.flash'), desc: t('color_strip.notification.effect.flash.desc') },
{ value: 'pulse', icon: _icon(P.activity), label: t('color_strip.notification.effect.pulse'), desc: t('color_strip.notification.effect.pulse.desc') },
{ value: 'sweep', icon: _icon(P.fastForward), label: t('color_strip.notification.effect.sweep'), desc: t('color_strip.notification.effect.sweep.desc') },
{ value: 'flash', icon: _icon(P.zap), label: t('color_strip.notification.effect.flash'), desc: t('color_strip.notification.effect.flash.desc') },
{ value: 'pulse', icon: _icon(P.activity), label: t('color_strip.notification.effect.pulse'), desc: t('color_strip.notification.effect.pulse.desc') },
{ value: 'sweep', icon: _icon(P.fastForward), label: t('color_strip.notification.effect.sweep'), desc: t('color_strip.notification.effect.sweep.desc') },
{ value: 'chase', icon: _icon(P.rocket), label: t('color_strip.notification.effect.chase'), desc: t('color_strip.notification.effect.chase.desc') },
{ value: 'gradient_flash', icon: _icon(P.rainbow), label: t('color_strip.notification.effect.gradient_flash'), desc: t('color_strip.notification.effect.gradient_flash.desc') },
];
if (_notificationEffectIconSelect) { _notificationEffectIconSelect.updateItems(items); return; }
_notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 3 });
_notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}
export function ensureNotificationFilterModeIconSelect() {

View File

@@ -15,11 +15,12 @@ import {
import { EntitySelect } from '../core/entity-palette.ts';
import { hexToRgbArray, getGradientStops } from './css-gradient-editor.ts';
import { testNotification, _getAnimationPayload, _colorCycleGetColors } from './color-strips.ts';
import { notificationGetAppColorsDict } from './color-strips-notification.ts';
/* ── Preview config builder ───────────────────────────────────── */
const _PREVIEW_TYPES = new Set([
'static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight',
'static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight', 'notification',
]);
function _collectPreviewConfig() {
@@ -44,6 +45,17 @@ function _collectPreviewConfig() {
config = { source_type: 'daylight', speed: parseFloat((document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value), use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked, latitude: parseFloat((document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value), longitude: parseFloat((document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value) };
} else if (sourceType === 'candlelight') {
config = { source_type: 'candlelight', color: hexToRgbArray((document.getElementById('css-editor-candlelight-color') as HTMLInputElement).value), intensity: parseFloat((document.getElementById('css-editor-candlelight-intensity') as HTMLInputElement).value), num_candles: parseInt((document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value) || 3, speed: parseFloat((document.getElementById('css-editor-candlelight-speed') as HTMLInputElement).value), wind_strength: parseFloat((document.getElementById('css-editor-candlelight-wind') as HTMLInputElement).value), candle_type: (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value };
} else if (sourceType === 'notification') {
const filterList = (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value.split('\n').map(s => s.trim()).filter(Boolean);
config = {
source_type: 'notification',
notification_effect: (document.getElementById('css-editor-notification-effect') as HTMLInputElement).value,
duration_ms: parseInt((document.getElementById('css-editor-notification-duration') as HTMLInputElement).value) || 1500,
default_color: (document.getElementById('css-editor-notification-default-color') as HTMLInputElement).value,
app_filter_mode: (document.getElementById('css-editor-notification-filter-mode') as HTMLInputElement).value,
app_filter_list: filterList,
app_colors: notificationGetAppColorsDict(),
};
}
const clockEl = document.getElementById('css-editor-clock') as HTMLSelectElement | null;
if (clockEl && clockEl.value) config.clock_id = clockEl.value;
@@ -245,6 +257,14 @@ function _cssTestConnect(sourceId: string, ledCount: number, fps?: number) {
_cssTestWs.onopen = () => {
if (gen !== _cssTestGeneration) return;
_cssTestWs!.send(JSON.stringify(_cssTestTransientConfig));
// Auto-fire notification after stream starts so user sees the effect immediately
if (_cssTestTransientConfig.source_type === 'notification') {
setTimeout(() => {
if (gen === _cssTestGeneration && _cssTestWs && _cssTestWs.readyState === WebSocket.OPEN) {
_cssTestWs.send(JSON.stringify({ action: 'fire', color: _cssTestTransientConfig.default_color }));
}
}, 300);
}
};
}
@@ -826,6 +846,12 @@ function _cssTestRenderStripAxis(canvasId: string, ledCount: number) {
}
export function fireCssTestNotification() {
// Transient preview: fire via WebSocket command
if (_cssTestTransientConfig && _cssTestWs && _cssTestWs.readyState === WebSocket.OPEN) {
_cssTestWs.send(JSON.stringify({ action: 'fire', color: _cssTestTransientConfig.default_color }));
return;
}
// Saved source: fire via REST endpoint
for (const id of _cssTestNotificationIds) {
testNotification(id);
}