mirror of
https://github.com/esphome/esphome.git
synced 2024-11-22 23:18:10 +01:00
Addressable light transition (#750)
* Improve addressable light transition behavior Fixes https://github.com/esphome/issues/issues/555 * Improve addressable flicker effect See also https://github.com/esphome/feature-requests/issues/348 * Update addressable_light_effect.h * Refactor * Format * Prevent divide by zero * Fixes
This commit is contained in:
parent
b78b28ea0e
commit
95a74a7f19
6 changed files with 117 additions and 42 deletions
|
@ -179,5 +179,101 @@ void AddressableLight::call_setup() {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ESPColor esp_color_from_light_color_values(LightColorValues val) {
|
||||||
|
auto r = static_cast<uint8_t>(roundf(val.get_red() * 255.0f));
|
||||||
|
auto g = static_cast<uint8_t>(roundf(val.get_green() * 255.0f));
|
||||||
|
auto b = static_cast<uint8_t>(roundf(val.get_blue() * 255.0f));
|
||||||
|
auto w = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
|
||||||
|
this->gamma_reverse_table_[i] = uncorrected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace light
|
} // namespace light
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -189,23 +189,7 @@ class ESPColorCorrection {
|
||||||
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
|
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
|
||||||
void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; }
|
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 set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
|
||||||
void calculate_gamma_table(float gamma) {
|
void calculate_gamma_table(float gamma);
|
||||||
for (uint16_t i = 0; i < 256; i++) {
|
|
||||||
// corrected = val ^ gamma
|
|
||||||
auto corrected = static_cast<uint8_t>(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<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
|
|
||||||
this->gamma_reverse_table_[i] = uncorrected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE {
|
inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE {
|
||||||
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
|
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
|
||||||
return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green),
|
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_; }
|
bool is_effect_active() const { return this->effect_active_; }
|
||||||
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
|
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
|
||||||
void write_state(LightState *state) override {
|
void write_state(LightState *state) override;
|
||||||
auto val = state->current_values;
|
|
||||||
auto max_brightness = static_cast<uint8_t>(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 set_correction(float red, float green, float blue, float white = 1.0f) {
|
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)),
|
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))));
|
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_;
|
power_supply::PowerSupplyRequester power_;
|
||||||
#endif
|
#endif
|
||||||
LightState *state_parent_{nullptr};
|
LightState *state_parent_{nullptr};
|
||||||
|
float last_transition_progress_{0.0f};
|
||||||
|
float accumulated_alpha_{0.0f};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace light
|
} // namespace light
|
||||||
|
|
|
@ -318,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect {
|
||||||
const uint8_t inv_intensity = 255 - intensity;
|
const uint8_t inv_intensity = 255 - intensity;
|
||||||
if (now - this->last_update_ < this->update_interval_)
|
if (now - this->last_update_ < this->update_interval_)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this->last_update_ = now;
|
this->last_update_ = now;
|
||||||
fast_random_set_seed(random_uint32());
|
fast_random_set_seed(random_uint32());
|
||||||
for (auto var : it) {
|
for (auto var : it) {
|
||||||
const uint8_t flicker = fast_random_8() % intensity;
|
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; }
|
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
|
||||||
|
|
|
@ -463,7 +463,7 @@ LightColorValues LightCall::validate_() {
|
||||||
this->transition_length_.reset();
|
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
|
// nothing specified and light supports transitions, set default transition length
|
||||||
this->transition_length_ = this->parent_->default_transition_length_;
|
this->transition_length_ = this->parent_->default_transition_length_;
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,6 +277,7 @@ class LightState : public Nameable, public Component {
|
||||||
protected:
|
protected:
|
||||||
friend LightOutput;
|
friend LightOutput;
|
||||||
friend LightCall;
|
friend LightCall;
|
||||||
|
friend class AddressableLight;
|
||||||
|
|
||||||
uint32_t hash_base() override;
|
uint32_t hash_base() override;
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class LightTransformer {
|
||||||
LightTransformer() = delete;
|
LightTransformer() = delete;
|
||||||
|
|
||||||
/// Whether this transformation is finished
|
/// 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.
|
/// This will be called to get the current values for output.
|
||||||
virtual LightColorValues get_values() = 0;
|
virtual LightColorValues get_values() = 0;
|
||||||
|
@ -29,11 +29,11 @@ class LightTransformer {
|
||||||
virtual LightColorValues get_end_values() { return this->get_target_values_(); }
|
virtual LightColorValues get_end_values() { return this->get_target_values_(); }
|
||||||
|
|
||||||
virtual bool publish_at_end() = 0;
|
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:
|
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_start_values_() const { return this->start_values_; }
|
||||||
|
|
||||||
const LightColorValues &get_target_values_() const { return this->target_values_; }
|
const LightColorValues &get_target_values_() const { return this->target_values_; }
|
||||||
|
@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer {
|
||||||
}
|
}
|
||||||
|
|
||||||
LightColorValues get_values() override {
|
LightColorValues get_values() override {
|
||||||
float x = this->get_progress_();
|
float v = LightTransitionTransformer::smoothed_progress(this->get_progress());
|
||||||
float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f);
|
|
||||||
return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v);
|
return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool publish_at_end() override { return false; }
|
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 {
|
class LightFlashTransformer : public LightTransformer {
|
||||||
|
@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer {
|
||||||
LightColorValues get_end_values() override { return this->get_start_values_(); }
|
LightColorValues get_end_values() override { return this->get_start_values_(); }
|
||||||
|
|
||||||
bool publish_at_end() override { return true; }
|
bool publish_at_end() override { return true; }
|
||||||
|
bool is_transition() override { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace light
|
} // namespace light
|
||||||
|
|
Loading…
Reference in a new issue