Last active
May 12, 2026 12:14
-
-
Save Steven24K/53b331be453a1673713636b6d350f685 to your computer and use it in GitHub Desktop.
A starting point for an animation library build on top of FastLED to manage smooth transitions. Using a statemachine monad and parallel processes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <FastLED.h> | |
| #define NUM_LEDS 60 | |
| #define LED_PIN 2 | |
| struct LEDState { | |
| CRGB* leds; | |
| int count; | |
| CHSV baseColor; | |
| LEDState(int numLeds, CHSV color = CHSV(0, 0, 0)) | |
| : count(numLeds), baseColor(color) { | |
| leds = new CRGB[count]; | |
| FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, count); | |
| } | |
| ~LEDState() { | |
| delete[] leds; | |
| } | |
| }; | |
| template<typename TState> | |
| struct StateMachine { | |
| bool busy = true; | |
| virtual void update(TState& state) = 0; | |
| virtual void reset() { | |
| busy = true; | |
| } | |
| virtual ~StateMachine() {} | |
| }; | |
| template<typename TState> | |
| struct IdentityMachine : StateMachine<TState> { | |
| void update(TState&) override { | |
| this->busy = false; | |
| } | |
| }; | |
| template<typename TState> | |
| struct Seq : StateMachine<TState> { | |
| StateMachine<TState>* a; | |
| StateMachine<TState>* b; | |
| StateMachine<TState>* current; | |
| Seq(StateMachine<TState>* s1, StateMachine<TState>* s2) | |
| : a(s1), b(s2), current(s1) {} | |
| void update(TState& state) override { | |
| if (!this->busy) return; | |
| current->update(state); | |
| if (!current->busy) { | |
| if (current == a) current = b; | |
| else this->busy = false; | |
| } | |
| } | |
| void reset() override { | |
| a->reset(); | |
| b->reset(); | |
| current = a; | |
| this->busy = true; | |
| } | |
| }; | |
| struct ListSeq : StateMachine<LEDState> { | |
| std::vector<StateMachine<LEDState>*> steps; | |
| size_t index = 0; | |
| ListSeq(std::initializer_list<StateMachine<LEDState>*> s) | |
| : steps(s) {} | |
| ListSeq(std::vector<StateMachine<LEDState>*> s) | |
| : steps(s) {} | |
| void update(LEDState& state) override { | |
| if (index < steps.size()) { | |
| steps[index]->update(state); | |
| if (!steps[index]->busy) | |
| index++; | |
| } else { | |
| this->busy = false; | |
| } | |
| } | |
| void reset() override { | |
| for (auto* s : steps) s->reset(); | |
| index = 0; | |
| this->busy = true; | |
| } | |
| }; | |
| template<typename TState> | |
| struct Parallel : StateMachine<TState> { | |
| StateMachine<TState>* a; | |
| StateMachine<TState>* b; | |
| Parallel(StateMachine<TState>* s1, StateMachine<TState>* s2) | |
| : a(s1), b(s2) {} | |
| void update(TState& state) override { | |
| if (!this->busy) return; | |
| bool active = false; | |
| if (a->busy) { | |
| a->update(state); | |
| active = true; | |
| } | |
| if (b->busy) { | |
| b->update(state); | |
| active = true; | |
| } | |
| this->busy = active; | |
| } | |
| void reset() override { | |
| a->reset(); | |
| b->reset(); | |
| this->busy = true; | |
| } | |
| }; | |
| template<typename TState> | |
| struct ForkJoin : StateMachine<TState> { | |
| StateMachine<TState>* mainBranch; | |
| StateMachine<TState>* sideBranch; | |
| ForkJoin(StateMachine<TState>* mainB, StateMachine<TState>* sideB) | |
| : mainBranch(mainB), sideBranch(sideB) {} | |
| void update(TState& state) override { | |
| if (!this->busy) return; | |
| if (sideBranch->busy) | |
| sideBranch->update(state); | |
| if (mainBranch->busy) | |
| mainBranch->update(state); | |
| if (!mainBranch->busy) | |
| this->busy = false; | |
| } | |
| void reset() override { | |
| mainBranch->reset(); | |
| sideBranch->reset(); | |
| this->busy = true; | |
| } | |
| }; | |
| struct Call : StateMachine<LEDState> { | |
| StateMachine<LEDState>* inner; | |
| Call(StateMachine<LEDState>* m) | |
| : inner(m) {} | |
| void update(LEDState& state) override { | |
| inner->update(state); | |
| this->busy = inner->busy; | |
| } | |
| void reset() override { | |
| inner->reset(); | |
| this->busy = true; | |
| } | |
| }; | |
| struct SetPixel : StateMachine<LEDState> { | |
| int pos; | |
| SetPixel(int p) | |
| : pos(p) {} | |
| void update(LEDState& s) override { | |
| if (this->busy && pos >= 0 && pos < s.count) | |
| s.leds[pos] = s.baseColor; | |
| this->busy = false; | |
| } | |
| }; | |
| struct UnSetPixel : StateMachine<LEDState> { | |
| int pos; | |
| UnSetPixel(int p) | |
| : pos(p) {} | |
| void update(LEDState& s) override { | |
| if (this->busy && pos >= 0 && pos < s.count) | |
| s.leds[pos] = CRGB::Black; | |
| this->busy = false; | |
| } | |
| }; | |
| struct ColorPushMachine : StateMachine<LEDState> { | |
| CHSV newColor; | |
| ColorPushMachine(CHSV c) | |
| : newColor(c) {} | |
| void update(LEDState& s) override { | |
| s.baseColor = newColor; | |
| this->busy = false; | |
| } | |
| }; | |
| struct ColorTransformMachine : StateMachine<LEDState> { | |
| uint8_t step; | |
| ColorTransformMachine(uint8_t s) | |
| : step(s) {} | |
| void update(LEDState& s) override { | |
| s.baseColor.h += step; | |
| this->busy = false; | |
| } | |
| }; | |
| struct Clear : StateMachine<LEDState> { | |
| void update(LEDState& s) override { | |
| for (int i = 0; i < s.count; i++) | |
| s.leds[i] = CRGB::Black; | |
| this->busy = false; | |
| } | |
| }; | |
| struct Timer : StateMachine<LEDState> { | |
| unsigned long duration, start; | |
| bool running = false; | |
| Timer(unsigned long ms) | |
| : duration(ms) {} | |
| void update(LEDState&) override { | |
| if (!this->busy) return; | |
| if (!running) { | |
| start = millis(); | |
| running = true; | |
| } | |
| if (millis() - start >= duration) { | |
| this->busy = false; | |
| running = false; | |
| } | |
| } | |
| }; | |
| struct WaitUntil : StateMachine<LEDState> { | |
| std::function<bool()> predicate; | |
| WaitUntil(std::function<bool()> p) | |
| : predicate(p) {} | |
| void update(LEDState&) override { | |
| if (predicate()) | |
| this->busy = false; | |
| } | |
| }; | |
| struct Repeat : StateMachine<LEDState> { | |
| StateMachine<LEDState>* inner; | |
| Repeat(StateMachine<LEDState>* m) | |
| : inner(m) {} | |
| void update(LEDState& s) override { | |
| if (!inner->busy) | |
| inner->reset(); | |
| inner->update(s); | |
| } | |
| }; | |
| struct RepeatCount : StateMachine<LEDState> { | |
| StateMachine<LEDState>* inner; | |
| int count, current = 0; | |
| RepeatCount(int c, StateMachine<LEDState>* m) | |
| : count(c), inner(m) {} | |
| void update(LEDState& s) override { | |
| if (current < count) { | |
| inner->update(s); | |
| if (!inner->busy) { | |
| current++; | |
| if (current < count) | |
| inner->reset(); | |
| } | |
| } else { | |
| this->busy = false; | |
| } | |
| } | |
| void reset() override { | |
| current = 0; | |
| inner->reset(); | |
| this->busy = true; | |
| } | |
| }; | |
| struct CometMachine : StateMachine<LEDState> { | |
| float pos, speed, start; | |
| CometMachine(float spd, float st = 0) | |
| : pos(st), speed(spd), start(st) {} | |
| void update(LEDState& s) override { | |
| if (!this->busy) return; | |
| pos += speed; | |
| int head = (int)pos; | |
| if (head >= 0 && head < s.count) | |
| s.leds[head] = s.baseColor; | |
| if ((speed > 0 && pos >= s.count) || (speed < 0 && pos < 0)) | |
| this->busy = false; | |
| } | |
| void reset() override { | |
| pos = start; | |
| this->busy = true; | |
| } | |
| }; | |
| struct FadeMachine : StateMachine<LEDState> { | |
| uint8_t amount; | |
| FadeMachine(uint8_t a) | |
| : amount(a) {} | |
| void update(LEDState& s) override { | |
| fadeToBlackBy(s.leds, s.count, amount); | |
| this->busy = true; | |
| } | |
| }; | |
| struct GlitchDecorator : StateMachine<LEDState> { | |
| StateMachine<LEDState>* inner; | |
| GlitchDecorator(StateMachine<LEDState>* m) | |
| : inner(m) {} | |
| void update(LEDState& s) override { | |
| inner->update(s); | |
| if (random8() > 240) { | |
| int idx = random16(s.count); | |
| s.leds[idx] = CRGB::White; | |
| } | |
| this->busy = inner->busy; | |
| } | |
| }; | |
| struct RainbowMachine : StateMachine<LEDState> { | |
| int hue = 0; | |
| void update(LEDState& state) override { | |
| hue++; | |
| fill_rainbow(state.leds, state.count, hue, 7); | |
| } | |
| }; | |
| template<typename TState> | |
| struct AnimationBuilder { | |
| using Machine = StateMachine<TState>; | |
| Machine* machine; | |
| AnimationBuilder() | |
| : machine(new IdentityMachine<TState>()) {} | |
| AnimationBuilder(Machine* m) | |
| : machine(m) {} | |
| AnimationBuilder then(Machine* next) const { | |
| return AnimationBuilder(new Seq<TState>(machine, next)); | |
| } | |
| AnimationBuilder listSeq( | |
| std::initializer_list<std::function<AnimationBuilder(const AnimationBuilder&)>> fns) const { | |
| std::vector<Machine*> machines; | |
| machines.reserve(fns.size()); | |
| for (auto& fn : fns) { | |
| auto b = fn(*this); | |
| machines.push_back(b.machine); | |
| } | |
| return then(new ListSeq(machines)); | |
| } | |
| AnimationBuilder parallel( | |
| std::function<AnimationBuilder(const AnimationBuilder&)> a, | |
| std::function<AnimationBuilder(const AnimationBuilder&)> b) const { | |
| auto A = a(*this); | |
| auto B = b(*this); | |
| return then(new Parallel<TState>(A.machine, B.machine)); | |
| } | |
| AnimationBuilder forkJoin( | |
| std::function<AnimationBuilder(const AnimationBuilder&)> mainFn, | |
| std::function<AnimationBuilder(const AnimationBuilder&)> sideFn) const { | |
| auto mainB = mainFn(*this); | |
| auto sideB = sideFn(*this); | |
| return then(new ForkJoin<TState>(mainB.machine, sideB.machine)); | |
| } | |
| AnimationBuilder call( | |
| std::function<AnimationBuilder(const AnimationBuilder&)> fn) const { | |
| auto b = fn(*this); | |
| return then(new Call(b.machine)); | |
| } | |
| AnimationBuilder bind( | |
| std::function<AnimationBuilder(const AnimationBuilder&)> fn) const { | |
| return fn(*this); | |
| } | |
| AnimationBuilder setPixel(int pos) const { | |
| return then(new SetPixel(pos)); | |
| } | |
| AnimationBuilder unsetPixel(int pos) const { | |
| return then(new UnSetPixel(pos)); | |
| } | |
| AnimationBuilder pushColor(CHSV c) const { | |
| return then(new ColorPushMachine(c)); | |
| } | |
| AnimationBuilder transformColor(uint8_t step) const { | |
| return then(new ColorTransformMachine(step)); | |
| } | |
| AnimationBuilder clear() const { | |
| return then(new Clear()); | |
| } | |
| AnimationBuilder timer(unsigned long ms) const { | |
| return then(new Timer(ms)); | |
| } | |
| AnimationBuilder waitUntil(std::function<bool()> predicate) const { | |
| return then(new WaitUntil(predicate)); | |
| } | |
| AnimationBuilder repeat( | |
| std::function<AnimationBuilder(const AnimationBuilder&)> inner) const { | |
| auto b = inner(*this); | |
| return then(new Repeat(b.machine)); | |
| } | |
| AnimationBuilder repeatCount( | |
| int count, | |
| std::function<AnimationBuilder(const AnimationBuilder&)> inner) const { | |
| auto b = inner(*this); | |
| return then(new RepeatCount(count, b.machine)); | |
| } | |
| AnimationBuilder comet(float speed, float start = 0) const { | |
| return then(new CometMachine(speed, start)); | |
| } | |
| AnimationBuilder fadeBy(uint8_t amount) const { | |
| return then(new FadeMachine(amount)); | |
| } | |
| AnimationBuilder glitch() const { | |
| return then(new GlitchDecorator(machine)); | |
| } | |
| AnimationBuilder rainbow() const { | |
| return then(new RainbowMachine()); | |
| } | |
| Machine* build() const { | |
| return machine; | |
| } | |
| }; | |
| StateMachine<LEDState>* myProgram; | |
| LEDState* lightState; | |
| void setup() { | |
| FastLED.setBrightness(100); | |
| lightState = new LEDState(NUM_LEDS); | |
| AnimationBuilder<LEDState> b; | |
| // TODO: | |
| // - Turn Parallel into a list as well to accept mutliple lanes | |
| // - Separate builder into interfaces, infinite state machines and finite patterns | |
| // - next: animiations | |
| // - cancel() | |
| // - race() | |
| // - parallelAny() | |
| // - parallelAll() | |
| // - layer() | |
| // - blend() | |
| // - Extend color state into: Shared state, Static values and derived values. | |
| myProgram = | |
| b | |
| // .pushColor(CHSV(0, 255, 255)) // red flash | |
| // .setPixel(10) | |
| // .timer(800) | |
| // .unsetPixel(10) | |
| // .pushColor(CHSV(192, 255, 255)) // restore comet color | |
| // .pushColor(CHSV(192, 255, 255)) | |
| // .parallel( | |
| // [](const auto& s) { | |
| // return s.fadeBy(50).timer(100); | |
| // }, | |
| // [](const auto& s) { | |
| // // return s.repeat([](const auto& s) { | |
| // return s.parallel( | |
| // [](const auto& s) { | |
| // return s.comet(0.5f, 0); | |
| // }, | |
| // [](const auto& s) { | |
| // return s.comet(-0.54f, NUM_LEDS - 1); | |
| // }) | |
| // .transformColor(50); | |
| // // }); | |
| // }) | |
| .rainbow() | |
| .build(); | |
| } | |
| void loop() { | |
| if (myProgram->busy) | |
| myProgram->update(*lightState); | |
| FastLED.show(); | |
| FastLED.delay(60); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment