Skip to content

Power functions — bottom-up analysis

Forward-looking research document — exception to CLAUDE.md present-tense rule. This is a Stage-1 bottom-up survey of power functions: the shared primitives (drawing, fields, physics, color, time) that LED effects are really made of, which MoonLive should expose as built-ins so scripts stay compact. It inventories three sources read on 2026-08-05: (a) our own 39 compiled effects and the helpers they share, (b) WLED / the WLED Particle System / FastLED as prior art, (c) the industry-standard algorithm canon with originators. The top-down companion (power-functions-analysis-top-down.md) turns the catalog into the implementation spec. Modelled on livescripts-analysis-bottom-up.md. Source citations are file:line against this repo, or repo-relative paths for external sources; usage counts come from reading every effect header and grepping the cloned externals.

TL;DR

  • The gap is stark and measurable. Our compiled effects draw on ~40 shared helpers (draw:: primitives, the sin8/beat8 family, 1D/2D/3D value noise, palettes, Random8, fonts). MoonLive scripts can call exactly three functionssetRGB(i,r,g,b), fill(r,g,b), random16(n) — by flat index only: no x/y/z, no dimensions, no time (MoonLiveBuiltins_light.h:27-36). Every power function this document catalogs is something a script cannot express today.
  • Our own effects prove the demand. Even with the shared library, the 39 effects hand-roll: the same depthDim() helper 16×, a BPM phase accumulator 9×, an integer map() 6×, five particle systems in five different representations, four different distance approximations, a private plot() that re-implements draw::pixel, and a byte-identical sine-blob oscillator in two effects. Each repeat is a power function asking to exist (§ What our effects hand-roll).
  • The WLED Particle System is found, and it is the single richest prior-art source for "gravity and inertia". wled00/FXparticleSystem.cpp/.h, author Damian Schneider (DedeHai), licensed EUPL v1.2, merged into mainline WLED via PR #4506 on 2025-02-17 (the 0.16 line); 32 effects are built on it. WLED-MM carries a diverged 2025 variant. Its design vocabulary — integer sub-pixel positions (1 pixel = 64 units), 3.4-fixed-point force accumulators, binned impulse collisions, a 2×2 bilinear splat with inverse-gamma weights — is the measured, ESP32-proven shape of an LED physics engine (§ WLED-PS).
  • One organizing principle covers almost everything. Read at function level, WLED's ~200 effects and our 39 decompose the same way: time-phase generators (beat/sin/noise/random) → palette lookup → additive sub-pixel compositing → decay (fade/blur). The particle system is exactly that pipeline made stateful. A power-function set that serves those four stages plus a physics kernel covers the overwhelming majority of known effects.
  • "Gravity and inertia" has a textbook answer — and an industry name: particles. Semi-implicit (symplectic) Euler — v += a; x += v in fixed point — is what game engines, the demoscene, and WLED-PS all use: two adds per axis, stable at large timesteps (Fiedler, gafferongames.com; Hairer et al.). Restitution bounce is v = -(v·e)>>8; drag is v *= (256-k)/256; the "smooth follow" every audio meter wants is a one-pole filter x += (target-x)>>n (the critically-damped-smoothing family, Game Programming Gems 4). None of it needs float (§ Physics).
  • The shader question has a precise, honest answer. Per-pixel budget at 240 MHz: ~15,600 cycles/pixel on 16×16@60 (anything goes) but ~293 cycles/pixel at 128×128@50 — one noise sample + one palette map + one blend, nothing more. PixelBlaze (Ben Hencke) proves the ergonomics of per-pixel scripting (normalized 0..1 coordinates, time(n) sawtooths, hsv out) but its interpreted VM measures ~48k pixel-evals/s on ESP32 — an order of magnitude short of large matrices. No shipping project runs real GLSL on an MCU; GPU shading exists only on Pi/desktop. Conclusion: power functions are compiled fixed-point kernels that scripts compose — per-frame calls into native code, not per-pixel interpretation. That is exactly the PO's "one set, same everywhere" with the desktop ceiling preserved: desktop may accelerate the same functions (or interpret richer per-pixel expressions on top), but the contract is the portable kernel set (§ Shaders).
  • Modern additions the classic canon lacks. Two primitives from the shader world earn a place on CPU: 2D signed distance functions (circle/box/segment + smooth-min; Quilez) — anti-aliased shapes, outlines, glow and metaball-morphing from a few fixed-point ops — and cosine gradient palettes (12 constants = a whole palette, bakeable to a LUT). Plus the Wu sub-pixel splat, which WLED treats as the difference between 8-bit-console and modern motion on a coarse matrix.
  • Fixed-point is settled policy, and the conventions already exist in-repo. Coding standards mandate integer-first (coding-standards § numeric types); effects document the working idioms: uint8 angle (256 = full turn), palette index mod-256, noise coords 16.0 fixed, the uint64 BPM phase numerator divided late, 12.4 particle positions. Power functions adopt these, not float. The known trap to design around: naive signed right-shift rounds asymmetrically (−1>>1 = −1) — WLED-PS documents the sign-corrected form.
  • Prior art is cataloged, credited, and not ported. Standing rule (no-WLED-MM-derivation) plus license reality: WLED and the PS are EUPL v1.2. This document takes concepts, measurements and API shapes; implementations come fresh from the textbook sources named per primitive (Bresenham 1965, Wu 1991, Blinn 1982/1996, Reynolds 1987, Penner 2002, Kriegsman's fire2012, Elias's ripple, Quilez's articles).
  • Recommendation for the top-down doc: a ~34-function core in nine families (§ The candidate set), dimension-generic per the PO decision, each function: one canonical algorithm, integer form, one home in core or light. The three MoonLive-side constraints that must be lifted for scripts to use any of this: the 16-entry builtin table, the one-arg-in/one-out host-call ABI, and a grammar with no variables, loops, or coordinate/time symbols (MoonLiveBuiltins.h:40-54, MoonLiveCompiler.h:11-15).
  • Out of scope for Stage 1. API naming and exact signatures; which functions land in draw:: vs a new namespace; the MoonLive grammar redesign; benchmarks on hardware; scheduling of the three build stages. All Stage 2 (top-down).

Why this document exists

The goal is that power functions carry the weight, so the code around them stays small — an effect writer expresses the idea, not the machinery. Code around the calls is expected and welcome; what should disappear is re-solving the same sub-problems. The two contexts differ only in how much surrounding code is reasonable: a compiled effect is unlimited (any effect-local logic that makes it better), while a MoonLive script is more limited by intent — not a hard line count, but a script that grows to 100-200+ lines is a signal the mechanics it needs belong in a power function rather than in the script.

Today the script side offers three functions against a flat index, so almost nothing is expressible whatever the length. Meanwhile the compiled effects each re-solve the same sub-problems privately. Power functions are the common denominators — implemented once, natively, and exposed three ways, in the product owner's stated order:

  1. Use them in existing effects — all current effects are demo effects, and all can be rewritten to the new standard. The default bar is runs exactly the same; divergence (e.g. replacing float with fixed point) is a case-by-case call, with large fixtures as the guard: an effect must stay smooth at 12K+ lights, which is what the 16-bit variants and sub-pixel splat exist for. Rewriting also extracts hidden modifiers (see the effects-vs-modifiers decision below).
  2. Create new example effects — effects written only in power functions, proving coverage.
  3. Expose them to MoonLive — the same natives become script builtins, so a script composes what compiled effects compose.

Product owner decisions taken for this analysis (2026-08-05):

  • Dimension-generic from day one. Every power function is defined for 1D/2D/3D where meaningful, the way effects' dim() and draw::blur (one call, every axis with extent >1) already work. No 2D-first API that fits strips and volumes badly.
  • One set, same everywhere — without capping desktop. The contract is identical on every target (fixed-point CPU kernels). A platform may implement the same function faster (desktop SIMD, GPU); advanced desktop-side capability on top of the contract is allowed, but is not part of it.
  • Current effects are demo effects; all are rewrite candidates. Per default a rewrite is pixel-identical; modifying one (float → fixed point, cleanup) is decided per effect, judged on large fixtures where smoothness is hardest.
  • Effects and modifiers stay distinct concepts. An effect must not carry hidden modifiers — mirroring, coordinate transforms, symmetry folding baked into the effect body get extracted into real modifiers during the rewrite. (The inventory found one: FreqSaws' invert mirrors even columns in-effect, while mirroring otherwise correctly lives in MirrorModifier.) Consequence for the candidate set: power functions serve both module kinds, and transform-shaped entries (toPolar, kaleido) are modifier material first.
  • Stefan Petrick's style is a supported target. Petrick is a friend of projectMM; his Animartrix idiom — polar coordinates, layered/warped noise, palette mapping composed per pixel — must be writable with our power-function set. His engine is float-per-pixel and FPU-bound (Teensy/S3-class); our expression of the same idiom is the fields family (polar LUT + fbm + warp + palette) on the portable contract, with per-target acceleration free to close the gap on FPU-strong targets.
  • One consistent codebase. The power-function set is written as one architecture in one style — one coordinate model, one fixed-point vocabulary, one naming convention — not a mix of idioms accumulated per family. Consistency is itself a requirement the top-down designs for.
  • The physics family is named particles — the industry term (Reeves, SIGGRAPH 1983; Unity ParticleSystem, Unreal Niagara, WLED-PS). "Gravity and inertia" are not a separate concept: inertia is the integrated state, and the forces carry their standard names (gravity, force, drag, bounce, attract, emitters). The non-particle scalar physics (smoothFollow) stays in time-and-motion under its own standard name.
  • A particle-system effect replaces its non-PS twin — no parallel variants. WLED's precedent: PS Fire replaced Fire 2012, PS Pinball replaced Bouncing Balls (~12 KB flash saved). The five in-repo particle-shaped effects converge onto the one kernel; this is a named case of the pixel-identical-by-default rule's divergence clause (an analytic float trajectory folded onto the Euler kernel is not bit-identical — judged on the bench).
  • The contract is 16-bit; 8-bit is internal only. One sin (0..65535 = full turn), one beatsin, 16-bit easing and noise in the API — because every effect must support big displays, and 8-bit outputs position to 256 levels, which visibly steps on a 12K-light wall. There is no auto-switching "sin816": the angle domain IS the API, so the contract picks one. Implementation stays cheap — the existing 256-entry LUT plus linear interpolation yields smooth 16-bit output (the FastLED sin16 shape; WLED 0.16 moved wholesale to sin16_t). 8-bit survives only where the domain is inherently 8-bit (palette index, hue — mod-256 by design), as a fast path the API never exposes.

What we already have (and the MoonLive gap)

The shared library effects use

Home Contents
draw.h pixel (clipped), line (3D Bresenham + shorten), get, blendPixel, addPixel (saturating), fade, blur (separable, every axis, 1D/2D/3D in one call), fill, glyph/text (two built-in fonts), offsetOf
math8.h sin8/cos8 (256-entry LUT), triwave8, atan2_8, dist8 (octagonal, no sqrt), qadd8/qsub8/nscale8, map8, beat8/beatsin8/beatsin16 (ms passed explicitly), Random8 (xorshift)
noise.h inoise8 in 1D/2D/3D — value noise, 16.0 fixed coordinates
color.h / Palette.h RGB, hsvToRgb, scale8 (with the /255 rounding), colorFromPalette (the hot-path seam), blend, fadeToBlackBy, 63 built-in gradients
Layer.h width()/height()/depth(), elapsed(), the collected once-per-frame fadeToBlackBy, persistent frame buffer (FastLED/WLED convention), extrude(Dim)

Notably absent even for compiled effects: circle, rect/bar, scroll/shift, polar/rotate, gradient fill, easing, any physics, sin16, scale16.

What MoonLive scripts can reach

Three builtins — setRGB, fill, random16 (MoonLiveBuiltins_light.h:27-36). Structural constraints recorded for the top-down:

  • BuiltinTable capacity kMax = 16 (MoonLiveBuiltins.h:54) — the candidate set below needs ~30-40 entries.
  • HostCallFn = uint32_t(*)(uint32_t) — one arg in, one out (MoonLiveBuiltins.h:40); drawLine(x0,y0,x1,y1,c) is not expressible. Multi-arg host calls (or packed-arg convention) are a prerequisite.
  • Grammar is call ";" only — no variables, operators in source, loops, conditionals, and no x/y/index/time symbols (MoonLiveCompiler.h:11-15). The runtime already receives t (elapsed ms) and dims (MoonLive.h:54) but nothing exposes them to script code.
  • What already works and carries forward: @control script-declared controls surfacing as real UI controls, and the bounds-guarded inline ops (StoreElem, FillElems).

What our effects hand-roll (the demand evidence)

From reading all 39 effect headers. Each row is a power-function candidate with its in-repo demand:

Pattern Count Examples Power function it implies
depthDim() copy-paste (depth()>0 ? depth() : 1) 16 effects LissajousEffect.h:78, TetrixEffect.h:162 dims as a first-class value (safe extents)
Coord3D dims{...} + Buffer& buf preamble 22 effects BouncingBallsEffect.h:55 a draw context carrying buffer+dims
BPM phase accumulator (phase_ += dt*bpm, divide late, uint64) 9 effects + 3 members each PlasmaEffect.h:38-45, NoiseEffect.h:39-43 beatPhase(bpm) — the stateful, sub-ms-safe time base
Integer map() with zero-span guard 6 effects GEQEffect.h:150 map16/map32 beside the existing map8
Raw flat-index writes bypassing draw::pixel 14 effects LinesEffect.h:91-98 (a local setRGB lambda) flat-index + row-pointer fast paths as library fast paths
Private scratch plane, fade, blit 3 (+13 with ScratchBuffer state) ParticlesEffect.h:52-84, WaveEffect.h:72-103 trails/decay owned by the library
Palette lookup colorFromPalette(*Palettes::active(), …) 27 effects FireEffect.h:96 already a power function — carry to scripts
Random8 rng_ + rand8() adapter + constrained-random forms 12 effects StarSkyEffect.h:132-140 bounded random (randomBelow, randomRange, grid-safe)
Sine oscillator vs uint8 angle (3 hand-rolled shapes; one byte-identical in 2 effects) 8 effects LavaLampEffect.h:57MetaballsEffect.h:58 oscillator family incl. sin16, wave shapes
Radial/polar/distance — 4 different implementations 5 effects dist8 vs squared-field vs sqrtf vs hand-rolled isqrt (PaintBrushEffect.h:132) one distance/polar family (isqrt, dist16, polar LUT)
Particle state — five different representations 5 effects 12.4 fixed (ParticlesEffect.h:88), float analytic (BouncingBallsEffect.h:85), SoA aging, perspective float, state machine the particle kernel (§ Physics)
Bar/column fill from a value 4 effects GEQEffect.h:108-129 drawBar/fillRect
Buffer scroll via read-back 1 effect (N-step shift) FreqMatrixEffect.h:123-126 scroll(axis, delta, wrap)
Off-by-one-safe extent mapping (each site carries a bug comment) 4+ effects LinesEffect.h:100-108 mapping helpers own the fencepost, once

Absent from our effects entirely (so: candidates justified by prior art, not in-repo demand): easing curves, springs/inertia, kaleidoscope-in-effect (lives in modifiers), circle/rect primitives, ripple-as-propagating-field, collisions between particles.

Prior art 1 — WLED and the WLED Particle System

Where everything lives (the PO asked)

What Where Author
Effect library (~200 effects) wled/WLEDwled00/FX.cpp (11,224 lines), Segment model in FX.h Aircoookie + community; many effects credit Andrew Tuline (WLED-SR)
Shared helpers wled00/FX_fcn.cpp (1D), FX_2Dfcn.cpp (2D), colors.cpp, util.cpp, wled_math.cpp WLED 2D functionality originated in the WLED-SR repo, original author ewowi (Ewoud Wijma); migrated into wled/wled for v14 by ewowi + blazoncek, then developed further partly in wled/wled and partly in WLED-MM
Particle System wled00/FXparticleSystem.cpp (1,945 lines) + .h (422) Damian Schneider (DedeHai), 2013–2024, EUPL v1.2
WLED-MM variant MoonModules/WLED branch mdev, same two files, diverged 2025 (CRGB framebuffer, renderonly fire flag, no mass-ratio collisions) DedeHai; MoonModules carry

PS integration history: PR #4506 merged 2025-02-17 (the 0.16 line), refined in PR #4630. To save ~12 KB flash it replaced classics (Fire 2012 → PS Fire, Bouncing Balls/Rolling Balls/Multi Comet → PS Pinball, …); WLED_PS_DONT_REPLACE_FX restores the originals. 16 2D + 16 1D effects are built on it — the physics kernel earns its bytes there.

Also worth knowing when reading either codebase: mainline 0.16 replaced FastLED math with its own (sin16_t, perlin8 with an inoise8 alias, re-implemented beatsin) and reads the ESP32 hardware RNG register for hw_random8/16 — free real entropy, faster than the FastLED LCG. WLED-MM still uses FastLED's originals.

The PS design vocabulary (measured, ESP32-proven — concepts to learn, not code to port)

  • Integer sub-pixel space: 1 pixel = 64 units 2D (>>6 to pixels), 32 units 1D. Positions int16_t, velocities int8_t clamped ±120 so collision math can't overflow. A 10-byte particle: x, y, ttl, vx, vy, hue, sat; flags live in a separate parallel byte array for alignment.
  • 3.4 fixed-point forces: a force of 16 = +1 velocity/frame; smaller forces accumulate in a 4-bit per-particle counter until they overflow into a ±1 step. This is how sub-unit acceleration stays smooth with 1-byte velocities — the key trick for "inertia" feel.
  • Frame order: gravity → size animation → collisions → move → render; collisions run before move so pushes can't render out of bounds.
  • Physics ops: applyForce (the accumulator), applyAngleForce (polar via sin16/cos16), applyGravity (one shared dv per frame, applied to all — not skipping dead particles because the branch costs more), applyFriction (v·(255−k)/255, exponential decay), pointAttractor (inverse-square, clamped near-field, optional "swallow"), bounce (invert, scale by wall hardness, snap inside; wall roughness transfers perpendicular into parallel speed for diffuse scattering).
  • Collisions: broad phase = spatial binning in x only (y-binning tried and measurably not worth it — documented in-code); narrow phase = axis-separated distance checks with one-frame velocity lookahead against tunneling; response = textbook elastic impulse in int32, mass ratio ∝ size², sub-threshold hardness adds periodic "sticky" friction so soft particles pile instead of sloshing; overlap resolved by pushing one particle chosen by a free pseudo-random bit (pushing both oscillates — documented).
  • Rendering: 2×2 bilinear splat — corner weights (64−dx)(64−dy)·b >> 12 — with brightness gamma-corrected up front and each sub-pixel weight passed through inverse gamma, so after the global output gamma the spatial distribution is linear: no flicker as particles cross pixel boundaries. Compositing is a SWAR saturating add that rescales all channels on overflow (preserves hue instead of clipping to white). Motion blur = scale-framebuffer decay; optional smear blur after.
  • Rounding trap, documented: never plain right-shift signed values (−1>>1 = −1, asymmetric drift); use divide (1-cycle on ESP32) or the sign-corrected shift.

WLED's top-10 primitives by counted use in FX.cpp

  1. setPixelColor/setPixelColorXY (213+59 — the float XY overload is anti-aliased) · 2. palette lookup (129+45) · 3. hardware random (141+120) · 4. beatsin8/16 (53+15) · 5. sin8/sin16 (79+31) · 6. fade-toward-background (30+26) · 7. color_blend (59) · 8. blur (27) · 9. fill (45) · 10. perlin8/16 (30+10).

One-line synthesis: WLED effects = time-phase generators → palette lookup → additive sub-pixel compositing → decay. The PS is that pipeline made stateful.

Prior art 2 — the industry-standard canon (per primitive: name, source, fixed-point verdict)

Rasterization. Bresenham line (IBM Systems Journal, 1965 — pure integer, ideal) and midpoint circle (Bresenham 1977 / Van Aken 1984 — ideal); Xiaolin Wu anti-aliased line (SIGGRAPH 1991 — excellent in 8.8/16.16); the Wu pixel 2×2 bilinear splat (the single-point case; WLED's wu_pixel, the PS's renderer — 4 muls + 4 saturating adds, the primitive that makes motion smooth on a coarse matrix); thick lines (Murphy/IBM 1978, perpendicular Bresenham — no trig); scanline polygon fill (Foley & van Dam — fine, but few LED effects decompose into it: low priority); bitmap fonts (BDF/Adafruit-GFX convention — we already have draw::glyph/text).

Physics. Semi-implicit Euler (Fiedler; Hairer/Lubich/Wanner) — v += a; x += v, energy-bounded at fixed timestep, the right default; Verlet + Jakobsen constraints (GDC 2001) only when rope/cloth chains arrive (restitution is awkward in Verlet — a reason particle systems prefer Euler); restitution bounce v = -(v·e8)>>8; Stokes drag v *= (256−k)/256; critically damped smoothing (Lowe, Game Programming Gems 4 — Unity's SmoothDamp) with its cheap degenerate the one-pole x += (target−x)>>n, the standard VU smoother; boids (Reynolds, SIGGRAPH 1987 — beautiful, O(n²), fine to ~32 agents, decomposes only swarm effects: below the cut); cellular automata (Gardner 1970 Life; Wolfram 1983; Margolus block-CA for falling sand — one byte-grid + rule kernel covers Life/sand/matrix-rain; the LED-canonical sand is Adafruit_PixelDust); fire2012 (Kriegsman 2013 — cool/drift-up/spark on a heat byte-plane; our FireEffect.h already is this family) vs noise-fire (Petrick lineage — needs the noise primitive); Elias two-buffer ripple (new = neighbors/2 − old, damp, swap — the discretized wave equation, adds and shifts only); metaballs (Blinn, ACM TOG 1982 — per-pixel field sums; on big matrices the SDF/smooth-min form is cheaper).

Fields & signal. Perlin gradient noise (SIGGRAPH 1985/2002; FastLED inoise8/16 is the embedded reference — known quirk: output clusters mid-range, budget a rescale; ~1-2 µs/sample); our inoise8 is value noise — cheaper, blobbier; the top-down should decide whether to add gradient noise or rescale ours. fBm octaves (Mandelbrot; 2-3 octaves is the LED sweet spot); domain warping (Quilez — noise(p + a·noise(p)), one composition rule, enormous payoff); plasma (Vandevenne's tutorial — sum of phase-shifted sin8, trivially cheap); Lissajous (1857 — one particle + trail); polar/kaleidoscope LUT (precompute per-pixel r,θ once — every 1D effect becomes a mandala; PixelBlaze/Animartrix's "expensive look for free"; per the PO decision, the Petrick/Animartrix idiom — polar + layered warped noise + palette — is an explicit coverage target for this family); Penner easings (2002; FastLED ease8InOut* integer forms — the primitive separating programmer motion from designer motion).

Shaders (feasibility, honestly). The Shadertoy model is color = f(x,y,t), stateless. Budget at 240 MHz: 16×16@60 ≈ 15,600 cycles/pixel (anything goes); 32×32@60 ≈ 3,900 (comfortable fixed point); 128×128@50 ≈ 293 cycles/pixel — one field sample + palette map + blend, only as compiled code. PixelBlaze (Hencke) is the interpreted-VM precedent: JS-like source → bytecode → 16.16 VM, render2D(index,x,y) per pixel, coordinates pre-normalized 0..1, time(n) sawtooths, ~48k pixel-evals/s on ESP32 — proves the ergonomics, an order of magnitude short of large matrices. No embedded project interprets or JITs real GLSL on an MCU; GLSL-class shading exists on Pi (GPU) and desktop only. Two shader-world primitives that DO earn a CPU place: 2D SDFs (Quilez's catalog — circle |p|−r, box, segment, + polynomial smooth-min; free AA via clamp(0.5−d/px), free outline |d|−w, free glow via LUT; subsumes metaballs) and cosine gradient palettes (Quilez — a + b·cos(2π(c·t+d)), 12 constants per palette, bake to LUT on parameter change).

Color. HSV→RGB rainbow vs spectrum (Smith 1978; FastLED's hsv2rgb_rainbow is the LED de-facto — perceptually balanced yellow; WLED uses spectrum where round-trip fidelity matters); gamma via 256-byte LUT (Adafruit canon; trap: 8→8-bit LUT posterizes low fades — fix via 16-bit + temporal dithering or CIE lightness); color temperature (Planckian locus, curve-fits by Krystek/McCamy/Helland — bake presets); saturating 8-bit arithmetic with the correct /255 rounding (Blinn, "Three Wrongs Make a Right", Dirty Pixels 1996 — the substrate of everything); Porter-Duff over (SIGGRAPH 1984) only when sprites/layers-with-alpha arrive — additive + scale covers light-native compositing.

The candidate set (synthesis)

These families group functions by algorithm, which is how they were discovered. The build order is the five phases in the top-down plan, which group the same functions by what lands in the repo together and name the families each phase carries; "what are we building next" is answered there, not here.

Merging in-repo demand, WLED's usage counts, and the canon's coverage-per-byte ranking — nine families, ~34 functions (family 9, Projection, was added on review; the gather group below came from the canon survey). Dimension-generic per the PO decision; every entry is integer/fixed-point; (have) = exists for compiled effects today, so the work is exposure + adoption, not invention.

# Family Functions Grounding
1 Frame ops fill (have), fade (have), blur (have — already dimension-generic), scroll(axis, delta, wrap) WLED #6/#8/#9; FreqMatrix's hand-rolled shift
2 Pixel ops pixel/get/addPixel/blendPixel (have), splat(fx, fy, c) — the Wu sub-pixel writer, 12.4 or 16.16 coords WLED-PS renderer; ParticlesEffect's private 12.4 math; "modern motion" on coarse matrices
3 Geometry line (have), lineAA (Wu 1991), circle/fillCircle (midpoint), rect/fillRect/bar (the audio-meter staple), text (have); SDF trio sdCircle/sdBox/sdSegment + smin + coverage-AA 4 effects hand-roll bars; SDFs subsume metaballs/glow/outline
4 Fields noise 1/2/3D (have — value; decide gradient vs rescale), fbm(octaves), warp (as a composition rule), plasma (or just document sum-of-sin8), polar/kaleido LUT toPolar, kaleido(n) WLED #10; LavaLamp/Metaballs/Rings/Spiral's four distance implementations
5 Time & motion sin/beatsin (16-bit contract; the 8-bit forms become internal), beatPhase(bpm) — the stateful uint64 accumulator 9 effects hand-roll, triwave/quadwave/cubicwave (16-bit), easeInOutQuad/Cubic (Penner, 16-bit), smoothFollow (one-pole + critically-damped forms), peakHold(value, decay) — the falling-peak meter idiom (instant attack, slow decay), the standard VU primitive the single biggest hand-roll count in-repo; GEQ's hand-rolled peak dot; the big-display stepping rule
6 particles (the industry name — Reeves 1983) SoA pool, semi-implicit Euler step(), gravity, force (3.4 accumulator), drag, bounce (restitution + wall roughness), attract (inverse-square), emitters (spray, angleEmit), optional binned collide; plus ripple (Elias two-buffer) and a CA step (Life/sand share one kernel) five in-repo particle representations; 32 WLED-PS effects; BouncingBalls/Tetrix/StarField/Particles/StarSky converge onto it, replacing their non-PS forms
7 Color colorFromPalette (have), hsvToRgb (have), blend (have), cosPalette (Quilez, baked), gamma8 LUT, saturating math (have — qadd8/scale8) + sin16/scale16 gaps WLED #2/#7; 27 in-repo users
8 Random Random8 (have), bounded forms below/range as builtins, hardware-RNG seed on ESP32 (free entropy, per WLED) 12 in-repo users each with an adapter; WLED #3
9 Projection project(Coord3D, fov) — pinhole/perspective 3D→2D in fixed point; painter's-order depth sort; the vanishing-point line form Three effects hand-roll it: StarField's 1/z pinhole, GEQ3D's converging foreshortening, RubiksCube's voxel-to-face classification. Same repeated-pattern evidence that justified beatPhase; the prerequisite for any "3D scene on a 2D panel" effect
Support map16/map32 (fencepost-safe), isqrt, dist16, dims/time as script symbols 6 in-repo imap copies; PaintBrush's isqrt; the MoonLive gap

Added on review (2026-08-06), from a second pass over the effects that fit none of the eight original families: projection (family 9) and peakHold. Both are repeat-count-justified in the same way the original entries were, and both were missed because the first pass grouped by algorithm (drawing, fields, physics) rather than by what the leftover effects actually do.

The gather gap (found 2026-08-06 by a canon-vs-us survey)

A survey against WLED, FastLED master, Pixelblaze and the demoscene canon found the set strong on generation (noise, SDF, palettes) and simulation (particles, ripple, fire, CA), with the gaps clustered on one structural absence:

There is no way to READ the framebuffer as a texture at a transformed coordinate. The Wu splat is the write side (scatter with interpolation); the gather side is missing, and the two are transposes — neither builds the other. Roughly a third of the classic canon is that one primitive wearing different hats: rotozoom, tunnel, lens/glass distortion, twister, feedback/zoomblur, Voxel Space, texture kaleidoscope, wobbly text. FastLED ships it (fl::sampleBilinear, src/fl/gfx/sample.h); WLED hand-rolls it inside both mode_2Dsoap and mode_2Dplasmarotozoom for want of a shared version — the same duplication evidence that justified beatPhase.

# Primitive Why it is new (not composable)
G1 sampleWrap(src, u, v) — bilinear gather, Q16.16, power-of-2 wrap The transpose of splat. Destination-driven with a constant per-pixel step, so no division or trig in the inner loop (~8 MACs/pixel). Needs a second buffer: cannot resample in place
G2 combine(a, b, op) — per-pixel two-buffer arithmetic (add/sub/mul/screen/min/max/difference) blend blends colors; this blends buffers with an operator. Highest composability leverage found: makes bump mapping, moiré, XOR texture, glow/bloom compositions rather than primitives. WLED independently ships 17 of these as segment blend modes
G3 mat23 — fixed-point 2D affine transform with push/pop The API-shape gap: we have sin16/cos16 and 3D projection but no reusable 2D transform, so every effect hand-rolls its rotation. With G1 it gives inverse mapping for one division per frame. Pixelblaze exposes exactly this
G4 Asymmetric attack/release envelope Sharpens the planned smoothFollow: the symmetric one-pole is the WRONG ballistic for a meter — it makes attacks as sluggish as decays and rounds off drum hits. WLED, FastLED and LedFx all converged independently on the asymmetric form (~5 cycles)
G5 Beat-phase PLL + spectral-flux onset The missing input to beatPhase: period by autocorrelation with harmonic enhancement, phase by a gated P/I loop. Elegant in fixed point — phase as a uint32 where full range is one beat, so beat detection IS the overflow and reinterpreting as int32 IS the wrapped error. Turns every existing beatsin effect beat-locked. Belongs in the audio service, not the power functions (it is signal analysis; every effect then gets it free)
G6 fillTriangle — two-edge integer DDA fillRect is the axis-aligned degenerate case and cannot make a rotated quad. Unlocks filled vectors, vectorballs, 3D cube, Kefrens bars, twister slices
G7 Bayer 8×8 ordered dither (64 bytes) Neither WLED nor FastLED ships this — a gap in the canon rather than versus it. Directly relevant to LED bit depth: visibly better gradients. Ordered, not Floyd–Steinberg: error diffusion crawls between frames on animated content
G8 Worley/cellular noise A noise class value noise + fBm cannot synthesize: crystalline/organic-cell/caustic structure. ~9 distance evals per pixel — budget as an effect, not a free primitive

Smaller, cheap, high value: map8_to_16-style bit-replication rescalers (map8_to_16(255) == 65535 exactly, where x<<8 gives 65280 — silently fixes full-scale loss when widening); hashInt — stateless position-addressable randomness, distinct from an xorshift stream, which is what lets a dissolve transition carry zero per-pixel state; sub-LSB force dithering (accumulate sub-unit forces, emit ±1 on overflow) — the mechanism that makes weak gravity work at 8-bit velocity precision, already noted from WLED-PS.

Rejected as composable (the useful half of the survey): feedback/zoomblur/motion-blur/bloom (= fade + G1 resample + draw — what is actually needed is a ping-pong buffer convention, infrastructure not a primitive); bump mapping (= scroll + G2 + palette); metaballs (smin of circle SDFs already IS metaballs); starfield (the particle pool + projection); flow-field/curl advection (p.v += vecFromAngle(noise(...))); boids (particle pool + the binned neighbour queries we already have); copper bars, scrollers, palette cycling, Lissajous, moiré, XOR texture (all beatsin/sin16 + bar/text/combine); reaction-diffusion (the 3×3 Laplacian is our separable blur); AGC (= G4 in the dB domain + clamp + gate). Rejected outright: fractal flame (needs megapixels and float histograms — expensive and pointless at 64×64), Scheirer comb-filter beat tracking (RAM-disqualified: ~320 KB of delay lines, more than a classic ESP32's DRAM; autocorrelation gets the same tempo for ~1% of it).

Two cross-cutting MCU notes: every effect in this canon hoists reciprocals to row/slice setup to keep division out of the inner loop — worth preserving in the API shape; and the Xtensa 64-bit variable shift lesson bites directly on Q16.16, so shift amounts in sampleWrap/mat23 stay compile-time constants.

Below the cut, with reasons: boids (only swarm effects), filled polygons (few LED effects decompose into them), Verlet+constraints (until rope/cloth), Porter-Duff (until sprite layers), font additions (cost is fonts, not code), GPU anything (not portable; a desktop accelerator of the same contract later).

Constraints the top-down must respect

  • Hot path: power functions run inside tick() per frame at up to 16K+ lights; per-light work stays integer, per-frame float is allowed where already conventional (EffectBase.h:128). No allocation in any power function; particle pools and LUTs allocate at prepare() via the existing ScratchBuffer discipline.
  • MoonLive ABI: multi-arg host calls, a bigger builtin table, and coordinate/time symbols are prerequisites for family exposure (§ the MoonLive gap). The runtime already threads t and dims to the entry point — the gap is grammar/ABI, not plumbing.
  • Buffer model: the frame buffer persists across frames (trails are a feature); power functions compose with the collected fadeToBlackBy rather than each fading privately — three effects' private-plane idiom migrates onto this.
  • Licensing/derivation: WLED + PS are EUPL v1.2; standing rule is fresh implementations from the textbook sources named above. Concepts, measurements, and API shapes are fair learning; code is not.
  • Naming/credit: each shipped power function's doc block names its canonical source (Bresenham 1965, Wu 1991, …) — same convention the effects already follow for their origins.
  • One style: a single coherent architecture across all eight families — shared coordinate model, one fixed-point vocabulary (the in-repo idioms above), uniform naming — so the set reads as one library, not eight provenances.
  • Module boundary: power functions are usable from effects and modifiers; the effect/modifier concept split stays intact, and the Stage-1 rewrite audits each effect for hidden modifiers to extract.

Out of scope for Stage 1

Exact signatures and namespaces; the draw:: vs new-namespace split; MoonLive grammar redesign (variables, loops, per-pixel vs per-frame model — the livescripts top-down owns the engine, this doc feeds it the builtin surface); hardware benchmarks; migration order for the 39 effects; palette-system changes. All Stage 2: power-functions-analysis-top-down.md.

Sources

In-repo: every file cited inline above. External, read 2026-08-05: wled/WLED @ c1838ed, MoonModules/WLED @ 7c55f91, FastLED/FastLED @ b2a1344 (clones under the session scratchpad, disposable); WLED PRs #4506, #4630, #4543. Canon: Bresenham 1965/1977; Van Aken 1984; Wu, SIGGRAPH 1991; Murphy 1978; Foley/van Dam; Reeves 1983; Reynolds 1987; Verlet 1967; Jakobsen GDC 2001; Fiedler, gafferongames.com; Lowe, Game Programming Gems 4; Gardner 1970; Wolfram 1983; Toffoli & Margolus 1987; Kriegsman fire2012 (FastLED examples); Hugo Elias, "2D Water"; Blinn 1982 & Dirty Pixels 1996; Perlin 1985/2002; Mandelbrot 1982; Quilez (distfunctions2d, smin, palettes, warp — iquilezles.org); Vandevenne (plasma); Penner 2002 / easings.net; Smith 1978; Porter & Duff 1984; Poynton; Adafruit "LED Tricks: Gamma Correction"; Adafruit_PixelDust (Burgess); PixelBlaze (Hencke — bhencke.com/pixelblazegettingstarted). Credits: Damian Schneider (DedeHai) for the WLED Particle System; ewowi (Ewoud Wijma) for WLED 2D (originated in WLED-SR, migrated to wled/wled v14 with blazoncek); Aircoookie, blazoncek, Andrew Tuline for WLED/WLED-SR; Mark Kriegsman & Daniel Garcia for FastLED; Stefan Petrick (Animartrix — friend of projectMM) whose polar-noise idiom is a named coverage target.