Browse Source

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)
pull/2561/head
Maxim Prokhorov 1 year ago
parent
commit
9126a9832b
4 changed files with 108 additions and 117 deletions
  1. +42
    -32
      code/espurna/light.cpp
  2. +23
    -12
      code/html/custom.css
  3. +25
    -59
      code/html/custom.js
  4. +18
    -14
      code/html/index.html

+ 42
- 32
code/espurna/light.cpp View File

@ -504,15 +504,6 @@ struct Pointers {
return _data[4];
}
template <typename ...Args>
void maybeApply(Args&&... args) const {
for (auto ptr : _data) {
if (ptr) {
(*ptr).apply(std::forward<Args>(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);
}
}


+ 23
- 12
code/html/custom.css View File

@ -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;
}

+ 25
- 59
code/html/custom.js View File

@ -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) {
<option value="${value.warm}">Warm</option>
`;
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) {


+ 18
- 14
code/html/index.html View File

@ -232,12 +232,15 @@
<!-- removeIf(!light) -->
<div id="light">
<div class="pure-control-group light-control">
<div id="light-state" class="pure-control-group">
<label>Lights</label>
<div><input type="checkbox" name="light-state"></div>
<div><input type="checkbox" name="light-state-value"></div>
</div>
<div class="pure-control-group">
<div id="light-picker"></div>
<div id="light-cct"></div>
<div id="light-brightness"></div>
<div id="light-channels"></div>
</div>
</div>
@ -660,7 +663,8 @@
<label>Use color</label>
<div><input type="checkbox" name="useColor" data-action="reload"></div>
<span class="pure-form-message">
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.
<br>This option hides <strong>R</strong> (red), <strong>G</strong> (green) and <strong>B</strong> (blue) channel sliders.
</span>
</div>
@ -668,15 +672,15 @@
<label>Use RGB picker</label>
<div><input type="checkbox" name="useRGB" data-action="reload"></div>
<span class="pure-form-message">
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).
</span>
</div>
<div class="pure-control-group">
<label>Use white channel</label>
<label>Use white channel(s)</label>
<div><input type="checkbox" name="useWhite" data-action="reload"></div>
<span class="pure-form-message">
When device is configured as RGB + WW (warm white), use RGB to balance the &quot;warm white&quot; channel, while the &quot;Cold channel&quot; is still configurable.
When device is configured as RGB + WW (warm white) or RGB + WW CC (cold white), use RGB to balance the &quot;warm white&quot; channel output, leaving &quot;Cold channel&quot; for manual adjustments.
<br>This option hides <strong>RGB</strong> and <strong>WW</strong> sliders!
</span>
</div>
@ -685,8 +689,8 @@
<label>Use color temperature</label>
<div><input type="checkbox" name="useCCT" data-action="reload"></div>
<span class="pure-form-message">
Balance between &quot;cold&quot; and &quot;warm&quot; 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 &quot;Mireds&quot; value.
<br>This option hides <strong>all channel sliders</strong>
Balance between &quot;cold&quot; and &quot;warm&quot; color temperature when channel configuration has WW (warm white) and CW (cold white) channels available. When RGB is available but <code>Use white channel(s)</code> is disabled, replace RGB values with an approximation based on the current &quot;Mireds&quot; value. Otherwise, RGB channels will be disabled.
<br>This option hides either <strong>WWn class="p"></strong> and <strong>CW</strong> channel sliders, or <strong>RGB</strong> color picker.
</span>
</div>
@ -2417,18 +2421,18 @@
</template>
<template id="template-brightness-control">
<div class="pure-control-group">
<div>
<label>Brightness</label>
<input type="range" min="0" max="255" class="slider pure-input-2-3" id="brightness">
<span class="slider brightness"></span>
<input type="range" min="0" max="255" class="slider pure-input-2-3" id="brightness-value">
<span class="slider"></span>
</div>
</template>
<template id="template-mireds-control">
<div class="pure-control-group">
<div>
<label>Mireds</label>
<input type="range" class="slider pure-input-2-3" id="mireds" list="mired-range">
<span class="slider mireds"></span>
<input type="range" class="slider pure-input-2-3" id="mireds-value" list="mired-range">
<span class="slider"></span>
<datalist id="mired-range">
</datalist>
</div>


Loading…
Cancel
Save