diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 6b1fa36bc1..b5dc70a083 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -179,5 +179,101 @@ void AddressableLight::call_setup() { #endif } +ESPColor esp_color_from_light_color_values(LightColorValues val) { + auto r = static_cast(roundf(val.get_red() * 255.0f)); + auto g = static_cast(roundf(val.get_green() * 255.0f)); + auto b = static_cast(roundf(val.get_blue() * 255.0f)); + auto w = static_cast(roundf(val.get_white() * val.get_state() * 255.0f)); + return ESPColor(r, g, b, w); +} + +void AddressableLight::write_state(LightState *state) { + auto val = state->current_values; + auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); + 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 + + 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 + + // 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 + + 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; + + auto end_values = state->transformer_->get_end_values(); + ESPColor target_color = esp_color_from_light_color_values(end_values); + + // our transition will handle brightness, disable brightness in correction. + this->correction_.set_local_brightness(255); + uint8_t orig_w = target_color.w; + target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); + // w is not scaled by brightness + target_color.w = orig_w; + + float denom = (1.0f - new_smoothed); + float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; + + // 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; + + this->accumulated_alpha_ += alpha255remainder; + float alpha_add = floorf(this->accumulated_alpha_); + this->accumulated_alpha_ -= alpha_add; + + alpha255 += alpha_add; + alpha255 = clamp(alpha255, 0.0f, 255.0f); + auto alpha8 = static_cast(alpha255); + + if (alpha8 != 0) { + uint8_t inv_alpha8 = 255 - alpha8; + ESPColor add = target_color * alpha8; + + for (auto led : *this) + led = add + led.get() * inv_alpha8; + } + } + + this->schedule_show(); +} + +void ESPColorCorrection::calculate_gamma_table(float gamma) { + for (uint16_t i = 0; i < 256; i++) { + // corrected = val ^ gamma + auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + this->gamma_table_[i] = corrected; + } + if (gamma == 0.0f) { + for (uint16_t i = 0; i < 256; i++) + this->gamma_reverse_table_[i] = i; + return; + } + for (uint16_t i = 0; i < 256; i++) { + // val = corrected ^ (1/gamma) + auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + this->gamma_reverse_table_[i] = uncorrected; + } +} + } // namespace light } // namespace esphome diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 4383b4b245..a95d70f274 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -189,23 +189,7 @@ class ESPColorCorrection { ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); - this->gamma_reverse_table_[i] = uncorrected; - } - } + void calculate_gamma_table(float gamma); inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -468,23 +452,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 { - auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); - this->correction_.set_local_brightness(max_brightness); - - if (this->is_effect_active()) - return; - - // don't use LightState helper, gamma correction+brightness is handled by ESPColorView - ESPColor color = ESPColor(uint8_t(roundf(val.get_red() * 255.0f)), uint8_t(roundf(val.get_green() * 255.0f)), - uint8_t(roundf(val.get_blue() * 255.0f)), - // white is not affected by brightness; so manually scale by state - uint8_t(roundf(val.get_white() * val.get_state() * 255.0f))); - - this->all() = color; - this->schedule_show(); - } + void write_state(LightState *state) override; void set_correction(float red, float green, float blue, float white = 1.0f) { this->correction_.set_max_brightness(ESPColor(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); @@ -524,6 +492,8 @@ class AddressableLight : public LightOutput, public Component { power_supply::PowerSupplyRequester power_; #endif LightState *state_parent_{nullptr}; + float last_transition_progress_{0.0f}; + float accumulated_alpha_{0.0f}; }; } // namespace light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 9cdc9628c6..78ae41baad 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -318,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect { const uint8_t inv_intensity = 255 - intensity; if (now - this->last_update_ < this->update_interval_) return; + this->last_update_ = now; fast_random_set_seed(random_uint32()); for (auto var : it) { const uint8_t flicker = fast_random_8() % intensity; - var = (var.get() * inv_intensity) + (current_color * flicker); + // scale down by random factor + var = var.get() * (255 - flicker); + + // slowly fade back to "real" value + var = (var.get() * inv_intensity) + (current_color * intensity); } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 2b16319ddb..9d698e644e 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -463,7 +463,7 @@ LightColorValues LightCall::validate_() { this->transition_length_.reset(); } - if (!this->has_transition_() && !this->has_flash_() && !this->has_effect_() && supports_transition) { + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index c460be09be..07a0e3147b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -277,6 +277,7 @@ class LightState : public Nameable, public Component { protected: friend LightOutput; friend LightCall; + friend class AddressableLight; uint32_t hash_base() override; diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 91a406f425..222be7802c 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -17,7 +17,7 @@ class LightTransformer { LightTransformer() = delete; /// Whether this transformation is finished - virtual bool is_finished() { return this->get_progress_() >= 1.0f; } + virtual bool is_finished() { return this->get_progress() >= 1.0f; } /// This will be called to get the current values for output. virtual LightColorValues get_values() = 0; @@ -29,11 +29,11 @@ class LightTransformer { virtual LightColorValues get_end_values() { return this->get_target_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); } protected: - /// Get the completion of this transformer, 0 to 1. - float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } - const LightColorValues &get_start_values_() const { return this->start_values_; } const LightColorValues &get_target_values_() const { return this->target_values_; } @@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer { } LightColorValues get_values() override { - float x = this->get_progress_(); - float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); + float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); } bool publish_at_end() override { return false; } + bool is_transition() override { return true; } + + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } }; class LightFlashTransformer : public LightTransformer { @@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer { 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