Figures: several albums per figure, picked on the LED ring
A figure folder that holds album folders instead of audio can play any of them, up to six, in folder order (depth-first, alphabetical). Each is its own album, titled "Fuchs - <folder>" and of the figure's kind. Putting the figure down plays the album it played last, or the first, straight away; another figure placed in between forgets the position in that album, not the album itself. Meanwhile the ring offers all of them: one section per album from a fixed palette, edges shifting towards the neighbours' hue, the selected one bright and wobbling. The knob moves the selection one section per detent and wraps; pressing plays the selected album, and left alone for 8 s the picker fades into the figure's colours and the music carries on. While open, the device hands the knob to the picker (`capture_knob`): turns do not change the volume and the press is not published to Home Assistant. A tick per step and a confirm "pling" go with it. The firmware only draws: one new ring effect, LED_WHEEL_EFFECT_SEGMENT_SELECT (23), re-sent on every detent with the section to glide from. The look is tunable through a temporary `general.figure_selection` config section until the right values become constants. Every UI sound - the screen's blubb, the parents' timer, the picker - now follows the music volume with a floor, times a new parent-mode "Effekt-Lautstärke" (`general.effects_volume`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ enum class EffectId
|
||||
SWIPE_AND_CHANGE, // combination of ALEXA_SWIPE and RANDOM_TWO_COLOR_INTERPOLATION
|
||||
REVERSE_SWIPE,
|
||||
STATIC_DETAILED,
|
||||
SEGMENT_SELECT,
|
||||
};
|
||||
|
||||
template <EffectId id>
|
||||
|
||||
212
esp-firmware/lib/ledtl/effects/SegmentSelect.h
Normal file
212
esp-firmware/lib/ledtl/effects/SegmentSelect.h
Normal file
@@ -0,0 +1,212 @@
|
||||
#pragma once
|
||||
|
||||
#include "effects/Common.h"
|
||||
#include "helpers/ColorRGBW.h"
|
||||
#include "helpers/ColorHSV.h"
|
||||
#include "helpers/ColorConversions.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
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 <typename TLedStrip>
|
||||
class EffectSegmentSelect
|
||||
{
|
||||
public:
|
||||
static constexpr auto NUM_LEDS = numLeds<TLedStrip>();
|
||||
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<EffectId::SEGMENT_SELECT>
|
||||
{
|
||||
using type = EffectSegmentSelectConfig;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct EffectConfigToId<EffectSegmentSelectConfig>
|
||||
{
|
||||
static constexpr auto id = EffectId::SEGMENT_SELECT;
|
||||
};
|
||||
|
||||
template <typename TLedStrip>
|
||||
struct EffectIdToClass<EffectId::SEGMENT_SELECT, TLedStrip>
|
||||
{
|
||||
using type = EffectSegmentSelect<TLedStrip>;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "effects/ReverseSwipe.h"
|
||||
#include "effects/RandomTwoColorInterpolation.h"
|
||||
#include "effects/SwipeAndChange.h"
|
||||
#include "effects/SegmentSelect.h"
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <cstdint>
|
||||
@@ -120,6 +121,8 @@ enum class MessageHostToFw : uint8_t
|
||||
|
||||
PREV_BUTTON_LED = 21,
|
||||
NEXT_BUTTON_LED = 22,
|
||||
|
||||
LED_WHEEL_EFFECT_SEGMENT_SELECT = 23,
|
||||
};
|
||||
|
||||
template <>
|
||||
@@ -214,6 +217,7 @@ inline void handleIncomingMessagesFromHost(LedTask1 *ledTaskCircle, LedTask2 *le
|
||||
else if(handleLedEffect<EffectRandomTwoColorInterpolationConfig>(ledTaskCircle, MessageHostToFw::LED_WHEEL_EFFECT_RANDOM_TWO_COLOR_INTERPOLATION, msgType, msgBuffer)) {}
|
||||
else if(handleLedEffect<EffectSwipeAndChangeConfig >(ledTaskCircle, MessageHostToFw::LED_WHEEL_EFFECT_SWIPE_AND_CHANGE, msgType, msgBuffer)) {}
|
||||
else if(handleLedEffect<EffectReverseSwipeConfig >(ledTaskCircle, MessageHostToFw::LED_WHEEL_EFFECT_REVERSE_SWIPE, msgType, msgBuffer)) {}
|
||||
else if(handleLedEffect<EffectSegmentSelectConfig >(ledTaskCircle, MessageHostToFw::LED_WHEEL_EFFECT_SEGMENT_SELECT, msgType, msgBuffer)) {}
|
||||
|
||||
// Mouse LEDs
|
||||
else if(handleLedEffect<EffectStaticConfig >(ledTaskMouse, MessageHostToFw::MOUSE_LED_EFFECT_STATIC, msgType, msgBuffer)) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "effects/Circular.h"
|
||||
#include "effects/Common.h"
|
||||
#include "effects/SegmentSelect.h"
|
||||
#include "drivers/Esp32DriverRGBW.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -82,6 +83,7 @@ void _led_task_func(void *params)
|
||||
else if (dispatchEffectId<EffectId::RANDOM_TWO_COLOR_INTERPOLATION>(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
|
||||
else if (dispatchEffectId<EffectId::SWIPE_AND_CHANGE >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
|
||||
else if (dispatchEffectId<EffectId::REVERSE_SWIPE >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
|
||||
else if (dispatchEffectId<EffectId::SEGMENT_SELECT >(id, effectFunction, ledStrip, msgBuffer, effectStorage)) {}
|
||||
// clang-format on
|
||||
|
||||
timeoutMsForEffect = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "effects/AlexaSwipe.h"
|
||||
#include "effects/ReverseSwipe.h"
|
||||
#include "effects/SegmentSelect.h"
|
||||
#include "helpers/ColorConversions.h"
|
||||
|
||||
#include <iostream>
|
||||
@@ -100,6 +101,29 @@ int main(int argc, char **argv)
|
||||
effectToFile("reverse_swipe.py", effect, strip, 200);
|
||||
}
|
||||
|
||||
{
|
||||
EffectSegmentSelectConfig cfg{};
|
||||
cfg.count = 4;
|
||||
cfg.selected = 0;
|
||||
cfg.previous = 3;
|
||||
cfg.gapLeds = 1;
|
||||
cfg.colors[0] = ColorRGBW{255, 0, 0, 0};
|
||||
cfg.colors[1] = ColorRGBW{255, 160, 0, 0};
|
||||
cfg.colors[2] = ColorRGBW{0, 200, 0, 0};
|
||||
cfg.colors[3] = ColorRGBW{0, 60, 255, 0};
|
||||
cfg.offsetDegrees = 180;
|
||||
cfg.direction = 1;
|
||||
cfg.dimFactor = 0.15f;
|
||||
cfg.wobbleAmplitude = 1.5f;
|
||||
cfg.wobblePeriodMs = 1000;
|
||||
cfg.slideMs = 200;
|
||||
cfg.edgeBlend = 0.25f;
|
||||
LedStripRGBW<51> strip;
|
||||
|
||||
EffectSegmentSelect<decltype(strip)> effect(cfg, strip);
|
||||
effectToFile("segment_select.py", effect, strip, 200);
|
||||
}
|
||||
|
||||
/*
|
||||
effect.currentPosition_ = 150;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user