From 4a2e2dcf63a92b12a0732a65caaa8566110c7434 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Tue, 10 Feb 2026 21:40:55 +0100 Subject: [PATCH] Fixes --- main/WidgetConfig.cpp | 13 +++++ main/WidgetConfig.hpp | 50 ++++++++++++++++++- main/WidgetManager.cpp | 38 +++++++++++++- main/WidgetManager.hpp | 4 ++ main/widgets/ArcWidget.cpp | 15 +++--- main/widgets/Widget.cpp | 26 +++++++--- main/widgets/Widget.hpp | 6 +++ web-interface/src/components/CanvasArea.vue | 12 ++++- .../src/components/ScreenSettingsModal.vue | 4 ++ web-interface/src/components/SidebarRight.vue | 1 + .../src/components/WidgetElement.vue | 23 ++++++++- web-interface/src/constants.js | 39 ++++++++++----- web-interface/src/stores/editor.js | 6 ++- web-interface/src/utils.js | 39 +++++++++++++++ 14 files changed, 242 insertions(+), 34 deletions(-) diff --git a/main/WidgetConfig.cpp b/main/WidgetConfig.cpp index 24f0c3a..d3bec2e 100644 --- a/main/WidgetConfig.cpp +++ b/main/WidgetConfig.cpp @@ -164,6 +164,9 @@ void WidgetConfig::serialize(uint8_t* buf) const { buf[pos++] = arcValueColor.g; buf[pos++] = arcValueColor.b; buf[pos++] = arcValueFontSize; + + // Theme + buf[pos++] = themeFixed ? 1 : 0; } void WidgetConfig::deserialize(const uint8_t* buf) { @@ -376,6 +379,13 @@ void WidgetConfig::deserialize(const uint8_t* buf) { arcValueColor = textColor; arcValueFontSize = fontSize; } + + // Theme + if (pos + 1 <= SERIALIZED_SIZE) { + themeFixed = buf[pos++] != 0; + } else { + themeFixed = false; + } } WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { @@ -408,6 +418,7 @@ WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const c cfg.arcScaleColor = cfg.textColor; cfg.arcValueColor = cfg.textColor; cfg.arcValueFontSize = cfg.fontSize; + cfg.themeFixed = false; cfg.shadow.enabled = false; // Icon defaults cfg.iconCodepoint = 0; @@ -469,6 +480,7 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y, cfg.arcScaleColor = cfg.textColor; cfg.arcValueColor = cfg.textColor; cfg.arcValueFontSize = cfg.fontSize; + cfg.themeFixed = false; cfg.shadow.enabled = true; cfg.shadow.offsetX = 2; cfg.shadow.offsetY = 2; @@ -507,6 +519,7 @@ void ScreenConfig::clear(uint8_t newId, const char* newName) { id = newId; mode = ScreenMode::FULLSCREEN; backgroundColor = {26, 26, 46}; // Dark blue background + nightBackgroundColor = {14, 18, 23}; // Darker night background bgImagePath[0] = '\0'; // No background image bgImageMode = BgImageMode::STRETCH; widgetCount = 0; diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index d232e7e..6ee212f 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -107,6 +107,49 @@ struct Color { .b = static_cast(hex & 0xFF) }; } + + // Invert lightness via HSL: light colors become dark, dark become light + Color invertLightness() const { + float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f; + float maxC = rf > gf ? (rf > bf ? rf : bf) : (gf > bf ? gf : bf); + float minC = rf < gf ? (rf < bf ? rf : bf) : (gf < bf ? gf : bf); + float delta = maxC - minC; + float L = (maxC + minC) * 0.5f; + float S = 0.0f, H = 0.0f; + if (delta > 0.0001f) { + S = L < 0.5f ? delta / (maxC + minC) : delta / (2.0f - maxC - minC); + if (maxC == rf) H = (gf - bf) / delta + (gf < bf ? 6.0f : 0.0f); + else if (maxC == gf) H = (bf - rf) / delta + 2.0f; + else H = (rf - gf) / delta + 4.0f; + H /= 6.0f; + } + // Invert lightness + L = 1.0f - L; + // HSL to RGB + auto hue2rgb = [](float p, float q, float t) -> float { + if (t < 0.0f) t += 1.0f; + if (t > 1.0f) t -= 1.0f; + if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t; + if (t < 0.5f) return q; + if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; + return p; + }; + float ro, go, bo; + if (S < 0.0001f) { + ro = go = bo = L; + } else { + float q = L < 0.5f ? L * (1.0f + S) : L + S - L * S; + float p = 2.0f * L - q; + ro = hue2rgb(p, q, H + 1.0f / 3.0f); + go = hue2rgb(p, q, H); + bo = hue2rgb(p, q, H - 1.0f / 3.0f); + } + return { + .r = static_cast(ro * 255.0f + 0.5f), + .g = static_cast(go * 255.0f + 0.5f), + .b = static_cast(bo * 255.0f + 0.5f) + }; + } }; // Condition operator for conditional styling @@ -298,9 +341,11 @@ struct WidgetConfig { Color arcValueColor; // Center value text color uint8_t arcValueFontSize; // Center value font size index + // Theme + bool themeFixed; // true = colors stay fixed in night mode + // Serialization size (fixed for NVS storage) - // 369 + 36 (6 subbuttons * 6 bytes for icon colors) = 405 - static constexpr size_t SERIALIZED_SIZE = 405; + static constexpr size_t SERIALIZED_SIZE = 406; void serialize(uint8_t* buf) const; void deserialize(const uint8_t* buf); @@ -323,6 +368,7 @@ struct ScreenConfig { char name[MAX_SCREEN_NAME_LEN]; ScreenMode mode; Color backgroundColor; + Color nightBackgroundColor; // Background color for night mode char bgImagePath[MAX_BG_IMAGE_PATH_LEN]; // Background image path (e.g., "/images/bg.png") BgImageMode bgImageMode; // 0=none, 1=stretch, 2=center, 3=tile uint8_t widgetCount; diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index 36812f3..33d565e 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -1137,6 +1137,22 @@ void WidgetManager::refreshChartWidgets() { refreshChartWidgetsLocked(); } +void WidgetManager::applyNightMode(bool night) { + // Update screen background + const ScreenConfig* screen = activeScreen(); + if (screen && screen_) { + Color bg = night ? screen->nightBackgroundColor : screen->backgroundColor; + lv_obj_set_style_bg_color(screen_, lv_color_make(bg.r, bg.g, bg.b), 0); + } + + // Re-apply style to all widgets (they check nightMode_ internally) + for (auto& widget : widgets_) { + if (widget) { + widget->applyStyle(); + } + } +} + void WidgetManager::updateSystemTimeWidgets() { int64_t nowUs = esp_timer_get_time(); if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms @@ -1222,7 +1238,10 @@ void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) { } if (config_->knxNightModeAddress != 0 && groupAddr == config_->knxNightModeAddress) { - nightMode_ = value; + if (nightMode_ != value) { + nightMode_ = value; + applyNightMode(value); + } } } @@ -1545,6 +1564,11 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b); cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr); + char nightBgColorStr[8]; + snprintf(nightBgColorStr, sizeof(nightBgColorStr), "#%02X%02X%02X", + screen.nightBackgroundColor.r, screen.nightBackgroundColor.g, screen.nightBackgroundColor.b); + cJSON_AddStringToObject(screenJson, "nightBgColor", nightBgColorStr); + // Background image if (screen.bgImagePath[0] != '\0') { cJSON_AddStringToObject(screenJson, "bgImage", screen.bgImagePath); @@ -1638,6 +1662,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddStringToObject(widget, "arcValueColor", arcValueColorStr); cJSON_AddNumberToObject(widget, "arcValueFontSize", w.arcValueFontSize); + cJSON_AddBoolToObject(widget, "themeFixed", w.themeFixed); + cJSON_AddNumberToObject(widget, "parentId", w.parentId); // Secondary KNX address (left value) @@ -2003,7 +2029,10 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* arcValueFontSize = cJSON_GetObjectItem(widget, "arcValueFontSize"); if (cJSON_IsNumber(arcValueFontSize)) w.arcValueFontSize = arcValueFontSize->valueint; - + + cJSON* themeFixed = cJSON_GetObjectItem(widget, "themeFixed"); + if (cJSON_IsBool(themeFixed)) w.themeFixed = cJSON_IsTrue(themeFixed); + cJSON* parentId = cJSON_GetObjectItem(widget, "parentId"); if (cJSON_IsNumber(parentId)) { w.parentId = static_cast(parentId->valueint); @@ -2362,6 +2391,11 @@ bool WidgetManager::updateConfigFromJson(const char* json) { screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring)); } + cJSON* nightBgColor = cJSON_GetObjectItem(screenJson, "nightBgColor"); + if (cJSON_IsString(nightBgColor)) { + screen.nightBackgroundColor = Color::fromHex(parseHexColor(nightBgColor->valuestring)); + } + // Parse background image cJSON* bgImage = cJSON_GetObjectItem(screenJson, "bgImage"); if (cJSON_IsString(bgImage)) { diff --git a/main/WidgetManager.hpp b/main/WidgetManager.hpp index 99625f6..5b27979 100644 --- a/main/WidgetManager.hpp +++ b/main/WidgetManager.hpp @@ -66,6 +66,9 @@ public: // KNX write (for RoomCard sub-buttons etc.) void sendKnxSwitch(uint16_t groupAddr, bool value); + // Night mode + bool isNightMode() const { return nightMode_; } + // Direct config access GuiConfig& getConfig() { return *config_; } const GuiConfig& getConfig() const { return *config_; } @@ -148,6 +151,7 @@ private: static bool isNumericTextSource(TextSource source); void refreshChartWidgetsLocked(); void refreshChartWidgets(); + void applyNightMode(bool night); void createDefaultConfig(); void applyScreen(uint8_t screenId); diff --git a/main/widgets/ArcWidget.cpp b/main/widgets/ArcWidget.cpp index 7b457b6..f902a95 100644 --- a/main/widgets/ArcWidget.cpp +++ b/main/widgets/ArcWidget.cpp @@ -82,11 +82,12 @@ void ArcWidget::applyStyle() { if (arcWidth < 2) arcWidth = 2; if (arcWidth > 48) arcWidth = 48; - lv_color_t trackColor = lv_color_make(config_.bgColor.r, config_.bgColor.g, config_.bgColor.b); + Color trackCol = themeColor(config_.bgColor); + lv_color_t trackColor = lv_color_make(trackCol.r, trackCol.g, trackCol.b); uint8_t trackOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast(LV_OPA_40); - lv_color_t indicatorColor = lv_color_make( - config_.textColor.r, config_.textColor.g, config_.textColor.b); + Color indicatorCol = themeColor(config_.textColor); + lv_color_t indicatorColor = lv_color_make(indicatorCol.r, indicatorCol.g, indicatorCol.b); lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_MAIN); lv_obj_set_style_arc_color(obj_, trackColor, LV_PART_MAIN); @@ -135,8 +136,8 @@ void ArcWidget::applyStyle() { lv_obj_set_style_arc_width(scale_, 1, LV_PART_MAIN); lv_obj_set_style_arc_opa(scale_, LV_OPA_TRANSP, LV_PART_MAIN); - lv_color_t scaleColor = lv_color_make( - config_.arcScaleColor.r, config_.arcScaleColor.g, config_.arcScaleColor.b); + Color scaleCol = themeColor(config_.arcScaleColor); + lv_color_t scaleColor = lv_color_make(scaleCol.r, scaleCol.g, scaleCol.b); lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_INDICATOR); lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_ITEMS); lv_obj_set_style_line_width(scale_, 2, LV_PART_INDICATOR); @@ -155,8 +156,8 @@ void ArcWidget::applyStyle() { } if (valueLabel_) { - lv_color_t valueColor = lv_color_make( - config_.arcValueColor.r, config_.arcValueColor.g, config_.arcValueColor.b); + Color valueCol = themeColor(config_.arcValueColor); + lv_color_t valueColor = lv_color_make(valueCol.r, valueCol.g, valueCol.b); lv_obj_set_style_text_color(valueLabel_, valueColor, 0); lv_obj_set_style_text_font(valueLabel_, getFontBySize(config_.arcValueFontSize), 0); lv_obj_center(valueLabel_); diff --git a/main/widgets/Widget.cpp b/main/widgets/Widget.cpp index 061dcee..0346b2f 100644 --- a/main/widgets/Widget.cpp +++ b/main/widgets/Widget.cpp @@ -1,5 +1,6 @@ #include "Widget.hpp" #include "../Fonts.hpp" +#include "../WidgetManager.hpp" #include "lvgl.h" Widget::Widget(const WidgetConfig& config) @@ -161,20 +162,30 @@ void Widget::applyConditionStyle(const ConditionStyle& style) { } } +bool Widget::shouldTransformColors() const { + return WidgetManager::instance().isNightMode() && !config_.themeFixed; +} + +Color Widget::themeColor(const Color& c) const { + return shouldTransformColors() ? c.invertLightness() : c; +} + void Widget::applyCommonStyle() { if (obj_ == nullptr) return; + Color textCol = themeColor(config_.textColor); + Color bgCol = themeColor(config_.bgColor); + Color borderCol = themeColor(config_.borderColor); + // Text color - lv_obj_set_style_text_color(obj_, lv_color_make( - config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); + lv_obj_set_style_text_color(obj_, lv_color_make(textCol.r, textCol.g, textCol.b), 0); // Font lv_obj_set_style_text_font(obj_, getFontBySize(config_.fontSize), 0); // Background (for buttons and labels with bg) if (config_.bgOpacity > 0) { - lv_obj_set_style_bg_color(obj_, lv_color_make( - config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); + lv_obj_set_style_bg_color(obj_, lv_color_make(bgCol.r, bgCol.g, bgCol.b), 0); lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0); } @@ -186,8 +197,7 @@ void Widget::applyCommonStyle() { // Border if (config_.borderWidth > 0 && config_.borderOpacity > 0) { lv_obj_set_style_border_width(obj_, config_.borderWidth, 0); - lv_obj_set_style_border_color(obj_, lv_color_make( - config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0); + lv_obj_set_style_border_color(obj_, lv_color_make(borderCol.r, borderCol.g, borderCol.b), 0); lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0); } else { lv_obj_set_style_border_width(obj_, 0, 0); @@ -213,8 +223,8 @@ void Widget::applyShadowStyle() { if (spread < 0) spread = 0; if (blur == 0) return; - lv_color_t shadowColor = lv_color_make( - config_.shadow.color.r, config_.shadow.color.g, config_.shadow.color.b); + Color sc = themeColor(config_.shadow.color); + lv_color_t shadowColor = lv_color_make(sc.r, sc.g, sc.b); // Default state shadow lv_obj_set_style_shadow_color(obj_, shadowColor, 0); diff --git a/main/widgets/Widget.hpp b/main/widgets/Widget.hpp index abbb1ea..ae45624 100644 --- a/main/widgets/Widget.hpp +++ b/main/widgets/Widget.hpp @@ -4,6 +4,8 @@ #include "lvgl.h" #include +class WidgetManager; + class Widget { public: explicit Widget(const WidgetConfig& config); @@ -70,6 +72,10 @@ protected: void applyConditionStyle(const ConditionStyle& style); static const lv_font_t* getFontBySize(uint8_t sizeIndex); + // Night mode color transformation + bool shouldTransformColors() const; + Color themeColor(const Color& c) const; + const WidgetConfig& config_; lv_obj_t* obj_ = nullptr; diff --git a/web-interface/src/components/CanvasArea.vue b/web-interface/src/components/CanvasArea.vue index 45a741b..588bbfd 100644 --- a/web-interface/src/components/CanvasArea.vue +++ b/web-interface/src/components/CanvasArea.vue @@ -8,6 +8,14 @@ {{ store.config.screens.length }}
+
+
+ + +
+
import { computed, markRaw } from 'vue'; +import { useEditorStore } from '../stores/editor'; +import { invertLightness } from '../utils'; import { WIDGET_TYPES } from '../constants'; // Import all widget element components @@ -56,7 +58,26 @@ const componentMap = { [WIDGET_TYPES.BUTTONMATRIX]: markRaw(ButtonMatrixElement) }; +const store = useEditorStore(); + const widgetComponent = computed(() => { return componentMap[props.widget.type] || LabelElement; }); + +const colorKeys = ['textColor', 'bgColor', 'borderColor', 'arcScaleColor', 'arcValueColor']; + +const themedWidget = computed(() => { + const w = props.widget; + if (!store.nightPreview || w.themeFixed) return w; + const copy = { ...w }; + for (const key of colorKeys) { + if (copy[key] && typeof copy[key] === 'string' && copy[key].startsWith('#')) { + copy[key] = invertLightness(copy[key]); + } + } + if (copy.shadow && copy.shadow.enabled && copy.shadow.color) { + copy.shadow = { ...copy.shadow, color: invertLightness(copy.shadow.color) }; + } + return copy; +}); diff --git a/web-interface/src/constants.js b/web-interface/src/constants.js index d77b089..4db5441 100644 --- a/web-interface/src/constants.js +++ b/web-interface/src/constants.js @@ -208,7 +208,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 1, - iconGap: 8 + iconGap: 8, + themeFixed: false }, button: { w: 130, @@ -232,6 +233,7 @@ export const WIDGET_DEFAULTS = { iconPosition: 0, iconSize: 1, iconGap: 8, + themeFixed: false, conditions: [] }, led: { @@ -254,7 +256,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 1, - iconGap: 8 + iconGap: 8, + themeFixed: false }, icon: { w: 48, @@ -276,7 +279,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0xe88a, iconPosition: 0, iconSize: 3, - iconGap: 8 + iconGap: 8, + themeFixed: false }, tabview: { w: 400, @@ -298,7 +302,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, // 0=Top, 1=Bottom, 2=Left, 3=Right iconSize: 5, // Used for Tab Height (x10) -> 50px - iconGap: 0 + iconGap: 0, + themeFixed: false }, tabpage: { w: 0, // Ignored @@ -320,7 +325,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 1, - iconGap: 0 + iconGap: 0, + themeFixed: false }, powerflow: { w: 720, @@ -342,7 +348,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 1, - iconGap: 0 + iconGap: 0, + themeFixed: false }, powernode: { w: 120, @@ -374,6 +381,7 @@ export const WIDGET_DEFAULTS = { text3: '', knxAddr3: 0, // Conditions + themeFixed: false, conditions: [] }, powerlink: { @@ -396,7 +404,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 0, - iconGap: 6 + iconGap: 6, + themeFixed: false }, chart: { w: 360, @@ -419,6 +428,7 @@ export const WIDGET_DEFAULTS = { iconPosition: 0, iconSize: 1, iconGap: 0, + themeFixed: false, chart: { period: 0, series: [ @@ -446,7 +456,8 @@ export const WIDGET_DEFAULTS = { iconCodepoint: 0, iconPosition: 0, iconSize: 1, - iconGap: 0 + iconGap: 0, + themeFixed: false }, roomcard: { w: 200, @@ -474,7 +485,8 @@ export const WIDGET_DEFAULTS = { subButtonOpacity: 255, // Sub-button opacity (0-255) cardStyle: 0, // 0=Bubble (round), 1=Tile (rectangular) subButtons: [], - textLines: [] // Variable text lines with icon, text, textSrc, knxAddr, fontSize + textLines: [], // Variable text lines with icon, text, textSrc, knxAddr, fontSize + themeFixed: false }, rectangle: { w: 220, @@ -501,7 +513,8 @@ export const WIDGET_DEFAULTS = { iconSize: 1, iconGap: 0, iconPositionX: 0, - iconPositionY: 0 + iconPositionY: 0, + themeFixed: false }, arc: { w: 180, @@ -536,7 +549,8 @@ export const WIDGET_DEFAULTS = { iconSize: 1, iconGap: 0, iconPositionX: 0, - iconPositionY: 0 + iconPositionY: 0, + themeFixed: false }, buttonmatrix: { w: 240, @@ -563,6 +577,7 @@ export const WIDGET_DEFAULTS = { iconSize: 1, iconGap: 0, iconPositionX: 0, - iconPositionY: 0 + iconPositionY: 0, + themeFixed: false } }; diff --git a/web-interface/src/stores/editor.js b/web-interface/src/stores/editor.js index 26e83de..8f66135 100644 --- a/web-interface/src/stores/editor.js +++ b/web-interface/src/stores/editor.js @@ -19,6 +19,7 @@ export const useEditorStore = defineStore('editor', () => { const showGrid = ref(true); const snapToGrid = ref(true); const gridSize = ref(20); + const nightPreview = ref(false); const powerLinkMode = reactive({ active: false, powerflowId: null, fromNodeId: null }); const nextScreenId = ref(0); @@ -273,6 +274,7 @@ export const useEditorStore = defineStore('editor', () => { name: `Screen ${id}`, mode: 0, bgColor: '#1A1A2E', + nightBgColor: '#0E1217', bgImage: '', bgImageMode: 1, widgets: [] @@ -406,7 +408,8 @@ export const useEditorStore = defineStore('editor', () => { iconSize: defaults.iconSize || 1, iconGap: defaults.iconGap || 8, iconPositionX: defaults.iconPositionX || 8, - iconPositionY: defaults.iconPositionY || 8 + iconPositionY: defaults.iconPositionY || 8, + themeFixed: defaults.themeFixed ?? false }; if (defaults.conditions !== undefined) { @@ -656,6 +659,7 @@ export const useEditorStore = defineStore('editor', () => { knxAddresses, selectedWidgetId, activeScreenId, + nightPreview, canvasScale, showGrid, snapToGrid, diff --git a/web-interface/src/utils.js b/web-interface/src/utils.js index f2665f7..13669b0 100644 --- a/web-interface/src/utils.js +++ b/web-interface/src/utils.js @@ -4,6 +4,44 @@ export function typeKeyFor(type) { return TYPE_KEYS[type] || 'label'; } +export function invertLightness(hex) { + const r = parseInt(hex.slice(1, 3), 16) / 255; + const g = parseInt(hex.slice(3, 5), 16) / 255; + const b = parseInt(hex.slice(5, 7), 16) / 255; + const maxC = Math.max(r, g, b); + const minC = Math.min(r, g, b); + const delta = maxC - minC; + let L = (maxC + minC) * 0.5; + let S = 0, H = 0; + if (delta > 0.0001) { + S = L < 0.5 ? delta / (maxC + minC) : delta / (2 - maxC - minC); + if (maxC === r) H = ((g - b) / delta + (g < b ? 6 : 0)) / 6; + else if (maxC === g) H = ((b - r) / delta + 2) / 6; + else H = ((r - g) / delta + 4) / 6; + } + L = 1 - L; + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + let ro, go, bo; + if (S < 0.0001) { + ro = go = bo = L; + } else { + const q = L < 0.5 ? L * (1 + S) : L + S - L * S; + const p = 2 * L - q; + ro = hue2rgb(p, q, H + 1/3); + go = hue2rgb(p, q, H); + bo = hue2rgb(p, q, H - 1/3); + } + const toHex = v => Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, '0'); + return `#${toHex(ro)}${toHex(go)}${toHex(bo)}`; +} + export function clamp(value, min, max) { if (Number.isNaN(value)) return min; return Math.max(min, Math.min(max, value)); @@ -171,6 +209,7 @@ export function normalizeScreen(screen, nextScreenIdRef, nextWidgetIdRef) { if (!screen.name) screen.name = `Screen ${screen.id}`; if (screen.mode === undefined || screen.mode === null) screen.mode = 0; if (!screen.bgColor) screen.bgColor = '#1A1A2E'; + if (!screen.nightBgColor) screen.nightBgColor = '#0E1217'; if (screen.bgImage === undefined) screen.bgImage = ''; if (screen.bgImageMode === undefined) screen.bgImageMode = 1; if (!Array.isArray(screen.widgets)) screen.widgets = [];