#pragma once #include "esphome.h" // #include "esphome/components/sensor/sensor.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 BrightnessTransitionTransformer { public: virtual ~BrightnessTransitionTransformer() = 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(); } virtual bool is_finished() { return this->get_progress_() >= 1.0f; } virtual void start() {} // virtual optional apply() = 0; virtual optional apply() { float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_()); return esphome::lerp(v, this->start_values_, this->target_values_); } 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); } static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } uint32_t start_time_; uint32_t length_; uint8_t start_values_; uint8_t target_values_; }; class Wordclock : public esphome::PollingComponent { public: Wordclock( esphome::time::RealTimeClock *time, esphome::addressable_light::AddressableLightDisplay *display, esphome::light::AddressableLightState *light_state ); void setup(); void update(); void set_brightness(uint8_t brightness) { this->brightness = brightness; } bool get_effect_active() { return this->effect_active; } void set_effect_active(bool x) { this->effect_active = x; } 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, }); } Color on_color {Color(0xFFFFFF)}; Color off_color {Color(0x000000)}; uint8_t brightness{255}; 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 *staying_segments; std::vector *current_segments; std::vector *previous_segments; uint32_t current_position{0}; bool effect_active{true}; Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color ¤t_color); Color get_next_color(uint32_t position, const Color ¤t_color); // 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 segment_effect_base(uint16_t segment_id, bool to_effect, Color base_color, uint8_t transition_progress) { SegmentCoords s = this->segments->at(segment_id); int x1 = s.x1; int y1 = s.y1; int x2 = s.x2; int y2 = s.y2; Color color_to_draw; Color effect_color; const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; while (true) { effect_color = this->get_next_color(x1 + y1, Color(0)); if (to_effect) { color_to_draw = base_color.gradient(effect_color, transition_progress); } else { color_to_draw = effect_color.gradient(base_color, transition_progress); } this->display->draw_pixel_at(x1, y1, color_to_draw); if (x1 == x2 && y1 == y2) break; int32_t e2 = 2 * err; if (e2 >= dy) { err += dy; x1 += sx; } if (e2 <= dx) { err += dx; y1 += sy; } } } void enable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) { segment_effect_base(segment_id, true, off_color, transition_progress); } void disable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) { segment_effect_base(segment_id, false, off_color, transition_progress); } void apply_segment_effect(uint16_t segment_id) { segment_effect_base(segment_id, true, Color(0), 255); } 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(); } }; } // namespace wordclock } // namespace esphome