diff --git a/server/src/wled_controller/static/js/app.ts b/server/src/wled_controller/static/js/app.ts index 1c3b37d..273abae 100644 --- a/server/src/wled_controller/static/js/app.ts +++ b/server/src/wled_controller/static/js/app.ts @@ -145,7 +145,7 @@ import { testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer, addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange, mathWaveAddLayer, mathWaveRemoveLayer, -} from './features/color-strips.ts'; +} from './features/color-strips/index.ts'; // Layer 5: audio sources import { diff --git a/server/src/wled_controller/static/js/features/calibration.ts b/server/src/wled_controller/static/js/features/calibration.ts index b165651..eee7e52 100644 --- a/server/src/wled_controller/static/js/features/calibration.ts +++ b/server/src/wled_controller/static/js/features/calibration.ts @@ -11,7 +11,7 @@ import { t } from '../core/i18n.ts'; import { showToast } from '../core/ui.ts'; import { Modal } from '../core/modal.ts'; import { closeTutorial, startCalibrationTutorial } from './tutorials.ts'; -import { startCSSOverlay, stopCSSOverlay } from './color-strips.ts'; +import { startCSSOverlay, stopCSSOverlay } from './color-strips/index.ts'; import { ICON_WARNING, ICON_ROTATE_CW, ICON_ROTATE_CCW } from '../core/icons.ts'; import type { Calibration } from '../types.ts'; diff --git a/server/src/wled_controller/static/js/features/color-strips.ts b/server/src/wled_controller/static/js/features/color-strips.ts deleted file mode 100644 index cc19941..0000000 --- a/server/src/wled_controller/static/js/features/color-strips.ts +++ /dev/null @@ -1,3127 +0,0 @@ -/** - * Color Strip Sources — CRUD, card rendering, calibration bridge. - */ - -import { fetchWithAuth, escapeHtml } from '../core/api.ts'; -import { _cachedSyncClocks, _cachedCSPTemplates, _cachedWeatherSources, _cachedValueSources, audioSourcesCache, streamsCache, colorStripSourcesCache, valueSourcesCache, csptCache, gradientsCache, weatherSourcesCache, GradientEntity, _cachedGameIntegrations, _cachedGameAdapters, gameIntegrationsCache } from '../core/state.ts'; -import { t } from '../core/i18n.ts'; -import { showToast, showConfirm, desktopFocus } from '../core/ui.ts'; -import { Modal } from '../core/modal.ts'; -import { - getColorStripIcon, getPictureSourceIcon, getAudioSourceIcon, - ICON_CLONE, ICON_EDIT, ICON_CALIBRATION, ICON_OVERLAY, - ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC, - ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM, - ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_TEST, - ICON_AUTOMATION, ICON_FAST_FORWARD, ICON_THERMOMETER, ICON_TRASH, ICON_PATTERN_TEMPLATE, - ICON_GAMEPAD, -} from '../core/icons.ts'; -import * as P from '../core/icon-paths.ts'; -import { wrapCard } from '../core/card-colors.ts'; -import type { ColorStripSource } from '../types.ts'; -import { bindableValue, bindableColor } from '../types.ts'; -import { TagInput, renderTagChips } from '../core/tag-input.ts'; -import { IconSelect, showTypePicker, type IconSelectItem } from '../core/icon-select.ts'; -import { EntitySelect } from '../core/entity-palette.ts'; -import { BindableScalarWidget } from '../core/bindable-scalar.ts'; -import { BindableColorWidget } from '../core/bindable-color.ts'; -import { getBaseOrigin } from './settings.ts'; -import { - rgbArrayToHex, hexToRgbArray, - gradientInit, gradientRenderAll, gradientAddStop, - getGradientStops, gradientSetIdPrefix, -} from './css-gradient-editor.ts'; -import { - compositeDestroyEntitySelects, compositeSetAvailableSources, compositeGetAvailableSources, - compositeRenderList, compositeAddLayer, compositeRemoveLayer, compositeGetLayers, loadCompositeState, - compositeGetRawLayers, -} from './color-strips-composite.ts'; -import { - ensureNotificationEffectIconSelect, ensureNotificationFilterModeIconSelect, - onNotificationFilterModeChange, - notificationAddAppOverride, notificationRemoveAppOverride, - notificationGetAppColorsDict, notificationGetAppSoundsDict, notificationGetRawAppOverrides, - testNotification, showNotificationHistory, closeNotificationHistory, refreshNotificationHistory, - loadNotificationState, resetNotificationState, showNotificationEndpoint, - destroyNotificationDurationWidget, getNotificationDurationValue, getNotificationDurationSnapshot, - getNotificationDefaultColorValue, getNotificationDefaultColorSnapshot, - getNotificationVolumeValue, getNotificationVolumeSnapshot, -} from './color-strips-notification.ts'; - -// Re-export for app.js window global bindings -export { gradientInit, gradientRenderAll, gradientAddStop }; -export { compositeAddLayer, compositeRemoveLayer }; -export { - onNotificationFilterModeChange, - notificationAddAppOverride, notificationRemoveAppOverride, - testNotification, showNotificationHistory, closeNotificationHistory, refreshNotificationHistory, -}; -export { _getAnimationPayload, _colorCycleGetColors }; - -class CSSEditorModal extends Modal { - constructor() { - super('css-editor-modal'); - } - - onForceClose() { - if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; } - if (_smoothingWidget) { _smoothingWidget.destroy(); _smoothingWidget = null; } - if (_audioSensitivityWidget) { _audioSensitivityWidget.destroy(); _audioSensitivityWidget = null; } - if (_audioSmoothingWidget) { _audioSmoothingWidget.destroy(); _audioSmoothingWidget = null; } - if (_audioBeatDecayWidget) { _audioBeatDecayWidget.destroy(); _audioBeatDecayWidget = null; } - if (_effectIntensityWidget) { _effectIntensityWidget.destroy(); _effectIntensityWidget = null; } - if (_effectScaleWidget) { _effectScaleWidget.destroy(); _effectScaleWidget = null; } - if (_apiInputTimeoutWidget) { _apiInputTimeoutWidget.destroy(); _apiInputTimeoutWidget = null; } - if (_candlelightIntensityWidget) { _candlelightIntensityWidget.destroy(); _candlelightIntensityWidget = null; } - if (_candlelightSpeedWidget) { _candlelightSpeedWidget.destroy(); _candlelightSpeedWidget = null; } - if (_candlelightWindWidget) { _candlelightWindWidget.destroy(); _candlelightWindWidget = null; } - if (_weatherSpeedWidget) { _weatherSpeedWidget.destroy(); _weatherSpeedWidget = null; } - if (_weatherTempInfluenceWidget) { _weatherTempInfluenceWidget.destroy(); _weatherTempInfluenceWidget = null; } - if (_staticColorWidget) { _staticColorWidget.destroy(); _staticColorWidget = null; } - if (_effectColorWidget) { _effectColorWidget.destroy(); _effectColorWidget = null; } - if (_audioColorWidget) { _audioColorWidget.destroy(); _audioColorWidget = null; } - if (_audioColorPeakWidget) { _audioColorPeakWidget.destroy(); _audioColorPeakWidget = null; } - if (_apiInputFallbackColorWidget) { _apiInputFallbackColorWidget.destroy(); _apiInputFallbackColorWidget = null; } - if (_candlelightColorWidget) { _candlelightColorWidget.destroy(); _candlelightColorWidget = null; } - destroyNotificationDurationWidget(); - if (_kcPictureSourceEntitySelect) { _kcPictureSourceEntitySelect.destroy(); _kcPictureSourceEntitySelect = null; } - if (_kcInterpolationIconSelect) { _kcInterpolationIconSelect.destroy(); _kcInterpolationIconSelect = null; } - if (_kcSmoothingWidget) { _kcSmoothingWidget.destroy(); _kcSmoothingWidget = null; } - if (_kcBrightnessWidget) { _kcBrightnessWidget.destroy(); _kcBrightnessWidget = null; } - if (_gameEventIdleColorWidget) { _gameEventIdleColorWidget.destroy(); _gameEventIdleColorWidget = null; } - if (_cssGameIntegrationEntitySelect) { _cssGameIntegrationEntitySelect.destroy(); _cssGameIntegrationEntitySelect = null; } - _destroyCSSGameMappingIconSelects(); - if (_cssGamePresetIconSelect) { _cssGamePresetIconSelect.destroy(); _cssGamePresetIconSelect = null; } - if (_mathWaveSpeedWidget) { _mathWaveSpeedWidget.destroy(); _mathWaveSpeedWidget = null; } - if (_mathWaveGradientEntitySelect) { _mathWaveGradientEntitySelect.destroy(); _mathWaveGradientEntitySelect = null; } - _mathWaveWaveformIconSelects.forEach(s => s.destroy()); _mathWaveWaveformIconSelects = []; - compositeDestroyEntitySelects(); - } - - snapshotValues() { - const type = (document.getElementById('css-editor-type') as HTMLInputElement).value; - return { - name: (document.getElementById('css-editor-name') as HTMLInputElement).value, - type, - picture_source: (document.getElementById('css-editor-picture-source') as HTMLInputElement).value, - interpolation: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value, - smoothing: _smoothingWidget ? JSON.stringify(_smoothingWidget.getValue()) : '0.3', - color: _staticColorWidget ? JSON.stringify(_staticColorWidget.getValue()) : '[]', - led_count: (document.getElementById('css-editor-led-count') as HTMLInputElement).value, - gradient_stops: type === 'gradient' ? JSON.stringify(getGradientStops()) : '[]', - animation_type: (document.getElementById('css-editor-animation-type') as HTMLInputElement).value, - cycle_colors: JSON.stringify(_colorCycleColors), - effect_type: (document.getElementById('css-editor-effect-type') as HTMLInputElement).value, - effect_palette: (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value, - effect_color: _effectColorWidget ? JSON.stringify(_effectColorWidget.getValue()) : '[]', - effect_intensity: _effectIntensityWidget ? JSON.stringify(_effectIntensityWidget.getValue()) : '1.0', - effect_scale: _effectScaleWidget ? JSON.stringify(_effectScaleWidget.getValue()) : '1.0', - effect_mirror: (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked, - composite_layers: JSON.stringify(compositeGetRawLayers()), - mapped_zones: JSON.stringify(_mappedZones), - audio_viz: (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value, - audio_source: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value, - audio_sensitivity: _audioSensitivityWidget ? JSON.stringify(_audioSensitivityWidget.getValue()) : '1.0', - audio_smoothing: _audioSmoothingWidget ? JSON.stringify(_audioSmoothingWidget.getValue()) : '0.3', - audio_beat_decay: _audioBeatDecayWidget ? JSON.stringify(_audioBeatDecayWidget.getValue()) : '0.15', - audio_palette: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value, - audio_color: _audioColorWidget ? JSON.stringify(_audioColorWidget.getValue()) : '[]', - audio_color_peak: _audioColorPeakWidget ? JSON.stringify(_audioColorPeakWidget.getValue()) : '[]', - audio_mirror: (document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked, - api_input_fallback_color: _apiInputFallbackColorWidget ? JSON.stringify(_apiInputFallbackColorWidget.getValue()) : '[]', - api_input_timeout: _apiInputTimeoutWidget ? JSON.stringify(_apiInputTimeoutWidget.getValue()) : '5.0', - api_input_interpolation: (document.getElementById('css-editor-api-input-interpolation') as HTMLInputElement).value, - notification_os_listener: (document.getElementById('css-editor-notification-os-listener') as HTMLInputElement).checked, - notification_effect: (document.getElementById('css-editor-notification-effect') as HTMLInputElement).value, - notification_duration: getNotificationDurationSnapshot(), - notification_default_color: getNotificationDefaultColorSnapshot(), - notification_filter_mode: (document.getElementById('css-editor-notification-filter-mode') as HTMLInputElement).value, - notification_filter_list: (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value, - notification_app_overrides: JSON.stringify(notificationGetRawAppOverrides()), - clock_id: (document.getElementById('css-editor-clock') as HTMLInputElement).value, - daylight_speed: (document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value, - daylight_use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked, - daylight_latitude: (document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value, - candlelight_color: _candlelightColorWidget ? JSON.stringify(_candlelightColorWidget.getValue()) : '[]', - candlelight_intensity: _candlelightIntensityWidget ? JSON.stringify(_candlelightIntensityWidget.getValue()) : '1.0', - candlelight_num_candles: (document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value, - candlelight_speed: _candlelightSpeedWidget ? JSON.stringify(_candlelightSpeedWidget.getValue()) : '1.0', - processed_input: (document.getElementById('css-editor-processed-input') as HTMLInputElement).value, - processed_template: (document.getElementById('css-editor-processed-template') as HTMLInputElement).value, - kc_smoothing: _kcSmoothingWidget ? JSON.stringify(_kcSmoothingWidget.getValue()) : '0.3', - kc_brightness: _kcBrightnessWidget ? JSON.stringify(_kcBrightnessWidget.getValue()) : '1.0', - kc_rects: JSON.stringify(_kcEditorRects), - ge_integration: (document.getElementById('css-editor-game-integration') as HTMLInputElement)?.value || '', - ge_idle_color: _gameEventIdleColorWidget ? JSON.stringify(_gameEventIdleColorWidget.getValue()) : '[]', - ge_mappings: JSON.stringify(_cssGameMappings), - mw_gradient: (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement)?.value || '', - mw_speed: _mathWaveSpeedWidget ? JSON.stringify(_mathWaveSpeedWidget.getValue()) : '1.0', - mw_waves: JSON.stringify(_mathWaveGetLayers()), - tags: JSON.stringify(_cssTagsInput ? _cssTagsInput.getValue() : []), - }; - } -} - -const cssEditorModal = new CSSEditorModal(); - -let _cssTagsInput: any = null; -let _smoothingWidget: BindableScalarWidget | null = null; -let _audioSensitivityWidget: BindableScalarWidget | null = null; -let _audioSmoothingWidget: BindableScalarWidget | null = null; -let _audioBeatDecayWidget: BindableScalarWidget | null = null; -let _effectIntensityWidget: BindableScalarWidget | null = null; -let _effectScaleWidget: BindableScalarWidget | null = null; -let _apiInputTimeoutWidget: BindableScalarWidget | null = null; -let _candlelightIntensityWidget: BindableScalarWidget | null = null; -let _candlelightSpeedWidget: BindableScalarWidget | null = null; -let _candlelightWindWidget: BindableScalarWidget | null = null; -let _weatherSpeedWidget: BindableScalarWidget | null = null; -let _weatherTempInfluenceWidget: BindableScalarWidget | null = null; -let _mathWaveSpeedWidget: BindableScalarWidget | null = null; -let _mathWaveGradientEntitySelect: EntitySelect | null = null; -let _mathWaveWaveformIconSelects: IconSelect[] = []; - -// ── BindableColorWidget instances for CSS editor ── -let _staticColorWidget: BindableColorWidget | null = null; -let _effectColorWidget: BindableColorWidget | null = null; -let _audioColorWidget: BindableColorWidget | null = null; -let _audioColorPeakWidget: BindableColorWidget | null = null; -let _apiInputFallbackColorWidget: BindableColorWidget | null = null; -let _candlelightColorWidget: BindableColorWidget | null = null; -let _gameEventIdleColorWidget: BindableColorWidget | null = null; - -// ── EntitySelect instances for CSS editor ── -let _cssPictureSourceEntitySelect: any = null; -let _cssAudioSourceEntitySelect: any = null; -let _cssClockEntitySelect: any = null; -let _processedInputEntitySelect: any = null; -let _processedTemplateEntitySelect: any = null; -let _kcPictureSourceEntitySelect: any = null; -let _kcInterpolationIconSelect: any = null; -let _kcSmoothingWidget: BindableScalarWidget | null = null; -let _kcBrightnessWidget: BindableScalarWidget | null = null; - -// ── Key Colors rectangle editor state ── -let _kcEditorRects: Array<{ name: string; x: number; y: number; width: number; height: number }> = []; - -function _renderKCRectSummary(): void { - const el = document.getElementById('css-editor-kc-rect-summary'); - if (!el) return; - if (_kcEditorRects.length === 0) { - el.textContent = t('color_strip.key_colors.no_rects'); - } else { - const names = _kcEditorRects.map(r => r.name).join(', '); - el.textContent = `${_kcEditorRects.length} region${_kcEditorRects.length !== 1 ? 's' : ''}: ${names}`; - } -} - -function _openKCRegionEditor(): void { - // Open the pattern template canvas editor in inline mode - const { showPatternTemplateEditor } = window as any; - if (!showPatternTemplateEditor) return; - showPatternTemplateEditor(null, null, { - rects: _kcEditorRects.map(r => ({ ...r })), - onSave: (rects: any[]) => { - _kcEditorRects = rects; - _renderKCRectSummary(); - }, - }); -} - -(window as any)._openKCRegionEditor = _openKCRegionEditor; - -async function configureKCRegions(sourceId: string): Promise { - // Fetch source to get current rectangles - try { - const resp = await fetchWithAuth(`/color-strip-sources/${sourceId}`); - if (!resp.ok) throw new Error('Failed to load source'); - const source = await resp.json(); - const rects = source.rectangles || []; - - const { showPatternTemplateEditor } = window as any; - if (!showPatternTemplateEditor) return; - showPatternTemplateEditor(null, null, { - rects: rects.map((r: any) => ({ ...r })), - pictureSourceId: source.picture_source_id || '', - onSave: async (newRects: any[]) => { - // Save rectangles back to the CSS source - try { - const putResp = await fetchWithAuth(`/color-strip-sources/${sourceId}`, { - method: 'PUT', - body: JSON.stringify({ rectangles: newRects }), - }); - if (!putResp.ok) throw new Error('Failed to save'); - showToast(t('color_strip.updated'), 'success'); - colorStripSourcesCache.invalidate(); - if (window.loadPictureSources) await window.loadPictureSources(); - } catch (e: any) { - showToast(e.message, 'error'); - } - }, - }); - } catch (e: any) { - if (e.isAuth) return; - showToast(e.message, 'error'); - } -} -(window as any).configureKCRegions = configureKCRegions; - -/* ── Icon-grid type selector ──────────────────────────────────── */ - -const CSS_TYPE_KEYS = [ - 'picture', 'picture_advanced', 'static', 'gradient', 'color_cycle', - 'effect', 'composite', 'mapped', 'audio', - 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors', - 'game_event', 'math_wave', -]; - -function _buildCSSTypeItems() { - return CSS_TYPE_KEYS.map(key => ({ - value: key, - icon: getColorStripIcon(key), - label: t(`color_strip.type.${key}`), - desc: t(`color_strip.type.${key}.desc`), - })); -} - -let _cssTypeIconSelect: any = null; - -function _ensureCSSTypeIconSelect() { - const sel = document.getElementById('css-editor-type') as HTMLSelectElement; - if (!sel) return; - if (_cssTypeIconSelect) { - // Refresh labels (language may have changed) - _cssTypeIconSelect.updateItems(_buildCSSTypeItems()); - return; - } - _cssTypeIconSelect = new IconSelect({ - target: sel, - items: _buildCSSTypeItems(), - columns: 2, - }); -} - -/* ── Type-switch helper ───────────────────────────────────────── */ - -const CSS_SECTION_MAP: Record = { - 'picture': 'css-editor-picture-section', - 'picture_advanced': 'css-editor-picture-section', - 'static': 'css-editor-static-section', - 'color_cycle': 'css-editor-color-cycle-section', - 'gradient': 'css-editor-gradient-section', - 'effect': 'css-editor-effect-section', - 'composite': 'css-editor-composite-section', - 'mapped': 'css-editor-mapped-section', - 'audio': 'css-editor-audio-section', - 'api_input': 'css-editor-api-input-section', - 'notification': 'css-editor-notification-section', - 'daylight': 'css-editor-daylight-section', - 'candlelight': 'css-editor-candlelight-section', - 'weather': 'css-editor-weather-section', - 'processed': 'css-editor-processed-section', - 'key_colors': 'css-editor-key-colors-section', - 'game_event': 'css-editor-game-event-section', - 'math_wave': 'css-editor-math-wave-section', -}; - -const CSS_ALL_SECTION_IDS = [...new Set(Object.values(CSS_SECTION_MAP))]; - -const CSS_TYPE_SETUP: Record void> = { - processed: () => _populateProcessedSelectors(), - effect: () => { _ensureEffectTypeIconSelect(); _ensureEffectPaletteEntitySelect(); onEffectTypeChange(); }, - audio: () => { _ensureAudioVizIconSelect(); _ensureAudioPaletteEntitySelect(); onAudioVizChange(); _loadAudioSources(); }, - gradient: () => { _ensureGradientPresetEntitySelect(); _ensureGradientEasingIconSelect(); requestAnimationFrame(() => gradientRenderAll()); }, - notification: () => { ensureNotificationEffectIconSelect(); ensureNotificationFilterModeIconSelect(); }, - candlelight: () => _ensureCandleTypeIconSelect(), - game_event: () => { _populateGameIntegrationDropdownCSS(); _initCSSGamePresetIconSelect(); }, - math_wave: () => { _ensureMathWaveGradientEntitySelect(); }, - weather: () => { weatherSourcesCache.fetch().then(() => _populateWeatherSourceDropdown()); }, - composite: () => compositeRenderList(), - mapped: () => _mappedRenderList(), -}; - -export function onCSSTypeChange() { - const type = (document.getElementById('css-editor-type') as HTMLInputElement).value; - // Sync icon-select trigger display - if (_cssTypeIconSelect) _cssTypeIconSelect.setValue(type); - - // Hide all type-specific sections, then show the active one - CSS_ALL_SECTION_IDS.forEach(id => { - const el = document.getElementById(id); - if (el) el.style.display = 'none'; - }); - const activeSection = CSS_SECTION_MAP[type]; - if (activeSection) { - const el = document.getElementById(activeSection); - if (el) el.style.display = ''; - } - - // Hide picture source dropdown for advanced (sources are per-line in calibration) - const psGroup = document.getElementById('css-editor-picture-source-group') as HTMLElement | null; - if (psGroup) psGroup.style.display = (type === 'picture') ? '' : 'none'; - - const isPictureType = type === 'picture' || type === 'picture_advanced'; - if (isPictureType) _ensureInterpolationIconSelect(); - - // Run type-specific setup - const setupFn = CSS_TYPE_SETUP[type]; - if (setupFn) setupFn(); - - // Animation section — shown for static/gradient only - const animSection = document.getElementById('css-editor-animation-section') as HTMLElement; - const animTypeSelect = document.getElementById('css-editor-animation-type') as HTMLSelectElement; - if (type === 'static' || type === 'gradient') { - animSection.style.display = ''; - const opts = type === 'gradient' - ? ['none','breathing','gradient_shift','wave','noise_perturb','hue_rotate','strobe','sparkle','pulse','candle','rainbow_fade'] - : ['none','breathing','strobe','sparkle','pulse','candle','rainbow_fade']; - animTypeSelect.innerHTML = opts.map(v => - `` - ).join(''); - _ensureAnimationTypeIconSelect(type); - } else { - animSection.style.display = 'none'; - } - _syncAnimationSpeedState(); - - // LED count — only shown for picture, picture_advanced - const hasLedCount = ['picture', 'picture_advanced']; - (document.getElementById('css-editor-led-count-group') as HTMLElement).style.display = - hasLedCount.includes(type) ? '' : 'none'; - - // Sync clock — shown for animated types - const clockTypes = ['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave']; - (document.getElementById('css-editor-clock-group') as HTMLElement).style.display = clockTypes.includes(type) ? '' : 'none'; - if (clockTypes.includes(type)) _populateClockDropdown(); - - _autoGenerateCSSName(); -} - -function _populateClockDropdown(selectedId?: any) { - const sel = document.getElementById('css-editor-clock') as HTMLSelectElement; - const prev = selectedId !== undefined ? selectedId : sel.value; - sel.innerHTML = `` + - _cachedSyncClocks.map(c => ``).join(''); - sel.value = prev || ''; - - // Entity palette for clock - if (_cssClockEntitySelect) _cssClockEntitySelect.destroy(); - _cssClockEntitySelect = new EntitySelect({ - target: sel, - getItems: () => _cachedSyncClocks.map(c => ({ - value: c.id, - label: c.name, - icon: ICON_CLOCK, - desc: `${c.speed}x`, - })), - placeholder: t('palette.search'), - allowNone: true, - noneLabel: t('common.none_own_speed'), - }); -} - -export function onCSSClockChange() { - // No-op: speed sliders removed; speed is now clock-only -} - -let _weatherSourceEntitySelect: any = null; - -function _populateWeatherSourceDropdown() { - const sources = _cachedWeatherSources || []; - const sel = document.getElementById('css-editor-weather-source') as HTMLSelectElement; - const prev = sel.value; - sel.innerHTML = `` + - sources.map(s => ``).join(''); - sel.value = prev || ''; - if (_weatherSourceEntitySelect) _weatherSourceEntitySelect.destroy(); - if (sources.length > 0) { - _weatherSourceEntitySelect = new EntitySelect({ - target: sel, - getItems: () => (_cachedWeatherSources || []).map(s => ({ - value: s.id, - label: s.name, - icon: getColorStripIcon('weather'), - })), - placeholder: t('palette.search'), - }); - } -} - -function _populateProcessedSelectors() { - const editingId = (document.getElementById('css-editor-id') as HTMLInputElement).value; - const allSources = (colorStripSourcesCache.data || []) as any[]; - // Exclude self and other processed sources to prevent cycles - const inputSources = allSources.filter(s => s.id !== editingId && s.source_type !== 'processed'); - const inputSel = document.getElementById('css-editor-processed-input') as HTMLSelectElement; - const prevInput = inputSel.value; - inputSel.innerHTML = `` + - inputSources.map(s => ``).join(''); - inputSel.value = prevInput || ''; - if (_processedInputEntitySelect) _processedInputEntitySelect.destroy(); - if (inputSources.length > 0) { - _processedInputEntitySelect = new EntitySelect({ - target: inputSel, - getItems: () => ((colorStripSourcesCache.data || []) as any[]) - .filter(s => s.id !== editingId && s.source_type !== 'processed') - .map(s => ({ value: s.id, label: s.name, icon: getColorStripIcon(s.source_type) })), - placeholder: t('palette.search'), - }); - } - - const templates = (csptCache.data || []) as any[]; - const tplSel = document.getElementById('css-editor-processed-template') as HTMLSelectElement; - const prevTpl = tplSel.value; - tplSel.innerHTML = `` + - templates.map(tp => ``).join(''); - tplSel.value = prevTpl || ''; - if (_processedTemplateEntitySelect) _processedTemplateEntitySelect.destroy(); - if (templates.length > 0) { - _processedTemplateEntitySelect = new EntitySelect({ - target: tplSel, - getItems: () => ((csptCache.data || []) as any[]).map(tp => ({ - value: tp.id, label: tp.name, icon: ICON_SPARKLES, - })), - placeholder: t('palette.search'), - }); - } -} - -function _getAnimationPayload() { - const type = (document.getElementById('css-editor-animation-type') as HTMLInputElement).value; - return { - enabled: type !== 'none', - type: type !== 'none' ? type : 'breathing', - }; -} - -function _loadAnimationState(anim: any) { - // Set type after onCSSTypeChange() has populated the dropdown - const val = (anim && anim.enabled && anim.type) ? anim.type : 'none'; - (document.getElementById('css-editor-animation-type') as HTMLInputElement).value = val; - if (_animationTypeIconSelect) _animationTypeIconSelect.setValue(val); - _syncAnimationSpeedState(); -} - -export function onAnimationTypeChange() { - if (_animationTypeIconSelect) _animationTypeIconSelect.setValue((document.getElementById('css-editor-animation-type') as HTMLInputElement).value); - _syncAnimationSpeedState(); -} - -function _syncAnimationSpeedState() { - const type = (document.getElementById('css-editor-animation-type') as HTMLInputElement).value; - const descEl = document.getElementById('css-editor-animation-type-desc') as HTMLElement | null; - if (descEl) { - const desc = t('color_strip.animation.type.' + type + '.desc') || ''; - descEl.textContent = desc; - descEl.style.display = desc ? '' : 'none'; - } -} - -/* ── Daylight real-time toggle helper ─────────────────────────── */ - -export function onDaylightRealTimeChange() { - _syncDaylightSpeedVisibility(); -} - -function _syncDaylightSpeedVisibility() { - const isRealTime = (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked; - (document.getElementById('css-editor-daylight-speed-group') as HTMLElement).style.display = isRealTime ? 'none' : ''; -} - -/* ── Gradient strip preview helper ────────────────────────────── */ - -/* ── Effect / audio palette IconSelect instances ─────────────── */ - -let _animationTypeIconSelect: any = null; -let _interpolationIconSelect: any = null; -let _effectTypeIconSelect: any = null; -let _effectPaletteEntitySelect: EntitySelect | null = null; -let _audioPaletteEntitySelect: EntitySelect | null = null; -let _audioVizIconSelect: any = null; -let _gradientPresetEntitySelect: EntitySelect | null = null; -let _gradientEasingIconSelect: any = null; -let _candleTypeIconSelect: any = null; -let _apiInputInterpolationIconSelect: any = null; -const _icon = (d: any) => `${d}`; - -function _ensureInterpolationIconSelect() { - const sel = document.getElementById('css-editor-interpolation') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'average', icon: _icon(P.slidersHorizontal), label: t('color_strip.interpolation.average'), desc: t('color_strip.interpolation.average.desc') }, - { value: 'median', icon: _icon(P.activity), label: t('color_strip.interpolation.median'), desc: t('color_strip.interpolation.median.desc') }, - { value: 'dominant', icon: _icon(P.target), label: t('color_strip.interpolation.dominant'), desc: t('color_strip.interpolation.dominant.desc') }, - ]; - if (_interpolationIconSelect) { _interpolationIconSelect.updateItems(items); return; } - _interpolationIconSelect = new IconSelect({ target: sel, items, columns: 3 }); -} - -function _ensureSmoothingWidget(): BindableScalarWidget { - const container = document.getElementById('css-editor-smoothing-container')!; - if (!_smoothingWidget) { - _smoothingWidget = new BindableScalarWidget({ - container, - min: 0, max: 1, step: 0.05, default: 0.3, - idPrefix: 'css-editor-smoothing', - valueSources: () => _cachedValueSources, - }); - } - return _smoothingWidget; -} - -function _ensureKcSmoothingWidget(): BindableScalarWidget { - if (!_kcSmoothingWidget) { - _kcSmoothingWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-kc-smoothing-container')!, - min: 0, max: 1, step: 0.05, default: 0.3, - idPrefix: 'css-editor-kc-smoothing', - format: (v) => v.toFixed(2), - valueSources: () => _cachedValueSources, - }); - } - return _kcSmoothingWidget; -} - -function _ensureKcBrightnessWidget(): BindableScalarWidget { - if (!_kcBrightnessWidget) { - _kcBrightnessWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-kc-brightness-container')!, - min: 0, max: 1, step: 0.05, default: 1.0, - idPrefix: 'css-editor-kc-brightness', - format: (v) => v.toFixed(2), - valueSources: () => _cachedValueSources, - }); - } - return _kcBrightnessWidget; -} - -// ── Game Event CSS helpers ── - -function _ensureGameEventIdleColorWidget(): BindableColorWidget { - if (!_gameEventIdleColorWidget) { - _gameEventIdleColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-game-event-idle-color-container')!, - default: [0, 0, 0], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-ge-idle-color', - }); - } - return _gameEventIdleColorWidget; -} - -let _cssGameIntegrationEntitySelect: EntitySelect | null = null; - -function _populateGameIntegrationDropdownCSS(selectedId: string = '') { - const sel = document.getElementById('css-editor-game-integration') as HTMLSelectElement; - const integrations = _cachedGameIntegrations || []; - const prev = selectedId || sel.value; - sel.innerHTML = `` + - integrations.map(gi => ``).join(''); - sel.value = prev || ''; - - if (_cssGameIntegrationEntitySelect) _cssGameIntegrationEntitySelect.destroy(); - _cssGameIntegrationEntitySelect = new EntitySelect({ - target: sel, - getItems: () => integrations.map(gi => ({ - value: gi.id, - label: gi.name, - icon: ICON_GAMEPAD, - desc: gi.adapter_type, - })), - allowNone: true, - noneLabel: t('common.none_no_input'), - placeholder: t('palette.search'), - }); -} - -let _cssGameMappings: any[] = []; -let _cssGameMappingIconSelects: IconSelect[] = []; -let _cssGamePresetIconSelect: IconSelect | null = null; - -function _destroyCSSGameMappingIconSelects() { - _cssGameMappingIconSelects.forEach(is => is.destroy()); - _cssGameMappingIconSelects = []; -} - -function _hexToRgbCSS(hex: string): number[] { - const m = hex.replace('#', '').match(/.{2}/g); - if (!m) return [255, 0, 0]; - return m.map(c => parseInt(c, 16)); -} - -function _rgbToHexCSS(rgb: number[]): string { - return '#' + rgb.map(c => c.toString(16).padStart(2, '0')).join(''); -} - -function _getCSSGameAvailableEventTypes(): string[] { - const giId = (document.getElementById('css-editor-game-integration') as HTMLSelectElement)?.value; - if (giId) { - const gi = (_cachedGameIntegrations || []).find(g => g.id === giId); - if (gi) { - const adapter = (_cachedGameAdapters || []).find(a => a.adapter_type === gi.adapter_type); - if (adapter && adapter.supported_events.length > 0) return adapter.supported_events; - } - } - return ['kill', 'death', 'health', 'armor', 'round_start', 'round_end', 'bomb_planted', 'bomb_defused', 'assist', 'headshot']; -} - -const _CSS_GE_EFFECT_TYPES: IconSelectItem[] = [ - { value: 'flash', label: 'Flash', icon: `${P.zap}` }, - { value: 'pulse', label: 'Pulse', icon: `${P.activity}` }, - { value: 'sweep', label: 'Sweep', icon: `${P.fastForward}` }, - { value: 'color_shift', label: 'Color Shift', icon: `${P.rainbow}` }, - { value: 'breathing', label: 'Breathing', icon: `${P.heart}` }, -]; - -const _CSS_GE_EVENT_ICONS: Record = { - kill: P.crosshair, death: P.xIcon, health: P.heart, armor: P.shield, - round_start: P.play, round_end: P.square, bomb_planted: P.flame, bomb_defused: P.circleCheck, - assist: P.swords, headshot: P.target, damage: P.zap, gold: P.star, - level_up: P.trendingUp, respawn: P.refreshCw, item_pickup: P.packageIcon, -}; - -function _buildCSSGameEventTypeItems(): IconSelectItem[] { - return _getCSSGameAvailableEventTypes().map(et => ({ - value: et, - label: et, - icon: `${_CSS_GE_EVENT_ICONS[et] || P.circleDot}`, - })); -} - -function _renderCSSGameMappingRow(mapping: any, index: number): string { - const eventTypes = _getCSSGameAvailableEventTypes(); - const eventOptions = eventTypes.map(et => - `` - ).join(''); - const effectOptions = _CSS_GE_EFFECT_TYPES.map(ef => - `` - ).join(''); - const effectLabel = _CSS_GE_EFFECT_TYPES.find(ef => ef.value === mapping.effect_type)?.label || mapping.effect_type; - const hexColor = _rgbToHexCSS(mapping.color || [255, 0, 0]); - - return ` -
-
- - - ${escapeHtml(mapping.event_type)} - ${escapeHtml(effectLabel)} - - - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
`; -} - -function _wireCSSGameMappingRows(container: HTMLElement) { - container.querySelectorAll('.gi-mapping-header').forEach(header => { - const item = header.closest('.gi-mapping-row') as HTMLElement; - header.addEventListener('click', (e: Event) => { - if ((e.target as HTMLElement).closest('.btn-remove-rule')) return; - item.classList.toggle('gi-mapping-expanded'); - }); - }); - - container.querySelectorAll('.gi-mapping-row').forEach(row => { - const eventSel = row.querySelector('[data-field="event_type"]') as HTMLSelectElement | null; - const effectSel = row.querySelector('[data-field="effect_type"]') as HTMLSelectElement | null; - const colorInput = row.querySelector('input[data-field="color"]') as HTMLInputElement | null; - const summaryEvent = row.querySelector('.gi-mapping-summary-event') as HTMLElement | null; - const summaryEffect = row.querySelector('.gi-mapping-summary-effect') as HTMLElement | null; - const summaryColor = row.querySelector('.gi-mapping-summary-color') as HTMLElement | null; - - if (eventSel) { - const is = new IconSelect({ target: eventSel, items: _buildCSSGameEventTypeItems(), columns: 4 }); - _cssGameMappingIconSelects.push(is); - if (summaryEvent) { - eventSel.addEventListener('change', () => { summaryEvent.textContent = eventSel.value; }); - } - } - if (effectSel) { - const is = new IconSelect({ target: effectSel, items: _CSS_GE_EFFECT_TYPES, columns: 3 }); - _cssGameMappingIconSelects.push(is); - if (summaryEffect) { - effectSel.addEventListener('change', () => { - const label = _CSS_GE_EFFECT_TYPES.find(ef => ef.value === effectSel.value)?.label || effectSel.value; - summaryEffect.textContent = label; - }); - } - } - if (colorInput && summaryColor) { - colorInput.addEventListener('input', () => { summaryColor.style.background = colorInput.value; }); - } - }); -} - -function _renderCSSGameMappings(mappings: any[]) { - _cssGameMappings = [...mappings]; - _destroyCSSGameMappingIconSelects(); - const container = document.getElementById('css-editor-ge-mappings-list'); - if (!container) return; - container.innerHTML = mappings.map((m, i) => _renderCSSGameMappingRow(m, i)).join(''); - _wireCSSGameMappingRows(container); -} - -function _collectCSSGameMappings(): any[] { - const rows = document.querySelectorAll('#css-editor-ge-mappings-list .gi-mapping-row'); - return Array.from(rows).map(row => { - const eventType = (row.querySelector('[data-field="event_type"]') as HTMLSelectElement)?.value || 'kill'; - const effectType = (row.querySelector('[data-field="effect_type"]') as HTMLSelectElement)?.value || 'flash'; - const colorInput = (row.querySelector('input[data-field="color"]') as HTMLInputElement)?.value || '#ff0000'; - const duration = parseFloat((row.querySelector('[data-field="duration_ms"]') as HTMLInputElement)?.value) || 500; - const intensity = parseFloat((row.querySelector('[data-field="intensity"]') as HTMLInputElement)?.value) || 1.0; - const priority = parseInt((row.querySelector('[data-field="priority"]') as HTMLInputElement)?.value) || 5; - return { event_type: eventType, effect_type: effectType, color: _hexToRgbCSS(colorInput), duration_ms: duration, intensity, priority }; - }); -} - -export function addCSSGameMapping() { - const collected = _collectCSSGameMappings(); - collected.push({ - event_type: _getCSSGameAvailableEventTypes()[0] || 'kill', - effect_type: 'flash', - color: [255, 0, 0], - duration_ms: 500, - intensity: 1.0, - priority: 5, - }); - _renderCSSGameMappings(collected); -} - -export function removeCSSGameMapping(index: number) { - const collected = _collectCSSGameMappings(); - collected.splice(index, 1); - _renderCSSGameMappings(collected); -} - -export function onCSSGameMappingPresetChange() { - const sel = document.getElementById('css-editor-ge-mapping-preset') as HTMLSelectElement; - if (!sel.value) return; - const presets: Record = { - fps_combat: [ - { event_type: 'kill', effect_type: 'flash', color: [0, 255, 0], duration_ms: 400, intensity: 1.0, priority: 8 }, - { event_type: 'death', effect_type: 'pulse', color: [255, 0, 0], duration_ms: 1500, intensity: 1.0, priority: 10 }, - { event_type: 'headshot', effect_type: 'flash', color: [255, 215, 0], duration_ms: 300, intensity: 1.0, priority: 9 }, - { event_type: 'health', effect_type: 'breathing', color: [255, 50, 50], duration_ms: 2000, intensity: 0.6, priority: 3 }, - { event_type: 'round_start', effect_type: 'sweep', color: [0, 100, 255], duration_ms: 800, intensity: 0.8, priority: 5 }, - ], - moba_health: [ - { event_type: 'health', effect_type: 'color_shift', color: [0, 255, 0], duration_ms: 1000, intensity: 0.7, priority: 4 }, - { event_type: 'kill', effect_type: 'flash', color: [255, 215, 0], duration_ms: 500, intensity: 1.0, priority: 8 }, - { event_type: 'death', effect_type: 'pulse', color: [255, 0, 0], duration_ms: 2000, intensity: 1.0, priority: 10 }, - { event_type: 'assist', effect_type: 'flash', color: [100, 200, 255], duration_ms: 300, intensity: 0.8, priority: 6 }, - ], - }; - const preset = presets[sel.value]; - if (preset) _renderCSSGameMappings(preset); - sel.value = ''; -} - -function _initCSSGamePresetIconSelect() { - const sel = document.getElementById('css-editor-ge-mapping-preset') as HTMLSelectElement | null; - if (!sel) return; - if (_cssGamePresetIconSelect) { _cssGamePresetIconSelect.destroy(); _cssGamePresetIconSelect = null; } - const items: IconSelectItem[] = [ - { value: '', label: t('game_integration.mapping.select_preset'), icon: '' }, - { value: 'fps_combat', label: t('game_integration.preset.fps_combat'), icon: `${P.crosshair}` }, - { value: 'moba_health', label: t('game_integration.preset.moba_health'), icon: `${P.heart}` }, - ]; - _cssGamePresetIconSelect = new IconSelect({ target: sel, items, columns: 2 }); -} - -function _ensureAudioSensitivityWidget(): BindableScalarWidget { - if (!_audioSensitivityWidget) { - _audioSensitivityWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-audio-sensitivity-container')!, - min: 0.1, max: 5.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-audio-sensitivity', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _audioSensitivityWidget; -} - -function _ensureAudioSmoothingWidget(): BindableScalarWidget { - if (!_audioSmoothingWidget) { - _audioSmoothingWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-audio-smoothing-container')!, - min: 0.0, max: 1.0, step: 0.05, default: 0.3, - idPrefix: 'css-editor-audio-smoothing', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(2), - }); - } - return _audioSmoothingWidget; -} - -function _ensureAudioBeatDecayWidget(): BindableScalarWidget { - if (!_audioBeatDecayWidget) { - _audioBeatDecayWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-audio-beat-decay-container')!, - min: 0.01, max: 0.5, step: 0.01, default: 0.15, - idPrefix: 'css-editor-audio-beat-decay', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(2), - }); - } - return _audioBeatDecayWidget; -} - -function _ensureEffectIntensityWidget(): BindableScalarWidget { - if (!_effectIntensityWidget) { - _effectIntensityWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-effect-intensity-container')!, - min: 0.1, max: 2.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-effect-intensity', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _effectIntensityWidget; -} - -function _ensureEffectScaleWidget(): BindableScalarWidget { - if (!_effectScaleWidget) { - _effectScaleWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-effect-scale-container')!, - min: 0.5, max: 5.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-effect-scale', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _effectScaleWidget; -} - -function _ensureApiInputTimeoutWidget(): BindableScalarWidget { - if (!_apiInputTimeoutWidget) { - _apiInputTimeoutWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-api-input-timeout-container')!, - min: 0, max: 60, step: 0.5, default: 5.0, - idPrefix: 'css-editor-api-input-timeout', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _apiInputTimeoutWidget; -} - -function _ensureCandlelightIntensityWidget(): BindableScalarWidget { - if (!_candlelightIntensityWidget) { - _candlelightIntensityWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-candlelight-intensity-container')!, - min: 0.1, max: 2.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-candlelight-intensity', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _candlelightIntensityWidget; -} - -function _ensureCandlelightSpeedWidget(): BindableScalarWidget { - if (!_candlelightSpeedWidget) { - _candlelightSpeedWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-candlelight-speed-container')!, - min: 0.1, max: 5.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-candlelight-speed', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _candlelightSpeedWidget; -} - -function _ensureCandlelightWindWidget(): BindableScalarWidget { - if (!_candlelightWindWidget) { - _candlelightWindWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-candlelight-wind-container')!, - min: 0.0, max: 2.0, step: 0.1, default: 0.0, - idPrefix: 'css-editor-candlelight-wind', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _candlelightWindWidget; -} - -function _ensureWeatherSpeedWidget(): BindableScalarWidget { - if (!_weatherSpeedWidget) { - _weatherSpeedWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-weather-speed-container')!, - min: 0.1, max: 5, step: 0.1, default: 1.0, - idPrefix: 'css-editor-weather-speed', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _weatherSpeedWidget; -} - -function _ensureWeatherTempInfluenceWidget(): BindableScalarWidget { - if (!_weatherTempInfluenceWidget) { - _weatherTempInfluenceWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-weather-temp-influence-container')!, - min: 0, max: 1, step: 0.05, default: 0.5, - idPrefix: 'css-editor-weather-temp-influence', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(2), - }); - } - return _weatherTempInfluenceWidget; -} - -function _ensureStaticColorWidget(): BindableColorWidget { - if (!_staticColorWidget) { - _staticColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-color-container')!, - default: [255, 255, 255], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-color', - }); - } - return _staticColorWidget; -} - -function _ensureEffectColorWidget(): BindableColorWidget { - if (!_effectColorWidget) { - _effectColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-effect-color-container')!, - default: [255, 80, 0], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-effect-color', - }); - } - return _effectColorWidget; -} - -function _ensureAudioColorWidget(): BindableColorWidget { - if (!_audioColorWidget) { - _audioColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-audio-color-container')!, - default: [0, 255, 0], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-audio-color', - }); - } - return _audioColorWidget; -} - -function _ensureAudioColorPeakWidget(): BindableColorWidget { - if (!_audioColorPeakWidget) { - _audioColorPeakWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-audio-color-peak-container')!, - default: [255, 0, 0], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-audio-color-peak', - }); - } - return _audioColorPeakWidget; -} - -function _ensureApiInputFallbackColorWidget(): BindableColorWidget { - if (!_apiInputFallbackColorWidget) { - _apiInputFallbackColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-api-input-fallback-color-container')!, - default: [0, 0, 0], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-api-input-fallback-color', - }); - } - return _apiInputFallbackColorWidget; -} - -function _ensureCandlelightColorWidget(): BindableColorWidget { - if (!_candlelightColorWidget) { - _candlelightColorWidget = new BindableColorWidget({ - container: document.getElementById('css-editor-candlelight-color-container')!, - default: [255, 147, 41], - valueSources: () => _cachedValueSources, - idPrefix: 'css-editor-candlelight-color', - }); - } - return _candlelightColorWidget; -} - -function _ensureApiInputInterpolationIconSelect() { - const sel = document.getElementById('css-editor-api-input-interpolation') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'linear', icon: _icon(P.activity), label: t('color_strip.api_input.interpolation.linear'), desc: t('color_strip.api_input.interpolation.linear.desc') }, - { value: 'nearest', icon: _icon(P.hash), label: t('color_strip.api_input.interpolation.nearest'), desc: t('color_strip.api_input.interpolation.nearest.desc') }, - { value: 'none', icon: _icon(P.circleOff), label: t('color_strip.api_input.interpolation.none'), desc: t('color_strip.api_input.interpolation.none.desc') }, - ]; - if (_apiInputInterpolationIconSelect) { _apiInputInterpolationIconSelect.updateItems(items); return; } - _apiInputInterpolationIconSelect = new IconSelect({ target: sel, items, columns: 3 }); -} - -function _ensureEffectTypeIconSelect() { - const sel = document.getElementById('css-editor-effect-type') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'fire', icon: _icon(P.zap), label: t('color_strip.effect.fire'), desc: t('color_strip.effect.fire.desc') }, - { value: 'meteor', icon: _icon(P.sparkles), label: t('color_strip.effect.meteor'), desc: t('color_strip.effect.meteor.desc') }, - { value: 'plasma', icon: _icon(P.rainbow), label: t('color_strip.effect.plasma'), desc: t('color_strip.effect.plasma.desc') }, - { value: 'noise', icon: _icon(P.activity), label: t('color_strip.effect.noise'), desc: t('color_strip.effect.noise.desc') }, - { value: 'aurora', icon: _icon(P.sparkles), label: t('color_strip.effect.aurora'), desc: t('color_strip.effect.aurora.desc') }, - { value: 'rain', icon: _icon(P.cloudSun), label: t('color_strip.effect.rain'), desc: t('color_strip.effect.rain.desc') }, - { value: 'comet', icon: _icon(P.rocket), label: t('color_strip.effect.comet'), desc: t('color_strip.effect.comet.desc') }, - { value: 'bouncing_ball', icon: _icon(P.activity), label: t('color_strip.effect.bouncing_ball'), desc: t('color_strip.effect.bouncing_ball.desc') }, - { value: 'fireworks', icon: _icon(P.sparkles), label: t('color_strip.effect.fireworks'), desc: t('color_strip.effect.fireworks.desc') }, - { value: 'sparkle_rain', icon: _icon(P.star), label: t('color_strip.effect.sparkle_rain'), desc: t('color_strip.effect.sparkle_rain.desc') }, - { value: 'lava_lamp', icon: _icon(P.flame), label: t('color_strip.effect.lava_lamp'), desc: t('color_strip.effect.lava_lamp.desc') }, - { value: 'wave_interference',icon: _icon(P.rainbow), label: t('color_strip.effect.wave_interference'),desc: t('color_strip.effect.wave_interference.desc') }, - ]; - if (_effectTypeIconSelect) { _effectTypeIconSelect.updateItems(items); return; } - _effectTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 }); -} - -function _ensureEffectPaletteEntitySelect() { - const sel = document.getElementById('css-editor-effect-palette') as HTMLSelectElement | null; - if (!sel) return; - const items = _buildGradientEntityItems(); - _syncSelectOptions(sel, items); - if (_effectPaletteEntitySelect) { _effectPaletteEntitySelect.refresh(); return; } - _effectPaletteEntitySelect = new EntitySelect({ - target: sel, - getItems: _buildGradientEntityItems, - placeholder: t('palette.search'), - }); -} - -function _ensureGradientEasingIconSelect() { - const sel = document.getElementById('css-editor-gradient-easing') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'linear', icon: _icon(P.trendingUp), label: t('color_strip.gradient.easing.linear'), desc: t('color_strip.gradient.easing.linear.desc') }, - { value: 'ease_in_out', icon: _icon(P.activity), label: t('color_strip.gradient.easing.ease_in_out'), desc: t('color_strip.gradient.easing.ease_in_out.desc') }, - { value: 'step', icon: _icon(P.layoutDashboard), label: t('color_strip.gradient.easing.step'), desc: t('color_strip.gradient.easing.step.desc') }, - { value: 'cubic', icon: _icon(P.slidersHorizontal), label: t('color_strip.gradient.easing.cubic'), desc: t('color_strip.gradient.easing.cubic.desc') }, - ]; - if (_gradientEasingIconSelect) { _gradientEasingIconSelect.updateItems(items); return; } - _gradientEasingIconSelect = new IconSelect({ target: sel, items, columns: 2 }); -} - -function _ensureCandleTypeIconSelect() { - const sel = document.getElementById('css-editor-candlelight-type') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'default', icon: _icon(P.flame), label: t('color_strip.candlelight.type.default'), desc: t('color_strip.candlelight.type.default.desc') }, - { value: 'taper', icon: _icon(P.activity), label: t('color_strip.candlelight.type.taper'), desc: t('color_strip.candlelight.type.taper.desc') }, - { value: 'votive', icon: _icon(P.lightbulb), label: t('color_strip.candlelight.type.votive'), desc: t('color_strip.candlelight.type.votive.desc') }, - { value: 'bonfire', icon: _icon(P.zap), label: t('color_strip.candlelight.type.bonfire'), desc: t('color_strip.candlelight.type.bonfire.desc') }, - ]; - if (_candleTypeIconSelect) { _candleTypeIconSelect.updateItems(items); return; } - _candleTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 }); -} - -/* ── Math Wave helpers ──────────────────────────────────────────── */ - -function _ensureMathWaveSpeedWidget(): BindableScalarWidget { - if (!_mathWaveSpeedWidget) { - _mathWaveSpeedWidget = new BindableScalarWidget({ - container: document.getElementById('css-editor-math-wave-speed-container')!, - min: 0.1, max: 10.0, step: 0.1, default: 1.0, - idPrefix: 'css-editor-math-wave-speed', - valueSources: () => _cachedValueSources, - format: (v) => v.toFixed(1), - }); - } - return _mathWaveSpeedWidget; -} - -function _ensureMathWaveGradientEntitySelect() { - const sel = document.getElementById('css-editor-math-wave-gradient') as HTMLSelectElement | null; - if (!sel) return; - const items = _buildGradientEntityItems(); - _syncSelectOptions(sel, items); - if (_mathWaveGradientEntitySelect) { _mathWaveGradientEntitySelect.refresh(); return; } - _mathWaveGradientEntitySelect = new EntitySelect({ - target: sel, - getItems: _buildGradientEntityItems, - placeholder: t('palette.search'), - }); -} - -function _buildMathWaveWaveformItems(): IconSelectItem[] { - return [ - { value: 'sine', icon: _icon(P.activity), label: t('color_strip.math_wave.waveform.sine'), desc: '' }, - { value: 'triangle', icon: _icon(P.trendingUp), label: t('color_strip.math_wave.waveform.triangle'), desc: '' }, - { value: 'sawtooth', icon: _icon(P.rotateCw), label: t('color_strip.math_wave.waveform.sawtooth'), desc: '' }, - { value: 'square', icon: _icon(P.layoutDashboard), label: t('color_strip.math_wave.waveform.square'), desc: '' }, - ]; -} - -function _mathWaveRenderLayers(waves: Array<{ waveform: string; frequency: number; amplitude: number; phase: number; offset: number }>) { - const container = document.getElementById('css-editor-math-wave-layers'); - if (!container) return; - // Destroy old waveform icon selects - _mathWaveWaveformIconSelects.forEach(s => s.destroy()); - _mathWaveWaveformIconSelects = []; - - container.innerHTML = ''; - waves.forEach((wave, idx) => { - const row = document.createElement('div'); - row.className = 'math-wave-layer-row'; - row.style.cssText = 'border:1px solid var(--border-color,#444);border-radius:6px;padding:8px;margin-bottom:6px;position:relative'; - row.innerHTML = ` -
- #${idx + 1} - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- `; - container.appendChild(row); - - // Set waveform value and attach IconSelect - const wfSelect = row.querySelector('.mw-waveform') as HTMLSelectElement; - wfSelect.value = wave.waveform || 'sine'; - const iconSel = new IconSelect({ - target: wfSelect, - items: _buildMathWaveWaveformItems(), - columns: 2, - }); - _mathWaveWaveformIconSelects.push(iconSel); - }); -} - -function _mathWaveGetLayers(): Array<{ waveform: string; frequency: number; amplitude: number; phase: number; offset: number }> { - const container = document.getElementById('css-editor-math-wave-layers'); - if (!container) return []; - const rows = container.querySelectorAll('.math-wave-layer-row'); - return Array.from(rows).map(row => ({ - waveform: (row.querySelector('.mw-waveform') as HTMLSelectElement).value, - frequency: parseFloat((row.querySelector('.mw-frequency') as HTMLInputElement).value) || 1.0, - amplitude: parseFloat((row.querySelector('.mw-amplitude') as HTMLInputElement).value) || 1.0, - phase: parseFloat((row.querySelector('.mw-phase') as HTMLInputElement).value) || 0.0, - offset: parseFloat((row.querySelector('.mw-offset') as HTMLInputElement).value) || 0.0, - })); -} - -export function mathWaveAddLayer() { - const current = _mathWaveGetLayers(); - current.push({ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 }); - _mathWaveRenderLayers(current); -} - -export function mathWaveRemoveLayer(idx: number) { - const current = _mathWaveGetLayers(); - current.splice(idx, 1); - _mathWaveRenderLayers(current); -} - -function _ensureAudioPaletteEntitySelect() { - const sel = document.getElementById('css-editor-audio-palette') as HTMLSelectElement | null; - if (!sel) return; - const items = _buildGradientEntityItems(); - _syncSelectOptions(sel, items); - if (_audioPaletteEntitySelect) { _audioPaletteEntitySelect.refresh(); return; } - _audioPaletteEntitySelect = new EntitySelect({ - target: sel, - getItems: _buildGradientEntityItems, - placeholder: t('palette.search'), - }); -} - -function _ensureAudioVizIconSelect() { - const sel = document.getElementById('css-editor-audio-viz') as HTMLSelectElement | null; - if (!sel) return; - const items = [ - { value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') }, - { value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') }, - { value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') }, - { value: 'pulse_on_beat', icon: _icon(P.heart), label: t('color_strip.audio.viz.pulse_on_beat'), desc: t('color_strip.audio.viz.pulse_on_beat.desc') }, - { value: 'energy_gradient', icon: _icon(P.flame), label: t('color_strip.audio.viz.energy_gradient'), desc: t('color_strip.audio.viz.energy_gradient.desc') }, - { value: 'spectrum_bands', icon: _icon(P.radio), label: t('color_strip.audio.viz.spectrum_bands'), desc: t('color_strip.audio.viz.spectrum_bands.desc') }, - { value: 'strobe_on_drop', icon: _icon(P.sparkles), label: t('color_strip.audio.viz.strobe_on_drop'), desc: t('color_strip.audio.viz.strobe_on_drop.desc') }, - ]; - if (_audioVizIconSelect) { _audioVizIconSelect.updateItems(items); return; } - _audioVizIconSelect = new IconSelect({ target: sel, items, columns: 4 }); -} - -function _buildGradientEntityItems() { - const gradients = _getGradients(); - return gradients.map(g => ({ - value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name, - })); -} - -/** Sync