mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01:00
Modular light transformers (#2124)
This commit is contained in:
parent
11477dbc03
commit
46f17bea66
8 changed files with 195 additions and 150 deletions
|
@ -24,6 +24,10 @@ void AddressableLight::call_setup() {
|
|||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<LightTransformer> AddressableLight::create_default_transition() {
|
||||
return make_unique<AddressableLightTransformer>(*this);
|
||||
}
|
||||
|
||||
Color esp_color_from_light_color_values(LightColorValues val) {
|
||||
auto r = to_uint8_scale(val.get_color_brightness() * val.get_red());
|
||||
auto g = to_uint8_scale(val.get_color_brightness() * val.get_green());
|
||||
|
@ -37,66 +41,67 @@ void AddressableLight::write_state(LightState *state) {
|
|||
auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
|
||||
this->correction_.set_local_brightness(max_brightness);
|
||||
|
||||
this->last_transition_progress_ = 0.0f;
|
||||
this->accumulated_alpha_ = 0.0f;
|
||||
|
||||
if (this->is_effect_active())
|
||||
return;
|
||||
|
||||
// don't use LightState helper, gamma correction+brightness is handled by ESPColorView
|
||||
this->all() = esp_color_from_light_color_values(val);
|
||||
}
|
||||
|
||||
if (state->transformer_ == nullptr || !state->transformer_->is_transition()) {
|
||||
// no transformer active or non-transition one
|
||||
this->all() = esp_color_from_light_color_values(val);
|
||||
} else {
|
||||
// transition transformer active, activate specialized transition for addressable effects
|
||||
// instead of using a unified transition for all LEDs, we use the current state each LED as the
|
||||
// start. Warning: ugly
|
||||
void AddressableLightTransformer::start() {
|
||||
auto end_values = this->target_values_;
|
||||
this->target_color_ = esp_color_from_light_color_values(end_values);
|
||||
|
||||
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
|
||||
// state of each LED at the start of the transition
|
||||
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
|
||||
// dynamically-calculated alpha values to match the look of the
|
||||
// our transition will handle brightness, disable brightness in correction.
|
||||
this->light_.correction_.set_local_brightness(255);
|
||||
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
}
|
||||
|
||||
float new_progress = state->transformer_->get_progress();
|
||||
float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_);
|
||||
float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress);
|
||||
this->last_transition_progress_ = new_progress;
|
||||
optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
// Don't try to transition over running effects, instead immediately use the target values. write_state() and the
|
||||
// effects pick up the change from current_values.
|
||||
if (this->light_.is_effect_active())
|
||||
return this->target_values_;
|
||||
|
||||
auto end_values = state->transformer_->get_end_values();
|
||||
Color target_color = esp_color_from_light_color_values(end_values);
|
||||
// Use a specialized transition for addressable lights: instead of using a unified transition for
|
||||
// all LEDs, we use the current state of each LED as the start.
|
||||
|
||||
// our transition will handle brightness, disable brightness in correction.
|
||||
this->correction_.set_local_brightness(255);
|
||||
target_color *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
|
||||
// state of each LED at the start of the transition.
|
||||
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
|
||||
// dynamically-calculated alpha values to match the look.
|
||||
|
||||
float denom = (1.0f - new_smoothed);
|
||||
float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom;
|
||||
float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_());
|
||||
|
||||
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
|
||||
// We solve this by accumulating the fractional part of the alpha over time.
|
||||
float alpha255 = alpha * 255.0f;
|
||||
float alpha255int = floorf(alpha255);
|
||||
float alpha255remainder = alpha255 - alpha255int;
|
||||
float denom = (1.0f - smoothed_progress);
|
||||
float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom;
|
||||
|
||||
this->accumulated_alpha_ += alpha255remainder;
|
||||
float alpha_add = floorf(this->accumulated_alpha_);
|
||||
this->accumulated_alpha_ -= alpha_add;
|
||||
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
|
||||
// We solve this by accumulating the fractional part of the alpha over time.
|
||||
float alpha255 = alpha * 255.0f;
|
||||
float alpha255int = floorf(alpha255);
|
||||
float alpha255remainder = alpha255 - alpha255int;
|
||||
|
||||
alpha255 += alpha_add;
|
||||
alpha255 = clamp(alpha255, 0.0f, 255.0f);
|
||||
auto alpha8 = static_cast<uint8_t>(alpha255);
|
||||
this->accumulated_alpha_ += alpha255remainder;
|
||||
float alpha_add = floorf(this->accumulated_alpha_);
|
||||
this->accumulated_alpha_ -= alpha_add;
|
||||
|
||||
if (alpha8 != 0) {
|
||||
uint8_t inv_alpha8 = 255 - alpha8;
|
||||
Color add = target_color * alpha8;
|
||||
alpha255 += alpha_add;
|
||||
alpha255 = clamp(alpha255, 0.0f, 255.0f);
|
||||
auto alpha8 = static_cast<uint8_t>(alpha255);
|
||||
|
||||
for (auto led : *this)
|
||||
led = add + led.get() * inv_alpha8;
|
||||
}
|
||||
if (alpha8 != 0) {
|
||||
uint8_t inv_alpha8 = 255 - alpha8;
|
||||
Color add = this->target_color_ * alpha8;
|
||||
|
||||
for (auto led : this->light_)
|
||||
led.set(add + led.get() * inv_alpha8);
|
||||
}
|
||||
|
||||
this->schedule_show();
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace light
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "esp_range_view.h"
|
||||
#include "light_output.h"
|
||||
#include "light_state.h"
|
||||
#include "transformers.h"
|
||||
|
||||
#ifdef USE_POWER_SUPPLY
|
||||
#include "esphome/components/power_supply/power_supply.h"
|
||||
|
@ -53,6 +54,7 @@ class AddressableLight : public LightOutput, public Component {
|
|||
bool is_effect_active() const { return this->effect_active_; }
|
||||
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
|
||||
void write_state(LightState *state) override;
|
||||
std::unique_ptr<LightTransformer> create_default_transition() override;
|
||||
void set_correction(float red, float green, float blue, float white = 1.0f) {
|
||||
this->correction_.set_max_brightness(
|
||||
Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white)));
|
||||
|
@ -70,6 +72,8 @@ class AddressableLight : public LightOutput, public Component {
|
|||
void call_setup() override;
|
||||
|
||||
protected:
|
||||
friend class AddressableLightTransformer;
|
||||
|
||||
bool should_show_() const { return this->effect_active_ || this->next_show_; }
|
||||
void mark_shown_() {
|
||||
this->next_show_ = false;
|
||||
|
@ -92,6 +96,18 @@ class AddressableLight : public LightOutput, public Component {
|
|||
power_supply::PowerSupplyRequester power_;
|
||||
#endif
|
||||
LightState *state_parent_{nullptr};
|
||||
};
|
||||
|
||||
class AddressableLightTransformer : public LightTransitionTransformer {
|
||||
public:
|
||||
AddressableLightTransformer(AddressableLight &light) : light_(light) {}
|
||||
|
||||
void start() override;
|
||||
optional<LightColorValues> apply() override;
|
||||
|
||||
protected:
|
||||
AddressableLight &light_;
|
||||
Color target_color_{};
|
||||
float last_transition_progress_{0.0f};
|
||||
float accumulated_alpha_{0.0f};
|
||||
};
|
||||
|
|
12
esphome/components/light/light_output.cpp
Normal file
12
esphome/components/light/light_output.cpp
Normal file
|
@ -0,0 +1,12 @@
|
|||
#include "light_output.h"
|
||||
#include "transformers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
||||
std::unique_ptr<LightTransformer> LightOutput::create_default_transition() {
|
||||
return make_unique<LightTransitionTransformer>();
|
||||
}
|
||||
|
||||
} // namespace light
|
||||
} // namespace esphome
|
|
@ -3,6 +3,7 @@
|
|||
#include "esphome/core/component.h"
|
||||
#include "light_traits.h"
|
||||
#include "light_state.h"
|
||||
#include "light_transformer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
@ -13,6 +14,9 @@ class LightOutput {
|
|||
/// Return the LightTraits of this LightOutput.
|
||||
virtual LightTraits get_traits() = 0;
|
||||
|
||||
/// Return the default transformer used for transitions.
|
||||
virtual std::unique_ptr<LightTransformer> create_default_transition();
|
||||
|
||||
virtual void setup_state(LightState *state) {}
|
||||
|
||||
virtual void write_state(LightState *state) = 0;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "light_state.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "light_state.h"
|
||||
#include "light_output.h"
|
||||
#include "transformers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
@ -105,19 +106,19 @@ void LightState::loop() {
|
|||
|
||||
// Apply transformer (if any)
|
||||
if (this->transformer_ != nullptr) {
|
||||
auto values = this->transformer_->apply();
|
||||
this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to
|
||||
if (values.has_value())
|
||||
this->current_values = *values;
|
||||
|
||||
if (this->transformer_->is_finished()) {
|
||||
this->remote_values = this->current_values = this->transformer_->get_end_values();
|
||||
this->target_state_reached_callback_.call();
|
||||
if (this->transformer_->publish_at_end())
|
||||
this->publish_state();
|
||||
this->transformer_->stop();
|
||||
this->transformer_ = nullptr;
|
||||
} else {
|
||||
this->current_values = this->transformer_->get_values();
|
||||
this->remote_values = this->transformer_->get_remote_values();
|
||||
this->target_state_reached_callback_.call();
|
||||
}
|
||||
this->next_write_ = true;
|
||||
}
|
||||
|
||||
// Write state to the light
|
||||
if (this->next_write_) {
|
||||
this->output_->write_state(this);
|
||||
this->next_write_ = false;
|
||||
|
@ -217,18 +218,21 @@ void LightState::stop_effect_() {
|
|||
}
|
||||
|
||||
void LightState::start_transition_(const LightColorValues &target, uint32_t length) {
|
||||
this->transformer_ = make_unique<LightTransitionTransformer>(millis(), length, this->current_values, target);
|
||||
this->remote_values = this->transformer_->get_remote_values();
|
||||
this->transformer_ = this->output_->create_default_transition();
|
||||
this->transformer_->setup(this->current_values, target, length);
|
||||
this->remote_values = target;
|
||||
}
|
||||
|
||||
void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
|
||||
LightColorValues end_colors = this->current_values;
|
||||
LightColorValues end_colors = this->remote_values;
|
||||
// If starting a flash if one is already happening, set end values to end values of current flash
|
||||
// Hacky but works
|
||||
if (this->transformer_ != nullptr)
|
||||
end_colors = this->transformer_->get_end_values();
|
||||
this->transformer_ = make_unique<LightFlashTransformer>(millis(), length, end_colors, target);
|
||||
this->remote_values = this->transformer_->get_remote_values();
|
||||
end_colors = this->transformer_->get_target_values();
|
||||
|
||||
this->transformer_ = make_unique<LightFlashTransformer>(*this);
|
||||
this->transformer_->setup(end_colors, target, length);
|
||||
this->remote_values = target;
|
||||
}
|
||||
|
||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
||||
|
@ -240,10 +244,6 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
|
|||
this->next_write_ = true;
|
||||
}
|
||||
|
||||
void LightState::set_transformer_(std::unique_ptr<LightTransformer> transformer) {
|
||||
this->transformer_ = std::move(transformer);
|
||||
}
|
||||
|
||||
void LightState::save_remote_values_() {
|
||||
LightStateRTCState saved;
|
||||
saved.color_mode = this->remote_values.get_color_mode();
|
||||
|
|
|
@ -157,9 +157,6 @@ class LightState : public Nameable, public Component {
|
|||
/// Internal method to set the color values to target immediately (with no transition).
|
||||
void set_immediately_(const LightColorValues &target, bool set_remote_values);
|
||||
|
||||
/// Internal method to start a transformer.
|
||||
void set_transformer_(std::unique_ptr<LightTransformer> transformer);
|
||||
|
||||
/// Internal method to save the current remote_values to the preferences
|
||||
void save_remote_values_();
|
||||
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "light_color_values.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
||||
/// Base-class for all light color transformers, such as transitions or flashes.
|
||||
/// Base class for all light color transformers, such as transitions or flashes.
|
||||
class LightTransformer {
|
||||
public:
|
||||
LightTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
|
||||
const LightColorValues &target_values)
|
||||
: start_time_(start_time), length_(length), start_values_(start_values), target_values_(target_values) {}
|
||||
void setup(const LightColorValues &start_values, const LightColorValues &target_values, uint32_t length) {
|
||||
this->start_time_ = millis();
|
||||
this->length_ = length;
|
||||
this->start_values_ = start_values;
|
||||
this->target_values_ = target_values;
|
||||
this->start();
|
||||
}
|
||||
|
||||
LightTransformer() = delete;
|
||||
/// Indicates whether this transformation is finished.
|
||||
virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
|
||||
|
||||
/// Whether this transformation is finished
|
||||
virtual bool is_finished() { return this->get_progress() >= 1.0f; }
|
||||
/// This will be called before the transition is started.
|
||||
virtual void start() {}
|
||||
|
||||
/// This will be called to get the current values for output.
|
||||
virtual LightColorValues get_values() = 0;
|
||||
/// This will be called while the transformer is active to apply the transition to the light. Can either write to the
|
||||
/// light directly, or return LightColorValues that will be applied.
|
||||
virtual optional<LightColorValues> apply() = 0;
|
||||
|
||||
/// The values that should be reported to the front-end.
|
||||
virtual LightColorValues get_remote_values() { return this->get_target_values_(); }
|
||||
/// This will be called after transition is finished.
|
||||
virtual void stop() {}
|
||||
|
||||
/// The values that should be set after this transformation is complete.
|
||||
virtual LightColorValues get_end_values() { return this->get_target_values_(); }
|
||||
const LightColorValues &get_start_values() const { return this->start_values_; }
|
||||
|
||||
virtual bool publish_at_end() = 0;
|
||||
virtual bool is_transition() = 0;
|
||||
|
||||
float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
|
||||
const LightColorValues &get_target_values() const { return this->target_values_; }
|
||||
|
||||
protected:
|
||||
const LightColorValues &get_start_values_() const { return this->start_values_; }
|
||||
|
||||
const LightColorValues &get_target_values_() const { return this->target_values_; }
|
||||
/// The progress of this transition, on a scale of 0 to 1.
|
||||
float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
|
||||
|
||||
uint32_t start_time_;
|
||||
uint32_t length_;
|
||||
|
@ -44,69 +44,5 @@ class LightTransformer {
|
|||
LightColorValues target_values_;
|
||||
};
|
||||
|
||||
class LightTransitionTransformer : public LightTransformer {
|
||||
public:
|
||||
LightTransitionTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
|
||||
const LightColorValues &target_values)
|
||||
: LightTransformer(start_time, length, start_values, target_values) {
|
||||
// When turning light on from off state, use colors from new.
|
||||
if (!this->start_values_.is_on() && this->target_values_.is_on()) {
|
||||
this->start_values_ = LightColorValues(target_values);
|
||||
this->start_values_.set_brightness(0.0f);
|
||||
}
|
||||
|
||||
// When changing color mode, go through off state, as color modes are orthogonal and there can't be two active.
|
||||
if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) {
|
||||
this->changing_color_mode_ = true;
|
||||
this->intermediate_values_ = LightColorValues(this->get_start_values_());
|
||||
this->intermediate_values_.set_state(false);
|
||||
}
|
||||
}
|
||||
|
||||
LightColorValues get_values() override {
|
||||
float p = this->get_progress();
|
||||
|
||||
// Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off.
|
||||
if (this->changing_color_mode_ && p > 0.5f &&
|
||||
this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) {
|
||||
this->intermediate_values_ = LightColorValues(this->get_end_values());
|
||||
this->intermediate_values_.set_state(false);
|
||||
}
|
||||
|
||||
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
|
||||
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_;
|
||||
if (this->changing_color_mode_)
|
||||
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
||||
|
||||
float v = LightTransitionTransformer::smoothed_progress(p);
|
||||
return LightColorValues::lerp(start, end, v);
|
||||
}
|
||||
|
||||
bool publish_at_end() override { return false; }
|
||||
bool is_transition() override { return true; }
|
||||
|
||||
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
||||
// transition from 0 to 1 on x = [0, 1]
|
||||
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
||||
|
||||
protected:
|
||||
bool changing_color_mode_{false};
|
||||
LightColorValues intermediate_values_{};
|
||||
};
|
||||
|
||||
class LightFlashTransformer : public LightTransformer {
|
||||
public:
|
||||
LightFlashTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
|
||||
const LightColorValues &target_values)
|
||||
: LightTransformer(start_time, length, start_values, target_values) {}
|
||||
|
||||
LightColorValues get_values() override { return this->get_target_values_(); }
|
||||
|
||||
LightColorValues get_end_values() override { return this->get_start_values_(); }
|
||||
|
||||
bool publish_at_end() override { return true; }
|
||||
bool is_transition() override { return false; }
|
||||
};
|
||||
|
||||
} // namespace light
|
||||
} // namespace esphome
|
||||
|
|
75
esphome/components/light/transformers.h
Normal file
75
esphome/components/light/transformers.h
Normal file
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "light_color_values.h"
|
||||
#include "light_state.h"
|
||||
#include "light_transformer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
||||
class LightTransitionTransformer : public LightTransformer {
|
||||
public:
|
||||
void start() override {
|
||||
// When turning light on from off state, use colors from target state.
|
||||
if (!this->start_values_.is_on() && this->target_values_.is_on()) {
|
||||
this->start_values_ = LightColorValues(this->target_values_);
|
||||
this->start_values_.set_brightness(0.0f);
|
||||
}
|
||||
|
||||
// When changing color mode, go through off state, as color modes are orthogonal and there can't be two active.
|
||||
if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) {
|
||||
this->changing_color_mode_ = true;
|
||||
this->intermediate_values_ = this->start_values_;
|
||||
this->intermediate_values_.set_state(false);
|
||||
}
|
||||
}
|
||||
|
||||
optional<LightColorValues> apply() override {
|
||||
float p = this->get_progress_();
|
||||
|
||||
// Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off.
|
||||
if (this->changing_color_mode_ && p > 0.5f &&
|
||||
this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) {
|
||||
this->intermediate_values_ = this->target_values_;
|
||||
this->intermediate_values_.set_state(false);
|
||||
}
|
||||
|
||||
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
|
||||
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_;
|
||||
if (this->changing_color_mode_)
|
||||
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
||||
|
||||
float v = LightTransitionTransformer::smoothed_progress(p);
|
||||
return LightColorValues::lerp(start, end, v);
|
||||
}
|
||||
|
||||
protected:
|
||||
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
||||
// transition from 0 to 1 on x = [0, 1]
|
||||
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
||||
|
||||
bool changing_color_mode_{false};
|
||||
LightColorValues intermediate_values_{};
|
||||
};
|
||||
|
||||
class LightFlashTransformer : public LightTransformer {
|
||||
public:
|
||||
LightFlashTransformer(LightState &state) : state_(state) {}
|
||||
|
||||
optional<LightColorValues> apply() override { return this->get_target_values(); }
|
||||
|
||||
// Restore the original values after the flash.
|
||||
void stop() override {
|
||||
this->state_.current_values = this->get_start_values();
|
||||
this->state_.remote_values = this->get_start_values();
|
||||
this->state_.publish_state();
|
||||
}
|
||||
|
||||
protected:
|
||||
LightState &state_;
|
||||
};
|
||||
|
||||
} // namespace light
|
||||
} // namespace esphome
|
Loading…
Reference in a new issue