Add Daylight Cycle value source type

New value source that outputs brightness (0-1) based on the daylight
color LUT, computing BT.601 luminance from the simulated sky color.
Supports real-time wall-clock mode or configurable simulation speed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:27:36 +03:00
parent 73562cd525
commit ee40d99067
13 changed files with 271 additions and 12 deletions

View File

@@ -132,6 +132,7 @@ import {
import {
showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource, closeTestValueSourceModal,
} from './features/value-sources.js';
@@ -409,6 +410,7 @@ Object.assign(window, {
cloneValueSource,
deleteValueSource,
onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource,
closeTestValueSourceModal,

View File

@@ -30,6 +30,7 @@ const _colorStripTypeIcons = {
const _valueSourceTypeIcons = {
static: _svg(P.layoutDashboard), animated: _svg(P.refreshCw), audio: _svg(P.music),
adaptive_time: _svg(P.clock), adaptive_scene: _svg(P.cloudSun),
daylight: _svg(P.sun),
};
const _audioSourceTypeIcons = { mono: _svg(P.mic), multichannel: _svg(P.volume2) };
const _deviceTypeIcons = {

View File

@@ -18,7 +18,7 @@ import { Modal } from '../core/modal.js';
import {
getValueSourceIcon, getAudioSourceIcon, getPictureSourceIcon,
ICON_CLONE, ICON_EDIT, ICON_TEST,
ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL,
ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL, ICON_CLOCK,
ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
} from '../core/icons.js';
import { wrapCard } from '../core/card-colors.js';
@@ -64,6 +64,9 @@ class ValueSourceModal extends Modal {
sceneSensitivity: document.getElementById('value-source-scene-sensitivity').value,
sceneSmoothing: document.getElementById('value-source-scene-smoothing').value,
schedule: JSON.stringify(_getScheduleFromUI()),
daylightSpeed: document.getElementById('value-source-daylight-speed').value,
daylightRealTime: document.getElementById('value-source-daylight-real-time').checked,
daylightLatitude: document.getElementById('value-source-daylight-latitude').value,
tags: JSON.stringify(_vsTagsInput ? _vsTagsInput.getValue() : []),
};
}
@@ -97,7 +100,7 @@ function _autoGenerateVSName() {
/* ── Icon-grid type selector ──────────────────────────────────── */
const VS_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene'];
const VS_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene', 'daylight'];
function _buildVSTypeItems() {
return VS_TYPE_KEYS.map(key => ({
@@ -213,6 +216,13 @@ export async function showValueSourceModal(editData) {
_setSlider('value-source-scene-smoothing', editData.smoothing ?? 0.3);
_setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
_setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
} else if (editData.source_type === 'daylight') {
_setSlider('value-source-daylight-speed', editData.speed ?? 1.0);
document.getElementById('value-source-daylight-real-time').checked = !!editData.use_real_time;
_setSlider('value-source-daylight-latitude', editData.latitude ?? 50);
_syncDaylightVSSpeedVisibility();
_setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
_setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
}
} else {
document.getElementById('value-source-name').value = '';
@@ -240,6 +250,11 @@ export async function showValueSourceModal(editData) {
_setSlider('value-source-scene-smoothing', 0.3);
_setSlider('value-source-adaptive-min-value', 0);
_setSlider('value-source-adaptive-max-value', 1);
// Daylight defaults
_setSlider('value-source-daylight-speed', 1.0);
document.getElementById('value-source-daylight-real-time').checked = false;
_setSlider('value-source-daylight-latitude', 50);
_syncDaylightVSSpeedVisibility();
_autoGenerateVSName();
}
@@ -271,8 +286,9 @@ export function onValueSourceTypeChange() {
if (type === 'audio') _ensureAudioModeIconSelect();
document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none';
document.getElementById('value-source-adaptive-scene-section').style.display = type === 'adaptive_scene' ? '' : 'none';
document.getElementById('value-source-daylight-section').style.display = type === 'daylight' ? '' : 'none';
document.getElementById('value-source-adaptive-range-section').style.display =
(type === 'adaptive_time' || type === 'adaptive_scene') ? '' : 'none';
(type === 'adaptive_time' || type === 'adaptive_scene' || type === 'daylight') ? '' : 'none';
// Populate audio dropdown when switching to audio type
if (type === 'audio') {
@@ -290,6 +306,17 @@ export function onValueSourceTypeChange() {
_autoGenerateVSName();
}
// ── Daylight helpers ──────────────────────────────────────────
export function onDaylightVSRealTimeChange() {
_syncDaylightVSSpeedVisibility();
}
function _syncDaylightVSSpeedVisibility() {
const rt = document.getElementById('value-source-daylight-real-time').checked;
document.getElementById('value-source-daylight-speed-group').style.display = rt ? 'none' : '';
}
// ── Save ──────────────────────────────────────────────────────
export async function saveValueSource() {
@@ -338,6 +365,12 @@ export async function saveValueSource() {
payload.smoothing = parseFloat(document.getElementById('value-source-scene-smoothing').value);
payload.min_value = parseFloat(document.getElementById('value-source-adaptive-min-value').value);
payload.max_value = parseFloat(document.getElementById('value-source-adaptive-max-value').value);
} else if (sourceType === 'daylight') {
payload.speed = parseFloat(document.getElementById('value-source-daylight-speed').value);
payload.use_real_time = document.getElementById('value-source-daylight-real-time').checked;
payload.latitude = parseFloat(document.getElementById('value-source-daylight-latitude').value);
payload.min_value = parseFloat(document.getElementById('value-source-adaptive-min-value').value);
payload.max_value = parseFloat(document.getElementById('value-source-adaptive-max-value').value);
}
try {
@@ -632,6 +665,13 @@ export function createValueSourceCard(src) {
<span class="stream-card-prop">${ICON_MAP_PIN} ${pts} ${t('value_source.schedule.points')}</span>
<span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}${src.max_value ?? 1}</span>
`;
} else if (src.source_type === 'daylight') {
if (src.use_real_time) {
propsHtml = `<span class="stream-card-prop">${ICON_CLOCK} ${t('value_source.daylight.real_time')}</span>`;
} else {
propsHtml = `<span class="stream-card-prop">${ICON_TIMER} ${t('value_source.daylight.speed_label')} ${src.speed ?? 1.0}x</span>`;
}
propsHtml += `<span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}\u2013${src.max_value ?? 1}</span>`;
} else if (src.source_type === 'adaptive_scene') {
const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
const psName = ps ? ps.name : (src.picture_source_id || '-');