#pragma once #include "effects/Common.h" #include "helpers/ColorRGBW.h" #include "helpers/ColorHSV.h" #include "helpers/ColorConversions.h" #include #include static constexpr int SEGMENT_SELECT_MAX_SEGMENTS = 6; // The ring as a picker: one section per choice, the selected one bright and wobbling. // // The host re-sends this on every rotary detent. Each message builds a fresh effect, so // `previous` is what makes the highlight glide over from where it was instead of // jumping; previous == selected means no glide. #pragma pack(push, 1) struct EffectSegmentSelectConfig { uint8_t count; // number of sections, 1..SEGMENT_SELECT_MAX_SEGMENTS uint8_t selected; // index of the highlighted section uint8_t previous; // index the highlight glides from uint8_t gapLeds; // dark LEDs between neighbouring sections ColorRGBW colors[SEGMENT_SELECT_MAX_SEGMENTS]; float offsetDegrees; // where section 0 starts int8_t direction; // +1 / -1: which way round the sections go float dimFactor; // brightness of the sections that are not selected, 0..1 float wobbleAmplitude; // in LEDs float wobblePeriodMs; float slideMs; // time for the highlight to glide from `previous` to `selected` float edgeBlend; // 0..0.5: how far a section's edges shift towards its neighbours' hue }; #pragma pack(pop) template class EffectSegmentSelect { public: static constexpr auto NUM_LEDS = numLeds(); static constexpr int DELAY_MS = 10; using ConfigType = EffectSegmentSelectConfig; EffectSegmentSelect(const EffectSegmentSelectConfig &cfg, TLedStrip &ledStrip) : config_(cfg), ledStrip_(ledStrip) { if (config_.count > SEGMENT_SELECT_MAX_SEGMENTS) config_.count = SEGMENT_SELECT_MAX_SEGMENTS; if (config_.count > 0) { config_.selected %= config_.count; config_.previous %= config_.count; } if (config_.direction == 0) config_.direction = 1; pitch_ = float(NUM_LEDS) / float(config_.count > 0 ? config_.count : 1); halfWidth_ = std::max(0.5f, (pitch_ - float(config_.gapLeds)) / 2.0f); offsetLeds_ = config_.offsetDegrees / 360.0f * float(NUM_LEDS); // The glide takes the short way round, so going from the last section to the first // moves one step forward rather than all the way back. int delta = int(config_.selected) - int(config_.previous); if (delta > config_.count / 2) delta -= config_.count; else if (delta < -(config_.count / 2)) delta += config_.count; glideFrom_ = float(int(config_.selected) - delta); // The dimmed background never changes, so it is computed once. for (int led = 0; led < NUM_LEDS; ++led) { const float u = ringPosition(led); const int section = std::min(int(u / pitch_), int(config_.count) - 1); ColorRGBW color{0, 0, 0, 0}; if (config_.count > 0) { const float center = (float(section) + 0.5f) * pitch_; const float coverage = clamp01(halfWidth_ + 0.5f - std::fabs(u - center)); color = sectionColor(section, (u - center) / pitch_) * (coverage * config_.dimFactor); } background_[led] = color; } } int operator()() { if (config_.count == 0) { clear(ledStrip_); return 10000; } const float glide = config_.slideMs > 0 ? clamp01(elapsedMs_ / config_.slideMs) : 1.0f; const float eased = glide * glide * (3.0f - 2.0f * glide); // smoothstep const float index = glideFrom_ + (float(config_.selected) - glideFrom_) * eased; float wobble = 0; if (config_.wobblePeriodMs > 0) wobble = config_.wobbleAmplitude * std::sin(2.0f * float(M_PI) * elapsedMs_ / config_.wobblePeriodMs); const float center = (index + 0.5f) * pitch_ + wobble; for (int led = 0; led < NUM_LEDS; ++led) { const float distance = wrappedDistance(ringPosition(led) - center); const float coverage = clamp01(halfWidth_ + 0.5f - std::fabs(distance)); if (coverage <= 0) { setLedRGBW(ledStrip_, led, background_[led]); continue; } // The highlight carries the selected section's own gradient along with it, and // changes colour from the previous section's as it glides. const float local = distance / pitch_; const ColorRGBW from = sectionColor(config_.previous, local); const ColorRGBW to = sectionColor(config_.selected, local); const ColorRGBW highlight = ColorRGBW::interpolate(from, to, eased); setLedRGBW(ledStrip_, led, ColorRGBW::interpolate(background_[led], highlight, coverage)); } elapsedMs_ += DELAY_MS; return DELAY_MS; } private: static float clamp01(float x) { return x < 0 ? 0 : (x > 1 ? 1 : x); } // An LED's position along the sections, 0..NUM_LEDS, section 0 starting at 0. float ringPosition(int led) const { float u = float(config_.direction) * (float(led) + 0.5f - offsetLeds_); u = std::fmod(u, float(NUM_LEDS)); return u < 0 ? u + float(NUM_LEDS) : u; } // Shortest signed distance on the ring. static float wrappedDistance(float d) { const float n = float(NUM_LEDS); d = std::fmod(d, n); if (d > n / 2) d -= n; else if (d < -n / 2) d += n; return d; } // Colour of a section at `local`, -0.5 (its start) .. +0.5 (its end): its palette // colour in the middle, shifting in hue towards each neighbour by up to edgeBlend. ColorRGBW sectionColor(int section, float local) const { const int count = config_.count; const int neighbour = local < 0 ? (section + count - 1) % count : (section + 1) % count; const float amount = config_.edgeBlend * clamp01(std::fabs(local) * 2.0f); return blendHue(config_.colors[section], config_.colors[neighbour], amount); } static ColorRGBW blendHue(const ColorRGBW &c1, const ColorRGBW &c2, float f) { if (f <= 0) return c1; ColorHSV h1 = rgb2hsv(c1); ColorHSV h2 = rgb2hsv(c2); // Greys have no hue of their own; borrow the other's so only saturation moves. if (h1.s <= 0) h1.h = h2.h; if (h2.s <= 0) h2.h = h1.h; float dh = h2.h - h1.h; if (dh > 180) dh -= 360; else if (dh < -180) dh += 360; float hue = h1.h + f * dh; if (hue < 0) hue += 360; else if (hue >= 360) hue -= 360; ColorRGBW out = hsv2rgb(ColorHSV{hue, h1.s + f * (h2.s - h1.s), h1.v + f * (h2.v - h1.v)}); out.w = uint8_t(float(c1.w) + f * (float(c2.w) - float(c1.w))); return out; } EffectSegmentSelectConfig config_; TLedStrip &ledStrip_; float pitch_ = 0; float halfWidth_ = 0; float offsetLeds_ = 0; float glideFrom_ = 0; float elapsedMs_ = 0; ColorRGBW background_[NUM_LEDS]; }; // Traits template <> struct EffectIdToConfig { using type = EffectSegmentSelectConfig; }; template <> struct EffectConfigToId { static constexpr auto id = EffectId::SEGMENT_SELECT; }; template struct EffectIdToClass { using type = EffectSegmentSelect; };