#include <Arduino.h>
#include <FastLED.h>
#include <avr/wdt.h>
// matrix dimensions
static constexpr uint8_t kMatrixWidth = 32;
static constexpr uint8_t kMatrixHeight = 32;
// matrix configuration
#define DATA_PIN 3
#define LED_COUNT (kMatrixWidth * kMatrixHeight)
CRGB leds[kMatrixWidth * kMatrixHeight];
#include "graphics.h"
static constexpr bool kDebugSerial = false; // set false to silence per-frame debug prints
static constexpr bool kFrameTimings = false; // set true to print per-frame render times
#include "graphics_prims.h"
// ── Crash / watchdog-reset detection via .noinit ────────────────────────────
// Variables in .noinit are never zeroed by the C runtime, so they survive a
// watchdog or brown-out reset but are random after a true power-on reset.
// Using a sentinel + bitwise-complement pair makes a false-positive
// vanishingly unlikely (1-in-65536) even across noisy power-rail transitions.
static constexpr uint8_t kCrashMagic = 0xA5;
static uint8_t gCrashSentinel __attribute__((section(".noinit")));
static uint8_t gCrashComplement __attribute__((section(".noinit")));
// Last recorded loop-iteration millisecond timestamp, also in .noinit so it
// survives the reset and can be reported in the crash message.
static uint32_t gLastLoopMs __attribute__((section(".noinit")));
// MCUSR captured in .init3 (before the C runtime can clear it).
static uint8_t gMCUSR __attribute__((section(".noinit")));
// Free-memory watermarks — kept in .noinit so they survive a crash reset and
// can be read from the post-crash serial banner alongside gLastLoopMs.
static uint16_t gMemLo __attribute__((section(".noinit")));
static uint16_t gMemHi __attribute__((section(".noinit")));
static uint16_t gLastFreeMem __attribute__((section(".noinit")));
// Sentinel: marks that the memory watermarks above were properly initialised
// (vs. random garbage after a true power-on reset).
static constexpr uint8_t kMemMagic = 0x3C;
static uint8_t gMemSentinel __attribute__((section(".noinit")));
// Crash-point register: set at each key sub-step inside treeSegment so the
// last value written before a reset reveals exactly where execution died.
// High nibble = depth parameter (0-7)
// Low nibble = step within the call:
// 1 = entered, before growth guards
// 2 = passed guards, about to call sin/cos
// 3 = sin/cos done, about to execute inlined Wu drawLine
// 4 = Wu drawLine done, about to recurse left child
// 5 = left child returned, about to recurse right child
// 6 = right child returned, about to --gTreeNest
static uint8_t gCP __attribute__((section(".noinit")));
static int32_t gCPGrow __attribute__((section(".noinit"))); // gTreeGrowFrac.raw() at last treeSegment entry
static uint8_t gCPFrame __attribute__((section(".noinit"))); // gTreeNest at last treeSegment entry
// Runs in .init3 — after the stack pointer is set but before .bss is zeroed.
// Captures MCUSR and immediately clears it + disables the watchdog so a WDT
// reset doesn't cause an infinite reset loop while the sketch starts up.
void _saveMCUSR(void) __attribute__((naked, used, section(".init3")));
void _saveMCUSR(void) {
gMCUSR = MCUSR;
MCUSR = 0;
wdt_disable();
}
// Decode and print the hardware reset cause from a saved MCUSR snapshot.
static void printResetReason(uint8_t mcusr) {
Serial.print(F("reset reason MCUSR=0x"));
if (mcusr < 0x10) Serial.print('0');
Serial.print(mcusr, HEX);
Serial.print(F(": "));
bool any = false;
if (mcusr & (1 << WDRF)) { Serial.print(F("WATCHDOG")); any = true; }
if (mcusr & (1 << BORF)) { if (any) Serial.print('+'); Serial.print(F("BROWNOUT")); any = true; }
if (mcusr & (1 << EXTRF)) { if (any) Serial.print('+'); Serial.print(F("EXTERNAL")); any = true; }
if (mcusr & (1 << PORF)) { if (any) Serial.print('+'); Serial.print(F("POWERON")); any = true; }
if (!any) Serial.print(F("(none)"));
Serial.println();
}
// ── Free-memory monitoring ─────────────────────────────────────────────────
// Returns the number of free SRAM bytes between heap-top and stack pointer.
// Works on AVR: heap grows up from __heap_start / __brkval; SP grows down.
static int freeMemory() {
extern int __heap_start, *__brkval;
int v;
return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}
// Sample free memory. Update gLastFreeMem every call (survives a crash reset).
// Print a line whenever a new low- or high-watermark is hit — silent otherwise.
// tag is a PROGMEM string naming the call site, e.g. F("loop") or F("tSeg").
static void checkMem(const __FlashStringHelper* tag) {
int fm = freeMemory();
if (fm < 0) fm = 0;
uint16_t ufm = (uint16_t)fm;
gLastFreeMem = ufm;
if (ufm < gMemLo) {
gMemLo = ufm;
Serial.print(F("MEM LO "));
Serial.print(ufm);
Serial.print(F(" @ "));
Serial.println(tag);
}
if (ufm > gMemHi) {
gMemHi = ufm;
Serial.print(F("MEM HI "));
Serial.print(ufm);
Serial.print(F(" @ "));
Serial.println(tag);
}
}
static const coord kTwoPi = coord::from_raw(411774); // 2π = 6.2831853
// Shared trail ring-buffer reused by demoSpirograph (60 pts) and demoLissajous (80 pts).
// Sized for the larger consumer; the two demos are never active on the same frame.
static constexpr uint8_t kTrailMax = 80;
static coord gTrailX[kTrailMax], gTrailY[kTrailMax];
static uint8_t gTrailHead = 0;
// Helper: render a rainbow-gradient trail connecting points in the shared trail buffer.
// Saturation differs between spirograph (220) and lissajous (230) for color tuning.
static void renderRainbowTrail(const coord& cx, const coord& cy, coord tScale,
uint8_t numPts, uint8_t saturation) {
for (uint8_t i = 0; i < numPts - 1; ++i) {
uint8_t a = (gTrailHead + i) % numPts;
uint8_t b = (gTrailHead + i + 1) % numPts;
uint8_t br = (uint8_t)((uint16_t)i * 255u / (numPts - 1));
coord ax = cx + (gTrailX[a] - cx) * tScale, ay = cy + (gTrailY[a] - cy) * tScale;
coord bx = cx + (gTrailX[b] - cx) * tScale, by = cy + (gTrailY[b] - cy) * tScale;
drawLine(ax, ay, bx, by, CHSV(br, saturation, br));
}
}
// ── clock face with three rotating hands ──────────────────────────
// Hour, minute and second hands rotate in the real 1:12:144 ratio, accelerated
// so one "second" completes in 6 s of wall time: second every 6 s, minute every
// 72 s, hour every 864 s. Tick marks at all 12 hour positions; heavier marks
// at 12, 3, 6, 9. Angles start at 12 o’clock (top) and run clockwise.
static uint8_t demoClock(coord cx, coord cy, coord r, uint32_t ms) {
static constexpr uint32_t kSecPeriod = 6000u;
static constexpr uint32_t kMinPeriod = 72000u;
static constexpr uint32_t kHrPeriod = 864000u;
static const coord kOffset = coord::from_raw(102943); // π/2 = 1.5707963, East→North offset
// angle = (ms % period) * 2π / period — pure integer arithmetic
coord secA = coord::from_raw((uint32_t)(ms % kSecPeriod) * (uint32_t)kTwoPi.raw() / kSecPeriod) - kOffset;
coord minA = coord::from_raw((int32_t)((uint64_t)(ms % kMinPeriod) * (uint32_t)kTwoPi.raw() / kMinPeriod)) - kOffset;
coord hrA = coord::from_raw((int32_t)((uint64_t)(ms % kHrPeriod) * (uint32_t)kTwoPi.raw() / kHrPeriod)) - kOffset;
// Zoom: slow sine (period 20 s). When positive, zoom in on the second-hand tip.
coord zPhase = coord::from_raw((int32_t)((uint64_t)(ms % 20000u) * (uint32_t)kTwoPi.raw() / 20000u));
coord zSin = fl::s16x16::sin(zPhase);
// zoom ∈ [1.0, 2.5]: 1 + max(0, zSin) * 1.5
coord zoom = coord::from_raw(65536) + (zSin.raw() > 0 ? zSin * coord::from_raw(98304) : coord{}); // 1.0 + max(0,zSin)*1.5
// Second hand trig (needed for both tip tracking and drawing)
coord sc = fl::s16x16::cos(secA), ss = fl::s16x16::sin(secA);
coord tipX = cx + sc * r;
coord tipY = cy + ss * r;
// Camera blends linearly from clock centre (zoom=1) to tip (zoom=max).
coord zoomFrac = (zoom - coord::from_raw(65536)) / coord::from_raw(98304); // (zoom-1)/1.5
coord camX = cx + (tipX - cx) * zoomFrac;
coord camY = cy + (tipY - cy) * zoomFrac;
// screen = displayCentre + (world - cam) * zoom
coord eff_cx = cx + (cx - camX) * zoom;
coord eff_cy = cy + (cy - camY) * zoom;
coord eff_r = r * zoom;
coord eff_zoom = zoom;
// Face ring
drawRing(eff_cx, eff_cy, eff_r, eff_zoom, CRGB(40, 40, 40)); // thickness = 1.0 * eff_zoom
// 12 tick marks; major ticks (every 3rd) are longer and brighter
for (uint8_t i = 0; i < 12; ++i) {
coord ta = coord::from_raw((int32_t)i * (kTwoPi.raw() / 12)) - kOffset;
coord tc = fl::s16x16::cos(ta), ts = fl::s16x16::sin(ta);
bool major = (i % 3 == 0);
coord inner = eff_r * coord::from_raw(major ? 47185 : 53739); // *0.72 or *0.82
drawLine(eff_cx + tc * inner, eff_cy + ts * inner,
eff_cx + tc * eff_r, eff_cy + ts * eff_r,
major ? CRGB(200, 200, 200) : CRGB(80, 80, 80));
}
// Hour hand: thick and short
{
coord hc = fl::s16x16::cos(hrA), hs = fl::s16x16::sin(hrA);
coord hLen = eff_r * coord::from_raw(36044); // *0.55
drawThickLine(eff_cx, eff_cy, eff_cx + hc * hLen, eff_cy + hs * hLen,
coord::from_raw(163840) * eff_zoom, CRGB(200, 200, 200)); // thick=2.5
}
// Minute hand: medium thickness, nearly full length
{
coord mc = fl::s16x16::cos(minA), ms_v = fl::s16x16::sin(minA);
coord mLen = eff_r * coord::from_raw(52428); // *0.80
drawThickLine(eff_cx, eff_cy, eff_cx + mc * mLen, eff_cy + ms_v * mLen,
coord::from_raw(98304) * eff_zoom, CRGB::White); // thick=1.5
}
// Second hand: thin red line + short counterbalance tail
{
coord tail = eff_r * coord::from_raw(16384); // *0.25 (exact)
drawLine(eff_cx - sc * tail, eff_cy - ss * tail,
eff_cx + sc * eff_r, eff_cy + ss * eff_r, CRGB(255, 40, 40));
}
// Centre cap
drawDisc(eff_cx, eff_cy, coord::from_raw(98304) * eff_zoom, CRGB(220, 220, 220)); // r=1.5
return 255;
}
// ── nested rings, each internally tangent to its parent ──────────────
// The chain is:
// outer ring (fixed, radius R at display centre)
// └─ ring[0] orbits inside R, outer edge touches R's inner edge
// └─ ring[1] orbits inside [0], outer edge touches [0]'s inner edge
// └─ ring[2] orbits inside [1], outer edge touches [1]'s inner edge
// └─ ring[3] orbits inside [2], outer edge touches [2]'s inner edge
//
// Each ring has inner radius r_i and thickness t. Its outer edge is at r_i + t.
// For the child's outer edge to sit exactly on the parent's inner edge:
// orbit = pr - ri - t
// Radii: 13, 9.5, 6, 3.5, 1.25 → orbit radii: 2.25, 2.25, 1.25, 0.98 with t=1.25.
// Each ring is drawn in a rainbow colour; no filled discs.
static uint8_t demoOrbitingDiscs(coord cx, coord cy, coord R, coord tScale) {
static const CRGB kColors[] = {
CRGB::Red, CRGB::Orange, CRGB::Yellow, CRGB::Green, CRGB::Blue,
};
struct Level { coord r; coord speed; };
static const Level levels[] = {
{ coord::from_raw(622592), coord::from_raw(65536) }, // 9.5, 1.00
{ coord::from_raw(393216), coord::from_raw(111411) }, // 6.0, 1.70
{ coord::from_raw(229376), coord::from_raw(190054) }, // 3.5, 2.90
{ coord::from_raw(81920), coord::from_raw(334233) }, // 1.25, 5.10
};
static const coord kRingThickness = coord::from_raw(81920); // 1.25 px
// Each ring gets its own angle that advances by baseDelta*speed per frame
// and wraps independently — avoids the phase jump that occurs when a shared
// spin counter is multiplied by a non-integer speed then wrapped to [0,2π).
static coord angles[4] = {coord{0.0f}, coord{0.0f}, coord{0.0f}, coord{0.0f}};
static const coord kBaseDelta = coord::from_raw(2621); // 0.04 rad/frame
// Scale all ring radii and thickness proportionally with tScale so the
// chain geometry stays consistent as the scene zooms in/out.
coord thick = kRingThickness * tScale;
drawRing(cx, cy, R, thick, kColors[0]);
// Walk down the chain; each iteration becomes the new parent.
coord pcx = cx, pcy = cy, pr = R;
for (uint8_t i = 0; i < sizeof(levels)/sizeof(levels[0]); ++i) {
coord ri = levels[i].r * tScale;
// orbit places child outer edge (ri + t) tangent to parent inner edge (pr)
coord orbit = pr - ri - thick;
coord ncx = pcx + fl::s16x16::cos(angles[i]) * orbit;
coord ncy = pcy + fl::s16x16::sin(angles[i]) * orbit;
drawRing(ncx, ncy, ri, thick, kColors[i + 1]);
// Advance and wrap this ring's angle independently
angles[i] = angles[i] + kBaseDelta * levels[i].speed;
if (angles[i].raw() > kTwoPi.raw()) angles[i] = angles[i] - kTwoPi;
pcx = ncx; pcy = ncy; pr = ri; // this ring becomes the next parent
}
return 255;
}
// ── rotating star polygon ───────────────────────────────────────────
// N points equally spaced on a circle just inside a bounding ring, connected
// by thick lines to the point K steps ahead — forming a {N/K} star polygon.
// A small disc marks each vertex. The star rotates and the line thickness
// pulses sinusoidally. Two concentric static rings (inner and outer) frame the star.
static uint8_t demoStarWeb(coord cx, coord cy, coord R, coord spin, coord tScale) {
static constexpr uint8_t N = 5; // pentagon vertices
static constexpr uint8_t K = 2; // connect to 2nd neighbour -> pentagram
// Inner accent ring at the natural inner-pentagon radius (golden ratio ≈ 0.382 R).
coord innerR = R * coord::from_raw(25034); // R * 0.382 (inner pentagon radius)
drawRing(cx, cy, R, coord::from_raw(131072) * tScale, CRGB::Blue); // thick=2.0
drawRing(cx, cy, innerR, tScale, CRGB(0, 60, 120));
// Vertex circle sits on the outer ring so the star fills the display.
coord pr = R;
// Compute N vertex positions
coord px[N], py[N];
for (uint8_t i = 0; i < N; ++i) {
coord a = spin + coord::from_raw((int32_t)i * (kTwoPi.raw() / N));
px[i] = cx + fl::s16x16::cos(a) * pr;
py[i] = cy + fl::s16x16::sin(a) * pr;
}
// Pulsing line thickness, scaled with tScale
coord thick = (coord::from_raw(81920) + fl::s16x16::sin(spin * coord::from_raw(196608)) * coord::from_raw(49152))
* tScale; // 1.25 + sin(3θ)*0.75
if (kDebugSerial) {
Serial.print(F("SW sp=")); Serial.print(spin.raw() >> 8);
Serial.print(F(" ts=")); Serial.print(tScale.raw() >> 8);
Serial.print(F(" th=")); Serial.println(thick.raw() >> 8);
for (uint8_t _di = 0; _di < N; ++_di) {
Serial.print(F(" V")); Serial.print(_di);
Serial.print(F(" (")); Serial.print(px[_di].raw() >> 8);
Serial.print(','); Serial.print(py[_di].raw() >> 8);
Serial.print(')');
}
Serial.println();
}
// Draw the star edges in warm white, then thin spokes from centre in dim white
for (uint8_t i = 0; i < N; ++i) {
uint8_t j = (i + K) % N;
if (kDebugSerial) {
Serial.print(F("TL ")); Serial.print(i); Serial.print('-'); Serial.print(j);
Serial.print(F(" (")); Serial.print(px[i].raw()>>8); Serial.print(','); Serial.print(py[i].raw()>>8);
Serial.print(F(")>(")); Serial.print(px[j].raw()>>8); Serial.print(','); Serial.print(py[j].raw()>>8);
Serial.print(F(") th=")); Serial.println(thick.raw()>>8);
}
drawThickLine(px[i], py[i], px[j], py[j], thick, CRGB(220, 220, 180));
if (kDebugSerial) Serial.println(F("TL ok"));
drawLine(cx, cy, px[i], py[i], CRGB(60, 60, 60));
}
// Vertex discs and centre disc, scaled with tScale
for (uint8_t i = 0; i < N; ++i)
drawDisc(px[i], py[i], coord::from_raw(98304) * tScale, CRGB::Yellow); // r=1.5
drawDisc(cx, cy, coord::from_raw(131072) * tScale, CRGB::Orange); // r=2.0
return 255;
}
// ── spirograph (hypotrochoid) with rainbow trail ─────────────────────
// An inner circle (radius r) rolls inside the outer track (radius R).
// A pen arm of length h traces a hypotrochoid. The last kPts positions are
// stored in a ring buffer and drawn as connected AA line segments with a
// rainbow gradient (oldest=dim, newest=bright).
//
// Parameters: R=13, r=6, h=5 → kRatio fixed at 3/2=1.5 for a clean 3-lobed curve
// closes after 2 full outer rotations (theta=4π)
// max pen radius = (R-r)+h = 7+5 = 12 px (fits the display)
static uint8_t demoSpirograph(coord cx, coord cy, coord R, coord tScale, bool reinit) {
// static constexpr float kInnerR = 6.0f; // rolling circle radius
// static constexpr float kPenArm = 5.0f; // pen arm length
// static constexpr float kRatio = 1.5f; // fixed 3/2 ratio → 3-lobed curve (independent of R)
static constexpr uint8_t kPts = 60; // ring-buffer trail length
static const coord kDelta = coord::from_raw(14417); // 0.22 rad/step
// The 3/2-ratio hypotrochoid closes after 2 full outer rotations (theta=4π),
// so wrap at 4π, not 2π, to avoid a mid-curve position discontinuity.
static const coord fourPi = coord::from_raw(823548); // 4π = 12.5663706
static coord theta{0};
// Rolling-circle centre
coord arm = R - coord::from_raw(393216); // R - kInnerR(6.0)
coord rcx = cx + fl::s16x16::cos(theta) * arm;
coord rcy = cy + fl::s16x16::sin(theta) * arm;
// Pen position: roll angle = theta * ratio; minus sign on sin gives correct
// rotation direction for a rolling (not sliding) inner circle.
coord roll = theta * coord::from_raw(98304); // kRatio = 1.5
coord penX = rcx + fl::s16x16::cos(roll) * coord::from_raw(327680); // kPenArm = 5.0
coord penY = rcy - fl::s16x16::sin(roll) * coord::from_raw(327680); // kPenArm = 5.0
// On (re-)activation pre-simulate kPts steps so the trail is fully populated.
if (reinit) {
gTrailHead = 0;
coord t = theta - kDelta * coord(kPts);
while (t.raw() < 0) t = t + fourPi;
for (uint8_t i = 0; i < kPts; ++i) {
coord pa = R - coord::from_raw(393216); // R - kInnerR(6.0)
coord prx = cx + fl::s16x16::cos(t) * pa;
coord pry = cy + fl::s16x16::sin(t) * pa;
coord pro = t * coord::from_raw(98304); // kRatio = 1.5
gTrailX[gTrailHead] = prx + fl::s16x16::cos(pro) * coord::from_raw(327680); // kPenArm=5.0
gTrailY[gTrailHead] = pry - fl::s16x16::sin(pro) * coord::from_raw(327680); // kPenArm=5.0
gTrailHead = (gTrailHead + 1) % kPts;
t = t + kDelta;
if (t.raw() > fourPi.raw()) t = t - fourPi;
}
}
gTrailX[gTrailHead] = penX;
gTrailY[gTrailHead] = penY;
gTrailHead = (gTrailHead + 1) % kPts;
// Scale all geometric elements toward the display centre during transitions.
// Geometry runs at full scale so arm = R - kInnerR is always positive.
coord s_rcx = cx + (rcx - cx) * tScale;
coord s_rcy = cy + (rcy - cy) * tScale;
coord s_penX = cx + (penX - cx) * tScale;
coord s_penY = cy + (penY - cy) * tScale;
drawRing(cx, cy, R * tScale, coord::from_raw(98304) * tScale, CRGB(0, 0, 180)); // thick=1.5
drawRing(s_rcx, s_rcy, coord::from_raw(393216) * tScale, tScale, CRGB(0, 100, 140)); // r=6.0
drawLine(s_rcx, s_rcy, s_penX, s_penY, CRGB(50, 50, 50));
// Rainbow trail: stored positions are in full-scale world space; scale at render time.
renderRainbowTrail(cx, cy, tScale, kPts, 220);
// Pen dot at scaled position
drawDisc(s_penX, s_penY, coord::from_raw(98304) * tScale, CRGB::White); // r=1.5
theta = theta + kDelta;
if (theta.raw() > fourPi.raw()) theta = theta - fourPi;
return 255;
}
// ── morphing Lissajous figure with rainbow trail ─────────────────────
// x = R·sin(3θ+φ), y = R·sin(2θ)
// φ advances slowly each frame, continuously morphing the knot through all
// Bowditch forms. θ wraps at 2π (the 3:2 figure closes at that period).
// An 80-point ring buffer stores the trail; drawn as fading rainbow AA lines.
static uint8_t demoLissajous(coord cx, coord cy, coord R, coord tScale, bool reinit) {
static constexpr uint8_t kPts = 80;
static const coord kStep = coord::from_raw(7864); // 0.12 rad/frame
static const coord twoPi = kTwoPi;
static coord theta{0};
static coord phase{0}; // φ: drifts to morph the figure
// Current pen position
coord px = cx + fl::s16x16::sin(theta * coord::from_raw(196608) + phase) * R; // sin(3θ+ϕ)
coord py = cy + fl::s16x16::sin(theta * coord::from_raw(131072)) * R; // sin(2θ)
// On (re-)activation pre-simulate kPts steps so the trail is fully populated.
if (reinit) {
gTrailHead = 0;
coord t = theta - kStep * coord(kPts);
coord p = phase - coord(0.007f * kPts);
while (t.raw() < 0) t = t + twoPi;
while (p.raw() < 0) p = p + twoPi;
for (uint8_t i = 0; i < kPts; ++i) {
gTrailX[gTrailHead] = cx + fl::s16x16::sin(t * coord::from_raw(196608) + p) * R; // sin(3t+p)
gTrailY[gTrailHead] = cy + fl::s16x16::sin(t * coord::from_raw(131072)) * R; // sin(2t)
gTrailHead = (gTrailHead + 1) % kPts;
t = t + kStep;
if (t.raw() > twoPi.raw()) t = t - twoPi;
p = p + coord::from_raw(458); // +0.007 rad
if (p.raw() > twoPi.raw()) p = p - twoPi;
}
}
gTrailX[gTrailHead] = px;
gTrailY[gTrailHead] = py;
gTrailHead = (gTrailHead + 1) % kPts;
// Trail: oldest → dim, newest → bright, full rainbow.
// Scale stored positions toward cx/cy by tScale to match the transition zoom.
renderRainbowTrail(cx, cy, tScale, kPts, 230);
// Leading dot — scaled toward display centre like the trail.
coord s_px = cx + (px - cx) * tScale;
coord s_py = cy + (py - cy) * tScale;
drawDisc(s_px, s_py, coord{1.5f} * tScale, CRGB::White);
theta = theta + kStep;
if (theta.raw() > twoPi.raw()) theta = theta - twoPi;
phase = phase + coord(0.007f);
if (phase.raw() > twoPi.raw()) phase = phase - twoPi;
return 255;
}
// ── spinning wireframe cube (thin and thick lines) ──────────────────────────────────
// 8 vertices (±kS, ±kS, ±kS) rotated around Y (fast) and X (slow) axes with
// perspective division. Both demos share a single rotation state so the
// transition from one to the other feels continuous.
// thin=true: drawLine — 1-px Wu AA; depth cued by brightness only.
// thin=false: drawThickLine — depth cued by both thickness and brightness.
static uint8_t demoCubeImpl(coord cx, coord cy, bool thin, coord tScale) {
static const coord kS = coord::from_raw(458752); // 7.0
static const coord kSThin = coord::from_raw(720896); // 11.0
static const coord kEyeDist = coord::from_raw(1703936); // 26.0
const coord kSeff = thin ? kSThin : kS;
static const int8_t kVerts[8][3] = {
{-1,-1,-1}, { 1,-1,-1}, { 1, 1,-1}, {-1, 1,-1}, // back face (z = -1)
{-1,-1, 1}, { 1,-1, 1}, { 1, 1, 1}, {-1, 1, 1}, // front face (z = +1)
};
static const uint8_t kEdges[12][2] = {
{0,1},{1,2},{2,3},{3,0}, // back face
{4,5},{5,6},{6,7},{7,4}, // front face
{0,4},{1,5},{2,6},{3,7}, // pillars
};
static coord spinY{0};
static coord spinX{0};
const coord cosY = fl::s16x16::cos(spinY);
const coord sinY = fl::s16x16::sin(spinY);
const coord cosX = fl::s16x16::cos(spinX);
const coord sinX = fl::s16x16::sin(spinX);
coord px[8], py[8], pz[8];
for (uint8_t i = 0; i < 8; ++i) {
coord x{kVerts[i][0] * kSeff};
coord y{kVerts[i][1] * kSeff};
coord z{kVerts[i][2] * kSeff};
coord x2 = x * cosY - z * sinY;
coord z2 = x * sinY + z * cosY;
coord y3 = y * cosX - z2 * sinX;
coord z3 = y * sinX + z2 * cosX;
coord denom = kEyeDist - z3;
if (denom.raw() < 65536) denom = coord::from_raw(65536); // clamp < 1.0 → 1.0
coord scale = kEyeDist / denom;
// Apply transition scale: zoom projected position toward display centre.
px[i] = cx + x2 * scale * tScale;
py[i] = cy + y3 * scale * tScale;
pz[i] = z3; // positive = towards viewer
}
for (uint8_t e = 0; e < 12; ++e) {
const uint8_t a = kEdges[e][0], b = kEdges[e][1];
coord avgZ = (pz[a] + pz[b]) / coord(2);
// avgZ in [-kS, +kS] → brightness in [100, 255]: near = bright, far = dim.
// Clamp before uint8_t cast: post-rotation z can exceed kS (up to kS·√2).
int br_i = 178 + avgZ.to_int() * 77 / kSeff.to_int();
if (br_i > 255) br_i = 255;
if (br_i < 0) br_i = 0;
CRGB col((uint8_t)br_i, (uint8_t)br_i, (uint8_t)br_i);
if (thin) {
drawLine(px[a], py[a], px[b], py[b], col);
} else {
coord thick = (coord::from_raw(117964) + avgZ * (coord::from_raw(65536) / kSeff)) * tScale; // (1.8 + z/kSeff)
drawThickLine(px[a], py[a], px[b], py[b], thick, col);
}
}
spinY = spinY + coord::from_raw(6553); // +0.10 rad/frame
if (spinY.raw() > kTwoPi.raw()) spinY = spinY - kTwoPi;
spinX = spinX + coord::from_raw(2490); // +0.038 rad/frame
if (spinX.raw() > kTwoPi.raw()) spinX = spinX - kTwoPi;
return 255;
}
// ── organic random walkers with fading trails ───────────────────────────────
// Six walkers undergo damped random walks with a soft spring pulling them back
// toward the display centre, so they wander freely without flying off-screen.
// Returns 20 so the caller dims the framebuffer slightly each frame, leaving
// long glowing colour trails that shift hue slowly over time.
static uint8_t demoOrganicWalkers(coord cx, coord cy, coord tScale, bool reinit) {
static constexpr uint8_t kN = 6;
static coord wx[kN], wy[kN], wvx[kN], wvy[kN];
static uint8_t whue[kN];
if (reinit) {
for (uint8_t i = 0; i < kN; ++i) {
wx[i] = cx;
wy[i] = cy;
wvx[i] = coord{};
wvy[i] = coord{};
whue[i] = (uint8_t)(i * 43u); // spread hues ~60° apart
}
}
// Motion constants (fixed-point)
// Equilibrium spread σ ≈ kJitter / sqrt(2·kSpring·(1−kDamp)) ≈ 9 px.
static const coord kDamp = coord::from_raw(61440); // ≈ 0.9375 velocity damping per frame
static const coord kJitter = coord::from_raw(16384); // ≈ 0.250 px/frame² random kick scale
static const coord kSpring = coord::from_raw(393); // ≈ 0.006 spring pull toward centre
static const coord kMaxV = coord::from_raw(196608); // 3.0 px/frame velocity cap
for (uint8_t i = 0; i < kN; ++i) {
// Random acceleration + weak spring toward display centre
coord ax = coord::from_raw(((int32_t)(int8_t)random8()) * kJitter.raw() / 128)
+ (cx - wx[i]) * kSpring;
coord ay = coord::from_raw(((int32_t)(int8_t)random8()) * kJitter.raw() / 128)
+ (cy - wy[i]) * kSpring;
wvx[i] = wvx[i] * kDamp + ax;
wvy[i] = wvy[i] * kDamp + ay;
// Clamp each velocity component independently
if (wvx[i].raw() > kMaxV.raw()) wvx[i] = kMaxV;
if (wvx[i].raw() < -kMaxV.raw()) wvx[i] = coord::from_raw(-kMaxV.raw());
if (wvy[i].raw() > kMaxV.raw()) wvy[i] = kMaxV;
if (wvy[i].raw() < -kMaxV.raw()) wvy[i] = coord::from_raw(-kMaxV.raw());
wx[i] = wx[i] + wvx[i];
wy[i] = wy[i] + wvy[i];
whue[i] += 1; // slowly drift hue each frame
// Scale position toward display centre during slot transitions
coord sx = cx + (wx[i] - cx) * tScale;
coord sy = cy + (wy[i] - cy) * tScale;
drawDisc(sx, sy, coord::from_raw(131072) * tScale, CHSV(whue[i], 230, 255)); // r=2.0
}
return 20; // caller applies fadeToBlackBy(20) → trails persist ~20 frames
}
// ── boids flocking simulation with fading trails ────────────────────────────
// N boids follow the classic three rules: separation, alignment, cohesion,
// plus a soft wall force keeping them inside the display area. Each boid is
// drawn as a short velocity-aligned stroke. Returns 18 so the caller dims
// the framebuffer each frame and trails persist for roughly 20 frames.
static uint8_t demoBoids(coord cx, coord cy, coord r, coord tScale, bool reinit) {
static constexpr uint8_t kN = 7;
static coord bx[kN], by[kN], bvx[kN], bvy[kN];
static uint8_t bhue[kN];
if (reinit) {
for (uint8_t i = 0; i < kN; ++i) {
bx[i] = cx + coord::from_raw(((int32_t)(int8_t)random8()) * 5120); // ±10 px
by[i] = cy + coord::from_raw(((int32_t)(int8_t)random8()) * 5120);
bvx[i] = coord::from_raw(((int32_t)(int8_t)random8()) * 512); // ±1.0 px/fr
bvy[i] = coord::from_raw(((int32_t)(int8_t)random8()) * 512);
bhue[i] = (uint8_t)(i * 36u); // spread across hue wheel
}
}
// Neighbour radii via L∞ (Chebyshev) distance — avoids overflow when
// squaring large s16.16 position values.
static const coord kSepR = coord::from_raw(3 * 65536); // separation zone: 3 px
static const coord kNbrR = coord::from_raw(8 * 65536); // alignment / cohesion zone: 8 px
// Steering weights
static const coord kSepW = coord::from_raw(8192); // ≈ 0.125
static const coord kAliW = coord::from_raw(4915); // ≈ 0.075
static const coord kCohW = coord::from_raw(1311); // ≈ 0.020
// Wall: proportional repulsion — force scales with depth inside the margin zone
static const coord kMargin = coord::from_raw(6 * 65536); // 6 px warning zone
static const coord kWallScale = coord::from_raw(5243); // ≈ 0.080 force/px penetration
// Per-component speed cap and velocity damping
static const coord kMaxV = coord::from_raw(131072); // 2.0 px/frame
static const coord kDamp = coord::from_raw(63897); // ≈ 0.975
for (uint8_t i = 0; i < kN; ++i) {
coord ax{}, ay{};
// ── Soft boundary repulsion ──────────────────────────────────────────
{
coord left = bx[i] - (cx - r);
coord right = (cx + r) - bx[i];
coord top = by[i] - (cy - r);
coord bot = (cy + r) - by[i];
if (left < kMargin) ax = ax + (kMargin - left) * kWallScale;
if (right < kMargin) ax = ax - (kMargin - right) * kWallScale;
if (top < kMargin) ay = ay + (kMargin - top) * kWallScale;
if (bot < kMargin) ay = ay - (kMargin - bot) * kWallScale;
}
// ── Boid rules (neighbour scan) ──────────────────────────────────────
coord sepX{}, sepY{}, aliX{}, aliY{}, cohX{}, cohY{};
uint8_t nbrCnt = 0;
for (uint8_t j = 0; j < kN; ++j) {
if (j == i) continue;
coord dx = bx[j] - bx[i];
coord dy = by[j] - by[i];
int32_t adx = dx.raw() < 0 ? -dx.raw() : dx.raw();
int32_t ady = dy.raw() < 0 ? -dy.raw() : dy.raw();
if (adx < kSepR.raw() && ady < kSepR.raw()) {
sepX = sepX - dx;
sepY = sepY - dy;
}
if (adx < kNbrR.raw() && ady < kNbrR.raw()) {
aliX = aliX + bvx[j]; aliY = aliY + bvy[j];
cohX = cohX + bx[j]; cohY = cohY + by[j];
nbrCnt++;
}
}
ax = ax + sepX * kSepW;
ay = ay + sepY * kSepW;
if (nbrCnt > 0) {
coord n = coord::from_raw((int32_t)nbrCnt << 16);
ax = ax + (aliX / n - bvx[i]) * kAliW + (cohX / n - bx[i]) * kCohW;
ay = ay + (aliY / n - bvy[i]) * kAliW + (cohY / n - by[i]) * kCohW;
}
// ── Integrate ────────────────────────────────────────────────────────
bvx[i] = (bvx[i] + ax) * kDamp;
bvy[i] = (bvy[i] + ay) * kDamp;
if (bvx[i].raw() > kMaxV.raw()) bvx[i] = kMaxV;
if (bvx[i].raw() < -kMaxV.raw()) bvx[i] = coord::from_raw(-kMaxV.raw());
if (bvy[i].raw() > kMaxV.raw()) bvy[i] = kMaxV;
if (bvy[i].raw() < -kMaxV.raw()) bvy[i] = coord::from_raw(-kMaxV.raw());
bx[i] = bx[i] + bvx[i];
by[i] = by[i] + bvy[i];
// Hard clamp — ensures boids never escape the display area
if (bx[i] < cx - r) { bx[i] = cx - r; if (bvx[i].raw() < 0) bvx[i] = coord{}; }
if (bx[i] > cx + r) { bx[i] = cx + r; if (bvx[i].raw() > 0) bvx[i] = coord{}; }
if (by[i] < cy - r) { by[i] = cy - r; if (bvy[i].raw() < 0) bvy[i] = coord{}; }
if (by[i] > cy + r) { by[i] = cy + r; if (bvy[i].raw() > 0) bvy[i] = coord{}; }
bhue[i] += 1;
// ── Draw ─────────────────────────────────────────────────────────────
coord hx = cx + (bx[i] - cx) * tScale;
coord hy = cy + (by[i] - cy) * tScale;
coord tx = hx - bvx[i] * coord(3) * tScale; // tail: 3 velocity steps back
coord ty = hy - bvy[i] * coord(3) * tScale;
drawLine(tx, ty, hx, hy, CHSV(bhue[i], 220, 160));
drawDisc(hx, hy, coord::from_raw(65536) * tScale, CHSV(bhue[i] + 20, 200, 160)); // r=1.0 head highlight
}
return 40; // caller applies fadeToBlackBy(40) → trails persist ~8 frames
}
// ── random hypotrochoid, drawn cumulatively ─────────────────────────────────
// Each cycle picks a random (r, h) pair from a curated table and draws the
// complete hypotrochoid one AA segment per frame with no dimming. When the
// curve closes the display is cleared and a fresh random variant begins.
// Closing angle = 2π × r/gcd(r, R−r); all entries use R=13 (prime) so gcd=1
// for every row, giving clean petal counts with 3–9 rotations each.
// Returns 0 while drawing (no fade), 255 on completion (clear next frame).
static uint8_t demoHypoRand(coord cx, coord cy, bool reinit) {
struct Params { uint8_t r; uint8_t h; };
// R=13. max pen radius = (R−r)+h. Closing angle = 2π × r (since gcd=1 always).
static const Params kTable[] = {
{5, 8}, // 5 rot, max_r=16, dramatic 5-petal with inner loops
{4, 8}, // 4 rot, max_r=17, wide 4-petal
{6, 7}, // 6 rot, max_r=14, 6-petal rosette
{8, 6}, // 8 rot, max_r=11, compact 8-petal
{9, 6}, // 9 rot, max_r=10, tight 9-petal
{3, 7}, // 3 rot, max_r=17, bold 3-lobed
};
static constexpr uint8_t kNTable = sizeof(kTable) / sizeof(kTable[0]);
static constexpr uint8_t kR_int = kMatrixWidth / 2 - 3; // 13
static const coord kR = coord::from_raw((int32_t)kR_int << 16);
static const coord kStep = coord::from_raw(16384); // 0.25 rad/frame
static uint8_t idx = 0;
static coord theta = coord{};
static coord rollPhase = coord{};
static coord prevX = coord{};
static coord prevY = coord{};
static bool firstPoint = true;
static uint8_t hue = 0;
static int32_t closeRaw = 0; // closing angle raw = 2π × r
// Pick a new random variant and reset curve state.
if (reinit || closeRaw == 0) {
idx = random8() % kNTable;
hue = random8();
theta = coord{};
rollPhase = coord{};
firstPoint = true;
// closeAngle = 2π × r (gcd(r, R−r) = 1 for all table entries with R=13 prime)
closeRaw = (int32_t)kTable[idx].r * kTwoPi.raw();
}
coord ri_c = coord::from_raw((int32_t)kTable[idx].r << 16);
coord h_c = coord::from_raw((int32_t)kTable[idx].h << 16);
coord arm = kR - ri_c; // (R − r)
coord rollDelta = kStep * (arm / ri_c); // advance roll by kStep × (R−r)/r
coord penX = cx + fl::s16x16::cos(theta) * arm
+ fl::s16x16::cos(rollPhase) * h_c;
coord penY = cy + fl::s16x16::sin(theta) * arm
- fl::s16x16::sin(rollPhase) * h_c;
if (!firstPoint) {
drawLine(prevX, prevY, penX, penY, CHSV(hue, 220, 255));
hue++;
} else {
firstPoint = false;
}
prevX = penX;
prevY = penY;
theta = theta + kStep;
rollPhase = rollPhase + rollDelta;
if (rollPhase.raw() >= kTwoPi.raw()) rollPhase = rollPhase - kTwoPi;
if (theta.raw() >= closeRaw) {
// Cycle complete — pick next variant; signal caller to clear the screen.
idx = random8() % kNTable;
hue = random8();
theta = coord{};
rollPhase = coord{};
firstPoint = true;
closeRaw = (int32_t)kTable[idx].r * kTwoPi.raw();
return 255;
}
return 0; // accumulate without any fading
}
// ── branching organic tree with wind sway ────────────────────────────────
// A 6-level fractal binary tree redrawn fresh every frame. A sinusoidal
// wind angle accumulates branch-by-branch toward the tips, so leaves sway
// while the trunk stays almost still. Colour grades from warm brown at the
// trunk to bright spring-green at tips. Returns 255: hard-clear each frame.
static coord gTreeSwayStep; // wind step/level; set by demoBranchingTree, read by treeSegment
static coord gTreeGrowFrac; // growth phase [0,1]: trunk appears first, tips last
static coord gTreeSpread; // current branch spread angle (opens as plant grows)
static uint8_t gTreeNest = 0; // current recursion depth inside treeSegment
static uint8_t gTreeNestMax = 0; // high-water mark of nesting depth this frame
static void treeSegment(coord x0, coord y0, coord angle, coord length, uint8_t depth) {
++gTreeNest;
// Update crash-point register at every entry so the last write before a reset
// tells exactly which depth and step was executing when the crash occurred.
gCPFrame = gTreeNest;
gCPGrow = gTreeGrowFrac.raw();
gCP = (uint8_t)((depth << 4) | 0x1); // step 1: entered, before guards
if (gTreeNest > gTreeNestMax) {
gTreeNestMax = gTreeNest;
checkMem(F("tSeg")); // new deepest stack — fires BEFORE sin/cos
}
if (depth == 0) { --gTreeNest; return; } // hard stop — guards against uint8_t wrap on runaway recursion
static constexpr uint8_t kMax = 6;
// Per-level growth: trunk (depth=kMax) appears first, tips (depth=1) last.
// levelGrow = clamp(growFrac*kMax − (kMax−depth), 0, 1)
int32_t lgRaw = (int32_t)gTreeGrowFrac.raw() * kMax - ((kMax - depth) << 16);
if (lgRaw <= 0) { --gTreeNest; return; }
coord levelGrow = coord::from_raw(lgRaw < 65536 ? lgRaw : 65536);
coord effLen = length * levelGrow;
if (effLen.raw() < 22938) { --gTreeNest; return; } // stop below 0.35 px
gCP = (uint8_t)((depth << 4) | 0x2); // step 2: passed guards, about to sin/cos
// Wind sway: 0 at trunk, 5× at tips
coord swayAcc = gTreeSwayStep * coord::from_raw((int32_t)(kMax - depth) << 16);
coord drawn = angle + swayAcc;
coord x1 = x0 + fl::s16x16::sin(drawn) * effLen;
coord y1 = y0 - fl::s16x16::cos(drawn) * effLen;
gCP = (uint8_t)((depth << 4) | 0x3); // step 3: sin/cos done, about to drawLine
// Mature colour: trunk → warm brown; tips → spring-green
uint8_t d = kMax - depth;
uint8_t gv = (uint8_t)(50u + (uint16_t)d * 34u);
uint8_t rv = (uint8_t)(80u - (uint16_t)d * 14u);
uint8_t bv = (depth == 1) ? 20u : 0u;
// Fresh growth blends toward bright yellow-white (255,255,100)
uint8_t young = (uint8_t)((65536u - (uint32_t)levelGrow.raw()) >> 8);
if (young > 0) {
rv = rv + (uint8_t)(((uint16_t)(255u - rv) * young) >> 8);
gv = gv + (uint8_t)(((uint16_t)(255u - gv) * young) >> 8);
bv = bv + (uint8_t)(((uint16_t)(100u - bv) * young) >> 8);
}
CRGB col(rv, gv, bv);
static const coord kScale = coord::from_raw(53739); // 0.82 branch-length ratio
drawLine(x0, y0, x1, y1, col);
gCP = (uint8_t)((depth << 4) | 0x4); // step 4: drawLine done, about to recurse left
coord childLen = length * kScale;
treeSegment(x1, y1, angle - gTreeSpread, childLen, depth - 1);
gCP = (uint8_t)((depth << 4) | 0x5); // step 5: left child returned, about to recurse right
treeSegment(x1, y1, angle + gTreeSpread, childLen, depth - 1);
gCP = (uint8_t)((depth << 4) | 0x6); // step 6: right child returned
--gTreeNest;
}
static uint8_t demoBranchingTree(coord cx, coord cy, coord r, uint32_t ms) {
// Wind sway: 9 s period
coord windPhase = coord::from_raw(
(int32_t)((uint64_t)(ms % 9000u) * (uint32_t)kTwoPi.raw() / 9000u));
gTreeSwayStep = fl::s16x16::sin(windPhase) * coord::from_raw(3932); // ±0.06 rad/level
// Growth cycle: smooth 0→1→0 via (1−cos)/2, period 10 s.
// Trunk grows first; tips emerge last. Reverse on the way down.
coord growPhase = coord::from_raw(
(int32_t)((uint64_t)(ms % 10000u) * (uint32_t)kTwoPi.raw() / 10000u));
gTreeGrowFrac = (coord::from_raw(65536) - fl::s16x16::cos(growPhase)) / coord(2);
if (gTreeGrowFrac.raw() > 65536) gTreeGrowFrac = coord::from_raw(65536); // clamp to 1.0
// Spread angle: opens from 0 to full 28° over the first half of growth
static const coord kFullSpread = coord::from_raw(32035); // 28°
coord spreadScale = gTreeGrowFrac * coord(2);
if (spreadScale.raw() > 65536) spreadScale = coord::from_raw(65536);
gTreeSpread = kFullSpread * spreadScale;
coord trunkX = cx;
coord trunkY = cy + r * coord::from_raw(65011); // cy + 0.99 r (near bottom)
coord trunkLen = r * coord::from_raw(36045); // 0.55 r ≈ 7 px
gTreeNest = 0;
gTreeNestMax = 0;
gCP = 0; // 0 = outside treeSegment
if (kDebugSerial) {
Serial.print(F("tree ms=")); Serial.print(ms);
Serial.print(F(" grow=")); Serial.print(gTreeGrowFrac.raw());
Serial.print(F(" spread=")); Serial.println(gTreeSpread.raw());
Serial.flush();
}
treeSegment(trunkX, trunkY, coord{}, trunkLen, 6);
// Report max nesting depth reached this frame (printed once per new maximum).
static uint8_t sLastNestMax = 0;
if (gTreeNestMax != sLastNestMax) {
sLastNestMax = gTreeNestMax;
if (kDebugSerial) {
Serial.print(F("tree nestMax="));
Serial.println(gTreeNestMax);
}
}
return 255;
}
void setup() {
Serial.begin(2000000);
// Always report the hardware reset cause captured in .init3.
printResetReason(gMCUSR);
// ── Crash detection ─────────────────────────────────────────────────────
// Check before arming: if the sentinels are intact the MCU was reset by
// something other than a clean power-on (watchdog timeout, brown-out, etc.).
if (gCrashSentinel == kCrashMagic && gCrashComplement == (uint8_t)~kCrashMagic) {
// Clear the sentinels so a manual reset / power-cycle won't re-trigger.
gCrashSentinel = 0;
gCrashComplement = 0;
Serial.println(F("*** CRASH / UNEXPECTED RESET DETECTED ***"));
Serial.print(F(" last recorded loop ms = "));
Serial.println(gLastLoopMs);
Serial.print(F(" last free mem (bytes) = "));
Serial.println(gLastFreeMem);
if (gMemSentinel == kMemMagic) {
Serial.print(F(" mem watermarks lo/hi = "));
Serial.print(gMemLo);
Serial.print(F(" / "));
Serial.println(gMemHi);
} else {
Serial.println(F(" mem watermarks: not initialised (power-on)"));
}
Serial.println(F(" halting — cycle power or press reset to continue"));
Serial.print(F(" crash point gCP=0x"));
if (gCP < 0x10) Serial.print('0');
Serial.print(gCP, HEX);
Serial.print(F(" (depth="));
Serial.print(gCP >> 4);
Serial.print(F(" step="));
Serial.print(gCP & 0x0F);
Serial.println(')');
Serial.print(F(" gTreeNest at entry = ")); Serial.println(gCPFrame);
Serial.print(F(" growFrac raw = ")); Serial.println(gCPGrow);
Serial.flush();
// Flash all LEDs red continuously and spin forever.
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, LED_COUNT);
while (true) {
FastLED.showColor(CRGB(80, 0, 0));
delay(300);
FastLED.showColor(CRGB(0, 0, 0));
delay(300);
}
}
// Arm the sentinels: any subsequent unexpected reset will trigger the halt above.
gCrashSentinel = kCrashMagic;
gCrashComplement = (uint8_t)~kCrashMagic;
gLastLoopMs = 0;
// Initialise free-memory watermarks for this run.
{
int fm = freeMemory();
if (fm < 0) fm = 0;
gMemLo = gMemHi = gLastFreeMem = (uint16_t)fm;
gMemSentinel = kMemMagic;
Serial.print(F("free SRAM at startup: "));
Serial.print(fm);
Serial.println(F(" bytes"));
}
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, LED_COUNT);
for (int y = 0; y < kMatrixHeight; ++y)
for (int x = 0; x < kMatrixWidth; ++x)
setPixelXY(x, y, CRGB(x * 8, y * 8, 0));
for (int i = 255; i > 0; i -= (i / 8 + 4))
FastLED.showColor(CRGB(i, i, i));
FastLED.show();
delay(500);
}
void loop() {
static coord starSpin{0};
static uint8_t dimAfterShow = 255;
coord cx = coord::from_raw((int32_t)kMatrixWidth << 15); // 16.0 px: matrix centre x
coord cy = coord::from_raw((int32_t)kMatrixHeight << 15); // 16.0 px: matrix centre y
coord r = coord::from_raw((int32_t)(kMatrixWidth/2 - 3) << 16); // 13.0 px: radius
uint32_t ms = fl::millis();
gLastLoopMs = ms; // updated each frame; preserved in .noinit across a crash-reset
static uint32_t sFrameCount = 0;
++sFrameCount;
checkMem(F("loop"));
coord t = coord::from_raw((int32_t)((uint32_t)(ms % 6284u) * (uint32_t)kTwoPi.raw() / 6284u));
// orbit the drawing origin to stress subpixel AA
coord ocx = cx + fl::s16x16::sin(t) * fl::s16x16::from_raw(163840); // *2.5 px
coord ocy = cy + fl::s16x16::cos(t) * fl::s16x16::from_raw(163840); // *2.5 px
static constexpr uint32_t kSlotMs = 8192u;
static constexpr uint32_t kTransMs = 768u; // half-transition window (ms)
static constexpr uint32_t kFadeMs = kTransMs; // fade spans the full zoom window
uint8_t demo = (ms / kSlotMs) % 11;
// demo = 10; // --- DEBUG: force a specific demo ---
uint32_t msInSlot = ms % kSlotMs;
static uint8_t lastDemo = 255;
bool reinit = (demo != lastDemo);
lastDemo = demo;
// Half-cosine scale envelope: 1 during the body, tapers to 0 at slot edges.
// zoom-out: last kTransMs of the slot (msInSlot in [kSlotMs-kTransMs, kSlotMs))
// zoom-in: first kTransMs of the slot (msInSlot in [0, kTransMs))
static const coord kPi = coord::from_raw(205887); // π = 3.14159265
coord tScale;
if (msInSlot < kTransMs) {
// phase ∈ [0,1): msInSlot*65536 max = 599*65536 = 39,255,664 < 2^31
coord phase = coord::from_raw((int32_t)msInSlot * 65536 / (int32_t)kTransMs);
tScale = (coord::from_raw(65536) - fl::s16x16::cos(kPi * phase)) / coord(2); // 0 -> 1
} else if (msInSlot >= kSlotMs - kTransMs) {
coord phase = coord::from_raw((int32_t)(msInSlot - (kSlotMs - kTransMs)) * 65536 / (int32_t)kTransMs);
tScale = (coord::from_raw(65536) + fl::s16x16::cos(kPi * phase)) / coord(2); // 1 -> 0
} else {
tScale = coord::from_raw(65536);
}
// Brightness fade: black in the final kFadeMs of the outgoing slot and the
// first kFadeMs of the incoming slot — sits inside the zoom envelope.
uint8_t fadeBr;
if (msInSlot < kFadeMs) {
fadeBr = (uint8_t)((uint32_t)msInSlot * 255u / kFadeMs);
} else if (msInSlot >= kSlotMs - kFadeMs) {
fadeBr = (uint8_t)((kSlotMs - msInSlot) * 255u / kFadeMs);
} else {
fadeBr = 255u;
}
// Scale radius; lerp orbiting origin toward display centre so transition stays centred.
coord tr = r * tScale;
coord tcx = cx + (ocx - cx) * tScale;
coord tcy = cy + (ocy - cy) * tScale;
// Apply previous frame's dim request; always hard-clear on a demo transition.
if (reinit || dimAfterShow == 255) {
FastLED.clear();
} else if (dimAfterShow > 0) {
fadeToBlackBy(leds, LED_COUNT, dimAfterShow);
}
// Breadcrumb A: survived clear/fade
if (kDebugSerial) {
Serial.print('A'); Serial.print(sFrameCount); Serial.flush();
}
auto renderTime = micros();
uint8_t wantDim;
if (demo == 0) {
wantDim = demoClock(cx, cy, tr, ms);
} else if (demo == 1) {
wantDim = demoOrbitingDiscs(tcx, tcy, tr, tScale);
} else if (demo == 2) {
wantDim = demoStarWeb(cx, cy, tr, starSpin, tScale);
} else if (demo == 3) {
wantDim = demoSpirograph(cx, cy, r, tScale, reinit); // full r: geometry must never shrink below kInnerR
} else if (demo == 4) {
wantDim = demoLissajous(cx, cy, r, tScale, reinit); // full r: matches spirograph; trail scaled at render
} else if (demo == 5) {
wantDim = demoCubeImpl(cx, cy, true, tScale); // thin Wu-AA lines
} else if (demo == 6) {
wantDim = demoCubeImpl(cx, cy, false, tScale); // depth-cued thick lines
} else if (demo == 7) {
wantDim = demoOrganicWalkers(cx, cy, tScale, reinit); // fading walker trails
} else if (demo == 8) {
wantDim = demoBoids(cx, cy, r, tScale, reinit); // boids flocking
} else if (demo == 9) {
wantDim = demoHypoRand(cx, cy, reinit); // cumulative random hypotrochoid
} else {
wantDim = demoBranchingTree(cx, cy, r, ms); // fractal wind-swaying tree
}
// Breadcrumb B: survived demo render
if (kDebugSerial) {
Serial.print('B'); Serial.print(sFrameCount); Serial.flush();
}
renderTime = micros() - renderTime;
if (kFrameTimings) {
Serial.print(F("frame=")); Serial.print(sFrameCount);
Serial.print(F(" demo=")); Serial.print(demo);
Serial.print(F(" render=")); Serial.print(renderTime);
Serial.print(F("us free=")); Serial.println(freeMemory());
}
// Fade entire frame to black at slot boundaries via global brightness.
FastLED.setBrightness(fadeBr);
checkMem(F("show"));
FastLED.show();
// Breadcrumb C: survived FastLED.show()
if (kDebugSerial) {
Serial.print('C'); Serial.print(sFrameCount); Serial.println();
}
dimAfterShow = wantDim;
starSpin = starSpin + coord::from_raw(1966); // +0.03 rad/frame
if (starSpin.raw() > kTwoPi.raw()) starSpin = starSpin - kTwoPi;
}
FPS: 0
Power: 0.00W
Power: 0.00W