|
|
|
@@ -91,6 +91,9 @@ class CSSEditorModal extends Modal {
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -149,6 +152,9 @@ class CSSEditorModal extends Modal {
|
|
|
|
|
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() : []),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -168,6 +174,9 @@ 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;
|
|
|
|
@@ -260,7 +269,7 @@ 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',
|
|
|
|
|
'game_event', 'math_wave',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function _buildCSSTypeItems() {
|
|
|
|
@@ -309,6 +318,7 @@ const CSS_SECTION_MAP: Record<string, string> = {
|
|
|
|
|
'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))];
|
|
|
|
@@ -321,6 +331,7 @@ const CSS_TYPE_SETUP: Record<string, () => void> = {
|
|
|
|
|
notification: () => { ensureNotificationEffectIconSelect(); ensureNotificationFilterModeIconSelect(); },
|
|
|
|
|
candlelight: () => _ensureCandleTypeIconSelect(),
|
|
|
|
|
game_event: () => { _populateGameIntegrationDropdownCSS(); _initCSSGamePresetIconSelect(); },
|
|
|
|
|
math_wave: () => { _ensureMathWaveGradientEntitySelect(); },
|
|
|
|
|
weather: () => { weatherSourcesCache.fetch().then(() => _populateWeatherSourceDropdown()); },
|
|
|
|
|
composite: () => compositeRenderList(),
|
|
|
|
|
mapped: () => _mappedRenderList(),
|
|
|
|
@@ -376,7 +387,7 @@ export function onCSSTypeChange() {
|
|
|
|
|
hasLedCount.includes(type) ? '' : 'none';
|
|
|
|
|
|
|
|
|
|
// Sync clock — shown for animated types
|
|
|
|
|
const clockTypes = ['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight', 'weather'];
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
@@ -1118,6 +1129,127 @@ function _ensureCandleTypeIconSelect() {
|
|
|
|
|
_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 = `
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
|
|
|
|
<strong>#${idx + 1}</strong>
|
|
|
|
|
<button type="button" class="btn btn-icon btn-danger btn-sm" onclick="mathWaveRemoveLayer(${idx})" title="${t('common.delete')}" style="margin-left:auto">${ICON_TRASH}</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:4px">
|
|
|
|
|
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.waveform">${t('color_strip.math_wave.waveform')}</label>
|
|
|
|
|
<select class="mw-waveform" data-idx="${idx}">
|
|
|
|
|
<option value="sine">${t('color_strip.math_wave.waveform.sine')}</option>
|
|
|
|
|
<option value="triangle">${t('color_strip.math_wave.waveform.triangle')}</option>
|
|
|
|
|
<option value="sawtooth">${t('color_strip.math_wave.waveform.sawtooth')}</option>
|
|
|
|
|
<option value="square">${t('color_strip.math_wave.waveform.square')}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.frequency">${t('color_strip.math_wave.frequency')}</label>
|
|
|
|
|
<input type="number" class="mw-frequency" min="0.1" max="20" step="0.1" value="${wave.frequency}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.amplitude">${t('color_strip.math_wave.amplitude')}</label>
|
|
|
|
|
<input type="number" class="mw-amplitude" min="0.0" max="2.0" step="0.1" value="${wave.amplitude}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.phase">${t('color_strip.math_wave.phase')}</label>
|
|
|
|
|
<input type="number" class="mw-phase" min="0.0" max="6.28" step="0.1" value="${wave.phase}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.offset">${t('color_strip.math_wave.offset')}</label>
|
|
|
|
|
<input type="number" class="mw-offset" min="-1.0" max="1.0" step="0.1" value="${wave.offset}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
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;
|
|
|
|
@@ -1177,13 +1309,14 @@ function _ensureGradientPresetEntitySelect() {
|
|
|
|
|
/** Rebuild the gradient picker after entity changes. */
|
|
|
|
|
export function refreshGradientPresetPicker() {
|
|
|
|
|
// Re-sync select options before refreshing entity selects
|
|
|
|
|
for (const selId of ['css-editor-gradient-preset', 'css-editor-effect-palette', 'css-editor-audio-palette']) {
|
|
|
|
|
for (const selId of ['css-editor-gradient-preset', 'css-editor-effect-palette', 'css-editor-audio-palette', 'css-editor-math-wave-gradient']) {
|
|
|
|
|
const sel = document.getElementById(selId) as HTMLSelectElement | null;
|
|
|
|
|
if (sel) _syncSelectOptions(sel, _buildGradientEntityItems());
|
|
|
|
|
}
|
|
|
|
|
if (_gradientPresetEntitySelect) _gradientPresetEntitySelect.refresh();
|
|
|
|
|
if (_effectPaletteEntitySelect) _effectPaletteEntitySelect.refresh();
|
|
|
|
|
if (_audioPaletteEntitySelect) _audioPaletteEntitySelect.refresh();
|
|
|
|
|
if (_mathWaveGradientEntitySelect) _mathWaveGradientEntitySelect.refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Render the user-created gradient list below the save button. */
|
|
|
|
@@ -1615,6 +1748,7 @@ type CardPropsRenderer = (source: ColorStripSource, opts: {
|
|
|
|
|
const NON_PICTURE_TYPES = new Set([
|
|
|
|
|
'static', 'gradient', 'color_cycle', 'effect', 'composite', 'mapped',
|
|
|
|
|
'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors',
|
|
|
|
|
'math_wave',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
|
|
|
|
@@ -1775,6 +1909,18 @@ const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
|
|
|
|
|
<span class="stream-card-prop">${mode}</span>
|
|
|
|
|
`;
|
|
|
|
|
},
|
|
|
|
|
math_wave: (source, { clockBadge }) => {
|
|
|
|
|
const waveCount = (source.waves || []).length;
|
|
|
|
|
const speedVal = bindableValue(source.speed, 1.0).toFixed(1);
|
|
|
|
|
const gr = source.gradient_id ? _getGradients().find(g => g.id === source.gradient_id) : null;
|
|
|
|
|
const grName = gr?.name || '—';
|
|
|
|
|
return `
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.math_wave.waves')}">${ICON_ACTIVITY} ${waveCount} wave${waveCount !== 1 ? 's' : ''}</span>
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.math_wave.gradient')}">${ICON_PALETTE} ${escapeHtml(grName)}</span>
|
|
|
|
|
<span class="stream-card-prop">${ICON_FAST_FORWARD} ${speedVal}x</span>
|
|
|
|
|
${clockBadge}
|
|
|
|
|
`;
|
|
|
|
|
},
|
|
|
|
|
processed: (source) => {
|
|
|
|
|
const inputSrc = ((colorStripSourcesCache.data || []) as any[]).find(s => s.id === source.input_source_id);
|
|
|
|
|
const inputName = inputSrc?.name || source.input_source_id || '—';
|
|
|
|
@@ -2382,6 +2528,39 @@ const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
math_wave: {
|
|
|
|
|
load(css: any) {
|
|
|
|
|
_ensureMathWaveGradientEntitySelect();
|
|
|
|
|
const gradientId = css.gradient_id || '';
|
|
|
|
|
(document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value = gradientId;
|
|
|
|
|
if (_mathWaveGradientEntitySelect) _mathWaveGradientEntitySelect.setValue(gradientId);
|
|
|
|
|
_ensureMathWaveSpeedWidget().setValue(css.speed ?? 1.0);
|
|
|
|
|
_mathWaveRenderLayers(css.waves || [{ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 }]);
|
|
|
|
|
},
|
|
|
|
|
reset() {
|
|
|
|
|
_ensureMathWaveGradientEntitySelect();
|
|
|
|
|
const gradients = _getGradients();
|
|
|
|
|
const defaultId = gradients.length > 0 ? gradients[0].id : '';
|
|
|
|
|
(document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value = defaultId;
|
|
|
|
|
if (_mathWaveGradientEntitySelect) _mathWaveGradientEntitySelect.setValue(defaultId);
|
|
|
|
|
_ensureMathWaveSpeedWidget().setValue(1.0);
|
|
|
|
|
_mathWaveRenderLayers([{ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 }]);
|
|
|
|
|
},
|
|
|
|
|
getPayload(name: any) {
|
|
|
|
|
const gradientId = (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value;
|
|
|
|
|
const waves = _mathWaveGetLayers();
|
|
|
|
|
if (waves.length === 0) {
|
|
|
|
|
cssEditorModal.showError(t('color_strip.math_wave.error.no_waves'));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
gradient_id: gradientId || null,
|
|
|
|
|
speed: _ensureMathWaveSpeedWidget().getValue(),
|
|
|
|
|
waves,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
game_event: {
|
|
|
|
|
load(css: any) {
|
|
|
|
|
_populateGameIntegrationDropdownCSS(css.game_integration_id || '');
|
|
|
|
@@ -2579,7 +2758,7 @@ export async function saveCSSEditor() {
|
|
|
|
|
payload.source_type = knownType ? sourceType : 'picture';
|
|
|
|
|
|
|
|
|
|
// Attach clock_id for animated types
|
|
|
|
|
const clockTypes = ['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight', 'weather'];
|
|
|
|
|
const clockTypes = ['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
|
|
|
|
|
if (clockTypes.includes(sourceType)) {
|
|
|
|
|
const clockVal = (document.getElementById('css-editor-clock') as HTMLInputElement).value;
|
|
|
|
|
payload.clock_id = clockVal || null;
|
|
|
|
|