Skip to content

Commit

Permalink
light: cct for rgb, rgbww and ww cases
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
mcspr committed Nov 30, 2022
1 parent 4021c4d commit 9126a98
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 117 deletions.
74 changes: 42 additions & 32 deletions code/espurna/light.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -963,17 +954,24 @@ struct LightScaledWhite {
};

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
Expand Down Expand Up @@ -1069,7 +1067,7 @@ struct LightRgbWithoutWhite {
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.
Expand Down Expand Up @@ -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;
Expand All @@ -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());
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
Expand Down
35 changes: 23 additions & 12 deletions code/html/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
84 changes: 25 additions & 59 deletions code/html/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2170,51 +2170,39 @@ 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});
});
}

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) {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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) {
Expand Down Expand Up @@ -2369,33 +2342,29 @@ 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;
}
}

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;
Expand All @@ -2408,9 +2377,6 @@ function initChannels(channels) {
enumerables.push({"id": channel, "name": label});
});

const light = document.getElementById("light");
light.appendChild(container);

addEnumerables("Channels", enumerables);
}

Expand Down Expand Up @@ -2678,7 +2644,7 @@ function processData(data) {
}

if ("ltRelay" === key) {
showLightState(value);
lightStateEnabled(value);
}

if ("useWhite" === key) {
Expand Down
Loading

0 comments on commit 9126a98

Please sign in to comment.