From 9126a9832b202b07ee00c0419b1a28333c2d96be Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Wed, 30 Nov 2022 23:57:38 +0300 Subject: [PATCH] light: cct for rgb, rgbww and ww cases - revert to ignoring rgb in color+white+cct case by resetting all its inputs to minimum and only controlling white channels - ignore ww and cw in rgb+cct case; use approximated color, thus allowing us to set a different 'cold temperature' limit - fix webui layout rules based on root element class(es) and consistent order for our control elements that does not depend on the order in which `processData` handles configuration kvs - internal refactoring to flatten input processing funcs selection (to possibly replace our boolean flags with a simple list) --- code/espurna/light.cpp | 74 +++++++++++++++++++++---------------- code/html/custom.css | 35 ++++++++++++------ code/html/custom.js | 84 +++++++++++++----------------------------- code/html/index.html | 32 +++++++++------- 4 files changed, 108 insertions(+), 117 deletions(-) diff --git a/code/espurna/light.cpp b/code/espurna/light.cpp index 4ac0d4e5..8aa3f98a 100644 --- a/code/espurna/light.cpp +++ b/code/espurna/light.cpp @@ -504,15 +504,6 @@ struct Pointers { return _data[4]; } - template - void maybeApply(Args&&... args) const { - for (auto ptr : _data) { - if (ptr) { - (*ptr).apply(std::forward(args)...); - } - } - } - private: void reset(LightChannels& channels); @@ -963,17 +954,24 @@ private: }; void _lightValuesWithCct(LightChannels& channels) { + const auto Brightness = _light_brightness; const auto CctFactor = _light_temperature.factor(); const auto White = LightScaledWhite(); auto ptr = espurna::light::Pointers(channels); (*ptr.warm()).apply( - LightResetInput::forWarm(CctFactor), White); + LightResetInput::forWarm(CctFactor), White, Brightness); (*ptr.cold()).apply( - LightResetInput::forCold(CctFactor), White); + LightResetInput::forCold(CctFactor), White, Brightness); - const auto Brightness = _light_brightness; - ptr.maybeApply(Brightness); + if (ptr.red() && ptr.green() && ptr.blue()) { + (*ptr.red()).apply( + LightResetInput{espurna::light::ValueMin}); + (*ptr.green()).apply( + LightResetInput{espurna::light::ValueMin}); + (*ptr.blue()).apply( + LightResetInput{espurna::light::ValueMin}); + } } // To handle both 4 and 5 channels, allow to 'adjust' internal factor calculation after construction @@ -1069,7 +1067,7 @@ private: float _luminance; }; -// When `useWhite` is enabled, white channels are 'detached' from the processing and their value depends on the RGB ones. +// When `useWhite` is enabled, warm white channel is 'detached' from the processing and its value depends on the input RGB. // Common calculation is to subtract 'white value' from the RGB based on the minimum channel value, e.g. [250, 150, 50] becomes [200, 100, 0, 50] // // General case when `useCCT` is disabled, but there are 4 channels. @@ -1104,7 +1102,9 @@ void _lightValuesWithRgbWhite(LightChannels& channels) { // Instead of the above, use `mireds` value as a range for warm and cold channels, based on the calculated rgb common values // Every value is also scaled by `brightness` after applying all of the previous steps // Notice that we completely ignore inputs and reset them to either kelvin'ized or hardcoded ValueMin or ValueMax -// (also, RED **always** stays at ValueMax b/c we never go above 6.6k kelvin) + +// Heavily depends on the used temperature range; by default (153...500), we stay on the 'warm side' +// of the scale and effectively never enable blue. Setting cold mireds to 100 will use the whole range. espurna::light::Rgb _lightKelvinRgb(espurna::light::Kelvin kelvin) { kelvin.value /= 100; @@ -1125,7 +1125,9 @@ espurna::light::Rgb _lightKelvinRgb(espurna::light::Kelvin kelvin) { void _lightValuesWithRgbCct(LightChannels& channels) { const auto Temperature = _light_temperature; - const auto RgbFromKelvin = _lightKelvinRgb(Temperature.kelvin()); + + const auto Kelvin = Temperature.kelvin(); + const auto RgbFromKelvin = _lightKelvinRgb(Kelvin); auto rgb = LightRgbWithoutWhite{RgbFromKelvin}; rgb.adjustOutput(rgb.inputMin()); @@ -1144,12 +1146,15 @@ void _lightValuesWithRgbCct(LightChannels& channels) { rgb, Brightness); const auto White = LightScaledWhite(rgb.factor()); - (*ptr.warm()).apply( - LightResetInput::forWarm(Temperature.factor()), - White, Brightness); - (*ptr.cold()).apply( - LightResetInput::forCold(Temperature.factor()), - White, Brightness); + if (ptr.warm() || ptr.cold()) { + if (ptr.warm()) { + (*ptr.warm()).apply(White, Brightness); + } + + if (ptr.cold()) { + (*ptr.cold()).apply(White, Brightness); + } + } } // UI hints about channel distribution @@ -3494,16 +3499,21 @@ void _lightConfigure() { } const auto last_process_input_values = _light_process_input_values; - _light_process_input_values = - (_light_use_color) ? ( - (_light_use_cct) ? _lightValuesWithRgbCct : - (_light_use_white) ? _lightValuesWithRgbWhite : - _lightValuesWithBrightnessExceptWhite) : - (_light_use_cct) ? - _lightValuesWithCct : - _lightValuesWithBrightness; - - if (!_light_update && (last_process_input_values != _light_process_input_values)) { + auto process_input_values = + (_light_use_color && _light_use_white && _light_use_cct) + ? _lightValuesWithCct : + (_light_use_color && _light_use_white) + ? _lightValuesWithRgbWhite : + (_light_use_color && _light_use_cct) + ? _lightValuesWithRgbCct : + (_light_use_color) + ? _lightValuesWithBrightnessExceptWhite : + (_light_use_cct) + ? _lightValuesWithCct + : _lightValuesWithBrightness; + + _light_process_input_values = process_input_values; + if (!_light_update && (last_process_input_values != process_input_values)) { lightUpdate(false); } } diff --git a/code/html/custom.css b/code/html/custom.css index 555806fd..b921bf1e 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -596,22 +596,33 @@ input::placeholder { Lights -------------------------------------------------------------------------- */ -#light-picker { - padding-bottom: 1em; +#light-brightness, +#light-cct, +#light-channels, +#light-picker, +#light-state { padding-top: 1em; - contain-intrinsic-size: 0px; - content-visibility: hidden; - opacity: 0; +} + +#light.light-cct #light-channel-c, +#light.light-cct #light-channel-w, +#light.light-color #light-channel-b, +#light.light-color #light-channel-g, +#light.light-color #light-channel-r, +#light.light-color.light-white #light-channel-w { display: none; } -#light-picker.light-color.light-on:not(.light-cct) { - display: block; - opacity: 1; - content-visibility: visible; - contain: style layout paint; +#light-cct, +#light-picker, +#light-state { + display: none; } -#light-cct { - content-visibility: hidden; +#light.light-cct #light-cct, +#light.light-cct:not(.light-white) #light-channel-c, +#light.light-cct:not(.light-white) #light-channel-w, +#light.light-color.light-on:not(.light-cct) #light-picker, +#light.light-state #light-state { + display: block; } diff --git a/code/html/custom.js b/code/html/custom.js index ec9d2f6a..b1a38e88 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -2170,14 +2170,14 @@ function colorUpdate(mode, value) { } } -function showLightState(value) { - styleInject([ - styleVisible(".light-control", !value) - ]); +function lightStateEnabled(value) { + if (!value) { + lightAddClass("light-state"); + } } function initLightState() { - const toggle = document.getElementById("light-state"); + const toggle = document.getElementById("light-state-value"); toggle.addEventListener("change", (event) => { event.preventDefault(); sendAction("light", {state: event.target.checked}); @@ -2185,36 +2185,24 @@ function initLightState() { } function updateLightState(value) { - const state = document.getElementById("light-state"); + const state = document.getElementById("light-state-value"); state.checked = value; colorPickerState(value); } -function colorPickerCct() { - const picker = document.getElementById("light-picker"); - picker.classList.add("light-cct"); -} - function colorPickerState(value) { - const picker = document.getElementById("light-picker"); + const light = document.getElementById("light"); if (value) { - picker.classList.add("light-on"); + light.classList.add("light-on"); } else { - picker.classList.remove("light-on"); + light.classList.remove("light-on"); } } function colorEnabled(value) { if (value) { - const picker = document.getElementById("light-picker"); - picker.classList.add("light-color"); + lightAddClass("light-color"); } - - channelVisible({ - "r": !value, - "g": !value, - "b": !value - }); } function colorInit(value) { @@ -2255,51 +2243,36 @@ function colorInit(value) { } function updateMireds(value) { - const mireds = document.getElementById("mireds"); + const mireds = document.getElementById("mireds-value"); if (mireds !== null) { mireds.value = value; mireds.nextElementSibling.textContent = value; } } -// Only allow to see specific channel(s) -function channelVisible(tags) { - const styles = []; - for (const [tag, visible] of Object.entries(tags)) { - styles.push(styleVisible(`.light-channel-${tag}`, visible)); - } - - styleInject(styles); +function lightAddClass(className) { + const light = document.getElementById("light"); + light.classList.add(className); } -// Only allow to see one of the channels +// White implies we should hide one or both white channels function whiteEnabled(value) { if (value) { - channelVisible({ - "w": false, - "c": true - }); + lightAddClass("light-white"); } } // When there are CCT controls, no need for raw white channel sliders function cctEnabled(value) { if (value) { - colorPickerCct(); - styleInject([ - styleVisible("#light-channels", false), - styleVisible("#light-cct", true), - ]); + lightAddClass("light-cct"); } } function cctInit(value) { const control = loadTemplate("mireds-control"); - const root = control.querySelector("div"); - root.setAttribute("id", "light-cct"); - - const slider = control.getElementById("mireds"); + const slider = control.getElementById("mireds-value"); slider.setAttribute("min", value.cold); slider.setAttribute("max", value.warm); slider.addEventListener("change", (event) => { @@ -2313,7 +2286,7 @@ function cctInit(value) { `; - mergeTemplate(document.getElementById("light"), control); + mergeTemplate(document.getElementById("light-cct"), control); } function updateLight(data) { @@ -2369,16 +2342,15 @@ function onBrightnessSliderChange(event) { function initBrightness() { const template = loadTemplate("brightness-control"); - template.querySelector("div").classList.add("light-brightness"); - const slider = template.getElementById("brightness"); + const slider = template.getElementById("brightness-value"); slider.addEventListener("change", onBrightnessSliderChange); - mergeTemplate(document.getElementById("light"), template); + mergeTemplate(document.getElementById("light-brightness"), template); } function updateBrightness(value) { - const brightness = document.getElementById("brightness"); + const brightness = document.getElementById("brightness-value"); if (brightness !== null) { brightness.value = value; brightness.nextElementSibling.textContent = value; @@ -2386,16 +2358,13 @@ function updateBrightness(value) { } function initChannels(channels) { - const container = document.createElement("div"); - container.setAttribute("id", "light-channels"); - container.classList.add("pure-control-group"); - + const container = document.getElementById("light-channels"); const enumerables = []; channels.forEach((tag, channel) => { const line = loadTemplate("channel-control"); line.querySelector("span.slider").dataset["id"] = channel; - line.querySelector("div").classList.add(`light-channel-${tag.toLowerCase()}`); + line.querySelector("div").setAttribute("id", `light-channel-${tag.toLowerCase()}`); const slider = line.querySelector("input.slider"); slider.dataset["id"] = channel; @@ -2408,9 +2377,6 @@ function initChannels(channels) { enumerables.push({"id": channel, "name": label}); }); - const light = document.getElementById("light"); - light.appendChild(container); - addEnumerables("Channels", enumerables); } @@ -2678,7 +2644,7 @@ function processData(data) { } if ("ltRelay" === key) { - showLightState(value); + lightStateEnabled(value); } if ("useWhite" === key) { diff --git a/code/html/index.html b/code/html/index.html index 94e93447..5d2b6a7e 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -232,12 +232,15 @@
-
+
-
+
+
+
+
@@ -660,7 +663,8 @@
- Use the first three channels as RGB channels. This will also enable the color picker in the web UI. Will only work if the device has at least 3 dimmable channels. + Enable color picker. +
This option hides R (red), G (green) and B (blue) channel sliders.
@@ -668,15 +672,15 @@
- Use RGB color picker if enabled (plus brightness), otherwise use HSV (hue-saturation-value) style + Use RGB color picker. When disabled (default), use HSV (hue-saturation-value).
- +
- When device is configured as RGB + WW (warm white), use RGB to balance the "warm white" channel, while the "Cold channel" is still configurable. + When device is configured as RGB + WW (warm white) or RGB + WW CC (cold white), use RGB to balance the "warm white" channel output, leaving "Cold channel" for manual adjustments.
This option hides RGB and WW sliders!
@@ -685,8 +689,8 @@
- Balance between "cold" and "warm" when channel configuration has WW (warm white) and CW (cold white) channels available. When RGB is also available, replace its values with an approximation based on the current "Mireds" value. -
This option hides all channel sliders + Balance between "cold" and "warm" color temperature when channel configuration has WW (warm white) and CW (cold white) channels available. When RGB is available but Use white channel(s) is disabled, replace RGB values with an approximation based on the current "Mireds" value. Otherwise, RGB channels will be disabled. +
This option hides either WW and CW channel sliders, or RGB color picker.
@@ -2417,18 +2421,18 @@