Skip to content

Instantly share code, notes, and snippets.

@Steven24K
Last active May 12, 2026 12:14
Show Gist options
  • Select an option

  • Save Steven24K/53b331be453a1673713636b6d350f685 to your computer and use it in GitHub Desktop.

Select an option

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.
#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