Files
Jackson AndresandClaude Sonnet 5.5 8df53d6d9f
Installer / Build (push) Failing after 4s
Installer / Publish (push) Skipped
Add Seasonal effect and animate the holiday effects
Seasonal picks the holiday effect from the date, using an NTP clock set to
the follow_me_timezone substitution (default UTC). Dates: New Year's, Valentine's,
St. Patrick's, Easter (Good Friday to Easter Monday), Haunted and Halloween,
Hanukkah (computed from the Hebrew calendar), Christmas and Autumn. Other
days show the plain glow. The date logic was checked against independent
Hebrew-calendar and Easter libraries for 2020 to 2060.

Autumn gets falling, swaying, tumbling leaves with periodic gusts and a brown
leaf color. Candy Cane gets a gloss glint, Easter a shimmer and sparkles,
Valentine's rising hearts, New Year's fireworks, and Halloween a bat flyby
and lightning.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 11:50:59 -05:00

562 lines
24 KiB
C++

#pragma once
// Effects for the follow-me glow (follow-me.yaml).
//
// Each effect paints one LED's colour inside a lit window; the renderer then
// scales it by the Follow-Me Glow brightness and the window's coverage and
// fade, exactly as it scales the plain colour. Patterns are fixed to the
// stairs (wall inches), not to the window, so a person walks through them.
// Wall inches increase down the stairs; "falling" patterns move toward larger
// wall values, "rising" ones toward smaller.
//
// Colours are written as perceptual (sRGB-like) values 0-1 per R, G, B and
// the strip's white channel; paint() squares them (gamma 2) so
// saturated mixes such as orange stay orange on the LEDs.
//
// Nothing here depends on ESPHome, so the file also compiles on a desktop for
// previews. EFFECT_NAMES must match the effect names on the Follow-Me Glow
// light in follow-me.yaml.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
namespace follow_me {
namespace fx {
enum Effect {
NONE = 0,
SEASONAL,
RAINBOW,
NEW_YEARS,
VALENTINES,
ST_PATRICKS,
EASTER,
FOURTH_OF_JULY,
AUTUMN,
HALLOWEEN,
HAUNTED,
HANUKKAH,
CHRISTMAS,
CANDY_CANE,
SNOWFALL,
FIREPLACE,
AURORA,
OCEAN,
EFFECT_COUNT,
};
static const char *const EFFECT_NAMES[EFFECT_COUNT] = {
"None", "Seasonal", "Rainbow", "New Year's", "Valentine's", "St. Patrick's",
"Easter", "Fourth of July", "Autumn", "Halloween", "Haunted", "Hanukkah",
"Christmas", "Candy Cane", "Snowfall", "Fireplace", "Aurora", "Ocean",
};
// Length of the stairs in wall inches, for patterns that span them
// (Snowfall). Set by the follow-me loop every frame.
inline float extent = 240.0f;
inline void set_extent(float wall_inches) { extent = fmaxf(wall_inches, 24.0f); }
inline Effect from_name(const char *name, size_t len) {
for (int i = 1; i < EFFECT_COUNT; i++) {
if (strlen(EFFECT_NAMES[i]) == len && strncmp(EFFECT_NAMES[i], name, len) == 0) return (Effect) i;
}
return NONE;
}
struct Rgbw {
float r, g, b, w;
};
inline Rgbw scale(const Rgbw &c, float k) { return {c.r * k, c.g * k, c.b * k, c.w * k}; }
inline Rgbw mix(const Rgbw &a, const Rgbw &b, float f) {
return {a.r + (b.r - a.r) * f, a.g + (b.g - a.g) * f, a.b + (b.b - a.b) * f, a.w + (b.w - a.w) * f};
}
inline Rgbw lighten(const Rgbw &a, const Rgbw &b) { // per-channel max
return {fmaxf(a.r, b.r), fmaxf(a.g, b.g), fmaxf(a.b, b.b), fmaxf(a.w, b.w)};
}
inline float clamp01(float x) { return x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x); }
inline float smoothstep(float x) {
x = clamp01(x);
return x * x * (3.0f - 2.0f * x);
}
inline float fract(float x) { return x - floorf(x); }
// x wrapped into [0, m), also for negative x.
inline float wrapf(float x, float m) { return x - floorf(x / m) * m; }
inline uint32_t hash(uint32_t x) {
x ^= x >> 16;
x *= 0x7feb352du;
x ^= x >> 15;
x *= 0x846ca68bu;
x ^= x >> 16;
return x;
}
// Deterministic random 0-1 for a (seed, n) pair.
inline float rnd(uint32_t seed, uint32_t n) { return (hash(n * 0x9e3779b1u ^ hash(seed + 0x632be5abu)) >> 8) / 16777216.0f; }
// Smooth 1D value noise, 0-1, one feature per unit of x.
inline float noise(uint32_t seed, float x) {
const float i = floorf(x);
const int32_t n = (int32_t) i;
return rnd(seed, (uint32_t) n) + (rnd(seed, (uint32_t) (n + 1)) - rnd(seed, (uint32_t) n)) * smoothstep(x - i);
}
// Smooth 2D value noise, 0-1, one feature per unit of x and of y.
inline float noise2(uint32_t seed, float x, float y) {
const float j = floorf(y);
const int32_t m = (int32_t) j;
const float a = noise(seed ^ hash((uint32_t) m), x), b = noise(seed ^ hash((uint32_t) (m + 1)), x);
return a + (b - a) * smoothstep(y - j);
}
// Soft-edged stripes: 1 for the first `duty` of each `period`, 0 for the
// rest, with linear edges `soft` units wide (anti-aliased between LEDs).
inline float stripe(float s, float period, float duty, float soft) {
const float p = fract(s / period) * period;
const float on = duty * period;
const float h = soft / 2.0f;
const float rise = clamp01((p + h) / soft); // edge at p = 0, wrapped below
const float fall = clamp01((on + h - p) / soft);
const float wrap = clamp01((p - (period - h)) / soft); // edge at p = period
return fmaxf(fminf(rise, fall), wrap);
}
// Blocks of `size` units, each a random entry of the palette, blended over the
// last `blend` fraction of each block.
inline Rgbw blocks(float s, float size, uint32_t seed, const Rgbw *pal, int n, float blend) {
const float u = s / size;
const float i = floorf(u);
const int32_t k = (int32_t) i;
const Rgbw a = pal[hash(seed ^ hash((uint32_t) k)) % n];
const Rgbw b = pal[hash(seed ^ hash((uint32_t) (k + 1))) % n];
return mix(a, b, smoothstep((u - i - (1.0f - blend)) / blend));
}
// Twinkle envelope 0-1 for one LED: in each `period` seconds the LED flashes
// with probability `chance` (fast rise, slower fall). *slot_out is the period
// index, for picking the twinkle's colour.
inline float twinkle(int led, float t, float period, float chance, uint32_t seed, uint32_t *slot_out = nullptr) {
const float u = t / period + rnd(seed, (uint32_t) led);
const uint32_t slot = (uint32_t) floorf(u);
if (slot_out) *slot_out = slot;
if (rnd(seed ^ 0x5bd1e995u, slot * 1031u + (uint32_t) led) > chance) return 0.0f;
const float f = u - floorf(u);
const float attack = 0.12f;
if (f < attack) return f / attack;
const float d = 1.0f - (f - attack) / (1.0f - attack);
return d * d;
}
inline Rgbw hsv(float h, float s, float v) {
h = fract(h) * 6.0f;
const int i = (int) h;
const float f = h - i;
const float p = v * (1.0f - s), q = v * (1.0f - s * f), t = v * (1.0f - s * (1.0f - f));
switch (i) {
case 0: return {v, t, p, 0};
case 1: return {q, v, p, 0};
case 2: return {p, v, t, 0};
case 3: return {p, q, v, 0};
case 4: return {t, p, v, 0};
default: return {v, p, q, 0};
}
}
// Firework burst: every `period` seconds a ring expands from a random point
// inside the window and fades. Returns 0-1; *slot_out picks its colour.
inline float burst(float wall, float t, float period, uint32_t seed, float focus, float span, uint32_t *slot_out) {
const float u = t / period;
const uint32_t slot = (uint32_t) floorf(u);
*slot_out = slot;
const float f = u - floorf(u);
const float life = 0.75f; // fraction of the period the burst is visible
if (f > life) return 0.0f;
const float age = f / life;
const float centre = focus + (rnd(seed, slot) - 0.5f) * span * 0.8f;
const float radius = 2.0f + 12.0f * sqrtf(age);
const float d = fabsf(wall - centre);
if (d > radius) return 0.0f;
const float ring = 0.35f + 0.65f * clamp01(1.0f - (radius - d) / 3.0f); // brighter rim
const float fade = (1.0f - age) * (1.0f - age);
return ring * fade;
}
// ---- Seasonal: the holiday effect for a calendar date -----------------------
// Days since 1 January of year 1 (Gregorian, day 1 = 1 Jan 1).
inline int gregorian_day(int y, int m, int d) {
const bool leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
return 365 * (y - 1) + (y - 1) / 4 - (y - 1) / 100 + (y - 1) / 400 + (367 * m - 362) / 12 +
(m <= 2 ? 0 : (leap ? -1 : -2)) + d;
}
// Easter Sunday (Gregorian), as a day number.
inline int easter_day(int y) {
const int a = y % 19, b = y / 100, c = y % 100, d = b / 4, e = b % 4, f = (b + 8) / 25, g = (b - f + 1) / 3;
const int h = (19 * a + b - d - g + 15) % 30, i = c / 4, k = c % 4;
const int l = (32 + 2 * e + 2 * i - h - k) % 7, m = (a + 11 * h + 22 * l) / 451;
const int month = (h + l - 7 * m + 114) / 31, day = (h + l - 7 * m + 114) % 31 + 1;
return gregorian_day(y, month, day);
}
// Hebrew calendar: day number of 1 Tishrei (Rosh Hashanah) of Hebrew year hy,
// in the same day numbering as gregorian_day.
inline int hebrew_elapsed_days(int hy) {
const int months = (235 * hy - 234) / 19;
const int parts = 12084 + 13753 * months;
int days = 29 * months + parts / 25920;
if ((3 * (days + 1)) % 7 < 3) days++;
return days;
}
inline int hebrew_new_year(int hy) {
const int ny0 = hebrew_elapsed_days(hy - 1), ny1 = hebrew_elapsed_days(hy), ny2 = hebrew_elapsed_days(hy + 1);
const int delay = ny2 - ny1 == 356 ? 2 : (ny1 - ny0 == 382 ? 1 : 0);
return -1373427 + ny1 + delay;
}
// First day (25 Kislev) of Hanukkah in Hebrew year hy.
inline int hanukkah_day(int hy) {
const int year_days = hebrew_new_year(hy + 1) - hebrew_new_year(hy);
const int cheshvan = year_days % 10 == 5 ? 30 : 29; // 30 days in a "complete" year
return hebrew_new_year(hy) + 30 + cheshvan + 24;
}
// The holiday effect for a date: Hanukkah (8 days), New Year's (31 Dec to
// 2 Jan), Valentine's (10 to 14 Feb), St. Patrick's (15 to 17 Mar), Easter
// (Good Friday to Easter Monday), Haunted (17 to 23 Oct), Halloween (24 to
// 31 Oct), Christmas (1 to 26 Dec) and, for the rest of 22 Sep to 30 Nov,
// Autumn. Any other date gives NONE (the plain glow).
inline Effect seasonal_effect(int y, int m, int d) {
const int today = gregorian_day(y, m, d);
int hy = y + 3760;
if (hebrew_new_year(hy + 1) <= today) hy++;
const int hanukkah = today - hanukkah_day(hy);
if (hanukkah >= 0 && hanukkah <= 7) return HANUKKAH;
if ((m == 12 && d == 31) || (m == 1 && d <= 2)) return NEW_YEARS;
if (m == 2 && d >= 10 && d <= 14) return VALENTINES;
if (m == 3 && d >= 15 && d <= 17) return ST_PATRICKS;
const int easter = today - easter_day(y);
if (easter >= -2 && easter <= 1) return EASTER;
if (m == 10 && d >= 17 && d <= 23) return HAUNTED;
if (m == 10 && d >= 24) return HALLOWEEN;
if (m == 12 && d <= 26) return CHRISTMAS;
if ((m == 9 && d >= 22) || m == 10 || m == 11) return AUTUMN;
return NONE;
}
// An effect with Seasonal resolved to today's effect. The date is looked up
// only when it changes. valid is false while the clock has not been set,
// which gives the plain glow.
inline Effect resolve(Effect e, bool valid, int y, int m, int d) {
if (e != SEASONAL) return e;
if (!valid) return NONE;
static int cached_key = 0;
static Effect cached = NONE;
const int key = (y * 13 + m) * 32 + d;
if (key != cached_key) {
cached = seasonal_effect(y, m, d);
cached_key = key;
}
return cached;
}
// Colour of one LED for an effect. led is the LED index, wall the LED's
// position in wall inches, t seconds (wraps daily), focus the centre of the
// window lighting this LED and span its length in inches.
inline Rgbw paint(Effect e, int led, float wall, float t, float focus, float span) {
Rgbw c = {0, 0, 0, 0};
switch (e) {
case NONE:
case SEASONAL: // resolved to a real effect by resolve() before painting
case EFFECT_COUNT:
break;
case RAINBOW:
// One full rainbow per 30 in of stairs, drifting slowly downward.
c = hsv(wall / 30.0f - t / 12.0f, 1.0f, 1.0f);
break;
case NEW_YEARS: {
// Champagne gold with fast silver and gold confetti.
static const Rgbw GOLD = {1.0f, 0.62f, 0.12f, 0.35f};
static const Rgbw CONFETTI[3] = {{0.55f, 0.6f, 0.9f, 1.0f}, {1.0f, 0.75f, 0.2f, 0.6f}, {0, 0, 0, 1.0f}};
c = scale(GOLD, 0.45f + 0.15f * noise(11, wall / 8.0f + t * 0.6f));
uint32_t slot;
const float tw = twinkle(led, t, 0.7f, 0.35f, 12, &slot);
if (tw > 0.0f) c = mix(c, CONFETTI[hash(slot * 7u + led) % 3], tw);
// Champagne fireworks burst around the person.
static const Rgbw FIREWORK[3] = {{1.0f, 0.7f, 0.2f, 0.4f}, {0.4f, 0.45f, 0.7f, 1.0f}, {1.0f, 0.35f, 0.3f, 0.2f}};
uint32_t fslot;
float b = burst(wall, t, 2.1f, 14, focus, span, &fslot);
if (b > 0.0f) c = lighten(c, scale(FIREWORK[fslot % 3], b));
b = burst(wall, t + 0.9f, 2.9f, 15, focus, span, &fslot);
if (b > 0.0f) c = lighten(c, scale(FIREWORK[(fslot + 1) % 3], b));
break;
}
case VALENTINES: {
// Pink and red drifting together, pulsing with a heartbeat.
static const Rgbw PINK = {1.0f, 0.3f, 0.55f, 0.1f};
static const Rgbw RED = {1.0f, 0.02f, 0.12f, 0};
c = mix(PINK, RED, smoothstep(noise(21, wall / 14.0f - t * 0.25f) * 1.6f - 0.3f));
const float p = fract(t / 1.4f);
const float lub = expf(-p * 14.0f), dub = p > 0.22f ? 0.7f * expf(-(p - 0.22f) * 14.0f) : 0.0f;
c = scale(c, 0.5f + 0.5f * fminf(1.0f, lub + dub));
// Hearts float up the stairs: from below the bottom to above the top,
// one per 14 in of rise on any stairs.
static const Rgbw HEART = {1.0f, 0.55f, 0.7f, 0.55f};
static const float MARGIN = 20.0f;
static const int MAX_HEARTS = 64;
const float loop = extent + 2.0f * MARGIN;
const int hearts = std::min(MAX_HEARTS, (int) (loop / 14.0f) + 1);
static float heart_t = -1.0f, heart_pos[MAX_HEARTS], heart_v[MAX_HEARTS];
if (t != heart_t) {
heart_t = t;
for (int k = 0; k < hearts; k++) {
const float speed = 2.5f + 2.5f * rnd(22, k);
const float sway = 1.2f * sinf(t * (1.0f + rnd(23, k)) + 6.2831853f * rnd(24, k));
heart_pos[k] = wrapf(rnd(25, k) * loop - speed * t, loop) - MARGIN + sway;
heart_v[k] = 0.65f + 0.35f * sinf(t * (2.0f + 2.0f * rnd(26, k)) + 6.2831853f * rnd(27, k));
}
}
for (int k = 0; k < hearts; k++) {
const float d = fabsf(wall - heart_pos[k]);
if (d < 2.0f) c = lighten(c, scale(HEART, heart_v[k] * (1.0f - d / 2.0f)));
}
break;
}
case ST_PATRICKS: {
// Drifting greens with gold glints.
static const Rgbw GREENS[3] = {{0, 1.0f, 0.2f, 0}, {0.3f, 1.0f, 0, 0}, {0, 0.85f, 0.45f, 0}};
static const Rgbw GOLD = {1.0f, 0.65f, 0, 0.4f};
c = blocks(wall - t * 2.0f, 7.0f, 31, GREENS, 3, 0.4f);
c = mix(c, GOLD, twinkle(led, t, 1.6f, 0.18f, 32));
break;
}
case EASTER: {
// Slow pastel blocks.
static const Rgbw PASTELS[5] = {{1.0f, 0.4f, 0.6f, 0.15f}, {0.35f, 1.0f, 0.55f, 0.1f}, {0.7f, 0.45f, 1.0f, 0.1f},
{1.0f, 0.8f, 0.3f, 0.15f}, {0.4f, 0.7f, 1.0f, 0.1f}};
c = blocks(wall - t * 1.5f, 9.0f, 41, PASTELS, 5, 0.5f);
// Breathing shimmer, and the odd soft sparkle.
static const Rgbw SPARKLE = {0.35f, 0.35f, 0.4f, 1.0f};
c = scale(c, 0.78f + 0.22f * noise(42, wall / 5.0f + t * 0.8f));
c = mix(c, SPARKLE, twinkle(led, t, 1.4f, 0.1f, 43));
break;
}
case FOURTH_OF_JULY: {
// Night-sky navy with red, white and blue fireworks around the person.
static const Rgbw SKY = {0, 0.02f, 0.3f, 0};
static const Rgbw SPARK[3] = {{1.0f, 0.02f, 0.05f, 0}, {0.2f, 0.2f, 0.35f, 1.0f}, {0.15f, 0.35f, 1.0f, 0}};
c = SKY;
uint32_t slot;
float b = burst(wall, t, 1.7f, 51, focus, span, &slot);
if (b > 0.0f) c = lighten(c, scale(SPARK[slot % 3], b));
b = burst(wall, t + 0.6f, 2.3f, 52, focus, span, &slot);
if (b > 0.0f) c = lighten(c, scale(SPARK[(slot + 1) % 3], b));
const float tw = twinkle(led, t, 0.9f, 0.08f, 53);
if (tw > 0.0f) c = lighten(c, scale(SPARK[1], tw));
break;
}
case AUTUMN: {
// Rust, amber, gold, russet and brown leaves drifting down the stairs.
static const Rgbw LEAVES[5] = {{0.85f, 0.2f, 0, 0}, {1.0f, 0.5f, 0, 0}, {1.0f, 0.75f, 0.08f, 0.1f},
{0.6f, 0.08f, 0.02f, 0}, {0.32f, 0.15f, 0.06f, 0}};
// Dim drifting colour blocks form the canopy; individual leaves fall
// through it, each at its own speed, swaying and tumbling.
c = blocks(wall - t * 3.0f, 5.0f, 61, LEAVES, 5, 0.35f);
c = scale(c, 0.55f * (0.8f + 0.2f * noise(62, wall / 3.0f - t * 1.5f)));
// Leaves fall from above the top to below the bottom and reappear at
// the top; their count keeps one leaf per 12 in of fall on any stairs.
static const float MARGIN = 20.0f;
static const int MAX_LEAVES = 96;
static const float GUST_CYCLE = 14.0f; // seconds between gusts
static const float GUST_PUSH = 14.0f; // inches a gust moves every leaf
static const float GUST_SECS = 2.0f; // how long a gust lasts
const float loop = extent + 2.0f * MARGIN;
const int leaves = std::min(MAX_LEAVES, (int) (loop / 12.0f) + 1);
// Leaf positions, computed once per frame.
static float leaf_t = -1.0f, leaf_pos[MAX_LEAVES], leaf_v[MAX_LEAVES];
static uint8_t leaf_col[MAX_LEAVES];
if (t != leaf_t) {
leaf_t = t;
const float cycle = floorf(t / GUST_CYCLE);
const float local = t - cycle * GUST_CYCLE;
const float gust_at = 2.0f + rnd(63, (uint32_t) cycle) * (GUST_CYCLE - GUST_SECS - 4.0f);
const float gust = GUST_PUSH * (cycle + smoothstep((local - gust_at) / GUST_SECS));
for (int k = 0; k < leaves; k++) {
const float speed = 3.0f + 4.0f * rnd(64, k);
const float sway = 1.6f * sinf(t * (1.3f + 1.2f * rnd(65, k)) + 6.2831853f * rnd(66, k));
leaf_pos[k] = fmodf(rnd(67, k) * loop + speed * t + gust, loop) - MARGIN + sway;
leaf_v[k] = 0.5f + 0.5f * sinf(t * (1.8f + 1.6f * rnd(68, k)) + 6.2831853f * rnd(69, k)); // tumble
leaf_col[k] = (uint8_t) (hash(k * 2654435761u + 70u) % 5u);
}
}
for (int k = 0; k < leaves; k++) {
const float d = fabsf(wall - leaf_pos[k]);
if (d < 2.4f) c = lighten(c, scale(LEAVES[leaf_col[k]], (0.45f + 0.55f * leaf_v[k]) * (1.0f - d / 2.4f)));
}
break;
}
case HALLOWEEN: {
// Jack-o'-lantern orange with candle flicker; purple patches drift
// through and the odd LED flashes slime green.
static const Rgbw ORANGE = {1.0f, 0.38f, 0, 0};
static const Rgbw PURPLE = {0.6f, 0, 1.0f, 0};
static const Rgbw SLIME = {0.35f, 1.0f, 0, 0};
const float m = smoothstep((noise(71, wall / 16.0f - t * 0.2f) - 0.5f) * 4.0f);
c = mix(ORANGE, PURPLE, m);
c = scale(c, 0.55f + 0.45f * noise2(72, t * 6.0f, wall / 4.0f)); // flickers in clumps of a few LEDs
c = mix(c, SLIME, twinkle(led, t, 2.5f, 0.05f, 73));
// A bat flies through every so often: a fast dark streak that flaps.
static const float MARGIN = 20.0f;
static const float BAT_CYCLE = 11.0f;
const float loop = extent + 2.0f * MARGIN;
const float bat_speed = fmaxf(45.0f, loop / 8.0f);
const uint32_t bslot = (uint32_t) floorf(t / BAT_CYCLE);
const float bt = t - bslot * BAT_CYCLE - 1.0f;
if (bt >= 0.0f && bt * bat_speed < loop) {
const float travel = bt * bat_speed;
const float bat = (rnd(74, bslot) < 0.5f ? travel : loop - travel) - MARGIN;
const float flap = 0.7f + 0.3f * sinf(t * 28.0f);
c = scale(c, 1.0f - 0.85f * flap * clamp01(1.0f - fabsf(wall - bat) / 3.5f));
}
// Now and then lightning flashes twice, pale purple-white.
static const Rgbw FLASH = {0.55f, 0.5f, 1.0f, 0.8f};
static const float STORM_CYCLE = 17.0f;
const uint32_t lslot = (uint32_t) floorf(t / STORM_CYCLE);
if (rnd(75, lslot) < 0.4f) {
const float lt = t - lslot * STORM_CYCLE - (1.0f + rnd(76, lslot) * 14.0f);
if ((lt >= 0.0f && lt < 0.06f) || (lt >= 0.14f && lt < 0.22f)) c = mix(c, FLASH, 0.8f);
}
break;
}
case HAUNTED: {
// Cold ghost light that breathes, with a slow dark patch wandering
// through it, and every few seconds the whole thing stutters like a
// dying bulb.
static const Rgbw GHOST = {0.12f, 0.45f, 0.75f, 0.55f};
float v = 0.75f + 0.25f * sinf(t * 1.3f);
v *= 0.35f + 0.65f * smoothstep((fabsf(wall - (focus + 0.6f * span * sinf(t * 0.4f))) - 2.0f) / 5.0f);
const float cycle = 6.0f;
const uint32_t slot = (uint32_t) floorf(t / cycle);
const float local = t - slot * cycle - rnd(81, slot) * 4.5f;
if (rnd(82, slot) < 0.65f && local >= 0.0f && local < 1.1f) {
const uint32_t q = (uint32_t) (local * 18.0f);
if (rnd(83, slot * 64u + q) < 0.5f) v *= 0.06f;
}
c = scale(GHOST, v);
break;
}
case HANUKKAH: {
// Blue and white blocks with a soft shimmer and white sparkles.
static const Rgbw BLUE = {0, 0.2f, 1.0f, 0};
static const Rgbw WHITE = {0.25f, 0.3f, 0.55f, 0.9f};
c = mix(BLUE, WHITE, stripe(wall - t * 1.2f, 14.0f, 0.4f, 3.0f));
c = scale(c, 0.75f + 0.25f * noise(91, wall / 4.0f + t * 1.2f));
c = mix(c, WHITE, twinkle(led, t, 1.8f, 0.15f, 92));
break;
}
case CHRISTMAS: {
// Red and green bands marching down the stairs with warm-white twinkles.
static const Rgbw RED = {1.0f, 0.02f, 0.02f, 0};
static const Rgbw GREEN = {0, 1.0f, 0.12f, 0};
static const Rgbw TWINKLE = {0.25f, 0.18f, 0.05f, 1.0f};
c = mix(GREEN, RED, stripe(wall - t * 2.0f, 12.0f, 0.5f, 1.5f));
c = mix(c, TWINKLE, twinkle(led, t, 1.5f, 0.2f, 101));
break;
}
case CANDY_CANE: {
// Red and white stripes scrolling down.
static const Rgbw RED = {1.0f, 0, 0.04f, 0};
static const Rgbw WHITE = {0.2f, 0.2f, 0.2f, 1.0f};
c = mix(WHITE, RED, stripe(wall - t * 3.0f, 8.0f, 0.5f, 1.5f));
// A soft gloss highlight slides along the stripes, faster than they scroll.
static const Rgbw GLOSS = {0.55f, 0.5f, 0.5f, 1.0f};
const float h = clamp01(1.0f - fabsf(wrapf(wall - t * 9.0f, 36.0f) - 18.0f) / 4.0f);
c = mix(c, GLOSS, 0.6f * h * h);
break;
}
case SNOWFALL: {
// Icy blue with snowflakes falling down the stairs.
static const Rgbw ICE = {0, 0.12f, 0.45f, 0.05f};
static const Rgbw FLAKE = {0.3f, 0.4f, 0.6f, 1.0f};
// Flakes fall from above the top to below the bottom and reappear at
// the top; their count keeps one flake per 8.6 in of fall on any stairs.
static const float MARGIN = 20.0f;
static const int MAX_FLAKES = 160;
const float loop = extent + 2.0f * MARGIN;
const int flakes = std::min(MAX_FLAKES, (int) (loop / 8.6f) + 1);
// Flake positions, computed once per frame.
static float flake_t = -1.0f, flake_pos[MAX_FLAKES], flake_v[MAX_FLAKES];
if (t != flake_t) {
flake_t = t;
for (int k = 0; k < flakes; k++) {
const float speed = 4.0f + 5.0f * rnd(111, k);
flake_pos[k] = fmodf(rnd(112, k) * loop + speed * t, loop) - MARGIN;
flake_v[k] = 0.55f + 0.45f * rnd(113, k);
}
}
c = ICE;
for (int k = 0; k < flakes; k++) {
const float d = fabsf(wall - flake_pos[k]);
if (d < 1.6f) c = lighten(c, scale(FLAKE, flake_v[k] * (1.0f - d / 1.6f)));
}
break;
}
case FIREPLACE: {
// Flames rising up the stairs: deep red embers to orange to yellow tips.
static const Rgbw EMBER = {0.45f, 0, 0, 0};
static const Rgbw FLAME = {1.0f, 0.32f, 0, 0};
static const Rgbw TIP = {1.0f, 0.72f, 0.1f, 0.35f};
float heat = 0.6f * noise(121, (wall + t * 10.0f) / 4.0f) + 0.4f * noise(122, (wall + t * 18.0f) / 1.5f);
heat = clamp01((heat - 0.2f) * 1.7f);
c = heat < 0.5f ? mix(EMBER, FLAME, heat * 2.0f) : mix(FLAME, TIP, (heat - 0.5f) * 2.0f);
break;
}
case AURORA: {
// Green, teal and violet curtains drifting slowly with a shimmer.
static const Rgbw GREEN = {0, 1.0f, 0.35f, 0};
static const Rgbw TEAL = {0, 0.7f, 0.85f, 0};
static const Rgbw VIOLET = {0.55f, 0.05f, 1.0f, 0};
const float h = noise(131, wall / 30.0f - t * 0.08f);
c = h < 0.5f ? mix(GREEN, TEAL, h * 2.0f) : mix(TEAL, VIOLET, (h - 0.5f) * 2.0f);
c = scale(c, 0.45f + 0.55f * noise(132, wall / 9.0f + t * 0.35f));
break;
}
case OCEAN: {
// Deep blue swells rolling down the stairs with the odd white cap.
static const Rgbw DEEP = {0, 0.06f, 0.6f, 0};
static const Rgbw SHALLOW = {0, 0.6f, 0.9f, 0};
static const Rgbw FOAM = {0.3f, 0.5f, 0.7f, 0.8f};
const float swell = 0.5f + 0.5f * sinf(6.2831853f * (wall / 40.0f - t / 7.0f));
const float n = noise(141, wall / 10.0f - t * 0.4f);
const float h = clamp01(0.65f * swell + 0.35f * n);
c = mix(DEEP, SHALLOW, h);
c = mix(c, FOAM, smoothstep((h - 0.82f) * 6.0f));
break;
}
}
// Gamma 2 per channel.
return {clamp01(c.r) * clamp01(c.r), clamp01(c.g) * clamp01(c.g), clamp01(c.b) * clamp01(c.b),
clamp01(c.w) * clamp01(c.w)};
}
} // namespace fx
} // namespace follow_me