initial commit

This commit is contained in:
2026-02-12 00:45:31 -08:00
commit 5f168f370b
3024 changed files with 804889 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
# FastLED FX: 1D Effects
This folder contains one-dimensional (strip) visual effects. All effects derive from `fl::Fx1d` and render into a linear `CRGB* leds` buffer using a common `DrawContext`.
If youre new to FastLED and C++: think of an effect as a small object you construct once and then call `draw(...)` every frame with the current time and your LED buffer.
### Whats here
- **Cylon (`cylon.h`)**: A single dot sweeps back and forth (Larson scanner). Tunable `fade_amount`, `delay_ms`.
- **DemoReel100 (`demoreel100.h`)**: Classic demo with rotating patterns (rainbow, glitter, confetti, sinelon, bpm, juggle). Auto-advances every ~10 seconds.
- **Fire2012 (`fire2012.h`)**: Procedural fire simulation with parameters `cooling` and `sparking`, optional reverse direction and palette.
- **NoiseWave (`noisewave.h`)**: Smooth noise-driven red/blue waves across the strip.
- **Pacifica (`pacifica.h`)**: Gentle blue-green ocean waves layered for depth.
- **Pride2015 (`pride2015.h`)**: Ever-changing rainbow animation.
- **TwinkleFox (`twinklefox.h`)**: Twinkling holiday-style lights using palettes; compile-time knobs for speed/density and incandescent-style cooling.
### Common usage
- Include the header for the effect you want and construct it with the LED count (e.g., `fl::Cylon fx(num_leds);`).
- Each frame (e.g., in your loop), call `fx.draw(fl::Fx::DrawContext(now, leds));`.
- Most effects expose small parameters via constructor (e.g., `Fire2012(num_leds, cooling, sparking)`), or have internal timing based on `now` from `DrawContext`.
### Notes and tips
- Effects guard against `nullptr` and zero length, but you should pass a valid `CRGB*` and correct `num_leds` to see output.
- Try different frame rates. Many patterns look good at ~50100 FPS, but 30 FPS is fine for most.
- For palette-based effects (e.g., TwinkleFox, Pride2015, DemoReels BPM), experiment with `CRGBPalette16` to customize colors.
- These 1D effects are lightweight to medium-weight. They depend on FastLED helpers such as `CHSV`, `beatsin*`, `nblend`, palettes, and random noise.
### Key types referenced
- **`fl::Fx1d`**: Base class for 1D effects.
- **`fl::Fx::DrawContext`**: Carries `now` (time in ms), `CRGB* leds`, and optional per-frame hints.
- **`CRGB` / `CHSV`**: FastLEDs color types.
Explore the headers to see per-effect parameters and internal logic; everything here is self-contained and ready to drop into your render loop.
### Examples
- Cylon (from `examples/FxCylon/FxCylon.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/cylon.h"
using namespace fl;
#define NUM_LEDS 64
CRGB leds[NUM_LEDS];
Cylon cylon(NUM_LEDS);
void setup() {
FastLED.addLeds<WS2812, 2, RGB>(leds, NUM_LEDS);
}
void loop() {
cylon.draw(Fx::DrawContext(millis(), leds));
FastLED.show();
delay(cylon.delay_ms);
}
```
- DemoReel100 (from `examples/FxDemoReel100/FxDemoReel100.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/demoreel100.h"
using namespace fl;
#define NUM_LEDS 64
CRGB leds[NUM_LEDS];
DemoReel100Ptr fx = fl::make_shared<DemoReel100>(NUM_LEDS);
void setup() { FastLED.addLeds<WS2811, 3, GRB>(leds, NUM_LEDS); }
void loop() {
fx->draw(Fx::DrawContext(millis(), leds));
FastLED.show();
FastLED.delay(1000/120);
}
```
- Fire2012 (from `examples/FxFire2012/FxFire2012.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/fire2012.h"
using namespace fl;
#define NUM_LEDS 92
CRGB leds[NUM_LEDS];
Fire2012Ptr fire = fl::make_shared<Fire2012>(NUM_LEDS, 55, 120, false);
void setup() { FastLED.addLeds<WS2811, 5, GRB>(leds, NUM_LEDS); }
void loop() {
fire->draw(Fx::DrawContext(millis(), leds));
FastLED.show();
FastLED.delay(1000/30);
}
```
- Pacifica (from `examples/FxPacifica/FxPacifica.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/pacifica.h"
using namespace fl;
#define NUM_LEDS 60
CRGB leds[NUM_LEDS];
Pacifica pacifica(NUM_LEDS);
void setup() { FastLED.addLeds<WS2812B, 3, GRB>(leds, NUM_LEDS); }
void loop() {
pacifica.draw(Fx::DrawContext(millis(), leds));
FastLED.show();
}
```
- Pride2015 (from `examples/FxPride2015/FxPride2015.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/pride2015.h"
using namespace fl;
#define NUM_LEDS 200
CRGB leds[NUM_LEDS];
Pride2015 pride(NUM_LEDS);
void setup() { FastLED.addLeds<WS2811, 3, GRB>(leds, NUM_LEDS); }
void loop() {
pride.draw(Fx::DrawContext(millis(), leds));
FastLED.show();
}
```
- TwinkleFox (from `examples/FxTwinkleFox/FxTwinkleFox.ino`):
```cpp
#include <FastLED.h>
#include "fx/1d/twinklefox.h"
using namespace fl;
#define NUM_LEDS 100
CRGB leds[NUM_LEDS];
TwinkleFox fx(NUM_LEDS);
void setup() { FastLED.addLeds<WS2811, 3, GRB>(leds, NUM_LEDS).setRgbw(); }
void loop() {
EVERY_N_SECONDS(SECONDS_PER_PALETTE) { fx.chooseNextColorPalette(fx.targetPalette); }
fx.draw(Fx::DrawContext(millis(), leds));
FastLED.show();
}
```

View File

@@ -0,0 +1,61 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fx/fx1d.h"
namespace fl {
FASTLED_SMART_PTR(Cylon);
/// @brief An animation that moves a single LED back and forth (Larson Scanner
/// effect)
class Cylon : public Fx1d {
public:
uint8_t delay_ms;
Cylon(uint16_t num_leds, uint8_t fade_amount = 250, uint8_t delay_ms = 10)
: Fx1d(num_leds), delay_ms(delay_ms), fade_amount(fade_amount) {}
void draw(DrawContext context) override {
if (context.leds == nullptr || mNumLeds == 0) {
return;
}
CRGB *leds = context.leds;
// Set the current LED to the current hue
leds[position] = CHSV(hue++, 255, 255);
// Fade all LEDs
for (uint16_t i = 0; i < mNumLeds; i++) {
leds[i].nscale8(fade_amount);
}
// Move the position
if (reverse) {
position--;
if (position < 0) {
position = 1;
reverse = false;
}
} else {
position++;
if (position >= int16_t(mNumLeds)) {
position = mNumLeds - 2;
reverse = true;
}
}
}
fl::string fxName() const override { return "Cylon"; }
private:
uint8_t hue = 0;
uint8_t fade_amount;
bool reverse = false;
int16_t position = 0;
};
} // namespace fl

View File

@@ -0,0 +1,132 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fx/fx1d.h"
namespace fl {
// FastLED "100-lines-of-code" demo reel, showing just a few
// of the kinds of animation patterns you can quickly and easily
// compose using FastLED.
//
// This example also shows one easy way to define multiple
// animations patterns and have them automatically rotate.
//
// -Mark Kriegsman, December 2014
FASTLED_SMART_PTR(DemoReel100);
class DemoReel100 : public Fx1d {
public:
DemoReel100(uint16_t num_leds) : Fx1d(num_leds) {}
void draw(DrawContext context) override {
CRGB *leds = context.leds;
if (leds == nullptr || mNumLeds == 0) {
return;
}
if (start_time == 0) {
start_time = millis();
}
// Call the current pattern function once, updating the 'leds' array
runPattern(leds);
// do some periodic updates
EVERY_N_MILLISECONDS(20) {
hue++;
} // slowly cycle the "base color" through the rainbow
EVERY_N_SECONDS(10) { nextPattern(); } // change patterns periodically
}
fl::string fxName() const override { return "DemoReel100"; }
private:
uint8_t current_pattern_number = 0;
uint8_t hue = 0;
unsigned long start_time = 0;
void nextPattern() {
// add one to the current pattern number, and wrap around at the end
current_pattern_number =
(current_pattern_number + 1) % 6; // 6 is the number of patterns
}
void runPattern(CRGB *leds) {
switch (current_pattern_number) {
case 0:
rainbow(leds);
break;
case 1:
rainbowWithGlitter(leds);
break;
case 2:
confetti(leds);
break;
case 3:
sinelon(leds);
break;
case 4:
juggle(leds);
break;
case 5:
bpm(leds);
break;
}
}
void rainbow(CRGB *leds) {
// FastLED's built-in rainbow generator
fill_rainbow(leds, mNumLeds, hue, 7);
}
void rainbowWithGlitter(CRGB *leds) {
// built-in FastLED rainbow, plus some random sparkly glitter
rainbow(leds);
addGlitter(80, leds);
}
void addGlitter(fract8 chanceOfGlitter, CRGB *leds) {
if (random8() < chanceOfGlitter) {
leds[random16(mNumLeds)] += CRGB::White;
}
}
void confetti(CRGB *leds) {
// random colored speckles that blink in and fade smoothly
fadeToBlackBy(leds, mNumLeds, 10);
int pos = random16(mNumLeds);
leds[pos] += CHSV(hue + random8(64), 200, 255);
}
void sinelon(CRGB *leds) {
// a colored dot sweeping back and forth, with fading trails
fadeToBlackBy(leds, mNumLeds, 20);
int pos = beatsin16(13, 0, mNumLeds - 1);
leds[pos] += CHSV(hue, 255, 192);
}
void bpm(CRGB *leds) {
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
uint8_t BeatsPerMinute = 62;
CRGBPalette16 palette = PartyColors_p;
uint8_t beat = beatsin8(BeatsPerMinute, 64, 255);
for (uint16_t i = 0; i < mNumLeds; i++) {
leds[i] =
ColorFromPalette(palette, hue + (i * 2), beat - hue + (i * 10));
}
}
void juggle(CRGB *leds) {
// eight colored dots, weaving in and out of sync with each other
fadeToBlackBy(leds, mNumLeds, 20);
uint8_t dothue = 0;
for (uint16_t i = 0; i < 8; i++) {
leds[beatsin16(i + 7, 0, mNumLeds - 1)] |= CHSV(dothue, 200, 255);
dothue += 32;
}
}
};
} // namespace fl

View File

@@ -0,0 +1,110 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fl/vector.h"
#include "fx/fx1d.h"
namespace fl {
/// @brief Simple one-dimensional fire animation function
// Fire2012 by Mark Kriegsman, July 2012
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
////
// This basic one-dimensional 'fire' simulation works roughly as follows:
// There's a underlying array of 'heat' cells, that model the temperature
// at each point along the line. Every cycle through the simulation,
// four steps are performed:
// 1) All cells cool down a little bit, losing heat to the air
// 2) The heat from each cell drifts 'up' and diffuses a little
// 3) Sometimes randomly new 'sparks' of heat are added at the bottom
// 4) The heat from each cell is rendered as a color into the leds array
// The heat-to-color mapping uses a black-body radiation approximation.
//
// Temperature is in arbitrary units from 0 (cold black) to 255 (white hot).
//
// This simulation scales it self a bit depending on NUM_LEDS; it should look
// "OK" on anywhere from 20 to 100 LEDs without too much tweaking.
//
// I recommend running this simulation at anywhere from 30-100 frames per
// second, meaning an interframe delay of about 10-35 milliseconds.
//
// Looks best on a high-density LED setup (60+ pixels/meter).
//
//
// There are two main parameters you can play with to control the look and
// feel of your fire: COOLING (used in step 1 above), and SPARKING (used
// in step 3 above).
//
// COOLING: How much does the air cool as it rises?
// Less cooling = taller flames. More cooling = shorter flames.
// Default 50, suggested range 20-100
// SPARKING: What chance (out of 255) is there that a new spark will be lit?
// Higher chance = more roaring fire. Lower chance = more flickery fire.
// Default 120, suggested range 50-200.
FASTLED_SMART_PTR(Fire2012);
class Fire2012 : public Fx1d {
public:
Fire2012(uint16_t num_leds, uint8_t cooling = 55, uint8_t sparking = 120,
bool reverse_direction = false,
const CRGBPalette16 &palette = (const CRGBPalette16 &)HeatColors_p)
: Fx1d(num_leds), cooling(cooling), sparking(sparking),
reverse_direction(reverse_direction), palette(palette) {
heat.resize(num_leds); // Vector elements are default-initialized
}
~Fire2012() {}
void draw(DrawContext context) override {
CRGB *leds = context.leds;
if (leds == nullptr) {
return;
}
// Step 1. Cool down every cell a little
for (uint16_t i = 0; i < mNumLeds; i++) {
heat[i] =
qsub8(heat[i], random8(0, ((cooling * 10) / mNumLeds) + 2));
}
// Step 2. Heat from each cell drifts 'up' and diffuses a little
for (uint16_t k = mNumLeds - 1; k >= 2; k--) {
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
}
// Step 3. Randomly ignite new 'sparks' of heat near the bottom
if (random8() < sparking) {
int y = random8(7);
heat[y] = qadd8(heat[y], random8(160, 255));
}
// Step 4. Map from heat cells to LED colors
for (uint16_t j = 0; j < mNumLeds; j++) {
// Scale the heat value from 0-255 down to 0-240
// for best results with color palettes.
uint8_t colorindex = scale8(heat[j], 240);
CRGB color = ColorFromPalette(palette, colorindex);
int pixelnumber;
if (reverse_direction) {
pixelnumber = (mNumLeds - 1) - j;
} else {
pixelnumber = j;
}
leds[pixelnumber] = color;
}
}
fl::string fxName() const override { return "Fire2012"; }
private:
fl::vector<uint8_t, fl::allocator_psram<uint8_t>> heat;
uint8_t cooling;
uint8_t sparking;
bool reverse_direction;
CRGBPalette16 palette;
};
} // namespace fl

View File

@@ -0,0 +1,44 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fx/fx1d.h"
#include "noisegen.h"
namespace fl {
FASTLED_SMART_PTR(NoiseWave);
class NoiseWave : public Fx1d {
public:
NoiseWave(uint16_t num_leds)
: Fx1d(num_leds), noiseGeneratorRed(500, 14),
noiseGeneratorBlue(500, 10) {}
void draw(DrawContext context) override {
if (context.leds == nullptr || mNumLeds == 0) {
return;
}
if (start_time == 0) {
start_time = context.now;
}
unsigned long time_now = millis() - start_time;
for (int32_t i = 0; i < mNumLeds; ++i) {
int r = noiseGeneratorRed.LedValue(i, time_now);
int b = noiseGeneratorBlue.LedValue(i, time_now + 100000) >> 1;
int g = 0;
context.leds[i] = CRGB(r, g, b);
}
}
fl::string fxName() const override { return "NoiseWave"; }
private:
NoiseGenerator noiseGeneratorRed;
NoiseGenerator noiseGeneratorBlue;
fl::u32 start_time = 0;
};
} // namespace fl

View File

@@ -0,0 +1,135 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fx/fx1d.h"
namespace fl {
/// @file pacifica.hpp
/// @brief An animation that simulates gentle, blue-green ocean waves
/// @example Pacifica.ino
FASTLED_SMART_PTR(Pacifica);
class Pacifica : public Fx1d {
public:
Pacifica(uint16_t num_leds) : Fx1d(num_leds) {}
void draw(DrawContext context) override;
fl::string fxName() const override { return "Pacifica"; }
private:
uint16_t sCIStart1 = 0, sCIStart2 = 0, sCIStart3 = 0, sCIStart4 = 0;
fl::u32 sLastms = 0;
CRGBPalette16 pacifica_palette_1 = {0x000507, 0x000409, 0x00030B, 0x00030D,
0x000210, 0x000212, 0x000114, 0x000117,
0x000019, 0x00001C, 0x000026, 0x000031,
0x00003B, 0x000046, 0x14554B, 0x28AA50};
CRGBPalette16 pacifica_palette_2 = {0x000507, 0x000409, 0x00030B, 0x00030D,
0x000210, 0x000212, 0x000114, 0x000117,
0x000019, 0x00001C, 0x000026, 0x000031,
0x00003B, 0x000046, 0x0C5F52, 0x19BE5F};
CRGBPalette16 pacifica_palette_3 = {0x000208, 0x00030E, 0x000514, 0x00061A,
0x000820, 0x000927, 0x000B2D, 0x000C33,
0x000E39, 0x001040, 0x001450, 0x001860,
0x001C70, 0x002080, 0x1040BF, 0x2060FF};
void pacifica_one_layer(CRGB *leds, CRGBPalette16 &p, uint16_t cistart,
uint16_t wavescale, uint8_t bri, uint16_t ioff);
void pacifica_add_whitecaps(CRGB *leds);
void pacifica_deepen_colors(CRGB *leds);
};
void Pacifica::draw(DrawContext ctx) {
CRGB *leds = ctx.leds;
fl::u32 now = ctx.now;
if (leds == nullptr || mNumLeds == 0) {
return;
}
// Update the hue each time through the loop
fl::u32 ms = now;
fl::u32 deltams = ms - sLastms;
sLastms = ms;
uint16_t speedfactor1 = beatsin16(3, 179, 269);
uint16_t speedfactor2 = beatsin16(4, 179, 269);
fl::u32 deltams1 = (deltams * speedfactor1) / 256;
fl::u32 deltams2 = (deltams * speedfactor2) / 256;
fl::u32 deltams21 = (deltams1 + deltams2) / 2;
sCIStart1 += (deltams1 * beatsin88(1011, 10, 13));
sCIStart2 -= (deltams21 * beatsin88(777, 8, 11));
sCIStart3 -= (deltams1 * beatsin88(501, 5, 7));
sCIStart4 -= (deltams2 * beatsin88(257, 4, 6));
// Clear out the LED array to a dim background blue-green
fill_solid(leds, mNumLeds, CRGB(2, 6, 10));
// Render each of four layers, with different scales and speeds, that vary
// over time
pacifica_one_layer(leds, pacifica_palette_1, sCIStart1,
beatsin16(3, 11 * 256, 14 * 256), beatsin8(10, 70, 130),
0 - beat16(301));
pacifica_one_layer(leds, pacifica_palette_2, sCIStart2,
beatsin16(4, 6 * 256, 9 * 256), beatsin8(17, 40, 80),
beat16(401));
pacifica_one_layer(leds, pacifica_palette_3, sCIStart3, 6 * 256,
beatsin8(9, 10, 38), 0 - beat16(503));
pacifica_one_layer(leds, pacifica_palette_3, sCIStart4, 5 * 256,
beatsin8(8, 10, 28), beat16(601));
// Add brighter 'whitecaps' where the waves lines up more
pacifica_add_whitecaps(leds);
// Deepen the blues and greens a bit
pacifica_deepen_colors(leds);
}
// Add one layer of waves into the led array
void Pacifica::pacifica_one_layer(CRGB *leds, CRGBPalette16 &p,
uint16_t cistart, uint16_t wavescale,
uint8_t bri, uint16_t ioff) {
uint16_t ci = cistart;
uint16_t waveangle = ioff;
uint16_t wavescale_half = (wavescale / 2) + 20;
for (uint16_t i = 0; i < mNumLeds; i++) {
waveangle += 250;
uint16_t s16 = sin16(waveangle) + 32768;
uint16_t cs = scale16(s16, wavescale_half) + wavescale_half;
ci += cs;
uint16_t sindex16 = sin16(ci) + 32768;
uint8_t sindex8 = scale16(sindex16, 240);
CRGB c = ColorFromPalette(p, sindex8, bri, LINEARBLEND);
leds[i] += c;
}
}
// Add extra 'white' to areas where the four layers of light have lined up
// brightly
void Pacifica::pacifica_add_whitecaps(CRGB *leds) {
uint8_t basethreshold = beatsin8(9, 55, 65);
uint8_t wave = beat8(7);
for (uint16_t i = 0; i < mNumLeds; i++) {
uint8_t threshold = scale8(sin8(wave), 20) + basethreshold;
wave += 7;
uint8_t l = leds[i].getAverageLight();
if (l > threshold) {
uint8_t overage = l - threshold;
uint8_t overage2 = qadd8(overage, overage);
leds[i] += CRGB(overage, overage2, qadd8(overage2, overage2));
}
}
}
// Deepen the blues and greens
void Pacifica::pacifica_deepen_colors(CRGB *leds) {
for (uint16_t i = 0; i < mNumLeds; i++) {
leds[i].blue = scale8(leds[i].blue, 145);
leds[i].green = scale8(leds[i].green, 200);
leds[i] |= CRGB(2, 5, 7);
}
}
} // namespace fl

View File

@@ -0,0 +1,74 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fx/fx1d.h"
namespace fl {
/// @file pride2015.hpp
/// @brief Animated, ever-changing rainbows (Pride2015 effect)
/// @example Pride2015.ino
// Pride2015
// Animated, ever-changing rainbows.
// by Mark Kriegsman
FASTLED_SMART_PTR(Pride2015);
class Pride2015 : public Fx1d {
public:
Pride2015(uint16_t num_leds) : Fx1d(num_leds) {}
void draw(Fx::DrawContext context) override;
fl::string fxName() const override { return "Pride2015"; }
private:
uint16_t mPseudotime = 0;
uint16_t mLastMillis = 0;
uint16_t mHue16 = 0;
};
// This function draws rainbows with an ever-changing,
// widely-varying set of parameters.
void Pride2015::draw(Fx::DrawContext ctx) {
if (ctx.leds == nullptr || mNumLeds == 0) {
return;
}
uint8_t sat8 = beatsin88(87, 220, 250);
uint8_t brightdepth = beatsin88(341, 96, 224);
uint16_t brightnessthetainc16 = beatsin88(203, (25 * 256), (40 * 256));
uint8_t msmultiplier = beatsin88(147, 23, 60);
uint16_t hue16 = mHue16;
uint16_t hueinc16 = beatsin88(113, 1, 3000);
uint16_t ms = millis();
uint16_t deltams = ms - mLastMillis;
mLastMillis = ms;
mPseudotime += deltams * msmultiplier;
mHue16 += deltams * beatsin88(400, 5, 9);
uint16_t brightnesstheta16 = mPseudotime;
// set master brightness control
for (uint16_t i = 0; i < mNumLeds; i++) {
hue16 += hueinc16;
uint8_t hue8 = hue16 / 256;
brightnesstheta16 += brightnessthetainc16;
uint16_t b16 = sin16(brightnesstheta16) + 32768;
uint16_t bri16 = (fl::u32)((fl::u32)b16 * (fl::u32)b16) / 65536;
uint8_t bri8 = (fl::u32)(((fl::u32)bri16) * brightdepth) / 65536;
bri8 += (255 - brightdepth);
CRGB newcolor = CHSV(hue8, sat8, bri8);
uint16_t pixelnumber = (mNumLeds - 1) - i;
nblend(ctx.leds[pixelnumber], newcolor, 64);
}
}
} // namespace fl

View File

@@ -0,0 +1,306 @@
#pragma once
#include "FastLED.h"
#include "fl/namespace.h"
#include "fl/str.h"
#include "fx/fx1d.h"
namespace fl {
/// @file TwinkleFox.hpp
/// @brief Twinkling "holiday" lights that fade in and out.
/// @example TwinkleFox.ino
// TwinkleFOX: Twinkling 'holiday' lights that fade in and out.
// Colors are chosen from a palette; a few palettes are provided.
//
// This December 2015 implementation improves on the December 2014 version
// in several ways:
// - smoother fading, compatible with any colors and any palettes
// - easier control of twinkle speed and twinkle density
// - supports an optional 'background color'
// - takes even less RAM: zero RAM overhead per pixel
// - illustrates a couple of interesting techniques (uh oh...)
//
// The idea behind this (new) implementation is that there's one
// basic, repeating pattern that each pixel follows like a waveform:
// The brightness rises from 0..255 and then falls back down to 0.
// The brightness at any given point in time can be determined as
// as a function of time, for example:
// brightness = sine( time ); // a sine wave of brightness over time
//
// So the way this implementation works is that every pixel follows
// the exact same wave function over time. In this particular case,
// I chose a sawtooth triangle wave (triwave8) rather than a sine wave,
// but the idea is the same: brightness = triwave8( time ).
//
// The triangle wave function is a piecewise linear function that looks like:
//
// / \\
// / \\
// / \\
// / \\
//
// Of course, if all the pixels used the exact same wave form, and
// if they all used the exact same 'clock' for their 'time base', all
// the pixels would brighten and dim at once -- which does not look
// like twinkling at all.
//
// So to achieve random-looking twinkling, each pixel is given a
// slightly different 'clock' signal. Some of the clocks run faster,
// some run slower, and each 'clock' also has a random offset from zero.
// The net result is that the 'clocks' for all the pixels are always out
// of sync from each other, producing a nice random distribution
// of twinkles.
//
// The 'clock speed adjustment' and 'time offset' for each pixel
// are generated randomly. One (normal) approach to implementing that
// would be to randomly generate the clock parameters for each pixel
// at startup, and store them in some arrays. However, that consumes
// a great deal of precious RAM, and it turns out to be totally
// unnessary! If the random number generate is 'seeded' with the
// same starting value every time, it will generate the same sequence
// of values every time. So the clock adjustment parameters for each
// pixel are 'stored' in a pseudo-random number generator! The PRNG
// is reset, and then the first numbers out of it are the clock
// adjustment parameters for the first pixel, the second numbers out
// of it are the parameters for the second pixel, and so on.
// In this way, we can 'store' a stable sequence of thousands of
// random clock adjustment parameters in literally two bytes of RAM.
//
// There's a little bit of fixed-point math involved in applying the
// clock speed adjustments, which are expressed in eighths. Each pixel's
// clock speed ranges from 8/8ths of the system clock (i.e. 1x) to
// 23/8ths of the system clock (i.e. nearly 3x).
//
// On a basic Arduino Uno or Leonardo, this code can twinkle 300+ pixels
// smoothly at over 50 updates per seond.
//
// -Mark Kriegsman, December 2015
// Adapted for FastLED 3.x in August 2023 by Marlin Unruh
// TwinkleFox effect parameters
// Overall twinkle speed.
// 0 (VERY slow) to 8 (VERY fast).
// 4, 5, and 6 are recommended, default is 4.
#define TWINKLE_SPEED 4
// Overall twinkle density.
// 0 (NONE lit) to 8 (ALL lit at once).
// Default is 5.
#define TWINKLE_DENSITY 5
// How often to change color palettes.
#define SECONDS_PER_PALETTE 30
// If AUTO_SELECT_BACKGROUND_COLOR is set to 1,
// then for any palette where the first two entries
// are the same, a dimmed version of that color will
// automatically be used as the background color.
#define AUTO_SELECT_BACKGROUND_COLOR 0
// If COOL_LIKE_INCANDESCENT is set to 1, colors will
// fade out slighted 'reddened', similar to how
// incandescent bulbs change color as they get dim down.
#define COOL_LIKE_INCANDESCENT 1
FASTLED_SMART_PTR(TwinkleFox);
class TwinkleFox : public Fx1d {
public:
CRGBPalette16 targetPalette;
CRGBPalette16 currentPalette;
TwinkleFox(uint16_t num_leds)
: Fx1d(num_leds), backgroundColor(CRGB::Black),
twinkleSpeed(TWINKLE_SPEED), twinkleDensity(TWINKLE_DENSITY),
coolLikeIncandescent(COOL_LIKE_INCANDESCENT),
autoSelectBackgroundColor(AUTO_SELECT_BACKGROUND_COLOR) {
chooseNextColorPalette(targetPalette);
}
void draw(DrawContext context) override {
EVERY_N_MILLISECONDS(10) {
nblendPaletteTowardPalette(currentPalette, targetPalette, 12);
}
drawTwinkleFox(context.leds);
}
void chooseNextColorPalette(CRGBPalette16 &pal);
fl::string fxName() const override { return "TwinkleFox"; }
private:
CRGB backgroundColor;
uint8_t twinkleSpeed;
uint8_t twinkleDensity;
bool coolLikeIncandescent;
bool autoSelectBackgroundColor;
void drawTwinkleFox(CRGB *leds) {
// "PRNG16" is the pseudorandom number generator
// It MUST be reset to the same starting value each time
// this function is called, so that the sequence of 'random'
// numbers that it generates is (paradoxically) stable.
uint16_t PRNG16 = 11337;
fl::u32 clock32 = millis();
CRGB bg = backgroundColor;
if (autoSelectBackgroundColor &&
currentPalette[0] == currentPalette[1]) {
bg = currentPalette[0];
uint8_t bglight = bg.getAverageLight();
if (bglight > 64) {
bg.nscale8_video(16);
} else if (bglight > 16) {
bg.nscale8_video(64);
} else {
bg.nscale8_video(86);
}
}
uint8_t backgroundBrightness = bg.getAverageLight();
for (uint16_t i = 0; i < mNumLeds; i++) {
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384;
uint16_t myclockoffset16 = PRNG16;
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384;
uint8_t myspeedmultiplierQ5_3 =
((((PRNG16 & 0xFF) >> 4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08;
fl::u32 myclock30 =
(fl::u32)((clock32 * myspeedmultiplierQ5_3) >> 3) +
myclockoffset16;
uint8_t myunique8 = PRNG16 >> 8;
CRGB c = computeOneTwinkle(myclock30, myunique8);
uint8_t cbright = c.getAverageLight();
int16_t deltabright = cbright - backgroundBrightness;
if (deltabright >= 32 || (!bg)) {
leds[i] = c;
} else if (deltabright > 0) {
leds[i] = blend(bg, c, deltabright * 8);
} else {
leds[i] = bg;
}
}
}
CRGB computeOneTwinkle(fl::u32 ms, uint8_t salt) {
uint16_t ticks = ms >> (8 - twinkleSpeed);
uint8_t fastcycle8 = ticks;
uint16_t slowcycle16 = (ticks >> 8) + salt;
slowcycle16 += sin8(slowcycle16);
slowcycle16 = (slowcycle16 * 2053) + 1384;
uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8);
uint8_t bright = 0;
if (((slowcycle8 & 0x0E) / 2) < twinkleDensity) {
bright = attackDecayWave8(fastcycle8);
}
uint8_t hue = slowcycle8 - salt;
CRGB c;
if (bright > 0) {
c = ColorFromPalette(currentPalette, hue, bright, NOBLEND);
if (coolLikeIncandescent) {
coolLikeIncandescentFunction(c, fastcycle8);
}
} else {
c = CRGB::Black;
}
return c;
}
uint8_t attackDecayWave8(uint8_t i) {
if (i < 86) {
return i * 3;
} else {
i -= 86;
return 255 - (i + (i / 2));
}
}
void coolLikeIncandescentFunction(CRGB &c, uint8_t phase) {
if (phase < 128)
return;
uint8_t cooling = (phase - 128) >> 4;
c.g = qsub8(c.g, cooling);
c.b = qsub8(c.b, cooling * 2);
}
};
// Color palettes
// Color palette definitions
// A mostly red palette with green accents and white trim.
// "CRGB::Gray" is used as white to keep the brightness more uniform.
const TProgmemRGBPalette16 RedGreenWhite_p FL_PROGMEM = {
CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red,
CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
CRGB::Green, CRGB::Green, CRGB::Green, CRGB::Green};
const TProgmemRGBPalette16 Holly_p FL_PROGMEM = {
0x00580c, 0x00580c, 0x00580c, 0x00580c, 0x00580c, 0x00580c,
0x00580c, 0x00580c, 0x00580c, 0x00580c, 0x00580c, 0x00580c,
0x00580c, 0x00580c, 0x00580c, 0xB00402};
const TProgmemRGBPalette16 RedWhite_p FL_PROGMEM = {
CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
CRGB::Gray, CRGB::Gray, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red,
CRGB::Gray, CRGB::Gray, CRGB::Gray, CRGB::Gray};
const TProgmemRGBPalette16 BlueWhite_p FL_PROGMEM = {
CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue,
CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue,
CRGB::Blue, CRGB::Gray, CRGB::Gray, CRGB::Gray};
const TProgmemRGBPalette16 FairyLight_p = {
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight,
CRGB(CRGB::FairyLight).nscale8_constexpr(uint8_t(128)).as_uint32_t(),
CRGB(CRGB::FairyLight).nscale8_constexpr(uint8_t(128)).as_uint32_t(),
CRGB::FairyLight,
CRGB::FairyLight,
CRGB(CRGB::FairyLight).nscale8_constexpr(64).as_uint32_t(),
CRGB(CRGB::FairyLight).nscale8_constexpr(64).as_uint32_t(),
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight,
CRGB::FairyLight};
const TProgmemRGBPalette16 Snow_p FL_PROGMEM = {
0x304048, 0x304048, 0x304048, 0x304048, 0x304048, 0x304048,
0x304048, 0x304048, 0x304048, 0x304048, 0x304048, 0x304048,
0x304048, 0x304048, 0x304048, 0xE0F0FF};
const TProgmemRGBPalette16 RetroC9_p FL_PROGMEM = {
0xB80400, 0x902C02, 0xB80400, 0x902C02, 0x902C02, 0xB80400,
0x902C02, 0xB80400, 0x046002, 0x046002, 0x046002, 0x046002,
0x070758, 0x070758, 0x070758, 0x606820};
const TProgmemRGBPalette16 Ice_p FL_PROGMEM = {
0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040,
0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040, 0x0C1040,
0x182080, 0x182080, 0x182080, 0x5080C0};
// Add or remove palette names from this list to control which color
// palettes are used, and in what order.
const TProgmemRGBPalette16 *ActivePaletteList[] = {
&RetroC9_p, &BlueWhite_p, &RainbowColors_p, &FairyLight_p,
&RedGreenWhite_p, &PartyColors_p, &RedWhite_p, &Snow_p,
&Holly_p, &Ice_p};
void TwinkleFox::chooseNextColorPalette(CRGBPalette16 &pal) {
const uint8_t numberOfPalettes =
sizeof(ActivePaletteList) / sizeof(ActivePaletteList[0]);
static uint8_t whichPalette = -1;
whichPalette = addmod8(whichPalette, 1, numberOfPalettes);
pal = *(ActivePaletteList[whichPalette]);
}
} // namespace fl

View File

@@ -0,0 +1,81 @@
# FastLED FX: 2D Effects
This folder contains two-dimensional (matrix/panel) effects that derive from `fl::Fx2d`. 2D effects require an `XYMap` that translates a logical `(x, y)` coordinate into a linear LED index in your `CRGB*` matrix. For examples, most matrices are laid out in serpentine, back and forth layout. `XYMap` allows you to pretend an effect is a plain rectangular grid, then transform it to a matrix layout, (commonly serpentine back and forth.)
If youre new: construct the effect with your `XYMap` and call `draw(...)` each frame. The effect fills your LED buffer according to its internal animation.
### Whats here
- **RedSquare (`redsquare.h`)**: Simple centered red square demo. Good as a sanity check.
- **NoisePalette (`noisepalette.h`)**: 2D noise field mapped through color palettes. Includes helpers to randomize/select palettes and an optional fixed FPS hint.
- **WaveFx (`wave.h`)**: 2D wave simulation with configurable speed, dampening, supersampling, cylindrical wrapping, and color mapping via:
- `WaveCrgbMapDefault`: grayscale by wave amplitude.
- `WaveCrgbGradientMap`: map amplitude to a `CRGBPalette16` gradient.
- **ScaleUp (`scale_up.h`)**: Wrap another `Fx2d` and up-scale its output using bilinear interpolation. Useful when your MCU cant render full resolution in real time.
- **Blend2d (`blend.h`)**: Compose multiple `Fx2d` layers. Bottom layer draws at full strength; upper layers blend with optional blur passes.
- **Animartrix (`animartrix.hpp`, `animartrix_detail.hpp`)**: Integration of Stefen Petricks ANIMartRIX library providing many parameterized 2D animations. See Licensing below.
### Common usage
- Provide an `fl::XYMap` that matches your panel/strip weaving, then construct effects like `fl::NoisePalette mapFx(xyMap);`, `fl::WaveFx waves(xyMap);`, etc.
- Call `fx.draw(fl::Fx::DrawContext(now, leds));` each frame. Some effects (e.g., `NoisePalette`) advertise a fixed FPS via `hasFixedFrameRate` to help you pace the main loop.
- For `Blend2d`, create it with your `XYMap`, add `Fx2d` layers, optionally set per-layer blur, then call `draw(...)` on the blender.
- For `ScaleUp`, pass a low-res delegate `Fx2d`; it will render internally and upscale to your target resolution.
### Parameters to try
- WaveFx: `setSpeed`, `setDampening`, `setHalfDuplex`, `setSuperSample`, `setXCylindrical`, and switch CRGB mapping with `setCrgbMap(...)`.
- NoisePalette: `setPalette`, `setSpeed`, `setScale`, or `setPalettePreset(...)` to quickly swap among curated looks.
- Blend2d: `setGlobalBlurAmount`, `setGlobalBlurPasses`, or per-layer `Params` on `add(...)` / `setParams(...)`.
### Licensing note (Animartrix)
Animartrix is free for noncommercial use and requires a paid license otherwise. See the top-level `src/fx/readme` and `animartrix_detail.hpp` header comments.
### Key types referenced
- **`fl::Fx2d`**: Base for 2D effects, wraps an `XYMap` and provides `draw(DrawContext)`.
- **`fl::XYMap`**: Maps `(x, y)` to LED indices; can be rectangular or custom.
- **`CRGBPalette16`**: 16entry FastLED palette used by several mappers/effects.
These effects are designed for more capable MCUs, but many run well on modern 8bit boards at modest sizes. Start with `RedSquare` to validate mapping, then try `NoisePalette` and `WaveFx` for richer motion.
### Examples
- NoisePalette (from `examples/FxNoisePlusPalette/FxNoisePlusPalette.ino`):
```cpp
#include <FastLED.h>
#include "fx/2d/noisepalette.h"
using namespace fl;
#define W 16
#define H 16
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
XYMap xyMap(W, H, /* serpentine? */ true);
NoisePalette fx(xyMap);
void setup(){ FastLED.addLeds<WS2811, 3, GRB>(leds, NUM_LEDS); FastLED.setBrightness(96); }
void loop(){ fx.draw(Fx::DrawContext(millis(), leds)); FastLED.show(); }
```
- WaveFx layered with Blend2d (from `examples/FxWave2d/`):
```cpp
#include <FastLED.h>
#include "fx/2d/wave.h"
#include "fx/2d/blend.h"
using namespace fl;
#define W 64
#define H 64
CRGB leds[W*H];
XYMap xyMap(W, H, true);
Blend2d blender(xyMap);
auto lower = fl::make_shared<WaveFx>(xyMap, WaveFx::Args());
auto upper = fl::make_shared<WaveFx>(xyMap, WaveFx::Args());
void setup(){ FastLED.addLeds<WS2811, 3, GRB>(leds, W*H); blender.add(lower); blender.add(upper); }
void loop(){ blender.draw(Fx::DrawContext(millis(), leds)); FastLED.show(); }
```
- RedSquare (from concept in `redsquare.h`):
```cpp
#include <FastLED.h>
#include "fx/2d/redsquare.h"
using namespace fl;
#define W 16
#define H 16
CRGB leds[W*H];
XYMap xyMap(W, H, true);
RedSquare fx(xyMap);
void setup(){ FastLED.addLeds<WS2811,3,GRB>(leds, W*H); }
void loop(){ fx.draw(Fx::DrawContext(millis(), leds)); FastLED.show(); }
```

View File

@@ -0,0 +1,294 @@
#pragma once
// FastLED Adapter for the animartrix fx library.
// Copyright Stefen Petrick 2023.
// Adapted to C++ by Netmindz 2023.
// Adapted to FastLED by Zach Vorhies 2024.
// For details on the animartrix library and licensing information, see
// fx/aninamtrix_detail.hpp
#include "crgb.h"
#include "fl/dbg.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/unique_ptr.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "eorder.h"
#include "pixel_controller.h" // For RGB_BYTE_0, RGB_BYTE_1, RGB_BYTE_2
#define ANIMARTRIX_INTERNAL
#include "animartrix_detail.hpp"
namespace fl {
FASTLED_SMART_PTR(Animartrix);
enum AnimartrixAnim {
RGB_BLOBS5 = 0,
RGB_BLOBS4,
RGB_BLOBS3,
RGB_BLOBS2,
RGB_BLOBS,
POLAR_WAVES,
SLOW_FADE,
ZOOM2,
ZOOM,
HOT_BLOB,
SPIRALUS2,
SPIRALUS,
YVES,
SCALEDEMO1,
LAVA1,
CALEIDO3,
CALEIDO2,
CALEIDO1,
DISTANCE_EXPERIMENT,
CENTER_FIELD,
WAVES,
CHASING_SPIRALS,
ROTATING_BLOB,
RINGS,
COMPLEX_KALEIDO,
COMPLEX_KALEIDO_2,
COMPLEX_KALEIDO_3,
COMPLEX_KALEIDO_4,
COMPLEX_KALEIDO_5,
COMPLEX_KALEIDO_6,
WATER,
PARAMETRIC_WATER,
MODULE_EXPERIMENT1,
MODULE_EXPERIMENT2,
MODULE_EXPERIMENT3,
MODULE_EXPERIMENT4,
MODULE_EXPERIMENT5,
MODULE_EXPERIMENT6,
MODULE_EXPERIMENT7,
MODULE_EXPERIMENT8,
MODULE_EXPERIMENT9,
MODULE_EXPERIMENT10,
MODULE_EXPERIMENT_SM1,
MODULE_EXPERIMENT_SM2,
MODULE_EXPERIMENT_SM3,
MODULE_EXPERIMENT_SM4,
MODULE_EXPERIMENT_SM5,
MODULE_EXPERIMENT_SM6,
MODULE_EXPERIMENT_SM8,
MODULE_EXPERIMENT_SM9,
MODULE_EXPERIMENT_SM10,
NUM_ANIMATIONS
};
fl::string getAnimartrixName(int animation);
class FastLEDANIMartRIX;
class Animartrix : public Fx2d {
public:
Animartrix(const XYMap& xyMap, AnimartrixAnim which_animation) : Fx2d(xyMap) {
// Note: Swapping out height and width.
this->current_animation = which_animation;
mXyMap.convertToLookUpTable();
}
Animartrix(const Animartrix &) = delete;
void draw(DrawContext context) override;
int fxNum() const { return NUM_ANIMATIONS; }
void fxSet(int fx);
int fxGet() const { return static_cast<int>(current_animation); }
Str fxName() const override { return "Animartrix:"; }
void fxNext(int fx = 1) { fxSet(fxGet() + fx); }
void setColorOrder(EOrder order) { color_order = order; }
EOrder getColorOrder() const { return color_order; }
private:
friend void AnimartrixLoop(Animartrix &self, fl::u32 now);
friend class FastLEDANIMartRIX;
static const char *getAnimartrixName(AnimartrixAnim animation);
AnimartrixAnim prev_animation = NUM_ANIMATIONS;
fl::unique_ptr<FastLEDANIMartRIX> impl;
CRGB *leds = nullptr; // Only set during draw, then unset back to nullptr.
AnimartrixAnim current_animation = RGB_BLOBS5;
EOrder color_order = RGB;
};
void AnimartrixLoop(Animartrix &self, fl::u32 now);
/// ##################################################
/// Details with the implementation of Animartrix
struct AnimartrixEntry {
AnimartrixAnim anim;
const char *name;
void (FastLEDANIMartRIX::*func)();
};
class FastLEDANIMartRIX : public animartrix_detail::ANIMartRIX {
Animartrix *data = nullptr;
public:
FastLEDANIMartRIX(Animartrix *_data) {
this->data = _data;
this->init(data->getWidth(), data->getHeight());
}
void setPixelColor(int x, int y, CRGB pixel) {
data->leds[xyMap(x, y)] = pixel;
}
void setPixelColorInternal(int x, int y,
animartrix_detail::rgb pixel) override {
setPixelColor(x, y, CRGB(pixel.red, pixel.green, pixel.blue));
}
uint16_t xyMap(uint16_t x, uint16_t y) override {
return data->xyMap(x, y);
}
void loop();
};
void Animartrix::fxSet(int fx) {
int curr = fxGet();
if (fx < 0) {
fx = curr + fx;
if (fx < 0) {
fx = NUM_ANIMATIONS - 1;
}
}
fx = fx % NUM_ANIMATIONS;
current_animation = static_cast<AnimartrixAnim>(fx);
FASTLED_DBG("Setting animation to " << getAnimartrixName(current_animation));
}
void AnimartrixLoop(Animartrix &self, fl::u32 now) {
if (self.prev_animation != self.current_animation) {
if (self.impl) {
// Re-initialize object.
self.impl->init(self.getWidth(), self.getHeight());
}
self.prev_animation = self.current_animation;
}
if (!self.impl) {
self.impl.reset(new FastLEDANIMartRIX(&self));
}
self.impl->setTime(now);
self.impl->loop();
}
static const AnimartrixEntry ANIMATION_TABLE[] = {
{RGB_BLOBS5, "RGB_BLOBS5", &FastLEDANIMartRIX::RGB_Blobs5},
{RGB_BLOBS4, "RGB_BLOBS4", &FastLEDANIMartRIX::RGB_Blobs4},
{RGB_BLOBS3, "RGB_BLOBS3", &FastLEDANIMartRIX::RGB_Blobs3},
{RGB_BLOBS2, "RGB_BLOBS2", &FastLEDANIMartRIX::RGB_Blobs2},
{RGB_BLOBS, "RGB_BLOBS", &FastLEDANIMartRIX::RGB_Blobs},
{POLAR_WAVES, "POLAR_WAVES", &FastLEDANIMartRIX::Polar_Waves},
{SLOW_FADE, "SLOW_FADE", &FastLEDANIMartRIX::Slow_Fade},
{ZOOM2, "ZOOM2", &FastLEDANIMartRIX::Zoom2},
{ZOOM, "ZOOM", &FastLEDANIMartRIX::Zoom},
{HOT_BLOB, "HOT_BLOB", &FastLEDANIMartRIX::Hot_Blob},
{SPIRALUS2, "SPIRALUS2", &FastLEDANIMartRIX::Spiralus2},
{SPIRALUS, "SPIRALUS", &FastLEDANIMartRIX::Spiralus},
{YVES, "YVES", &FastLEDANIMartRIX::Yves},
{SCALEDEMO1, "SCALEDEMO1", &FastLEDANIMartRIX::Scaledemo1},
{LAVA1, "LAVA1", &FastLEDANIMartRIX::Lava1},
{CALEIDO3, "CALEIDO3", &FastLEDANIMartRIX::Caleido3},
{CALEIDO2, "CALEIDO2", &FastLEDANIMartRIX::Caleido2},
{CALEIDO1, "CALEIDO1", &FastLEDANIMartRIX::Caleido1},
{DISTANCE_EXPERIMENT, "DISTANCE_EXPERIMENT",
&FastLEDANIMartRIX::Distance_Experiment},
{CENTER_FIELD, "CENTER_FIELD", &FastLEDANIMartRIX::Center_Field},
{WAVES, "WAVES", &FastLEDANIMartRIX::Waves},
{CHASING_SPIRALS, "CHASING_SPIRALS", &FastLEDANIMartRIX::Chasing_Spirals},
{ROTATING_BLOB, "ROTATING_BLOB", &FastLEDANIMartRIX::Rotating_Blob},
{RINGS, "RINGS", &FastLEDANIMartRIX::Rings},
{COMPLEX_KALEIDO, "COMPLEX_KALEIDO", &FastLEDANIMartRIX::Complex_Kaleido},
{COMPLEX_KALEIDO_2, "COMPLEX_KALEIDO_2",
&FastLEDANIMartRIX::Complex_Kaleido_2},
{COMPLEX_KALEIDO_3, "COMPLEX_KALEIDO_3",
&FastLEDANIMartRIX::Complex_Kaleido_3},
{COMPLEX_KALEIDO_4, "COMPLEX_KALEIDO_4",
&FastLEDANIMartRIX::Complex_Kaleido_4},
{COMPLEX_KALEIDO_5, "COMPLEX_KALEIDO_5",
&FastLEDANIMartRIX::Complex_Kaleido_5},
{COMPLEX_KALEIDO_6, "COMPLEX_KALEIDO_6",
&FastLEDANIMartRIX::Complex_Kaleido_6},
{WATER, "WATER", &FastLEDANIMartRIX::Water},
{PARAMETRIC_WATER, "PARAMETRIC_WATER",
&FastLEDANIMartRIX::Parametric_Water},
{MODULE_EXPERIMENT1, "MODULE_EXPERIMENT1",
&FastLEDANIMartRIX::Module_Experiment1},
{MODULE_EXPERIMENT2, "MODULE_EXPERIMENT2",
&FastLEDANIMartRIX::Module_Experiment2},
{MODULE_EXPERIMENT3, "MODULE_EXPERIMENT3",
&FastLEDANIMartRIX::Module_Experiment3},
{MODULE_EXPERIMENT4, "MODULE_EXPERIMENT4",
&FastLEDANIMartRIX::Module_Experiment4},
{MODULE_EXPERIMENT5, "MODULE_EXPERIMENT5",
&FastLEDANIMartRIX::Module_Experiment5},
{MODULE_EXPERIMENT6, "MODULE_EXPERIMENT6",
&FastLEDANIMartRIX::Module_Experiment6},
{MODULE_EXPERIMENT7, "MODULE_EXPERIMENT7",
&FastLEDANIMartRIX::Module_Experiment7},
{MODULE_EXPERIMENT8, "MODULE_EXPERIMENT8",
&FastLEDANIMartRIX::Module_Experiment8},
{MODULE_EXPERIMENT9, "MODULE_EXPERIMENT9",
&FastLEDANIMartRIX::Module_Experiment9},
{MODULE_EXPERIMENT10, "MODULE_EXPERIMENT10",
&FastLEDANIMartRIX::Module_Experiment10},
{MODULE_EXPERIMENT_SM1, "MODULE_EXPERIMENT_SM1", &FastLEDANIMartRIX::SM1},
{MODULE_EXPERIMENT_SM2, "MODULE_EXPERIMENT_SM2", &FastLEDANIMartRIX::SM2},
{MODULE_EXPERIMENT_SM3, "MODULE_EXPERIMENT_SM3", &FastLEDANIMartRIX::SM3},
{MODULE_EXPERIMENT_SM4, "MODULE_EXPERIMENT_SM4", &FastLEDANIMartRIX::SM4},
{MODULE_EXPERIMENT_SM5, "MODULE_EXPERIMENT_SM5", &FastLEDANIMartRIX::SM5},
{MODULE_EXPERIMENT_SM6, "MODULE_EXPERIMENT_SM6", &FastLEDANIMartRIX::SM6},
{MODULE_EXPERIMENT_SM8, "MODULE_EXPERIMENT_SM8", &FastLEDANIMartRIX::SM8},
{MODULE_EXPERIMENT_SM9, "MODULE_EXPERIMENT_SM9", &FastLEDANIMartRIX::SM9},
{MODULE_EXPERIMENT_SM10, "MODULE_EXPERIMENT_SM10",
&FastLEDANIMartRIX::SM10},
};
fl::string getAnimartrixName(int animation) {
if (animation < 0 || animation >= NUM_ANIMATIONS) {
return "UNKNOWN";
}
return ANIMATION_TABLE[animation].name;
}
void FastLEDANIMartRIX::loop() {
for (const auto &entry : ANIMATION_TABLE) {
if (entry.anim == data->current_animation) {
(this->*entry.func)();
return;
}
}
// (this->*ANIMATION_TABLE[index].func)();
FASTLED_DBG("Animation not found for " << int(data->current_animation));
}
const char *Animartrix::getAnimartrixName(AnimartrixAnim animation) {
for (const auto &entry : ANIMATION_TABLE) {
if (entry.anim == animation) {
return entry.name;
}
}
FASTLED_DBG("Animation not found for " << int(animation));
return "UNKNOWN";
}
void Animartrix::draw(DrawContext ctx) {
this->leds = ctx.leds;
AnimartrixLoop(*this, ctx.now);
if (color_order != RGB) {
for (int i = 0; i < mXyMap.getTotal(); ++i) {
CRGB &pixel = ctx.leds[i];
const uint8_t b0_index = RGB_BYTE0(color_order);
const uint8_t b1_index = RGB_BYTE1(color_order);
const uint8_t b2_index = RGB_BYTE2(color_order);
pixel = CRGB(pixel.raw[b0_index], pixel.raw[b1_index],
pixel.raw[b2_index]);
}
}
this->leds = nullptr;
}
} // namespace fl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
/*
Fx2d class that allows to blend multiple Fx2d layers together.
The bottom layer is always drawn at full capacity. Upper layers
are blended by the the max luminance of the components.
*/
#include "blend.h"
#include "colorutils.h"
#include "pixelset.h"
#include "fl/stdint.h"
namespace fl {
Blend2d::Blend2d(const XYMap &xymap) : Fx2d(xymap) {
// Warning, the xyMap will be the final transrformation applied to the
// frame. If the delegate Fx2d layers have their own transformation then
// both will be applied.
mFrame = fl::make_shared<Frame>(mXyMap.getTotal());
mFrameTransform = fl::make_shared<Frame>(mXyMap.getTotal());
}
Str Blend2d::fxName() const {
fl::string out = "LayeredFx2d(";
for (size_t i = 0; i < mLayers.size(); ++i) {
out += mLayers[i].fx->fxName();
if (i != mLayers.size() - 1) {
out += ",";
}
}
out += ")";
return out;
}
void Blend2d::add(Fx2dPtr layer, const Params &p) {
if (!layer->getXYMap().isRectangularGrid()) {
if (!getXYMap().isRectangularGrid()) {
FASTLED_WARN("Blend2d has a xymap, but so does the Sub layer " << layer->fxName() << ", the sub layer will have it's map replaced with a rectangular map, to avoid double transformation.");
layer->setXYMap(XYMap::constructRectangularGrid(layer->getWidth(), layer->getHeight()));
}
}
uint8_t blurAmount = p.blur_amount;
uint8_t blurPasses = p.blur_passes;
Entry entry(layer, blurAmount, blurPasses);
mLayers.push_back(entry);
}
void Blend2d::add(Fx2d &layer, const Params &p) {
Fx2dPtr fx = fl::make_shared_no_tracking(layer);
this->add(fx, p);
}
void Blend2d::draw(DrawContext context) {
mFrame->clear();
mFrameTransform->clear();
// Draw each layer in reverse order and applying the blending.
bool first = true;
for (auto it = mLayers.begin(); it != mLayers.end(); ++it) {
DrawContext tmp_ctx = context;
tmp_ctx.leds = mFrame->rgb();
auto &fx = it->fx;
fx->draw(tmp_ctx);
DrawMode mode = first ? DrawMode::DRAW_MODE_OVERWRITE
: DrawMode::DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS;
first = false;
// Apply the blur effect per effect.
uint8_t blur_amount = it->blur_amount;
if (blur_amount > 0) {
const XYMap &xyMap = fx->getXYMap();
uint8_t blur_passes = MAX(1, it->blur_passes);
for (uint8_t i = 0; i < blur_passes; ++i) {
// Apply the blur effect
blur2d(mFrame->rgb(), mXyMap.getWidth(), mXyMap.getHeight(),
blur_amount, xyMap);
}
}
mFrame->draw(mFrameTransform->rgb(), mode);
}
if (mGlobalBlurAmount > 0) {
// Apply the blur effect
uint16_t width = mXyMap.getWidth();
uint16_t height = mXyMap.getHeight();
XYMap rect = XYMap::constructRectangularGrid(width, height);
CRGB *rgb = mFrameTransform->rgb();
uint8_t blur_passes = MAX(1, mGlobalBlurPasses);
for (uint8_t i = 0; i < blur_passes; ++i) {
// Apply the blur effect
blur2d(rgb, width, height, mGlobalBlurAmount, rect);
}
}
// Copy the final result to the output
// memcpy(mFrameTransform->rgb(), context.leds, sizeof(CRGB) *
// mXyMap.getTotal());
mFrameTransform->drawXY(context.leds, mXyMap,
DrawMode::DRAW_MODE_OVERWRITE);
}
void Blend2d::clear() { mLayers.clear(); }
bool Blend2d::setParams(Fx2dPtr fx, const Params &p) {
uint8_t blur_amount = p.blur_amount;
uint8_t blur_passes = p.blur_passes;
for (auto &layer : mLayers) {
if (layer.fx == fx) {
layer.blur_amount = blur_amount;
layer.blur_passes = blur_passes;
return true;
}
}
FASTLED_WARN("Fx2d not found in Blend2d::setBlurParams");
return false;
}
bool Blend2d::setParams(Fx2d &fx, const Params &p) {
Fx2dPtr fxPtr = fl::make_shared_no_tracking(fx);
return setParams(fxPtr, p);
}
} // namespace fl

View File

@@ -0,0 +1,65 @@
/*
Fx2d class that allows to blend multiple Fx2d layers together.
The bottom layer is always drawn at full capacity. Upper layers
are blended by the the max luminance of the components.
*/
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/vector.h"
#include "fl/warn.h"
#include "fl/xymap.h"
#include "fx/frame.h"
#include "fx/fx.h"
#include "fx/fx2d.h"
namespace fl {
struct Blend2dParams {
uint8_t blur_amount = 0;
uint8_t blur_passes = 1;
};
FASTLED_SMART_PTR(Blend2d);
class Blend2d : public Fx2d {
public:
using Params = Blend2dParams;
// Note that if this xymap is non rectangular then it's recommended that the
// Fx2d layers that are added should be rectangular.
Blend2d(const XYMap &xymap);
fl::string fxName() const override;
void add(Fx2dPtr layer, const Params &p = Params());
void add(Fx2d &layer, const Params &p = Params());
void draw(DrawContext context) override;
void clear();
void setGlobalBlurAmount(uint8_t blur_amount) {
mGlobalBlurAmount = blur_amount;
}
void setGlobalBlurPasses(uint8_t blur_passes) {
mGlobalBlurPasses = blur_passes;
}
bool setParams(Fx2dPtr fx, const Params &p);
bool setParams(Fx2d &fx, const Params &p);
protected:
struct Entry {
Fx2dPtr fx;
uint8_t blur_amount = 0;
uint8_t blur_passes = 1;
Entry() = default;
Entry(Fx2dPtr fx, uint8_t blur_amount, uint8_t blur_passes)
: fx(fx), blur_amount(blur_amount), blur_passes(blur_passes) {}
};
HeapVector<Entry> mLayers;
FramePtr mFrame;
FramePtr mFrameTransform;
uint8_t mGlobalBlurAmount = 0;
uint8_t mGlobalBlurPasses = 1;
};
} // namespace fl

View File

@@ -0,0 +1,128 @@
#ifndef FASTLED_INTERNAL
#define FASTLED_INTERNAL
#endif
#include "luminova.h"
namespace fl {
Luminova::Luminova(const XYMap &xyMap, const Params &params)
: Fx2d(xyMap), mParams(params) {
int cap = params.max_particles;
if (cap <= 0) {
cap = 1;
}
mParticles.resize(static_cast<size_t>(cap));
// Initialize as dead
for (size_t i = 0; i < mParticles.size(); ++i) {
mParticles[i].alive = false;
}
}
void Luminova::setMaxParticles(int max_particles) {
if (max_particles <= 0) {
max_particles = 1;
}
if (static_cast<int>(mParticles.size()) == max_particles) {
return;
}
mParticles.clear();
mParticles.resize(static_cast<size_t>(max_particles));
for (size_t i = 0; i < mParticles.size(); ++i) {
mParticles[i].alive = false;
}
}
void Luminova::resetParticle(Particle &p, fl::u32 tt) {
// Position at center
const float cx = static_cast<float>(getWidth() - 1) * 0.5f;
const float cy = static_cast<float>(getHeight() - 1) * 0.5f;
p.x = cx;
p.y = cy;
// Original used noise(I)*W, we approximate with 1D noise scaled to width
int I = static_cast<int>(tt / 50);
uint8_t n1 = inoise8(static_cast<uint16_t>(I * 19));
float noiseW = (static_cast<float>(n1) / 255.0f) * static_cast<float>(getWidth());
p.a = static_cast<float>(tt) * 1.25f + noiseW;
p.f = (tt & 1u) ? +1 : -1;
p.g = I;
p.s = 3.0f;
p.alive = true;
}
void Luminova::plotDot(CRGB *leds, int x, int y, uint8_t v) const {
if (!mXyMap.has(x, y)) {
return;
}
const uint16_t idx = mXyMap.mapToIndex(static_cast<uint16_t>(x), static_cast<uint16_t>(y));
leds[idx] += CHSV(0, 0, scale8(v, mParams.point_gain));
}
void Luminova::plotSoftDot(CRGB *leds, float fx, float fy, float s) const {
// Map s (decays from ~3) to a pixel radius 1..3
float r = fl::clamp<float>(s * 0.5f, 1.0f, 3.0f);
int R = static_cast<int>(fl::ceil(r));
// Avoid roundf() to prevent AVR macro conflicts; do manual symmetric rounding
int cx = static_cast<int>(fx + (fx >= 0.0f ? 0.5f : -0.5f));
int cy = static_cast<int>(fy + (fy >= 0.0f ? 0.5f : -0.5f));
float r2 = r * r;
for (int dy = -R; dy <= R; ++dy) {
for (int dx = -R; dx <= R; ++dx) {
float d2 = static_cast<float>(dx * dx + dy * dy);
if (d2 <= r2) {
float fall = 1.0f - (d2 / (r2 + 0.0001f));
float vf = 255.0f * fall;
if (vf < 0.0f) vf = 0.0f;
if (vf > 255.0f) vf = 255.0f;
uint8_t v = static_cast<uint8_t>(vf);
plotDot(leds, cx + dx, cy + dy, v);
}
}
}
}
void Luminova::draw(DrawContext context) {
// Fade + blur trails each frame
fadeToBlackBy(context.leds, getNumLeds(), mParams.fade_amount);
blur2d(context.leds, static_cast<fl::u8>(getWidth()), static_cast<fl::u8>(getHeight()),
mParams.blur_amount, mXyMap);
// Spawn/overwrite one particle per frame, round-robin across pool
if (!mParticles.empty()) {
size_t idx = static_cast<size_t>(mTick % static_cast<fl::u32>(mParticles.size()));
resetParticle(mParticles[idx], mTick);
}
// Update and draw all particles
for (size_t i = 0; i < mParticles.size(); ++i) {
Particle &p = mParticles[i];
if (!p.alive) {
continue;
}
// s *= 0.997
p.s *= 0.997f;
if (p.s < 0.5f) {
p.alive = false;
continue;
}
// angle jitter using 2D noise: (t/99, g)
float tOver99 = static_cast<float>(mTick) / 99.0f;
uint8_t n2 = inoise8(static_cast<uint16_t>(tOver99 * 4096.0f), static_cast<uint16_t>(p.g * 37));
float n2c = (static_cast<int>(n2) - 128) / 255.0f; // ~ -0.5 .. +0.5
p.a += (n2c) / 9.0f;
float aa = p.a * static_cast<float>(p.f);
p.x += ::cosf(aa);
p.y += ::sinf(aa);
plotSoftDot(context.leds, p.x, p.y, p.s);
}
++mTick;
}
} // namespace fl

View File

@@ -0,0 +1,68 @@
#pragma once
#include "fl/stdint.h"
#include "FastLED.h"
#include "fl/blur.h"
#include "fl/clamp.h"
#include "fl/math.h"
#include "fl/memory.h"
#include "fl/vector.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "noise.h"
namespace fl {
FASTLED_SMART_PTR(Luminova);
struct LuminovaParams {
// Global fade amount applied each frame (higher = faster fade)
uint8_t fade_amount = 18;
// Blur amount applied each frame for trail softness
uint8_t blur_amount = 24;
// Per-dot gain applied to plotted pixels to prevent blowout on small grids
uint8_t point_gain = 128; // 50%
// Number of particles alive in the system (upper bound)
int max_particles = 256;
};
// 2D particle field with soft white trails inspired by the Luminova example.
class Luminova : public Fx2d {
public:
using Params = LuminovaParams;
explicit Luminova(const XYMap &xyMap, const Params &params = Params());
void draw(DrawContext context) override;
fl::string fxName() const override { return "Luminova"; }
void setFadeAmount(uint8_t fade_amount) { mParams.fade_amount = fade_amount; }
void setBlurAmount(uint8_t blur_amount) { mParams.blur_amount = blur_amount; }
void setPointGain(uint8_t point_gain) { mParams.point_gain = point_gain; }
// Adjust maximum particle slots (reinitializes pool if size changes)
void setMaxParticles(int max_particles);
private:
struct Particle {
float x = 0.0f;
float y = 0.0f;
float a = 0.0f; // angle
int f = 0; // direction (+1 or -1)
int g = 0; // group id (derived from time)
float s = 0.0f; // stroke weight / intensity
bool alive = false;
};
void resetParticle(Particle &p, fl::u32 tick);
void plotDot(CRGB *leds, int x, int y, uint8_t v) const;
void plotSoftDot(CRGB *leds, float fx, float fy, float s) const;
Params mParams;
fl::u32 mTick = 0;
fl::vector<Particle> mParticles;
};
} // namespace fl

View File

@@ -0,0 +1,195 @@
#include "fl/stdint.h"
#ifndef FASTLED_INTERNAL
#define FASTLED_INTERNAL
#endif
#include "FastLED.h"
#include "fl/memory.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "lib8tion/random8.h"
#include "noise.h"
#include "noisepalette.h"
namespace fl {
NoisePalette::NoisePalette(XYMap xyMap, float fps)
: Fx2d(xyMap), speed(0), scale(0), colorLoop(1), mFps(fps) {
// currentPalette = PartyColors_p;
static_assert(sizeof(currentPalette) == sizeof(CRGBPalette16),
"Palette size mismatch");
currentPalette = PartyColors_p;
width = xyMap.getWidth();
height = xyMap.getHeight();
// Initialize our coordinates to some random values
mX = random16();
mY = random16();
mZ = random16();
setPalettePreset(0);
// Allocate memory for the noise array using vector
noise.resize(width * height);
}
void NoisePalette::setPalettePreset(int paletteIndex) {
currentPaletteIndex = paletteIndex % 12; // Ensure the index wraps around
switch (currentPaletteIndex) {
case 0:
currentPalette = RainbowColors_p;
speed = 20;
scale = 30;
colorLoop = 1;
break;
case 1:
SetupPurpleAndGreenPalette();
speed = 10;
scale = 50;
colorLoop = 1;
break;
case 2:
SetupBlackAndWhiteStripedPalette();
speed = 20;
scale = 30;
colorLoop = 1;
break;
case 3:
currentPalette = ForestColors_p;
speed = 8;
scale = 120;
colorLoop = 0;
break;
case 4:
currentPalette = CloudColors_p;
speed = 4;
scale = 30;
colorLoop = 0;
break;
case 5:
currentPalette = LavaColors_p;
speed = 8;
scale = 50;
colorLoop = 0;
break;
case 6:
currentPalette = OceanColors_p;
speed = 20;
scale = 90;
colorLoop = 0;
break;
case 7:
currentPalette = PartyColors_p;
speed = 20;
scale = 30;
colorLoop = 1;
break;
case 8:
case 9:
case 10:
SetupRandomPalette();
speed = 20 + (currentPaletteIndex - 8) * 5;
scale = 20 + (currentPaletteIndex - 8) * 5;
colorLoop = 1;
break;
case 11:
currentPalette = RainbowStripeColors_p;
speed = 2;
scale = 20;
colorLoop = 1;
break;
default:
break;
}
}
void NoisePalette::mapNoiseToLEDsUsingPalette(CRGB *leds) {
static uint8_t ihue = 0;
for (uint16_t i = 0; i < width; i++) {
for (uint16_t j = 0; j < height; j++) {
// We use the value at the (i,j) coordinate in the noise
// array for our brightness, and the flipped value from (j,i)
// for our pixel's index into the color palette.
uint8_t index = noise[i * height + j];
uint8_t bri = noise[j * width + i];
// if this palette is a 'loop', add a slowly-changing base value
if (colorLoop) {
index += ihue;
}
// brighten up, as the color palette itself often contains the
// light/dark dynamic range desired
if (bri > 127) {
bri = 255;
} else {
bri = dim8_raw(bri * 2);
}
CRGB color = ColorFromPalette(currentPalette, index, bri);
leds[XY(i, j)] = color;
}
}
ihue += 1;
}
void NoisePalette::fillnoise8() {
// If we're running at a low "speed", some 8-bit artifacts become
// visible from frame-to-frame. In order to reduce this, we can do some
// fast data-smoothing. The amount of data smoothing we're doing depends
// on "speed".
uint8_t dataSmoothing = 0;
if (speed < 50) {
dataSmoothing = 200 - (speed * 4);
}
for (uint16_t i = 0; i < width; i++) {
int ioffset = scale * i;
for (uint16_t j = 0; j < height; j++) {
int joffset = scale * j;
uint8_t data = inoise8(mX + ioffset, mY + joffset, mZ);
// The range of the inoise8 function is roughly 16-238.
// These two operations expand those values out to roughly
// 0..255 You can comment them out if you want the raw noise
// data.
data = qsub8(data, 16);
data = qadd8(data, scale8(data, 39));
if (dataSmoothing) {
uint8_t olddata = noise[i * height + j];
uint8_t newdata = scale8(olddata, dataSmoothing) +
scale8(data, 256 - dataSmoothing);
data = newdata;
}
noise[i * height + j] = data;
}
}
mZ += speed;
// apply slow drift to X and Y, just for visual variation.
mX += speed / 8;
mY -= speed / 16;
}
uint8_t NoisePalette::changeToRandomPalette() {
while (true) {
uint8_t new_idx = random8() % 12;
if (new_idx == currentPaletteIndex) {
continue;
}
currentPaletteIndex = new_idx;
setPalettePreset(currentPaletteIndex);
return currentPaletteIndex;
}
}
} // namespace fl

View File

@@ -0,0 +1,103 @@
/// @file noisepalette.h
/// @brief Demonstrates how to mix noise generation with color palettes on a
/// 2D LED matrix
#pragma once
#include "fl/stdint.h"
#include "FastLED.h"
#include "fl/memory.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "fx/time.h"
#include "lib8tion/random8.h"
#include "noise.h"
namespace fl {
FASTLED_SMART_PTR(NoisePalette);
class NoisePalette : public Fx2d {
public:
// Fps is used by the fx_engine to maintain a fixed frame rate, ignored
// otherwise.
NoisePalette(XYMap xyMap, float fps = 60.f);
bool hasFixedFrameRate(float *fps) const override {
*fps = mFps;
return true;
}
// No need for a destructor, scoped_ptr will handle memory deallocation
void draw(DrawContext context) override {
fillnoise8();
mapNoiseToLEDsUsingPalette(context.leds);
}
Str fxName() const override { return "NoisePalette"; }
void mapNoiseToLEDsUsingPalette(CRGB *leds);
uint8_t changeToRandomPalette();
// There are 12 palette indexes but they don't have names. Use this to set
// which one you want.
uint8_t getPalettePresetCount() const { return 12; }
uint8_t getPalettePreset() const { return currentPaletteIndex; }
void setPalettePreset(int paletteIndex);
void setPalette(const CRGBPalette16 &palette, uint16_t speed,
uint16_t scale, bool colorLoop) {
currentPalette = palette;
this->speed = speed;
this->scale = scale;
this->colorLoop = colorLoop;
}
void setSpeed(uint16_t speed) { this->speed = speed; }
void setScale(uint16_t scale) { this->scale = scale; }
private:
uint16_t mX, mY, mZ;
uint16_t width, height;
uint16_t speed = 0;
uint16_t scale = 0;
fl::vector<uint8_t, fl::allocator_psram<uint8_t>> noise;
CRGBPalette16 currentPalette;
bool colorLoop = 0;
int currentPaletteIndex = 0;
float mFps = 60.f;
void fillnoise8();
uint16_t XY(uint8_t x, uint8_t y) const { return mXyMap.mapToIndex(x, y); }
void SetupRandomPalette() {
CRGBPalette16 newPalette;
do {
newPalette = CRGBPalette16(
CHSV(random8(), 255, 32), CHSV(random8(), 255, 255),
CHSV(random8(), 128, 255), CHSV(random8(), 255, 255));
} while (newPalette == currentPalette);
currentPalette = newPalette;
}
void SetupBlackAndWhiteStripedPalette() {
fill_solid(currentPalette, 16, CRGB::Black);
currentPalette[0] = CRGB::White;
currentPalette[4] = CRGB::White;
currentPalette[8] = CRGB::White;
currentPalette[12] = CRGB::White;
}
void SetupPurpleAndGreenPalette() {
CRGB purple = CHSV(HUE_PURPLE, 255, 255);
CRGB green = CHSV(HUE_GREEN, 255, 255);
CRGB black = CRGB::Black;
currentPalette = CRGBPalette16(
green, green, black, black, purple, purple, black, black, green,
green, black, black, purple, purple, black, black);
}
};
} // namespace fl

View File

@@ -0,0 +1,46 @@
/// @brief Implements a simple red square effect for 2D LED grids.
#pragma once
#include "FastLED.h"
#include "fl/memory.h"
#include "fx/fx2d.h"
namespace fl {
FASTLED_SMART_PTR(RedSquare);
class RedSquare : public Fx2d {
public:
struct Math {
template <typename T> static T Min(T a, T b) { return a < b ? a : b; }
};
RedSquare(const XYMap& xymap) : Fx2d(xymap) {}
void draw(DrawContext context) override {
uint16_t width = getWidth();
uint16_t height = getHeight();
uint16_t square_size = Math::Min(width, height) / 2;
uint16_t start_x = (width - square_size) / 2;
uint16_t start_y = (height - square_size) / 2;
for (uint16_t x = 0; x < width; x++) {
for (uint16_t y = 0; y < height; y++) {
uint16_t idx = mXyMap.mapToIndex(x, y);
if (idx < mXyMap.getTotal()) {
if (x >= start_x && x < start_x + square_size &&
y >= start_y && y < start_y + square_size) {
context.leds[idx] = CRGB::Red;
} else {
context.leds[idx] = CRGB::Black;
}
}
}
}
}
fl::string fxName() const override { return "red_square"; }
};
} // namespace fl

View File

@@ -0,0 +1,92 @@
#include "fl/stdint.h"
#define FASTLED_INTERNAL
#include "FastLED.h"
#include "fl/upscale.h"
#include "fl/memory.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "lib8tion/random8.h"
#include "noise.h"
// Include here so that #define PI used in Arduino.h does not produce a warning.
#include "scale_up.h"
// Optimized for 2^n grid sizes in terms of both memory and performance.
// If you are somehow running this on AVR then you probably want this if
// you can make your grid size a power of 2.
#define FASTLED_SCALE_UP_ALWAYS_POWER_OF_2 0 // 0 for always power of 2.
// Uses more memory than FASTLED_SCALE_UP_ALWAYS_POWER_OF_2 but can handle
// arbitrary grid sizes.
#define FASTLED_SCALE_UP_HIGH_PRECISION 1 // 1 for always choose high precision.
// Uses the most executable memory because both low and high precision versions
// are compiled in. If the grid size is a power of 2 then the faster version is
// used. Note that the floating point version has to be directly specified
// because in testing it offered no benefits over the integer versions.
#define FASTLED_SCALE_UP_DECIDE_AT_RUNTIME 2 // 2 for runtime decision.
#define FASTLED_SCALE_UP_FORCE_FLOATING_POINT 3 // Warning, this is slow.
#ifndef FASTLED_SCALE_UP
#define FASTLED_SCALE_UP FASTLED_SCALE_UP_DECIDE_AT_RUNTIME
#endif
namespace fl {
ScaleUp::ScaleUp(const XYMap& xymap, Fx2dPtr fx) : Fx2d(xymap), mDelegate(fx) {
// Turn off re-mapping of the delegate's XYMap, since bilinearExpand needs
// to work in screen coordinates. The final mapping will for this class will
// still be performed.
mDelegate->getXYMap().setRectangularGrid();
}
void ScaleUp::draw(DrawContext context) {
if (mSurface.empty()) {
mSurface.resize(mDelegate->getNumLeds());
}
DrawContext delegateContext = context;
delegateContext.leds = mSurface.data();
mDelegate->draw(delegateContext);
uint16_t in_w = mDelegate->getWidth();
uint16_t in_h = mDelegate->getHeight();
uint16_t out_w = getWidth();
uint16_t out_h = getHeight();
;
if (in_w == out_w && in_h == out_h) {
noExpand(mSurface.data(), context.leds, in_w, in_h);
} else {
expand(mSurface.data(), context.leds, in_w, in_h, mXyMap);
}
}
void ScaleUp::expand(const CRGB *input, CRGB *output, uint16_t width,
uint16_t height, const XYMap& mXyMap) {
#if FASTLED_SCALE_UP == FASTLED_SCALE_UP_ALWAYS_POWER_OF_2
fl::upscalePowerOf2(input, output, static_cast<uint8_t>(width), static_cast<uint8_t>(height), mXyMap);
#elif FASTLED_SCALE_UP == FASTLED_SCALE_UP_HIGH_PRECISION
fl::upscaleArbitrary(input, output, width, height, mXyMap);
#elif FASTLED_SCALE_UP == FASTLED_SCALE_UP_DECIDE_AT_RUNTIME
fl::upscale(input, output, width, height, mXyMap);
#elif FASTLED_SCALE_UP == FASTLED_SCALE_UP_FORCE_FLOATING_POINT
fl::upscaleFloat(input, output, static_cast<uint8_t>(width), static_cast<uint8_t>(height), mXyMap);
#else
#error "Invalid FASTLED_SCALE_UP"
#endif
}
void ScaleUp::noExpand(const CRGB *input, CRGB *output, uint16_t width,
uint16_t height) {
uint16_t n = mXyMap.getTotal();
for (uint16_t w = 0; w < width; w++) {
for (uint16_t h = 0; h < height; h++) {
uint16_t idx = mXyMap.mapToIndex(w, h);
if (idx < n) {
output[idx] = input[w * height + h];
}
}
}
}
} // namespace fl

View File

@@ -0,0 +1,62 @@
/// @file scale_up.h
/// @brief Expands a grid using bilinear interpolation and scaling up. This is
/// useful for
/// under powered devices that can't handle the full resolution of the
/// grid, or if you suddenly need to increase the size of the grid and
/// don't want to re-create new assets at the new resolution.
#pragma once
#include "fl/stdint.h"
#include "fl/upscale.h"
#include "fl/memory.h"
#include "fl/vector.h"
#include "fl/xymap.h"
#include "fx/fx2d.h"
#include "lib8tion/random8.h"
#include "noise.h"
// Optimized for 2^n grid sizes in terms of both memory and performance.
// If you are somehow running this on AVR then you probably want this if
// you can make your grid size a power of 2.
#define FASTLED_SCALE_UP_ALWAYS_POWER_OF_2 0 // 0 for always power of 2.
// Uses more memory than FASTLED_SCALE_UP_ALWAYS_POWER_OF_2 but can handle
// arbitrary grid sizes.
#define FASTLED_SCALE_UP_HIGH_PRECISION 1 // 1 for always choose high precision.
// Uses the most executable memory because both low and high precision versions
// are compiled in. If the grid size is a power of 2 then the faster version is
// used. Note that the floating point version has to be directly specified
// because in testing it offered no benefits over the integer versions.
#define FASTLED_SCALE_UP_DECIDE_AT_RUNTIME 2 // 2 for runtime decision.
#define FASTLED_SCALE_UP_FORCE_FLOATING_POINT 3 // Warning, this is slow.
#ifndef FASTLED_SCALE_UP
#define FASTLED_SCALE_UP FASTLED_SCALE_UP_DECIDE_AT_RUNTIME
#endif
namespace fl {
FASTLED_SMART_PTR(ScaleUp);
// Uses bilearn filtering to double the size of the grid.
class ScaleUp : public Fx2d {
public:
ScaleUp(const XYMap& xymap, Fx2dPtr fx);
void draw(DrawContext context) override;
void expand(const CRGB *input, CRGB *output, uint16_t width,
uint16_t height, const XYMap& mXyMap);
fl::string fxName() const override { return "scale_up"; }
private:
// No expansion needed. Also useful for debugging.
void noExpand(const CRGB *input, CRGB *output, uint16_t width,
uint16_t height);
Fx2dPtr mDelegate;
fl::vector<CRGB, fl::allocator_psram<CRGB>> mSurface;
};
} // namespace fl

View File

@@ -0,0 +1,64 @@
#include "wave.h"
#include "fl/namespace.h"
#include "fl/pair.h"
#include "fl/vector.h"
namespace fl {
namespace {
struct BatchDraw {
BatchDraw(CRGB *leds, WaveCrgbGradientMap::Gradient *grad)
: mLeds(leds), mGradient(grad) {
mIndices.reserve(kMaxBatchSize); // Should be a no op for FixedVector.
mAlphas.reserve(kMaxBatchSize);
}
void push(fl::u32 index, uint8_t alpha) {
if (isFull()) {
flush();
}
mIndices.push_back(index);
mAlphas.push_back(alpha);
}
bool isFull() { return mIndices.size() >= kMaxBatchSize; }
void flush() {
span<const uint8_t> alphas(mAlphas);
CRGB rgb[kMaxBatchSize] = {};
mGradient->fill(mAlphas, rgb);
for (size_t i = 0; i < mIndices.size(); i++) {
mLeds[mIndices[i]] = rgb[i];
}
mAlphas.clear();
mIndices.clear();
}
static const size_t kMaxBatchSize = 32;
using ArrayIndices = fl::FixedVector<fl::u32, kMaxBatchSize>;
using ArrayAlphas = fl::FixedVector<uint8_t, kMaxBatchSize>;
ArrayIndices mIndices;
ArrayAlphas mAlphas;
CRGB *mLeds = nullptr;
WaveCrgbGradientMap::Gradient *mGradient = nullptr;
};
} // namespace
void WaveCrgbGradientMap::mapWaveToLEDs(const XYMap &xymap,
WaveSimulation2D &waveSim, CRGB *leds) {
BatchDraw batch(leds, &mGradient);
const fl::u32 width = waveSim.getWidth();
const fl::u32 height = waveSim.getHeight();
for (fl::u32 y = 0; y < height; y++) {
for (fl::u32 x = 0; x < width; x++) {
fl::u32 idx = xymap(x, y);
uint8_t value8 = waveSim.getu8(x, y);
batch.push(idx, value8);
}
}
batch.flush();
}
} // namespace fl

View File

@@ -0,0 +1,189 @@
#pragma once
#include "fl/stdint.h"
#include "fl/warn.h"
#include "fl/colorutils.h"
#include "fl/gradient.h"
#include "fl/memory.h"
#include "fl/wave_simulation.h"
#include "fl/xymap.h"
#include "fx/fx.h"
#include "fx/fx2d.h"
#include "pixelset.h"
namespace fl {
FASTLED_SMART_PTR(WaveFx);
FASTLED_SMART_PTR(WaveCrgbMap);
FASTLED_SMART_PTR(WaveCrgbMapDefault);
FASTLED_SMART_PTR(WaveCrgbGradientMap);
class WaveCrgbMap {
public:
virtual ~WaveCrgbMap() = default;
virtual void mapWaveToLEDs(const XYMap &xymap, WaveSimulation2D &waveSim,
CRGB *leds) = 0;
};
// A great deafult for the wave rendering. It will draw black and then the
// amplitude of the wave will be more white.
class WaveCrgbMapDefault : public WaveCrgbMap {
public:
void mapWaveToLEDs(const XYMap &xymap, WaveSimulation2D &waveSim,
CRGB *leds) override {
const fl::u32 width = waveSim.getWidth();
const fl::u32 height = waveSim.getHeight();
for (fl::u32 y = 0; y < height; y++) {
for (fl::u32 x = 0; x < width; x++) {
fl::u32 idx = xymap(x, y);
uint8_t value8 = waveSim.getu8(x, y);
leds[idx] = CRGB(value8, value8, value8);
}
}
}
};
class WaveCrgbGradientMap : public WaveCrgbMap {
public:
using Gradient = fl::GradientInlined;
WaveCrgbGradientMap(const CRGBPalette16 &palette) : mGradient(palette) {}
WaveCrgbGradientMap() = default;
void mapWaveToLEDs(const XYMap &xymap, WaveSimulation2D &waveSim,
CRGB *leds) override;
void setGradient(const Gradient &gradient) { mGradient = gradient; }
private:
Gradient mGradient;
};
struct WaveFxArgs {
WaveFxArgs() = default;
WaveFxArgs(SuperSample factor, bool half_duplex, bool auto_updates,
float speed, float dampening, WaveCrgbMapPtr crgbMap)
: factor(factor), half_duplex(half_duplex), auto_updates(auto_updates),
speed(speed), dampening(dampening), crgbMap(crgbMap) {}
WaveFxArgs(const WaveFxArgs &) = default;
WaveFxArgs &operator=(const WaveFxArgs &) = default;
SuperSample factor = SuperSample::SUPER_SAMPLE_2X;
bool half_duplex = true;
bool auto_updates = true;
float speed = 0.16f;
float dampening = 6.0f;
bool x_cyclical = false;
bool use_change_grid = false; // Whether to use change grid tracking (default: disabled for better visuals)
WaveCrgbMapPtr crgbMap;
};
// Uses bilearn filtering to double the size of the grid.
class WaveFx : public Fx2d {
public:
using Args = WaveFxArgs;
WaveFx(const XYMap& xymap, Args args = Args())
: Fx2d(xymap), mWaveSim(xymap.getWidth(), xymap.getHeight(),
args.factor, args.speed, args.dampening) {
// Initialize the wave simulation with the given parameters.
if (args.crgbMap == nullptr) {
// Use the default CRGB mapping function.
mCrgbMap = fl::make_shared<WaveCrgbMapDefault>();
} else {
// Set a custom CRGB mapping function.
mCrgbMap = args.crgbMap;
}
setAutoUpdate(args.auto_updates);
setXCylindrical(args.x_cyclical);
setUseChangeGrid(args.use_change_grid);
}
void setXCylindrical(bool on) { mWaveSim.setXCylindrical(on); }
void setSpeed(float speed) {
// Set the speed of the wave simulation.
mWaveSim.setSpeed(speed);
}
void setDampening(float dampening) {
// Set the dampening of the wave simulation.
mWaveSim.setDampening(dampening);
}
void setHalfDuplex(bool on) {
// Set whether the wave simulation is half duplex.
mWaveSim.setHalfDuplex(on);
}
void setSuperSample(SuperSample factor) {
// Set the supersampling factor of the wave simulation.
mWaveSim.setSuperSample(factor);
}
void setEasingMode(U8EasingFunction mode) {
// Set the easing mode for the 8-bit value.
mWaveSim.setEasingMode(mode);
}
void setUseChangeGrid(bool enabled) {
// Set whether to use the change grid tracking optimization.
mWaveSim.setUseChangeGrid(enabled);
}
bool getUseChangeGrid() const {
// Get the current change grid tracking setting.
return mWaveSim.getUseChangeGrid();
}
void setf(size_t x, size_t y, float value) {
// Set the value at the given coordinates in the wave simulation.
mWaveSim.setf(x, y, value);
}
void addf(size_t x, size_t y, float value) {
// Add a value at the given coordinates in the wave simulation.
float sum = value + mWaveSim.getf(x, y);
mWaveSim.setf(x, y, MIN(1.0f, sum));
}
uint8_t getu8(size_t x, size_t y) const {
// Get the 8-bit value at the given coordinates in the wave simulation.
return mWaveSim.getu8(x, y);
}
// This will now own the crgbMap.
void setCrgbMap(WaveCrgbMapPtr crgbMap) {
// Set a custom CRGB mapping function.
mCrgbMap = crgbMap;
}
void draw(DrawContext context) override {
// Update the wave simulation.
if (mAutoUpdates) {
mWaveSim.update();
}
// Map the wave values to the LEDs.
mCrgbMap->mapWaveToLEDs(mXyMap, mWaveSim, context.leds);
}
void setAutoUpdate(bool autoUpdate) {
// Set whether to automatically update the wave simulation.
mAutoUpdates = autoUpdate;
}
void update() {
// Called automatically in draw. Only invoke this if you want extra
// simulation updates.
// Update the wave simulation.
mWaveSim.update();
}
fl::string fxName() const override { return "WaveFx"; }
WaveSimulation2D mWaveSim;
WaveCrgbMapPtr mCrgbMap;
bool mAutoUpdates = true;
};
} // namespace fl

View File

@@ -0,0 +1,80 @@
# FastLED FX Library (`src/fx`)
The FX library adds optional, higherlevel visual effects and a small runtime around FastLED. It includes readytouse animations for strips (1D) and matrices (2D), utilities to layer/upscale/blend effects, and a simple video playback pipeline.
If youre new to FastLED and C++: an effect is an object that you construct once and call `draw(...)` on every frame, passing time and your LED buffer. Start with the 1D/2D examples, then explore composition and video as needed.
### Whats included
- [`1d/`](./1d/README.md): Strip effects like Cylon, DemoReel100, Fire2012, NoiseWave, Pacifica, Pride2015, and TwinkleFox.
- [`2d/`](./2d/README.md): Matrix effects including NoisePalette, WaveFx, ScaleUp, Blend2d (compositing effects), and Animartrix integrations.
- [`video/`](./video/README.md): Read frames from files/streams, buffer, and interpolate for smooth playback on your LEDs.
- [`detail/`](./detail/README.md): Internal helpers for draw context, layering, transitions, and compositing.
### Core concepts and types
- **Base classes**:
- `fl::Fx`: abstract base with a single method to implement: `void draw(DrawContext ctx)`.
- `fl::Fx1d`: base for strip effects; holds LED count and (optionally) an `XMap`.
- `fl::Fx2d`: base for matrix effects; holds an `XYMap` to convert `(x,y)` to LED indices.
- **DrawContext**: `Fx::DrawContext` carries perframe data: `now` (ms), `CRGB* leds`, optional `frame_time`, and a `speed` hint.
- **Palettes and helpers**: Many effects use FastLEDs `CRGB`, `CHSV`, `CRGBPalette16`, and timing helpers like `beatsin*`.
### Basic usage (1D example)
```cpp
#include "fx/1d/cylon.h"
fl::Cylon fx(num_leds);
...
fx.draw(fl::Fx::DrawContext(millis(), leds));
```
### Basic usage (2D example)
```cpp
#include "fx/2d/noisepalette.h"
fl::XYMap xy(width, height, /* your mapper */);
fl::NoisePalette fx(xy);
...
fx.draw(fl::Fx::DrawContext(millis(), leds));
```
### Composition and transitions
- Use `Blend2d` to stack multiple 2D effects and blur/blend them.
- The `detail/` components (`FxLayer`, `FxCompositor`, `Transition`) support crossfading between effects over time.
### Video playback
- The `video/` pipeline reads frames from a `FileHandle` or `ByteStream`, keeps a small buffer, and interpolates between frames to match your output rate.
### Performance and targets
- FX components may use more memory/CPU than the FastLED core. They are designed for more capable MCUs (e.g., ESP32/Teensy/RP2040), but many examples will still run on modest hardware at smaller sizes.
### Licensing
- Most code follows the standard FastLED license. Animartrix is free for noncommercial use and paid otherwise. See [`src/fx/readme`](./readme) and headers for details.
Explore each subfolders README to find the effect you want, then copy the corresponding header into your project and call `draw(...)` every frame.
### Examples (from `examples/Fx*`)
- 1D strip effects:
- Cylon: `examples/FxCylon/FxCylon.ino`
- DemoReel100: `examples/FxDemoReel100/FxDemoReel100.ino`
- Fire2012: `examples/FxFire2012/FxFire2012.ino`
- Pacifica: `examples/FxPacifica/FxPacifica.ino`
- Pride2015: `examples/FxPride2015/FxPride2015.ino`
- TwinkleFox: `examples/FxTwinkleFox/FxTwinkleFox.ino`
- 2D matrix effects:
- NoisePalette: `examples/FxNoisePlusPalette/FxNoisePlusPalette.ino`
- WaveFx layered: `examples/FxWave2d/`
- Video pipeline:
- Memory stream: `examples/FxGfx2Video/FxGfx2Video.ino`
- SD card playback: `examples/FxSdCard/FxSdCard.ino`
Minimal 1D call pattern:
```cpp
fx.draw(fl::Fx::DrawContext(millis(), leds));
FastLED.show();
```
Minimal 2D call pattern (requires `XYMap`):
```cpp
fl::NoisePalette fx(xyMap);
fx.draw(fl::Fx::DrawContext(millis(), leds));
FastLED.show();
```

View File

@@ -0,0 +1,33 @@
# FastLED FX: Internal Helpers (`detail/`)
This folder contains utilities used by the FX engine. Most apps wont include these directly; theyre pulled in by higherlevel classes.
### Components
- **`draw_context.h`**: Defines `fl::_DrawContext` (commonly referred to via `Fx::DrawContext`), which carries perframe data into effects: `now` (ms), `CRGB* leds`, `frame_time`, and a `speed` hint.
- **`fx_layer.h`**: Wraps an `Fx` and a `Frame` surface. Manages start/pause/draw and exposes the surface for compositing.
- **`fx_compositor.h`**: Crossfades between two `FxLayer`s over time using `Transition`. Produces a final buffer by blending surfaces each frame; automatically completes when progress reaches 100%.
- **`transition.h`**: Tracks a timebased 0255 progress value between a start time and duration. Used by the compositor to animate transitions.
### When youll encounter these
- If you use an FX engine/controller that switches effects with fades.
- If you build your own multieffect player, you may interact with `FxLayer` and `FxCompositor` directly.
These pieces are small and dependencylight, but theyre part of the internal plumbing for higherlevel modules.
### Examples
- Switching between effects with transitions (see `examples/FxEngine/FxEngine.ino`):
```cpp
#include <FastLED.h>
#include "fx/2d/noisepalette.h"
#include "fx/fx_engine.h"
using namespace fl;
#define W 22
#define H 22
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
XYMap xyMap(W, H, /* serpentine? */ true);
NoisePalette a(xyMap), b(xyMap);
FxEngine engine(NUM_LEDS);
void setup(){ FastLED.addLeds<WS2811,2,GRB>(leds, NUM_LEDS).setScreenMap(xyMap); engine.addFx(a); engine.addFx(b); }
void loop(){ EVERY_N_SECONDS(1){ engine.nextFx(500); } engine.draw(millis(), leds); FastLED.show(); }
```

View File

@@ -0,0 +1,21 @@
#pragma once
#include "crgb.h"
#include "fl/namespace.h"
#include "fl/stdint.h"
namespace fl {
// Abstract base class for effects on a strip/grid of LEDs.
struct _DrawContext {
fl::u32 now;
CRGB *leds;
uint16_t frame_time = 0;
float speed = 1.0f;
_DrawContext(fl::u32 now, CRGB *leds, uint16_t frame_time = 0,
float speed = 1.0f)
: now(now), leds(leds), frame_time(frame_time), speed(speed) {}
};
} // namespace fl

View File

@@ -0,0 +1,85 @@
#pragma once
#include "fl/stdint.h"
#include <string.h>
#include "crgb.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/vector.h"
#include "fx/detail/fx_layer.h"
#include "fx/fx.h"
#ifndef FASTLED_FX_ENGINE_MAX_FX
#define FASTLED_FX_ENGINE_MAX_FX 64
#endif
namespace fl {
// Takes two fx layers and composites them together to a final output buffer.
class FxCompositor {
public:
FxCompositor(fl::u32 numLeds) : mNumLeds(numLeds) {
mLayers[0] = fl::make_shared<FxLayer>();
mLayers[1] = fl::make_shared<FxLayer>();
}
void startTransition(fl::u32 now, fl::u32 duration, fl::shared_ptr<Fx> nextFx) {
completeTransition();
if (duration == 0) {
mLayers[0]->setFx(nextFx);
return;
}
mLayers[1]->setFx(nextFx);
mTransition.start(now, duration);
}
void completeTransition() {
if (mLayers[1]->getFx()) {
swapLayers();
mLayers[1]->release();
}
mTransition.end();
}
void draw(fl::u32 now, fl::u32 warpedTime, CRGB *finalBuffer);
private:
void swapLayers() {
FxLayerPtr tmp = mLayers[0];
mLayers[0] = mLayers[1];
mLayers[1] = tmp;
}
FxLayerPtr mLayers[2];
const fl::u32 mNumLeds;
Transition mTransition;
};
inline void FxCompositor::draw(fl::u32 now, fl::u32 warpedTime,
CRGB *finalBuffer) {
if (!mLayers[0]->getFx()) {
return;
}
mLayers[0]->draw(warpedTime);
uint8_t progress = mTransition.getProgress(now);
if (!progress) {
memcpy(finalBuffer, mLayers[0]->getSurface(), sizeof(CRGB) * mNumLeds);
return;
}
mLayers[1]->draw(warpedTime);
const CRGB *surface0 = mLayers[0]->getSurface();
const CRGB *surface1 = mLayers[1]->getSurface();
for (fl::u32 i = 0; i < mNumLeds; i++) {
const CRGB &p0 = surface0[i];
const CRGB &p1 = surface1[i];
CRGB out = CRGB::blend(p0, p1, progress);
finalBuffer[i] = out;
}
if (progress == 255) {
completeTransition();
}
}
} // namespace fl

View File

@@ -0,0 +1,55 @@
#include <string.h>
#include "fx_layer.h"
#include "fl/memfill.h"
namespace fl {
void FxLayer::setFx(fl::shared_ptr<Fx> newFx) {
if (newFx != fx) {
release();
fx = newFx;
}
}
void FxLayer::draw(fl::u32 now) {
// assert(fx);
if (!frame) {
frame = fl::make_shared<Frame>(fx->getNumLeds());
}
if (!running) {
// Clear the frame
fl::memfill((uint8_t*)frame->rgb(), 0, frame->size() * sizeof(CRGB));
fx->resume(now);
running = true;
}
Fx::DrawContext context = {now, frame->rgb()};
fx->draw(context);
}
void FxLayer::pause(fl::u32 now) {
if (fx && running) {
fx->pause(now);
running = false;
}
}
void FxLayer::release() {
pause(0);
fx.reset();
}
fl::shared_ptr<Fx> FxLayer::getFx() {
return fx;
}
CRGB* FxLayer::getSurface() {
return frame->rgb();
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "fl/stdint.h"
#include "crgb.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/vector.h"
#include "fl/warn.h"
#include "fx/frame.h"
#include "fx/fx.h"
namespace fl {
FASTLED_SMART_PTR(FxLayer);
class FxLayer {
public:
void setFx(fl::shared_ptr<Fx> newFx);
void draw(fl::u32 now);
void pause(fl::u32 now);
void release();
fl::shared_ptr<Fx> getFx();
CRGB *getSurface();
private:
fl::shared_ptr<Frame> frame;
fl::shared_ptr<Fx> fx;
bool running = false;
};
} // namespace fl

View File

@@ -0,0 +1,49 @@
#pragma once
#include "fl/namespace.h"
#include "fl/stdint.h"
#include "fl/int.h"
namespace fl {
// Logic to control the progression of a transition over time.
class Transition {
public:
Transition() : mStart(0), mDuration(0), mNotStarted(true) {}
~Transition() {}
uint8_t getProgress(fl::u32 now) {
if (mNotStarted) {
return 0;
}
if (now < mStart) {
return 0;
} else if (now >= mStart + mDuration) {
return 255;
} else {
return ((now - mStart) * 255) / mDuration;
}
}
void start(fl::u32 now, fl::u32 duration) {
mNotStarted = false;
mStart = now;
mDuration = duration;
}
void end() { mNotStarted = true; }
bool isTransitioning(fl::u32 now) {
if (mNotStarted) {
return false;
}
return now >= mStart && now < mStart + mDuration;
}
private:
fl::u32 mStart;
fl::u32 mDuration;
bool mNotStarted;
};
} // namespace fl

View File

@@ -0,0 +1,106 @@
#include <string.h>
#include "crgb.h"
#include "fl/allocator.h"
#include "fl/dbg.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/warn.h"
#include "fl/xymap.h"
#include "frame.h"
#include "fl/memfill.h"
namespace fl {
Frame::Frame(int pixels_count) : mPixelsCount(pixels_count), mRgb() {
mRgb.resize(pixels_count);
fl::memfill((uint8_t*)mRgb.data(), 0, pixels_count * sizeof(CRGB));
}
Frame::~Frame() {
// Vector will handle memory cleanup automatically
}
void Frame::draw(CRGB *leds, DrawMode draw_mode) const {
if (!mRgb.empty()) {
switch (draw_mode) {
case DRAW_MODE_OVERWRITE: {
memcpy(leds, mRgb.data(), mPixelsCount * sizeof(CRGB));
break;
}
case DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS: {
for (size_t i = 0; i < mPixelsCount; ++i) {
leds[i] = CRGB::blendAlphaMaxChannel(mRgb[i], leds[i]);
}
break;
}
}
}
}
void Frame::drawXY(CRGB *leds, const XYMap &xyMap, DrawMode draw_mode) const {
const uint16_t width = xyMap.getWidth();
const uint16_t height = xyMap.getHeight();
fl::u32 count = 0;
for (uint16_t h = 0; h < height; ++h) {
for (uint16_t w = 0; w < width; ++w) {
fl::u32 in_idx = xyMap(w, h);
fl::u32 out_idx = count++;
if (in_idx >= mPixelsCount) {
FASTLED_WARN(
"Frame::drawXY: in index out of range: " << in_idx);
continue;
}
if (out_idx >= mPixelsCount) {
FASTLED_WARN(
"Frame::drawXY: out index out of range: " << out_idx);
continue;
}
switch (draw_mode) {
case DRAW_MODE_OVERWRITE: {
leds[out_idx] = mRgb[in_idx];
break;
}
case DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS: {
leds[out_idx] =
CRGB::blendAlphaMaxChannel(mRgb[in_idx], leds[in_idx]);
break;
}
}
}
}
}
void Frame::clear() { fl::memfill((uint8_t*)mRgb.data(), 0, mPixelsCount * sizeof(CRGB)); }
void Frame::interpolate(const Frame &frame1, const Frame &frame2,
uint8_t amountofFrame2, CRGB *pixels) {
if (frame1.size() != frame2.size()) {
return; // Frames must have the same size
}
const CRGB *rgbFirst = frame1.rgb();
const CRGB *rgbSecond = frame2.rgb();
if (frame1.mRgb.empty() || frame2.mRgb.empty()) {
// Error, why are we getting null pointers?
return;
}
for (size_t i = 0; i < frame2.size(); ++i) {
pixels[i] = CRGB::blend(rgbFirst[i], rgbSecond[i], amountofFrame2);
}
// We will eventually do something with alpha.
}
void Frame::interpolate(const Frame &frame1, const Frame &frame2,
uint8_t amountOfFrame2) {
if (frame1.size() != frame2.size() || frame1.size() != mPixelsCount) {
FASTLED_DBG("Frames must have the same size");
return; // Frames must have the same size
}
interpolate(frame1, frame2, amountOfFrame2, mRgb.data());
}
} // namespace fl

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string.h>
#include "crgb.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/xymap.h"
#include "fl/vector.h"
#include "fl/allocator.h"
#include "fl/draw_mode.h"
namespace fl {
FASTLED_SMART_PTR(Frame);
// Frames are used to hold led data. This includes an optional alpha channel.
// This object is used by the fx and video engines. Most of the memory used for
// Fx and Video will be located in instances of this class. See
// Frame::SetAllocator() for custom memory allocation.
class Frame {
public:
// Frames take up a lot of memory. On some devices like ESP32 there is
// PSRAM available. You should see allocator.h ->
// SetPSRamAllocator(...) on setting a custom allocator for these large
// blocks.
explicit Frame(int pixels_per_frame);
~Frame();
CRGB *rgb() { return mRgb.data(); }
const CRGB *rgb() const { return mRgb.data(); }
size_t size() const { return mPixelsCount; }
void copy(const Frame &other);
void interpolate(const Frame &frame1, const Frame &frame2,
uint8_t amountOfFrame2);
static void interpolate(const Frame &frame1, const Frame &frame2,
uint8_t amountofFrame2, CRGB *pixels);
void draw(CRGB *leds, DrawMode draw_mode = DRAW_MODE_OVERWRITE) const;
void drawXY(CRGB *leds, const XYMap &xyMap,
DrawMode draw_mode = DRAW_MODE_OVERWRITE) const;
void clear();
private:
const size_t mPixelsCount;
fl::vector<CRGB, fl::allocator_psram<CRGB>> mRgb;
};
inline void Frame::copy(const Frame &other) {
memcpy(mRgb.data(), other.mRgb.data(), other.mPixelsCount * sizeof(CRGB));
}
} // namespace fl

View File

@@ -0,0 +1,56 @@
#pragma once
#include "fl/stdint.h"
#include "crgb.h"
#include "detail/draw_context.h"
#include "detail/transition.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/str.h"
#include "fl/unused.h"
namespace fl {
FASTLED_SMART_PTR(Fx);
// Abstract base class for effects on a strip/grid of LEDs.
class Fx {
public:
// Alias DrawContext for use within Fx
using DrawContext = _DrawContext;
Fx(uint16_t numLeds) : mNumLeds(numLeds) {}
/// @param now The current time in milliseconds. Fx writers are encouraged
/// to use this instead of millis() directly as this will more deterministic
/// behavior.
virtual void
draw(DrawContext context) = 0; // This is the only function that needs to be
// implemented everything else is optional.
// If true then this fx has a fixed frame rate and the fps parameter will be
// set to the frame rate.
virtual bool hasFixedFrameRate(float *fps) const {
FASTLED_UNUSED(fps);
return false;
}
// Get the name of the current fx.
virtual fl::string fxName() const = 0;
// Called when the fx is paused, usually when a transition has finished.
virtual void pause(fl::u32 now) { FASTLED_UNUSED(now); }
virtual void resume(fl::u32 now) {
FASTLED_UNUSED(now);
} // Called when the fx is resumed after a pause,
// usually when a transition has started.
uint16_t getNumLeds() const { return mNumLeds; }
protected:
virtual ~Fx() {} // Protected destructor
uint16_t mNumLeds;
};
} // namespace fl

View File

@@ -0,0 +1,24 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/namespace.h"
#include "fl/xmap.h"
#include "fx/fx.h"
namespace fl {
// Abstract base class for 1D effects that use a strip of LEDs.
class Fx1d : public Fx {
public:
Fx1d(u16 numLeds) : Fx(numLeds), mXMap(numLeds, false) {}
void setXmap(const XMap &xMap) { mXMap = xMap; }
u16 xyMap(u16 x) const { return mXMap.mapToIndex(x); }
protected:
XMap mXMap;
};
} // namespace fl

View File

@@ -0,0 +1,34 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/xymap.h"
#include "fx/fx.h"
namespace fl {
FASTLED_SMART_PTR(Fx2d);
// Abstract base class for 2D effects that use a grid, which is defined
// by an XYMap.
class Fx2d : public Fx {
public:
// XYMap holds either a function or a look up table to map x, y coordinates
// to a 1D index.
Fx2d(const XYMap &xyMap) : Fx(xyMap.getTotal()), mXyMap(xyMap) {}
uint16_t xyMap(uint16_t x, uint16_t y) const {
return mXyMap.mapToIndex(x, y);
}
uint16_t getHeight() const { return mXyMap.getHeight(); }
uint16_t getWidth() const { return mXyMap.getWidth(); }
void setXYMap(const XYMap &xyMap) { mXyMap = xyMap; }
XYMap &getXYMap() { return mXyMap; }
const XYMap &getXYMap() const { return mXyMap; }
protected:
XYMap mXyMap;
};
} // namespace fl

View File

@@ -0,0 +1,105 @@
#include "fx_engine.h"
#include "video.h"
namespace fl {
FxEngine::FxEngine(uint16_t numLeds, bool interpolate)
: mTimeFunction(0), mCompositor(numLeds), mCurrId(0),
mInterpolate(interpolate) {}
FxEngine::~FxEngine() {}
int FxEngine::addFx(FxPtr effect) {
float fps = 0;
if (mInterpolate && effect->hasFixedFrameRate(&fps)) {
// Wrap the effect in a VideoFxWrapper so that we can get
// interpolation.
VideoFxWrapperPtr vid_fx = fl::make_shared<VideoFxWrapper>(effect);
vid_fx->setFade(0, 0); // No fade for interpolated effects
effect = vid_fx;
}
bool auto_set = mEffects.empty();
bool ok = mEffects.insert(mCounter, effect).first;
if (!ok) {
return -1;
}
if (auto_set) {
mCurrId = mCounter;
mCompositor.startTransition(0, 0, effect);
}
return mCounter++;
}
bool FxEngine::nextFx(uint16_t duration) {
bool ok = mEffects.next(mCurrId, &mCurrId, true);
if (!ok) {
return false;
}
setNextFx(mCurrId, duration);
return true;
}
bool FxEngine::setNextFx(int index, uint16_t duration) {
if (!mEffects.has(index)) {
return false;
}
mCurrId = index;
mDuration = duration;
mDurationSet = true;
return true;
}
FxPtr FxEngine::removeFx(int index) {
if (!mEffects.has(index)) {
return FxPtr();
}
FxPtr removedFx;
bool ok = mEffects.get(index, &removedFx);
if (!ok) {
return FxPtr();
}
if (mCurrId == index) {
// If we're removing the current effect, switch to the next one
mEffects.next(mCurrId, &mCurrId, true);
mDurationSet = true;
mDuration = 0; // Instant transition
}
return removedFx;
}
FxPtr FxEngine::getFx(int id) {
if (mEffects.has(id)) {
FxPtr fx;
mEffects.get(id, &fx);
return fx;
}
return FxPtr();
}
bool FxEngine::draw(fl::u32 now, CRGB *finalBuffer) {
mTimeFunction.update(now);
fl::u32 warpedTime = mTimeFunction.time();
if (mEffects.empty()) {
return false;
}
if (mDurationSet) {
FxPtr fx;
bool ok = mEffects.get(mCurrId, &fx);
if (!ok) {
// something went wrong.
return false;
}
mCompositor.startTransition(now, mDuration, fx);
mDurationSet = false;
}
if (!mEffects.empty()) {
mCompositor.draw(now, warpedTime, finalBuffer);
}
return true;
}
} // namespace fl

View File

@@ -0,0 +1,127 @@
#pragma once
#include "crgb.h"
#include "fl/map.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/ui.h"
#include "fl/xymap.h"
#include "fx/detail/fx_compositor.h"
#include "fx/detail/fx_layer.h"
#include "fx/fx.h"
#include "fx/time.h"
#include "fx/video.h"
#include "fl/stdint.h"
// TimeFunction is defined in fx/time.h (fl::TimeFunction)
#ifndef FASTLED_FX_ENGINE_MAX_FX
#define FASTLED_FX_ENGINE_MAX_FX 64
#endif
namespace fl {
/**
* @class FxEngine
* @brief Manages and renders multiple visual effects (Fx) for LED strips.
*
* The FxEngine class is responsible for:
* - Storing and managing a collection of visual effects (Fx objects)
* - Handling transitions between effects
* - Rendering the current effect or transition to an output buffer
*/
class FxEngine {
public:
typedef fl::FixedMap<int, FxPtr, FASTLED_FX_ENGINE_MAX_FX> IntFxMap;
/**
* @brief Constructs an FxEngine with the specified number of LEDs.
* @param numLeds The number of LEDs in the strip.
*/
FxEngine(uint16_t numLeds, bool interpolate = true);
/**
* @brief Destructor for FxEngine.
*/
~FxEngine();
/**
* @brief Adds a new effect to the engine.
* @param effect Pointer to the effect to be added.
* @return The index of the added effect, or -1 if the effect couldn't be
* added.
*/
int addFx(FxPtr effect);
/**
* @brief Adds a new effect to the engine. Allocate from static memory.
* This is not reference tracked and an object passed in must never
* be deleted, as the engine will use a non tracking Ptr which may outlive
* a call to removeFx() and the engine will thefore not know that an
* object has been deleted. But if it's a static object that's
* then the object probably wasn't going to be deleted anyway.
*/
int addFx(Fx &effect) { return addFx(fl::make_shared_no_tracking(effect)); }
/**
* @brief Requests removal of an effect from the engine, which might not
* happen immediately (for example the Fx needs to finish a transition).
* @param index The index of the effect to remove.
* @return A pointer to the removed effect, or nullptr if the index was
* invalid.
*/
FxPtr removeFx(int index);
/**
* @brief Retrieves an effect from the engine without removing it.
* @param index The id of the effect to retrieve.
* @return A pointer to the effect, or nullptr if the index was invalid.
*/
FxPtr getFx(int index);
int getCurrentFxId() const { return mCurrId; }
/**
* @brief Renders the current effect or transition to the output buffer.
* @param now The current time in milliseconds.
* @param outputBuffer The buffer to render the effect into.
*/
bool draw(fl::u32 now, CRGB *outputBuffer);
/**
* @brief Transitions to the next effect in the sequence.
* @param duration The duration of the transition in milliseconds.
* @return True if the transition was initiated, false otherwise.
*/
bool nextFx(uint16_t transition_ms = 500);
/**
* @brief Sets the next effect to transition to.
* @param index The index of the effect to transition to.
* @param duration The duration of the transition in milliseconds.
* @return True if the transition was set, false if the index was invalid.
*/
bool setNextFx(int index, uint16_t duration);
IntFxMap &_getEffects() { return mEffects; }
/**
* @brief Sets the speed of the fx engine, which will impact the speed of
* all effects.
* @param timeScale The new time scale value.
*/
void setSpeed(float scale) { mTimeFunction.setSpeed(scale); }
private:
int mCounter = 0;
TimeWarp mTimeFunction; // FxEngine controls the clock, to allow
// "time-bending" effects.
IntFxMap mEffects; ///< Collection of effects
FxCompositor mCompositor; ///< Handles effect transitions and rendering
int mCurrId; ///< Id of the current effect
uint16_t mDuration = 0; ///< Duration of the current transition
bool mDurationSet =
false; ///< Flag indicating if a new transition has been set
bool mInterpolate = true;
};
} // namespace fl

View File

@@ -0,0 +1,29 @@
# FX Structure
The FX module is an optional component that is not compiled into the core driver by default.
You can include these modules to achieve advanced visual effects.
While the core driver is optimized to be as lightweight as possible, the rules for the FX directory are more flexible. These effects (FX) are intended for use on more powerful hardware, unlike more constrained platforms such as the Arduino UNO.
As a result, FX components can be "heavyweight," meaning they may include a larger portion of the standard library or even depend on libraries like `Arduino.h`, which the core driver prohibits.
## Why the *.hpp files?
*.hpp are somewhere between a standard header and a *.cpp file. While a standard header typically only attempts to declare data layout, functions and
classes, *.hpp files are typically much more heavy weight and will happy inject global data into your compilation unit. Because of this, *.hpp files should only ever be included once.
## Licensing
Everything in this library is under FastLED standard license except the following:
* Animartrix
Animartrix is free for non commercial use, paid license otherwise.
Optional code modules are tagged as *.hpp.
Happy coding!

View File

@@ -0,0 +1,89 @@
#include "time.h"
#include "fl/namespace.h"
#include "fl/dbg.h"
#include "fl/warn.h"
#define DBG FASTLED_DBG
namespace fl {
TimeWarp::TimeWarp(fl::u32 realTimeNow, float initialTimeScale)
: mLastRealTime(realTimeNow), mStartTime(realTimeNow),
mTimeScale(initialTimeScale) {}
TimeWarp::~TimeWarp() {}
void TimeWarp::setSpeed(float timeScale) { mTimeScale = timeScale; }
float TimeWarp::scale() const { return mTimeScale; }
void TimeWarp::pause(fl::u32 now) {
if (mPauseTime) {
FASTLED_WARN("TimeWarp::pause: already paused");
return;
}
mPauseTime = now;
}
void TimeWarp::resume(fl::u32 now) {
if (mLastRealTime == 0) {
reset(now);
return;
}
fl::u32 diff = now - mPauseTime;
mStartTime += diff;
mLastRealTime += diff;
mPauseTime = 0;
}
fl::u32 TimeWarp::update(fl::u32 timeNow) {
// DBG("TimeWarp::update: timeNow: " << timeNow << " mLastRealTime: " <<
// mLastRealTime
//<< " mRelativeTime: " << mRelativeTime << " mTimeScale: " << mTimeScale);
if (mLastRealTime > timeNow) {
DBG("TimeWarp::applyExact: mLastRealTime > timeNow: "
<< mLastRealTime << " > " << timeNow);
}
applyExact(timeNow);
return time();
}
fl::u32 TimeWarp::time() const { return mRelativeTime; }
void TimeWarp::reset(fl::u32 realTimeNow) {
mLastRealTime = realTimeNow;
mStartTime = realTimeNow;
mRelativeTime = 0;
}
void TimeWarp::applyExact(fl::u32 timeNow) {
fl::u32 elapsedRealTime = timeNow - mLastRealTime;
mLastRealTime = timeNow;
int32_t diff = static_cast<int32_t>(elapsedRealTime * mTimeScale);
if (diff == 0) {
return;
}
if (diff >= 0) {
mRelativeTime += diff;
return;
}
// diff < 0
fl::u32 abs_diff = -diff;
if (abs_diff > mRelativeTime) {
// Protection against rollover.
mRelativeTime = 0;
mLastRealTime = timeNow;
return;
}
mLastRealTime = timeNow;
mRelativeTime -= abs_diff;
}
void TimeWarp::setScale(float speed) { mTimeScale = speed; }
} // namespace fl

View File

@@ -0,0 +1,54 @@
#pragma once
#include "fl/stdint.h"
#include "fl/deprecated.h"
#include "fl/namespace.h"
#include "fl/memory.h"
namespace fl {
FASTLED_SMART_PTR(TimeFunction);
FASTLED_SMART_PTR(TimeWarp);
// Interface for time generation and time manipulation.
class TimeFunction {
public:
virtual ~TimeFunction() {}
virtual fl::u32
update(fl::u32 timeNow) = 0; // Inputs the real clock time and outputs the
// virtual time.
virtual fl::u32 time() const = 0;
virtual void reset(fl::u32 realTimeNow) = 0;
};
// Time clock. Use this to gracefully handle time manipulation. You can input a
// float value representing the current time scale and this will adjust a the
// clock smoothly. Updating requires inputing the real clock from the millis()
// function. HANDLES NEGATIVE TIME SCALES!!! Use this to control viusualizers
// back and forth motion which draw according to a clock value. Clock will never
// go below 0.
class TimeWarp : public TimeFunction {
public:
TimeWarp(fl::u32 realTimeNow = 0, float initialTimeScale = 1.0f);
~TimeWarp();
void setSpeed(float speedScale);
void setScale(float speed)
FASTLED_DEPRECATED("Use setSpeed(...) instead."); // Deprecated
float scale() const;
fl::u32 update(fl::u32 timeNow) override;
fl::u32 time() const override;
void reset(fl::u32 realTimeNow) override;
void pause(fl::u32 now);
void resume(fl::u32 now);
private:
void applyExact(fl::u32 timeNow);
fl::u32 mLastRealTime = 0;
fl::u32 mStartTime = 0;
fl::u32 mRelativeTime = 0;
float mTimeScale = 1.0f;
fl::u32 mPauseTime = 0;
};
} // namespace fl

View File

@@ -0,0 +1,197 @@
#include "fx/video.h"
#include "crgb.h"
#include "fl/bytestreammemory.h"
#include "fl/dbg.h"
#include "fl/math_macros.h"
#include "fl/str.h"
#include "fl/warn.h"
#include "fx/frame.h"
#include "fx/video/frame_interpolator.h"
#include "fx/video/pixel_stream.h"
#include "fx/video/video_impl.h"
#define DBG FASTLED_DBG
using fl::ByteStreamPtr;
using fl::FileHandlePtr;
#include "fl/namespace.h"
namespace fl {
FASTLED_SMART_PTR(PixelStream);
FASTLED_SMART_PTR(FrameInterpolator);
FASTLED_SMART_PTR(Frame);
Video::Video() : Fx1d(0) {}
Video::Video(size_t pixelsPerFrame, float fps, size_t frame_history_count)
: Fx1d(pixelsPerFrame) {
mImpl = fl::make_shared<VideoImpl>(pixelsPerFrame, fps, frame_history_count);
}
void Video::setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime) {
mImpl->setFade(fadeInTime, fadeOutTime);
}
void Video::pause(fl::u32 now) { mImpl->pause(now); }
void Video::resume(fl::u32 now) { mImpl->resume(now); }
Video::~Video() = default;
Video::Video(const Video &) = default;
Video &Video::operator=(const Video &) = default;
bool Video::begin(FileHandlePtr handle) {
if (!mImpl) {
FASTLED_WARN("Video::begin: mImpl is null, manually constructed videos "
"must include full parameters.");
return false;
}
if (!handle) {
mError = "FileHandle is null";
FASTLED_DBG(mError.c_str());
return false;
}
if (mError.size()) {
FASTLED_DBG(mError.c_str());
return false;
}
mError.clear();
mImpl->begin(handle);
return true;
}
bool Video::beginStream(ByteStreamPtr bs) {
if (!bs) {
mError = "FileHandle is null";
FASTLED_DBG(mError.c_str());
return false;
}
if (mError.size()) {
FASTLED_DBG(mError.c_str());
return false;
}
mError.clear();
mImpl->beginStream(bs);
return true;
}
bool Video::draw(fl::u32 now, CRGB *leds) {
if (!mImpl) {
FASTLED_WARN_IF(!mError.empty(), mError.c_str());
return false;
}
bool ok = mImpl->draw(now, leds);
if (!ok) {
// Interpret not being able to draw as a finished signal.
mFinished = true;
}
return ok;
}
void Video::draw(DrawContext context) {
if (!mImpl) {
FASTLED_WARN_IF(!mError.empty(), mError.c_str());
return;
}
mImpl->draw(context.now, context.leds);
}
int32_t Video::durationMicros() const {
if (!mImpl) {
return -1;
}
return mImpl->durationMicros();
}
Str Video::fxName() const { return "Video"; }
bool Video::draw(fl::u32 now, Frame *frame) {
if (!mImpl) {
return false;
}
return mImpl->draw(now, frame);
}
void Video::end() {
if (mImpl) {
mImpl->end();
}
}
void Video::setTimeScale(float timeScale) {
if (!mImpl) {
return;
}
mImpl->setTimeScale(timeScale);
}
float Video::timeScale() const {
if (!mImpl) {
return 1.0f;
}
return mImpl->timeScale();
}
Str Video::error() const { return mError; }
size_t Video::pixelsPerFrame() const {
if (!mImpl) {
return 0;
}
return mImpl->pixelsPerFrame();
}
bool Video::finished() {
if (!mImpl) {
return true;
}
return mFinished;
}
bool Video::rewind() {
if (!mImpl) {
return false;
}
return mImpl->rewind();
}
VideoFxWrapper::VideoFxWrapper(fl::shared_ptr<Fx> fx) : Fx1d(fx->getNumLeds()), mFx(fx) {
if (!mFx->hasFixedFrameRate(&mFps)) {
FASTLED_WARN("VideoFxWrapper: Fx does not have a fixed frame rate, "
"assuming 30fps.");
mFps = 30.0f;
}
mVideo = fl::make_shared<VideoImpl>(mFx->getNumLeds(), mFps, 2);
mByteStream = fl::make_shared<ByteStreamMemory>(mFx->getNumLeds() * sizeof(CRGB));
mVideo->beginStream(mByteStream);
}
VideoFxWrapper::~VideoFxWrapper() = default;
Str VideoFxWrapper::fxName() const {
Str out = "video_fx_wrapper: ";
out.append(mFx->fxName());
return out;
}
void VideoFxWrapper::draw(DrawContext context) {
if (mVideo->needsFrame(context.now)) {
mFx->draw(context); // use the leds in the context as a tmp buffer.
mByteStream->writeCRGB(
context.leds,
mFx->getNumLeds()); // now write the leds to the byte stream.
}
bool ok = mVideo->draw(context.now, context.leds);
if (!ok) {
FASTLED_WARN("VideoFxWrapper: draw failed.");
}
}
void VideoFxWrapper::setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime) {
mVideo->setFade(fadeInTime, fadeOutTime);
}
} // namespace fl

View File

@@ -0,0 +1,104 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/str.h"
#include "fx/fx1d.h"
#include "fx/time.h"
FASTLED_NAMESPACE_BEGIN
struct CRGB;
FASTLED_NAMESPACE_END
namespace fl {
// Forward declare classes
FASTLED_SMART_PTR(FileHandle);
FASTLED_SMART_PTR(ByteStream);
FASTLED_SMART_PTR(Frame);
FASTLED_SMART_PTR(VideoImpl);
FASTLED_SMART_PTR(VideoFxWrapper);
FASTLED_SMART_PTR(ByteStreamMemory);
// Video represents a video file that can be played back on a LED strip.
// The video file is expected to be a sequence of frames. You can either use
// a file handle or a byte stream to read the video data.
class Video : public Fx1d { // Fx1d because video can be irregular.
public:
static size_t DefaultFrameHistoryCount() {
#ifdef __AVR__
return 1;
#else
return 2; // Allow interpolation by default.
#endif
}
// frameHistoryCount is the number of frames to keep in the buffer after
// draw. This allows for time based effects like syncing video speed to
// audio triggers. If you are using a filehandle for you video then you can
// just leave this as the default. For streaming byte streams you may want
// to increase this number to allow momentary re-wind. If you'd like to use
// a Video as a buffer for an fx effect then please see VideoFxWrapper.
Video();
Video(size_t pixelsPerFrame, float fps = 30.0f,
size_t frameHistoryCount =
DefaultFrameHistoryCount()); // Please use FileSytem to construct
// a Video.
~Video();
Video(const Video &);
Video &operator=(const Video &);
// Fx Api
void draw(DrawContext context) override;
Str fxName() const override;
// Api
bool begin(fl::FileHandlePtr h);
bool beginStream(fl::ByteStreamPtr s);
bool draw(fl::u32 now, CRGB *leds);
bool draw(fl::u32 now, Frame *frame);
void end();
bool finished();
bool rewind();
void setTimeScale(float timeScale);
float timeScale() const;
Str error() const;
void setError(const Str &error) { mError = error; }
size_t pixelsPerFrame() const;
void pause(fl::u32 now) override;
void resume(fl::u32 now) override;
void setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime);
int32_t durationMicros() const; // -1 if this is a stream.
// make compatible with if statements
operator bool() const { return mImpl.get(); }
private:
bool mFinished = false;
VideoImplPtr mImpl;
Str mError;
Str mName;
};
// Wraps an Fx and stores a history of video frames. This allows
// interpolation between frames for FX for smoother effects.
// It also allows re-wind on fx that gnore time and always generate
// the next frame based on the previous frame and internal speed,
// for example NoisePalette.
class VideoFxWrapper : public Fx1d {
public:
VideoFxWrapper(FxPtr fx);
~VideoFxWrapper() override;
void draw(DrawContext context) override;
Str fxName() const override;
void setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime);
private:
FxPtr mFx;
VideoImplPtr mVideo;
ByteStreamMemoryPtr mByteStream;
float mFps = 30.0f;
};
} // namespace fl

View File

@@ -0,0 +1,60 @@
# FastLED FX: Video Subsystem
This folder implements a simple video playback pipeline for LED arrays. It reads frames from a file or stream, buffers them, and interpolates between them to produce smooth animation at your desired output rate.
### Building blocks
- **`PixelStream` (`pixel_stream.h`)**: Reads pixel data as bytes from either a file (`FileHandle`) or a live `ByteStream`. Knows the `bytesPerFrame`, can read by pixel, by frame, or at an absolute frame number. Reports availability and end-of-stream.
- **`FrameTracker` (`frame_tracker.h`)**: Converts wallclock time to frame numbers (current and next) at a fixed FPS. Also exposes exact timestamps and frame interval in microseconds.
- **`FrameInterpolator` (`frame_interpolator.h`)**: Holds a small history of frames and, given the current time, blends the nearest two frames to produce an inbetween result. Supports nonmonotonic time (e.g., pause/rewind, audio sync).
- **`VideoImpl` (`video_impl.h`)**: Highlevel orchestrator. Owns a `PixelStream` and a `FrameInterpolator`, manages fadein/out, time scaling, pause/resume, and draws into either a `Frame` or your `CRGB*` buffer.
### Typical flow
1. Create `VideoImpl` with your `pixelsPerFrame` and the source FPS.
2. Call `begin(...)` with a `FileHandle` or `ByteStream`.
3. Each frame, call `video.draw(now, leds)`.
- Internally maintains a buffer of recent frames.
- Interpolates between frames to match your output timing.
4. Use `setFade(...)`, `pause(...)`, `resume(...)`, and `setTimeScale(...)` as needed.
5. Call `end()` or `rewind()` to manage lifecycle.
### Notes
- Interpolation makes lowerFPS content look smooth on higherFPS refresh loops.
- For streaming sources, some random access features (e.g., `rewind`) may be limited.
- `durationMicros()` reports the full duration for file sources, and `-1` for streams.
This subsystem is optional, intended for MCUs with adequate RAM and I/O throughput.
### Examples
- Memory stream video (from `examples/FxGfx2Video/FxGfx2Video.ino`):
```cpp
#include <FastLED.h>
#include "fl/bytestreammemory.h"
#include "fx/video.h"
using namespace fl;
#define W 22
#define H 22
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
ByteStreamMemoryPtr stream = fl::make_shared<ByteStreamMemory>(3*NUM_LEDS*2);
Video video(NUM_LEDS, 2.0f); // 2 fps source
void setup(){ FastLED.addLeds<WS2811,2,GRB>(leds, NUM_LEDS); video.beginStream(stream); }
void loop(){ video.draw(millis(), leds); FastLED.show(); }
```
- SD card video (from `examples/FxSdCard/FxSdCard.ino`):
```cpp
#include <FastLED.h>
#include "fx/video.h"
#include "fl/file_system.h"
using namespace fl;
#define W 32
#define H 32
#define NUM_LEDS (W*H)
CRGB leds[NUM_LEDS];
FileSystem fs;
Video video;
void setup(){
FastLED.addLeds<WS2811,2,GRB>(leds, NUM_LEDS);
video = fs.openVideo("data/video.rgb", NUM_LEDS, 60, 2);
}
void loop(){ video.draw(millis(), leds); FastLED.show(); }
```

View File

@@ -0,0 +1,51 @@
#include "fx/video/frame_interpolator.h"
#include "fl/circular_buffer.h"
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fx/video/pixel_stream.h"
#include "fl/dbg.h"
#include "fl/math_macros.h"
#include <math.h>
#define DBG FASTLED_DBG
namespace fl {
FrameInterpolator::FrameInterpolator(size_t nframes, float fps)
: mFrameTracker(fps) {
size_t capacity = MAX(1, nframes);
mFrames.setMaxSize(capacity);
}
bool FrameInterpolator::draw(fl::u32 now, Frame *dst) {
bool ok = draw(now, dst->rgb());
return ok;
}
bool FrameInterpolator::draw(fl::u32 now, CRGB *leds) {
fl::u32 frameNumber, nextFrameNumber;
uint8_t amountOfNextFrame;
// DBG("now: " << now);
mFrameTracker.get_interval_frames(now, &frameNumber, &nextFrameNumber,
&amountOfNextFrame);
if (!has(frameNumber)) {
return false;
}
if (has(frameNumber) && !has(nextFrameNumber)) {
// just paint the current frame
Frame *frame = get(frameNumber).get();
frame->draw(leds);
return true;
}
Frame *frame1 = get(frameNumber).get();
Frame *frame2 = get(nextFrameNumber).get();
Frame::interpolate(*frame1, *frame2, amountOfNextFrame, leds);
return true;
}
} // namespace fl

View File

@@ -0,0 +1,107 @@
#pragma once
#include "fl/map.h"
#include "fl/namespace.h"
#include "fx/frame.h"
#include "fx/video/frame_tracker.h"
#include "fx/video/pixel_stream.h"
namespace fl {
FASTLED_SMART_PTR(FrameInterpolator);
// Holds onto frames and allow interpolation. This allows
// effects to have high effective frame rate and also
// respond to things like sound which can modify the timing.
class FrameInterpolator {
public:
struct Less {
bool operator()(fl::u32 a, fl::u32 b) const { return a < b; }
};
typedef fl::SortedHeapMap<fl::u32, FramePtr, Less> FrameBuffer;
FrameInterpolator(size_t nframes, float fpsVideo);
// Will search through the array, select the two frames that are closest to
// the current time and then interpolate between them, storing the results
// in the provided frame. The destination frame will have "now" as the
// current timestamp if and only if there are two frames that can be
// interpolated. Else it's set to the timestamp of the frame that was
// selected. Returns true if the interpolation was successful, false
// otherwise. If false then the destination frame will not be modified. Note
// that this adjustable_time is allowed to go pause or go backward in time.
bool draw(fl::u32 adjustable_time, Frame *dst);
bool draw(fl::u32 adjustable_time, CRGB *leds);
bool insert(fl::u32 frameNumber, FramePtr frame) {
InsertResult result;
mFrames.insert(frameNumber, frame, &result);
return result != InsertResult::kMaxSize;
}
// Clear all frames
void clear() { mFrames.clear(); }
bool empty() const { return mFrames.empty(); }
bool has(fl::u32 frameNum) const { return mFrames.has(frameNum); }
FramePtr erase(fl::u32 frameNum) {
FramePtr out;
auto it = mFrames.find(frameNum);
if (it == mFrames.end()) {
return out;
}
out = it->second;
mFrames.erase(it);
return out;
}
FramePtr get(fl::u32 frameNum) const {
auto it = mFrames.find(frameNum);
if (it != mFrames.end()) {
return it->second;
}
return FramePtr();
}
bool full() const { return mFrames.full(); }
size_t capacity() const { return mFrames.capacity(); }
FrameBuffer *getFrames() { return &mFrames; }
bool needsFrame(fl::u32 now, fl::u32 *currentFrameNumber,
fl::u32 *nextFrameNumber) const {
mFrameTracker.get_interval_frames(now, currentFrameNumber,
nextFrameNumber);
return !has(*currentFrameNumber) || !has(*nextFrameNumber);
}
bool get_newest_frame_number(fl::u32 *frameNumber) const {
if (mFrames.empty()) {
return false;
}
auto &front = mFrames.back();
*frameNumber = front.first;
return true;
}
bool get_oldest_frame_number(fl::u32 *frameNumber) const {
if (mFrames.empty()) {
return false;
}
auto &front = mFrames.front();
*frameNumber = front.first;
return true;
}
fl::u32 get_exact_timestamp_ms(fl::u32 frameNumber) const {
return mFrameTracker.get_exact_timestamp_ms(frameNumber);
}
FrameTracker &getFrameTracker() { return mFrameTracker; }
private:
FrameBuffer mFrames;
FrameTracker mFrameTracker;
};
} // namespace fl

View File

@@ -0,0 +1,52 @@
#include "frame_tracker.h"
#include "fl/int.h"
namespace fl {
namespace { // anonymous namespace
long linear_map(long x, long in_min, long in_max, long out_min, long out_max) {
const long run = in_max - in_min;
if (run == 0) {
return 0; // AVR returns -1, SAM returns 0
}
const long rise = out_max - out_min;
const long delta = x - in_min;
return (delta * rise) / run + out_min;
}
} // anonymous namespace
FrameTracker::FrameTracker(float fps) {
// Convert fps to microseconds per frame interval
mMicrosSecondsPerInterval = static_cast<fl::u32>(1000000.0f / fps + .5f);
}
void FrameTracker::get_interval_frames(fl::u32 now, fl::u32 *frameNumber,
fl::u32 *nextFrameNumber,
uint8_t *amountOfNextFrame) const {
// Account for any pause time
fl::u32 effectiveTime = now;
// Convert milliseconds to microseconds for precise calculation
fl::u64 microseconds = static_cast<fl::u64>(effectiveTime) * 1000ULL;
// Calculate frame number with proper rounding
*frameNumber = microseconds / mMicrosSecondsPerInterval;
*nextFrameNumber = *frameNumber + 1;
// Calculate interpolation amount if requested
if (amountOfNextFrame != nullptr) {
fl::u64 frame1_start = (*frameNumber * mMicrosSecondsPerInterval);
fl::u64 frame2_start = (*nextFrameNumber * mMicrosSecondsPerInterval);
fl::u32 rel_time = microseconds - frame1_start;
fl::u32 frame_duration = frame2_start - frame1_start;
uint8_t progress = uint8_t(linear_map(rel_time, 0, frame_duration, 0, 255));
*amountOfNextFrame = progress;
}
}
fl::u32 FrameTracker::get_exact_timestamp_ms(fl::u32 frameNumber) const {
fl::u64 microseconds = frameNumber * mMicrosSecondsPerInterval;
return static_cast<fl::u32>(microseconds / 1000) + mStartTime;
}
} // namespace fl

View File

@@ -0,0 +1,37 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/int.h"
// #include <iostream>
// using namespace std;
namespace fl {
// Tracks the current frame number based on the time elapsed since the start of
// the animation.
class FrameTracker {
public:
FrameTracker(float fps);
// Gets the current frame and the next frame number based on the current
// time.
void get_interval_frames(fl::u32 now, fl::u32 *frameNumber,
fl::u32 *nextFrameNumber,
uint8_t *amountOfNextFrame = nullptr) const;
// Given a frame number, returns the exact timestamp in milliseconds that
// the frame should be displayed.
fl::u32 get_exact_timestamp_ms(fl::u32 frameNumber) const;
fl::u32 microsecondsPerFrame() const { return mMicrosSecondsPerInterval; }
private:
fl::u32 mMicrosSecondsPerInterval;
fl::u32 mStartTime = 0;
};
} // namespace fl

View File

@@ -0,0 +1,196 @@
#include "fx/video/pixel_stream.h"
#include "fl/dbg.h"
#include "fl/namespace.h"
#ifndef INT32_MAX
#define INT32_MAX 0x7fffffff
#endif
#define DBG FASTLED_DBG
using fl::ByteStreamPtr;
using fl::FileHandlePtr;
namespace fl {
PixelStream::PixelStream(int bytes_per_frame)
: mbytesPerFrame(bytes_per_frame), mUsingByteStream(false) {}
PixelStream::~PixelStream() { close(); }
bool PixelStream::begin(FileHandlePtr h) {
close();
mFileHandle = h;
mUsingByteStream = false;
return mFileHandle->available();
}
bool PixelStream::beginStream(ByteStreamPtr s) {
close();
mByteStream = s;
mUsingByteStream = true;
return mByteStream->available(mbytesPerFrame);
}
void PixelStream::close() {
if (!mUsingByteStream && mFileHandle) {
mFileHandle.reset();
}
mByteStream.reset();
mFileHandle.reset();
}
int32_t PixelStream::bytesPerFrame() { return mbytesPerFrame; }
bool PixelStream::readPixel(CRGB *dst) {
if (mUsingByteStream) {
return mByteStream->read(&dst->r, 1) && mByteStream->read(&dst->g, 1) &&
mByteStream->read(&dst->b, 1);
} else {
return mFileHandle->read(&dst->r, 1) && mFileHandle->read(&dst->g, 1) &&
mFileHandle->read(&dst->b, 1);
}
}
bool PixelStream::available() const {
if (mUsingByteStream) {
return mByteStream->available(mbytesPerFrame);
} else {
return mFileHandle->available();
}
}
bool PixelStream::atEnd() const {
if (mUsingByteStream) {
return false;
} else {
return !mFileHandle->available();
}
}
bool PixelStream::readFrame(Frame *frame) {
if (!frame) {
return false;
}
if (!mUsingByteStream) {
if (!framesRemaining()) {
return false;
}
size_t n = mFileHandle->readCRGB(frame->rgb(), mbytesPerFrame / 3);
DBG("pos: " << mFileHandle->pos());
return n * 3 == size_t(mbytesPerFrame);
}
size_t n = mByteStream->readCRGB(frame->rgb(), mbytesPerFrame / 3);
return n * 3 == size_t(mbytesPerFrame);
}
bool PixelStream::hasFrame(fl::u32 frameNumber) {
if (mUsingByteStream) {
// ByteStream doesn't support seeking
DBG("Not implemented and therefore always returns true");
return true;
} else {
size_t total_bytes = mFileHandle->size();
return frameNumber * mbytesPerFrame < total_bytes;
}
}
bool PixelStream::readFrameAt(fl::u32 frameNumber, Frame *frame) {
// DBG("read frame at " << frameNumber);
if (mUsingByteStream) {
// ByteStream doesn't support seeking
FASTLED_DBG("ByteStream doesn't support seeking");
return false;
} else {
// DBG("mbytesPerFrame: " << mbytesPerFrame);
mFileHandle->seek(frameNumber * mbytesPerFrame);
if (mFileHandle->bytesLeft() == 0) {
return false;
}
size_t read =
mFileHandle->readCRGB(frame->rgb(), mbytesPerFrame / 3) * 3;
// DBG("read: " << read);
// DBG("pos: " << mFileHandle->Position());
bool ok = int(read) == mbytesPerFrame;
if (!ok) {
DBG("readFrameAt failed - read: "
<< read << ", mbytesPerFrame: " << mbytesPerFrame << ", frame:"
<< frameNumber << ", left: " << mFileHandle->bytesLeft());
}
return ok;
}
}
int32_t PixelStream::framesRemaining() const {
if (mbytesPerFrame == 0)
return 0;
int32_t bytes_left = bytesRemaining();
if (bytes_left <= 0) {
return 0;
}
return bytes_left / mbytesPerFrame;
}
int32_t PixelStream::framesDisplayed() const {
if (mUsingByteStream) {
// ByteStream doesn't have a concept of total size, so we can't
// calculate this
return -1;
} else {
int32_t bytes_played = mFileHandle->pos();
return bytes_played / mbytesPerFrame;
}
}
int32_t PixelStream::bytesRemaining() const {
if (mUsingByteStream) {
return INT32_MAX;
} else {
return mFileHandle->bytesLeft();
}
}
int32_t PixelStream::bytesRemainingInFrame() const {
return bytesRemaining() % mbytesPerFrame;
}
bool PixelStream::rewind() {
if (mUsingByteStream) {
// ByteStream doesn't support rewinding
return false;
} else {
mFileHandle->seek(0);
return true;
}
}
PixelStream::Type PixelStream::getType() const {
return mUsingByteStream ? Type::kStreaming : Type::kFile;
}
size_t PixelStream::readBytes(uint8_t *dst, size_t len) {
uint16_t bytesRead = 0;
if (mUsingByteStream) {
while (bytesRead < len && mByteStream->available(len)) {
// use pop_front()
if (mByteStream->read(dst + bytesRead, 1)) {
bytesRead++;
} else {
break;
}
}
} else {
while (bytesRead < len && mFileHandle->available()) {
if (mFileHandle->read(dst + bytesRead, 1)) {
bytesRead++;
} else {
break;
}
}
}
return bytesRead;
}
} // namespace fl

View File

@@ -0,0 +1,63 @@
#pragma once
#include "crgb.h"
#include "fl/bytestream.h"
#include "fl/file_system.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fx/frame.h"
#include "fl/int.h"
namespace fl {
FASTLED_SMART_PTR(FileHandle);
FASTLED_SMART_PTR(ByteStream);
} // namespace fl
namespace fl {
FASTLED_SMART_PTR(PixelStream);
// PixelStream takes either a file handle or a byte stream
// and reads frames from it in order to serve data to the
// video system.
class PixelStream {
public:
enum Type {
kStreaming,
kFile,
};
explicit PixelStream(int bytes_per_frame);
bool begin(fl::FileHandlePtr h);
bool beginStream(fl::ByteStreamPtr s);
void close();
int32_t bytesPerFrame();
bool readPixel(CRGB *dst); // Convenience function to read a pixel
size_t readBytes(uint8_t *dst, size_t len);
bool readFrame(Frame *frame);
bool readFrameAt(fl::u32 frameNumber, Frame *frame);
bool hasFrame(fl::u32 frameNumber);
int32_t framesRemaining() const; // -1 if this is a stream.
int32_t framesDisplayed() const;
bool available() const;
bool atEnd() const;
int32_t bytesRemaining() const;
int32_t bytesRemainingInFrame() const;
bool
rewind(); // Returns false on failure, which can happen for streaming mode.
Type getType()
const; // Returns the type of the video stream (kStreaming or kFile)
private:
fl::i32 mbytesPerFrame;
fl::FileHandlePtr mFileHandle;
fl::ByteStreamPtr mByteStream;
bool mUsingByteStream;
public:
virtual ~PixelStream();
};
} // namespace fl

View File

@@ -0,0 +1,355 @@
#include "video_impl.h"
#include "fl/assert.h"
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fl/warn.h"
namespace fl {
VideoImpl::VideoImpl(size_t pixelsPerFrame, float fpsVideo,
size_t nFramesInBuffer)
: mPixelsPerFrame(pixelsPerFrame),
mFrameInterpolator(
fl::make_shared<FrameInterpolator>(MAX(1, nFramesInBuffer), fpsVideo)) {}
void VideoImpl::pause(fl::u32 now) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
}
mTime->pause(now);
}
void VideoImpl::resume(fl::u32 now) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
}
mTime->resume(now);
}
void VideoImpl::setTimeScale(float timeScale) {
mTimeScale = timeScale;
if (mTime) {
mTime->setSpeed(timeScale);
}
}
void VideoImpl::setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime) {
mFadeInTime = fadeInTime;
mFadeOutTime = fadeOutTime;
}
bool VideoImpl::needsFrame(fl::u32 now) const {
fl::u32 f1, f2;
bool out = mFrameInterpolator->needsFrame(now, &f1, &f2);
return out;
}
VideoImpl::~VideoImpl() { end(); }
void VideoImpl::begin(FileHandlePtr h) {
end();
// Removed setStartTime call
mStream = fl::make_shared<PixelStream>(mPixelsPerFrame * kSizeRGB8);
mStream->begin(h);
mPrevNow = 0;
}
void VideoImpl::beginStream(ByteStreamPtr bs) {
end();
mStream = fl::make_shared<PixelStream>(mPixelsPerFrame * kSizeRGB8);
// Removed setStartTime call
mStream->beginStream(bs);
mPrevNow = 0;
}
void VideoImpl::end() {
mFrameInterpolator->clear();
// Removed resetFrameCounter and setStartTime calls
mStream.reset();
}
bool VideoImpl::full() const { return mFrameInterpolator->getFrames()->full(); }
bool VideoImpl::draw(fl::u32 now, Frame *frame) {
return draw(now, frame->rgb());
}
int32_t VideoImpl::durationMicros() const {
if (!mStream) {
return -1;
}
int32_t frames = mStream->framesRemaining();
if (frames < 0) {
return -1; // Stream case, duration unknown
}
fl::u32 micros_per_frame =
mFrameInterpolator->getFrameTracker().microsecondsPerFrame();
return (frames * micros_per_frame); // Convert to milliseconds
}
bool VideoImpl::draw(fl::u32 now, CRGB *leds) {
if (!mTime) {
mTime = fl::make_shared<TimeWarp>(now);
mTime->setSpeed(mTimeScale);
mTime->reset(now);
}
now = mTime->update(now);
if (!mStream) {
FASTLED_WARN("no stream");
return false;
}
bool ok = updateBufferIfNecessary(mPrevNow, now);
mPrevNow = now;
if (!ok) {
FASTLED_WARN("updateBufferIfNecessary failed");
return false;
}
mFrameInterpolator->draw(now, leds);
fl::u32 time = mTime->time();
fl::u32 brightness = 255;
// Compute fade in/out brightness.
if (mFadeInTime || mFadeOutTime) {
brightness = 255;
if (time <= mFadeInTime) {
if (mFadeInTime == 0) {
brightness = 255;
} else {
brightness = time * 255 / mFadeInTime;
}
} else if (mFadeOutTime) {
int32_t frames_remaining = mStream->framesRemaining();
if (frames_remaining < 0) {
// -1 means this is a stream.
brightness = 255;
} else {
FrameTracker &frame_tracker =
mFrameInterpolator->getFrameTracker();
fl::u32 micros_per_frame =
frame_tracker.microsecondsPerFrame();
fl::u32 millis_left =
(frames_remaining * micros_per_frame) / 1000;
if (millis_left < mFadeOutTime) {
brightness = millis_left * 255 / mFadeOutTime;
}
}
}
}
if (brightness < 255) {
if (brightness == 0) {
for (size_t i = 0; i < mPixelsPerFrame; ++i) {
leds[i] = CRGB::Black;
}
} else {
for (size_t i = 0; i < mPixelsPerFrame; ++i) {
leds[i].nscale8(brightness);
}
}
}
return true;
}
bool VideoImpl::updateBufferFromStream(fl::u32 now) {
FASTLED_ASSERT(mTime, "mTime is null");
if (!mStream) {
FASTLED_WARN("no stream");
return false;
}
if (mStream->atEnd()) {
return false;
}
fl::u32 currFrameNumber = 0;
fl::u32 nextFrameNumber = 0;
bool needs_frame =
mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
if (!needs_frame) {
return true;
}
if (mFrameInterpolator->capacity() == 0) {
FASTLED_WARN("capacity == 0");
return false;
}
const bool has_current_frame = mFrameInterpolator->has(currFrameNumber);
const bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);
fl::FixedVector<fl::u32, 2> frame_numbers;
if (!has_current_frame) {
frame_numbers.push_back(currFrameNumber);
}
size_t capacity = mFrameInterpolator->capacity();
if (capacity > 1 && !has_next_frame) {
frame_numbers.push_back(nextFrameNumber);
}
for (size_t i = 0; i < frame_numbers.size(); ++i) {
FramePtr recycled_frame;
if (mFrameInterpolator->full()) {
fl::u32 frame_to_erase = 0;
bool ok =
mFrameInterpolator->get_oldest_frame_number(&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_oldest_frame_number failed");
return false;
}
recycled_frame = mFrameInterpolator->erase(frame_to_erase);
if (!recycled_frame) {
FASTLED_WARN("erase failed for frame: " << frame_to_erase);
return false;
}
}
fl::u32 frame_to_fetch = frame_numbers[i];
if (!recycled_frame) {
// Happens when we are not full and we need to allocate a new frame.
recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
}
if (!mStream->readFrame(recycled_frame.get())) {
if (mStream->atEnd()) {
if (!mStream->rewind()) {
FASTLED_WARN("rewind failed");
return false;
}
mTime->reset(now);
frame_to_fetch = 0;
if (!mStream->readFrameAt(frame_to_fetch,
recycled_frame.get())) {
FASTLED_WARN("readFrameAt failed");
return false;
}
} else {
FASTLED_WARN("We failed for some other reason");
return false;
}
}
bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
if (!ok) {
FASTLED_WARN("insert failed");
return false;
}
}
return true;
}
bool VideoImpl::updateBufferFromFile(fl::u32 now, bool forward) {
fl::u32 currFrameNumber = 0;
fl::u32 nextFrameNumber = 0;
bool needs_frame =
mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
if (!needs_frame) {
return true;
}
bool has_curr_frame = mFrameInterpolator->has(currFrameNumber);
bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);
if (has_curr_frame && has_next_frame) {
return true;
}
if (mFrameInterpolator->capacity() == 0) {
FASTLED_WARN("capacity == 0");
return false;
}
fl::FixedVector<fl::u32, 2> frame_numbers;
if (!mFrameInterpolator->has(currFrameNumber)) {
frame_numbers.push_back(currFrameNumber);
}
if (mFrameInterpolator->capacity() > 1 &&
!mFrameInterpolator->has(nextFrameNumber)) {
frame_numbers.push_back(nextFrameNumber);
}
for (size_t i = 0; i < frame_numbers.size(); ++i) {
FramePtr recycled_frame;
if (mFrameInterpolator->full()) {
fl::u32 frame_to_erase = 0;
bool ok = false;
if (forward) {
ok = mFrameInterpolator->get_oldest_frame_number(
&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_oldest_frame_number failed");
return false;
}
} else {
ok = mFrameInterpolator->get_newest_frame_number(
&frame_to_erase);
if (!ok) {
FASTLED_WARN("get_newest_frame_number failed");
return false;
}
}
recycled_frame = mFrameInterpolator->erase(frame_to_erase);
if (!recycled_frame) {
FASTLED_WARN("erase failed for frame: " << frame_to_erase);
return false;
}
}
fl::u32 frame_to_fetch = frame_numbers[i];
if (!recycled_frame) {
// Happens when we are not full and we need to allocate a new frame.
recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
}
do { // only to use break
if (!mStream->readFrameAt(frame_to_fetch, recycled_frame.get())) {
if (!forward) {
// nothing more we can do, we can't go negative.
return false;
}
if (mStream->atEnd()) {
if (!mStream->rewind()) { // Is this still
FASTLED_WARN("rewind failed");
return false;
}
mTime->reset(now);
frame_to_fetch = 0;
if (!mStream->readFrameAt(frame_to_fetch,
recycled_frame.get())) {
FASTLED_WARN("readFrameAt failed");
return false;
}
break; // we have the frame, so we can break out of the loop
}
FASTLED_WARN("We failed for some other reason");
return false;
}
break;
} while (false);
bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
if (!ok) {
FASTLED_WARN("insert failed");
return false;
}
}
return true;
}
bool VideoImpl::updateBufferIfNecessary(fl::u32 prev, fl::u32 now) {
const bool forward = now >= prev;
PixelStream::Type type = mStream->getType();
switch (type) {
case PixelStream::kFile:
return updateBufferFromFile(now, forward);
case PixelStream::kStreaming:
return updateBufferFromStream(now);
default:
FASTLED_WARN("Unknown type: " << fl::u32(type));
return false;
}
}
bool VideoImpl::rewind() {
if (!mStream || !mStream->rewind()) {
return false;
}
mFrameInterpolator->clear();
return true;
}
} // namespace fl

View File

@@ -0,0 +1,65 @@
#pragma once
#include "fl/bytestream.h"
#include "fl/file_system.h"
#include "fx/video/frame_interpolator.h"
#include "fx/video/pixel_stream.h"
#include "fl/stdint.h"
#include "fl/namespace.h"
namespace fl {
FASTLED_SMART_PTR(FileHandle);
FASTLED_SMART_PTR(ByteStream);
} // namespace fl
namespace fl {
FASTLED_SMART_PTR(VideoImpl);
FASTLED_SMART_PTR(FrameInterpolator);
FASTLED_SMART_PTR(PixelStream)
class VideoImpl {
public:
enum {
kSizeRGB8 = 3,
};
// frameHistoryCount is the number of frames to keep in the buffer after
// draw. This allows for time based effects like syncing video speed to
// audio triggers.
VideoImpl(size_t pixelsPerFrame, float fpsVideo,
size_t frameHistoryCount = 0);
~VideoImpl();
// Api
void begin(fl::FileHandlePtr h);
void beginStream(fl::ByteStreamPtr s);
void setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime);
bool draw(fl::u32 now, CRGB *leds);
void end();
bool rewind();
// internal use
bool draw(fl::u32 now, Frame *frame);
bool full() const;
void setTimeScale(float timeScale);
float timeScale() const { return mTimeScale; }
size_t pixelsPerFrame() const { return mPixelsPerFrame; }
void pause(fl::u32 now);
void resume(fl::u32 now);
bool needsFrame(fl::u32 now) const;
int32_t durationMicros() const; // -1 if this is a stream.
private:
bool updateBufferIfNecessary(fl::u32 prev, fl::u32 now);
bool updateBufferFromFile(fl::u32 now, bool forward);
bool updateBufferFromStream(fl::u32 now);
fl::u32 mPixelsPerFrame = 0;
PixelStreamPtr mStream;
fl::u32 mPrevNow = 0;
FrameInterpolatorPtr mFrameInterpolator;
TimeWarpPtr mTime;
fl::u32 mFadeInTime = 1000;
fl::u32 mFadeOutTime = 1000;
float mTimeScale = 1.0f;
};
} // namespace fl