#pragma once #include "esphome.h" namespace esphome { namespace wordclock { struct Minute { uint8_t minute; uint8_t hour_offset; std::vector *segments; }; struct Hour { uint8_t hour; std::vector *segments; }; struct SegmentCoords { uint16_t x1; uint16_t y1; uint16_t x2; uint16_t y2; }; class BrightnessTransformer { public: virtual ~BrightnessTransformer() = default; void setup(const uint8_t start_values, const uint8_t target_values, uint32_t length) { this->start_time_ = millis(); this->length_ = length; this->start_values_ = start_values; this->target_values_ = target_values; this->start(); } /// Indicates 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 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; /// This will be called after transition is finished. virtual void stop() {} void reset() { this->start_time_ = millis(); } const uint8_t &get_start_values() const { return this->start_values_; } const uint8_t &get_target_values() const { return this->target_values_; } protected: /// The progress of this transition, on a scale of 0 to 1. float get_progress_() { uint32_t now = esphome::millis(); if (now < this->start_time_) return 0.0f; if (now >= this->start_time_ + this->length_) return 1.0f; return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); } uint32_t start_time_; uint32_t length_; uint8_t start_values_; uint8_t target_values_; }; class BrightnessTransitionTransformer : public BrightnessTransformer { public: optional apply() override { float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_()); return esphome::lerp(v, this->start_values_, this->target_values_); } 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); } }; class Wordclock : public esphome::PollingComponent { public: // Wordclock(); Wordclock( esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state ); void setup(); void update(); // void setup_color(uint8_t brightness, Color on_color, Color off_color); void set_brightness(uint8_t brightness) { this->brightness = brightness; } void set_on_color(Color on_color) { this->on_color = on_color; } void set_off_color(Color off_color) { this->off_color = off_color; } void setup_transitions(uint32_t milliseconds); void add_segment(SegmentCoords segment) { this->segments->push_back(segment); } void add_static(uint16_t segment_id) { this->static_segments->push_back(segment_id); } void add_hour(uint8_t hour, std::vector *segments_ptr) { this->hours->push_back(Hour{ .hour = hour, .segments = segments_ptr, }); } void add_minute(uint8_t minute, uint8_t hour_offset, std::vector *segments_ptr) { this->minutes->push_back(Minute{ .minute = minute, .hour_offset = hour_offset, .segments = segments_ptr, }); } // void add_hour_segment(uint8_t index, uint16_t segment_id) { // this->hours->at(index).segments->push_back(segment_id); // } // void add_minute_segment(uint8_t index, uint16_t segment_id) { // this->minutes->at(index).segments->push_back(segment_id); // } // void add_hour(uint8_t index, std::vector *segments); // void add_minute(uint8_t index, std::vector *segments); // void add_hour_offset(uint8_t index, int8_t offset) { // this->minutes->at(index).hour_offset = offset; // } protected: // ESPHome Components esphome::time::RealTimeClock *time; esphome::light::AddressableLightState *light_state; esphome::addressable_light::AddressableLightDisplay *display; // Time related state bool valid_time{false}; int8_t current_hour{-1}; int8_t current_minute{-1}; // Segments Configuration std::vector *segments; std::vector *static_segments; std::vector *minutes; std::vector *hours; BrightnessTransitionTransformer *off_transformer; BrightnessTransitionTransformer *on_transformer; std::vector *added_segments; std::vector *removed_segments; std::vector *current_segments; std::vector *previous_segments; // Color // Color get_on_color() { // return this->on_color * this->brightness; // } // Color get_off_color() { // return this->off_color * this->brightness; // } Color on_color {Color(0xFFFFFF)}; Color off_color {Color(0x000000)}; uint8_t brightness{255}; // Utils int8_t find_hour(uint8_t target_value); int8_t find_minute(uint8_t target_value); void find_difference(std::vector *a_vec, std::vector *b_vec); // void draw_segment(uint16_t segment_id) { // this->draw_segment(segment_id, this->brightness); // } void draw_segment(uint16_t segment_id, uint8_t brightness) { this->draw_segment(segment_id, this->on_color * brightness); } void draw_segment(uint16_t segment_id, Color color) { SegmentCoords s = this->segments->at(segment_id); // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32); this->display->line(s.x1, s.y1, s.x2, s.y2, color); } void start_idle_animation() { this->display->fill(this->off_color); this->display->set_enabled(false); this->light_state->turn_on().set_effect("random_twinkle").perform(); } void end_idle_animation() { this->light_state->turn_off().perform(); this->display->set_enabled(true); this->display->fill(this->off_color); this->previous_segments->clear(); this->current_segments->clear(); } // std::vector> *minutes; // std::vector> *hours; // std::vector *hour_offsets; }; } // namespace wordclock } // namespace esphome