diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 4eb9124a7b..12eab6a685 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -24,6 +24,10 @@ void AddressableLight::call_setup() { #endif } +std::unique_ptr AddressableLight::create_default_transition() { + return make_unique(*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 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(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(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 diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 54a1e3c6b4..ab1efdf160 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -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 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 apply() override; + + protected: + AddressableLight &light_; + Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; }; diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp new file mode 100644 index 0000000000..e805a0b694 --- /dev/null +++ b/esphome/components/light/light_output.cpp @@ -0,0 +1,12 @@ +#include "light_output.h" +#include "transformers.h" + +namespace esphome { +namespace light { + +std::unique_ptr LightOutput::create_default_transition() { + return make_unique(); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index d05cbf9436..7568ea6831 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -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 create_default_transition(); + virtual void setup_state(LightState *state) {} virtual void write_state(LightState *state) = 0; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 3c4a10c88e..278229fbd1 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -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(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(millis(), length, end_colors, target); - this->remote_values = this->transformer_->get_remote_values(); + end_colors = this->transformer_->get_target_values(); + + this->transformer_ = make_unique(*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 transformer) { - this->transformer_ = std::move(transformer); -} - void LightState::save_remote_values_() { LightStateRTCState saved; saved.color_mode = this->remote_values.get_color_mode(); diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 23527e8a47..dfea9a15f4 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -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 transformer); - /// Internal method to save the current remote_values to the preferences void save_remote_values_(); diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 7db6265a89..c5181abd4f 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -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 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 diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h new file mode 100644 index 0000000000..fd0bfd20f3 --- /dev/null +++ b/esphome/components/light/transformers.h @@ -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 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 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