Skip to content

Power functions

The shared toolbox the light domain is built from: a small set of named, integer-only routines that effects compose into a look. One home per idea — an effect that needs a distance, a bar, a noise field or a smooth follower calls the same one every other effect calls, so behaviour is consistent, the cost is measured once, and a fix reaches everything at once.

Three consumers share this vocabulary, and each uses a different slice of it:

  • Effects are the main consumer — they draw, so they reach for nearly all of it.
  • Modifiers fold coordinates through modifyLogical and never draw, so they reach for almost none of it. That asymmetry is the architecture, not a gap: an effect decides what a pixel looks like, a modifier decides where a pixel comes from.
  • MoonLive scripts reach the same routines through the builtin table (core/moonlive/MoonLiveBuiltins.h), which carries plain scalar arguments — so a script sees the flat form of a function, not the C++ callback form a compiled effect can use.

Sources: draw.h (drawing), core/math16.h (16-bit math), core/noise.h (fields). The caller lists below are generated by reading the call sites, so they record what the code does rather than what it intends.

Many of the names here are FastLED's, deliberately: scale8, sin8, the gradient-palette model (CRGBPalette16 / colorFromPalette), and the beatsin8 / inoise8 / qadd8 family are the vocabulary the LED-effect world already shares, so a contributor recognises them on sight. projectMM links no part of FastLED: the implementations are ours, integer-only and tuned for this render loop, with FastLED credited as the prior art behind the convention here and in each primitive's own notes (core/math8.h names Mark Kriegsman's lib8tion directly).

Migrating an effect — two steps, in this order

Step 1, the port: behave identically. Bringing an effect over from WLED or MoonLight reproduces the original's visual behaviour exactly, because the original is the best available description of what the effect should look like. At this stage a difference is a bug, not a variation — pin it with a golden so any drift is visible. Don't get creative with defaults, oscillator math, color mapping, or geometry, and don't silently drop a parameter that is the mechanism (the PaintBrush straight-vs-curved-lines bug was a dropped partial-line length; Game of Life was wrong the first time by not porting the real algorithm). Study the source for the algorithm, defaults, and visual result, then write our own implementation against EffectBase and our primitives — carry the behaviour forward, don't trace or copy the structure (see Industry standards, our own code). Credit the origin as prior art in the block below.

Step 2, the tuning: change it deliberately. Once the port is faithful it becomes ours to improve. Adopting a power function often makes an effect look better as a side effect — bouncing balls that collide with each other because the physics is now the shared kernel's, a gradient that stops banding because the maths went 16-bit — and that is a real gain, not a regression. The rule is only that the change is deliberate and visible: say what moved, re-baseline the golden in the same commit, and let the product owner judge it on the panel. What is forbidden is drifting silently.

Used by nearly everything

Not a category so much as the floor: three things almost every effect touches whatever else it does. If you read only one row of this page, read these — an effect that uses none of them is doing something unusual.

Power function What it does Effects Modifiers
colorFromPalette Reads a color out of the shared palette by index, so every effect follows the user's palette choice 39 of 47
draw::pixel Writes one pixel, clipped to the grid 24 of 47 — the rest reach it through a higher-level primitive
BeatPhase A BPM phase accumulator that keeps its numerator in 64 bits and divides late, so animation never freezes on sub-millisecond frames 13 — Dissolve, DistortionWaves, Echo, LavaLamp, Metaballs, Noise, Plasma, PolarNoise, SdfShapes, Sine, Spiral, Tunnel, Wave

Frame and pixel operations

Whole-buffer work: writing, reading, and moving what is already there.

These act on the grid as a surface rather than on a shape. Between them they cover the four things an effect does to a frame before it draws anything: clear it, dim what was there (the trail), blur it, or shift it bodily. Most effects open with one of these and close with per-pixel writes.

Power function What it does Effects Modifiers
draw::fill Fills every light with one color, leaving channels beyond RGB untouched AudioSpectrum, Blurz, RubiksCube, Solid, Spectrum, Text
draw::fade Fades every channel toward black — the trail primitive 13, through Layer::fadeToBlackBy — Blurz, BouncingBalls, Fireworks, FixedRectangle, FreqSaws, GEQ, GEQ3D, Lissajous, NoiseMeter, PaintBrush, Random, StarField, StarSky
draw::blur Separable box blur across every axis with extent > 1; one call covers 1D, 2D and 3D Blurz
draw::get Reads one pixel back, black outside the grid Echo, GameOfLife
draw::blendPixel Lerps a pixel toward a color by an amount, rather than replacing it GameOfLife, Tetrix
draw::addPixel Adds light to a pixel, saturating instead of wrapping to black Blurz
draw::scroll Shifts the whole grid along an axis, optionally wrapping — the shift register FreqMatrix
draw::splat Draws a point at a fractional position, splitting its light across neighbouring pixels so motion is smooth on a coarse grid (no caller yet — the particle kernel is its consumer)

Geometry

Drawing a shape by walking the pixels it covers.

The classical rasteriser: given endpoints, a centre and a radius, or a run length, light exactly the cells the shape passes through. Integer-only and exact, with no distance computed anywhere — which makes these the cheap way to draw when the shape sits on the grid and does not need to move smoothly between pixels.

Contrast with signed distance fields below: same shapes, opposite approach, different trade-off.

Power function What it does Effects Modifiers
draw::line A straight line between two points (3D Bresenham) GEQ3D, PaintBrush
draw::bar A run of cells growing from an origin along one axis, colored per cell — the audio-meter staple AudioSpectrum, GEQ, Spectrum
draw::fillCircle A filled disc (midpoint algorithm), colored per row Echo
draw::text Draws a string, returning its pixel width (glyph is reached through it, not called directly) DemoReel, Text
draw::sprite Blits one frame of a small palette-indexed bitmap (index 0 = transparent key), clipped at every edge — the multi-color sibling of glyph; movement belongs to the particle pool, never to the sprite FlyingToasters
draw::circle A circle outline on integer coordinates, exact and symmetric (no caller yet)
draw::rect, fillRect An axis-aligned rectangle, outlined or filled (no caller yet)
draw::lineAA An anti-aliased line (Wu 1991) that splits its light between the two cells straddling the true path (no caller yet)

Signed distance fields

Describing a shape as "how far away is it", then reading a picture out of that number.

Instead of drawing a circle, an SDF answers how far is this pixel from the circle's edge — negative inside, zero on it, positive outside. That one number does far more work than a rasteriser's yes/no: the sign fills the shape, its magnitude gives an anti-aliased edge for free, taking the absolute value turns it into an outline, and two distances combine into a third shape with a single min or smin.

This is what makes shapes composable and smooth-moving. It costs a distance per pixel, so it is the right tool when a shape moves sub-pixel or merges with another, and the wrong one for a static bar.

Power function What it does Effects Modifiers
sdCircle True signed distance to a circle's edge: negative inside, zero on the rim, positive outside SdfShapes
sdBox Signed distance to an axis-aligned box SdfShapes
smin Smooth minimum of two distances — the operator that makes shapes flow together instead of merely overlapping SdfShapes
coverage Turns a distance into 0..255 coverage, which is anti-aliasing for free SdfShapes
sdCircleSq The squared form: same sign contract without the square root, for a plain fill (no caller yet)
sdSegment Signed distance to a thick line segment (a capsule) (no caller yet)

Fields

Smooth pseudo-random values across space: everything organic.

Noise is the source of anything that should look natural rather than drawn — clouds, fire, smoke, water, marbling, drifting colour. The defining property is that nearby points get similar values (unlike a raw hash), so the result flows instead of flickering.

One sample is a soft blur; the character comes from composing them. Summing octaves adds structure at every scale, folding the field creases it into flame, and displacing the sample coordinate by another field is what produces the flowing, liquid look.

Power function What it does Effects Modifiers
inoise8 Perlin gradient noise in 1D, 2D or 3D: a smooth, deterministic pseudo-random field Noise, NoiseMeter, Wave noise
fbm8 Sums noise octaves at doubling frequency and halving amplitude, turning a blur into cloud and terrain structure. Re-widened per octave count, so the field keeps its full range however many are summed PolarNoise, Tunnel, Aurora, Nebula fbm
warp8 Samples noise at a coordinate that noise itself displaced — the flowing, marbled look PolarNoise, Aurora warp
turbulence8 Sums the folded absolute value of noise, whose creases read as billowing smoke and flame (no caller yet)
blobCentres + blobField Orbits N sources on sine paths and sums their inverse-square falloff — the metaball field behind anything fluid or molten LavaLamp, Metaballs
curl16 The perpendicular gradient of a noise potential: a flow field that is divergence-free by construction, so what it carries neither piles up nor drains away Nebula, Trails
Fluid A stable-fluid solver (Stam 1999) in Q16.16: the medium works out its own motion rather than reading it from a function, one independent medium per depth slice Fluid

Transport

A field says where things go; these carry light along one, frame after frame. The state they move is the effect's own plane rather than the layer buffer, held at 16 bits because a value multiplied by slightly less than one many times a second has nowhere to go at 8.

Each takes a depth, so a cube is the same call as a panel and a panel pays nothing for it (d = 1 reduces to the 2D loop exactly). What that buys on a cube differs by kernel: the noise fields sample a genuine third axis, so slices differ rather than one plane repeating, while the transport kernels carry light WITHIN each slice and not yet between them. Moving light through a volume needs a trilinear sampler and a third velocity component, which is open work.

Power function What it does Effects Modifiers
draw::advect, advect16 Moves a whole plane along a velocity rule by sampling BACKWARD, so every destination is written exactly once and nothing tears or duplicates Trails, Nebula, Fluid
draw::decay, decay16 Fades a plane by a HALF-LIFE in milliseconds, so a tail's length is stated in seconds and holds at any framerate Trails, Nebula, Fluid
draw::quantize Narrows 16 bits to 8, carrying the error into the next frame (temporal) or offsetting by a Bayer cell (ordered), so a slow fade stays smooth rather than stepping through blit16
draw::blit16 A 16-bit plane onto the canvas, dithered: the one narrowing step every wide-plane effect shares Trails, Nebula, Fluid, MoonLive scripts
draw::upscale16 Bilinearly stretches a small plane over a large one, so a smooth field can be computed at a fraction of the resolution (measured 3.0x at half, 6.6x at quarter) Nebula
halfLifeKeep What fraction of a value survives a given elapsed time at a given half-life: the framerate-independent decay both decay forms are built on through decay

Polar and geometry math

Addressing the grid by angle and radius instead of by x and y.

Swapping coordinate systems is the cheapest way to change what an effect looks like. Anything radial — rings, spirals, rotation, kaleidoscopes, tunnels, radial wipes, a spectrum bent around a circle — is an ordinary pattern read through polar coordinates rather than a special algorithm.

The 16-bit forms matter here: the 8-bit versions step visibly on a large fixture and their distance is an octagon rather than a circle.

Power function What it does Effects Modifiers
PolarLut Every pixel's angle and radius, built once per geometry and read as a table. 34% of a PolarNoise frame on an ESP32-S3, at 2 bytes per pixel (4 at full precision); declines the table when free heap minus the reserve cannot hold it, and the caller computes the address instead PolarNoise, Spiral, Tunnel, Aurora
atan16 The angle of a point as a 16-bit turn, smooth enough that a sweep shows no steps on a large fixture Rings, and the fallback path of every PolarLut caller
dist16 True Euclidean radius — not the octagon dist8 approximates, and it does not saturate at 255 Rings, and the fallback path of every PolarLut caller
kaleido Folds an angle into n mirrored wedges, giving any field n-fold symmetry for one modulo PolarNoise, Tunnel, Aurora
isqrt Integer square root with no divide and no float PaintBrush, WaterRipple
sin8 / cos8 The 8-bit oscillators — the internal fast path where a mod-256 result is exactly right (the 16-bit forms are the contract) Rotate

Time, motion and randomness

How a value changes between frames, and how to get randomness that behaves.

Two related problems. First, motion: raw linear movement reads as mechanical, so easings shape it, followers smooth it, and peak-hold gives a meter its characteristic instant-rise slow-fall. Second, randomness that is reproducible — addressed by position rather than drawn from a stream, so the same pixel gets the same value on every device and every frame.

The framerate rule lives here too: everything in this group is driven by elapsed time, never by frame count (architecture).

Power function What it does Effects Modifiers
sin16 / cos16 16-bit oscillators, smooth where the 8-bit forms visibly step on a large fixture Echo, SdfShapes
OscillatorBank N independent low-frequency oscillators advanced once per frame and read per pixel, each with its own rate, shape, range and phase offset. Their phases are held together, so oscillators sharing a rate keep their relationship for as long as the device runs Aurora, PolarNoise, Trails, Nebula, Fluid
map32 Maps a value between ranges, clamped, with the fencepost handled once so the last column is never lost FreqMatrix, FreqSaws, GEQ, GEQ3D, Spectrum, StarField
hashInt Hashes position and time to a random-looking but reproducible value, so devices agree without exchanging anything Dissolve, WaterRipple
peakHold Rises instantly to a new high then decays slowly — the falling peak dot every VU meter has Spectrum
smoothFollow Moves a value a fraction of the way toward its target each frame, so it stops jittering Spectrum
easeInOutQuad Accelerates from rest and decelerates to rest, so motion reads as deliberate rather than mechanical Dissolve
easeInOutCubic, easeOutQuad The same family with a longer settle, and a fast-start curve that arrives and rests (no caller yet)

Particles

Things that move under forces: sparks, rain, snow, smoke, confetti, debris, a swarm.

Anything that behaves like matter is the same handful of forces over the same state, and the part that differs between one look and another is which forces are applied and how particles are emitted — not the physics. So the state and the integrator live in particles.h and the character stays with the effect.

Storage is structure-of-arrays over the caller's own buffers, so a pass that touches only velocity walks only velocity, and the pool never allocates after prepare(). Positions are the same sub-pixel type splat takes, so a particle at x=3.5 lands half on each pixel instead of snapping.

Frame order matters and is the caller's to get right: forces, then collide(), then step(), then walls, then age(), then render(). Collisions run before the move because resolving an overlap afterwards can shove a particle through a wall the bounce pass already checked.

Prior art: the WLED Particle System by Damian Schneider (@DedeHai), whose vocabulary of emitters, forces and walls over one shared pool is the shape this follows, and Reeves 1983 for the name. The fixed-point implementation and the elapsed-time scaling are ours. His system also settled a design question by having answered it already: he documents trying y-binning in the collision broad phase and measuring it not worth the bookkeeping at these pool sizes, so collide keeps the cheaper sweep along X deliberately rather than by omission.

A script reaches the same kernel through MoonLive's pool / emit / step builtins.

Power function What it does Effects Modifiers
Pool The state: SoA positions, velocities, life, hue and optional size over caller-owned buffers. Owns nothing, allocates nothing Fireworks, Ballpit, Particles
gravity, force, drag, attract The forces. Each is one pass over one array, so an effect pays only for the ones it uses Fireworks, Ballpit
forceSmall A force too weak to move an integer velocity, accumulated until it does — what makes a light breeze read as inertia rather than as nothing (no caller yet)
step Semi-implicit Euler: position integrates the already-updated velocity, which is what stays stable under a constant force Fireworks, Ballpit, Particles
bounce, wrap, killOutside What happens at the walls: reflect with restitution, re-enter the opposite edge (snow, rain, marquee), or simply stop existing Fireworks, Ballpit, Particles
collide Particles notice each other. The one non-linear part of the kernel, so it is opt-in Ballpit
spawn, angleEmit, spray Emitters: one particle, a directed cone, or an undirected scatter Fireworks, Ballpit
age, render Life counts down and brightness rides it, so a particle fades as it dies Fireworks, Ballpit
FrameTime Converts elapsed time into a per-frame scale, so the same settings behave identically at 60 fps and at 5000 Fireworks, Ballpit, Particles, Echo, BouncingBalls, Lissajous, Tetrix

Shaders

One function of (position, time) evaluated per pixel — the other way to write an effect.

Everything above draws into a grid: set this pixel, walk this line, move this row. A shader inverts that — it never draws anything, it answers a question. Given where a pixel is and what time it is, what colour is it? The framework runs that function everywhere.

That inversion is why shaders compose so freely. There is no state to keep in step and no order of operations to get right, so an effect is built by transforming the coordinate before answering: fold space and one shape becomes a thousand, rotate it and the whole design turns, displace it by a noise field and everything flows.

shader.h is the standard GLSL vocabulary in fixed point, deliberately using the familiar names so anyone who has read shader code needs no translation. It runs on every target.

Power function What it does Effects Modifiers
each The runner: supply one function of position and time, and it handles the loop, the coordinate mapping and the write Truchet, Raymarch
uv Pixel to shader space, centred and scaled by the SHORT side — which is what keeps a circle circular on a non-square panel Truchet, Raymarch
clamp, mix, fract, step, smoothstep The five built-ins in essentially every shader. fract is the one that tiles a pattern; smoothstep is the one that anti-aliases an edge Truchet
length, rotate Vector basics. Rotating the coordinate spins the entire design for one operation Truchet
repeat, mirror Domain operators: fold space so one shape becomes a lattice. The objects do not multiply — the coordinate does the work Truchet
opUnion, opIntersect, opSubtract, opShell, opRound Combine two shapes into a third, which is how an SDF scene is composed rather than drawn Truchet
sdRoundBox, sdPolygon Shapes beyond the circle/box/segment trio in Signed distance fields (no caller yet)
cosPalette, mixColor A whole colour ramp as twelve numbers instead of a table (no caller yet)

Raymarching — one technique inside a shader

Raymarching is one technique a shader can use, for rendering 3D. A scene is described as a function: say how far the nearest surface is from any point, and the renderer walks a ray outward until it arrives. The world is arithmetic — geometry emerges from the distance function rather than being stored.

raymarch.h is compiled only where the SoC declares a hardware FPU, because a raymarch is per-pixel float by nature. That gate is the one bounded exception to the integer-only render path, and it is a whole-header switch rather than a rule weakened in place. Everything in shader.h stays fixed point and runs everywhere.

Power function What it does Effects Modifiers
march Sphere tracing: walk a ray until it hits. Takes the scene as a callable, so that function is the world Raymarch
normalAt The surface normal as the gradient of the distance field — which is why lighting works on a shape that was never modelled Raymarch
sdSphere, sdBox, sdPlane, sdTorus 3D distance primitives, same sign contract as the 2D family Raymarch
smin, opUnion, opIntersect, opSubtract, opRepeat The 3D operators. smin melts surfaces together; opRepeat tiles space into an endless lattice Raymarch
Camera, diffuse Where the viewer stands and how a surface is lit — the parts every raymarch effect would otherwise re-derive Raymarch

Gather

Reading the grid back as a texture.

Everything else writes; this reads. Once a frame can be sampled at an arbitrary sub-pixel coordinate, a whole family follows from a few lines each: feedback and motion trails, zoom, rotation, tunnels, plasma warping. Without it every one of those needs its own bespoke loop.

Power function What it does Effects Modifiers
sampleWrap Reads the grid as a texture at a sub-pixel coordinate, bilinear and wrapping — the primitive behind feedback, tunnels and zoom Echo
combineMax Combines two colors by the brighter channel, so a trail brightens instead of averaging away (no caller yet)

On the "no caller yet" entries. Each was added for a named consumer in the power-function plan: splat, combineMax and the remaining SDF and easing forms are what the particle kernel and the shader tier build on. They are listed rather than hidden so the gap between what exists and what is used stays visible.