Core & Formats
Raw .md

Animations, Generators & render_code

How nittygrid animations work, the five ways to build one, and the complete render_code JavaScript API and all built-in generators.

Every animation in nittygrid, no matter how you made it, does the same simple thing: on every frame it fills a grid of colored cells. That grid is called the PixelMatrix. Your display then shows those colors.

The grid and its colors

The PixelMatrix has W columns and H rows. Cell (0, 0) is the top-left. Each cell holds four numbers from 0 to 255: red, green, blue, and alpha ([r, g, b, a]).

Five ways to build an animation

Each animation node stores a small recipe (so you can always edit it), made in one of five ways:

WayGood forStored as
Built-in generatorFast effects such as Fire, Plasma, Rain, or Text, with ready-made sliders.type: "generator" + generatorId + params
JavaScript render_codeCustom scenes, or anything the AI writes for you.type: "code" + code + optional codeParams
Drawn frames and spritesHand-painted pixel art and short loops.generatorId: "draw-frame" or "sprite-anim"
Vector pathLight sweeps and glows travelling along a line.generatorId: "path_animation"
VideoSampling a video clip across your LEDs, live or baked into frames.generatorId: "video-input" or "flattened-animation"

Write your own effect with render_code

render_code is a short piece of JavaScript that runs once per frame. The AI writes it for you, and you can also edit it in the Animation Editor. This complete example draws a soft breathing glow in the middle of any display:

js
// 1. Start every frame with opaque black
clear([0, 0, 0, 255]);

// 2. A smooth 0 → 1 → 0 pulse every 2 seconds
const pulse = easeInOut(pingpong(t, 2.0));
const radius = map(pulse, 0, 1, 2, Math.min(W, H) * 0.35);

// 3. Draw a glow at the center
glow(W / 2, H / 2, [80, 180, 255, 255], radius);

Combine drawing and AI (_targetFrame)

You can lock a picture you drew as the exact start or end of an AI animation:

  1. Paint a logo, icon, or pattern with the Draw tool.
  2. Open the AI chat on that animation node and ask for an intro or transition, for example "assemble this icon from drifting embers".
  3. nittygrid locks your painted pixels as _targetFrame, so both the preview and the export land exactly on your artwork.

Technical reference

The tables below are generated from the code, so they always match the app. They are for technicians and AI agents.

render_code API

The same reference as the in-app References modal.

Core scope

NameTypeDescription
matrixPixelMatrixFull matrix object — use top-level helpers instead where possible
framenumberFrame counter, increments ~30×/s (normalized to 60 fps ticks during sequence export)
tnumberElapsed seconds
W, HnumberMatrix width and height in pixels
stateobjectPlain object persisting between frames — store particles, flags, etc. here
paramsobjectLive values of any codeParams you declared

Drawing

CallDescription
set(x, y, [r,g,b,a])Overwrite a single pixel. a=255 for fully opaque.
clear([r,g,b,a])Fill the whole matrix. Use at frame start for clean effects.
fade(factor)Darken all pixels (0–1). Creates decay trails. Affected by global Trail slider.
additive(x, y, [r,g,b,a])Additive blend onto existing pixel — stacks light sources.
glow(cx, cy, [r,g,b,a], radius)Radial additive glow, brightness falls off with distance². Sub-pixel center OK. Affected by global Glow Intensity/Radius sliders.

Shapes

CallDescription
line(x0, y0, x1, y1, color)Bresenham pixel line between two points.
circle(cx, cy, r, color, filled=false)Rasterized circle — outline by default, pass true for filled.
rect(x, y, w, h, color, filled=true)Rectangle — filled by default, pass false for outline only.
js
// Spinning clock hand
clear([0,0,0,255]);
const angle = t * 2;
line(W/2, H/2,  W/2 + Math.cos(angle)*6,  H/2 + Math.sin(angle)*6,  [0,255,180,255]);

Easings

FunctionBehaviour
easeIn(t)Slow start, accelerates (quadratic)
easeOut(t)Fast start, decelerates — most natural feel
easeInOut(t)Smooth S-curve
easeElastic(t)Springy overshoot at end
easeBounce(t)Bouncy landing
easeBack(t)Slight recoil before moving forward
js
// Pulsing circle with eased size
const phase = (t % 1.5) / 1.5;            // 0→1 over 1.5s
const r = map(easeInOut(phase), 0, 1, 1, 8);
clear([0,0,0,255]);
circle(W/2, H/2, r, [255,120,0,255], true);

Spring physics

ParameterDefaultEffect
stiffness0.1How strongly it pulls toward target — higher = snappier
damping0.8How quickly it settles — lower = more oscillation
js
// Dot that springs to a moving target
if (!state.s) state.s = { val: W/2, vel: 0 };
const target = W/2 + Math.sin(t * 0.8) * (W/2 - 2);
const r = spring(state.s.val, target, state.s.vel, 0.08, 0.75);
state.s.val = r.value;  state.s.vel = r.velocity;
clear([0,0,0,255]);
glow(state.s.val, H/2, [100,200,255,255], 3);

Math helpers

CallDescription
map(v, i0, i1, o0, o1)Remap v from input range to output range
lerp(a, b, t)Linear interpolate between a and b
constrain(v, lo, hi)Clamp v to [lo, hi]
dist(x1, y1, x2, y2)2D Euclidean distance
lerpColor(a, b, t)Interpolate two [r,g,b,a] colors
hsb(h, s, b, a?)h 0–360 · s/b 0–100 → [r,g,b,a]. Great for hue cycling.
js
// Rainbow hue cycle across columns
clear([0,0,0,255]);
for (let x = 0; x < W; x++) {
  const hue = (x / W * 360 + t * 60) % 360;
  for (let y = 0; y < H; y++) set(x, y, hsb(hue, 100, 80));
}

Noise & random

CallDescription
noise(x, y, z=0)Perlin-style smooth noise → -1..1. Pass t as z to animate.
noise01(x, y, z=0)Same but → 0..1, easier for brightness and opacity.
random(lo=0, hi=1)Random float in [lo, hi).
js
// Organic flowing colour field
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
  const v = noise01(x * 0.15, y * 0.15, t * 0.4);
  set(x, y, hsb(v * 240, 80, v * 100));
}

Time & loop helpers

CallDescription
phase(t, period)0..1 sawtooth — resets every period seconds. Clean loop phase.
pingpong(t, period)0..1..0 triangle — breathe/pulse/bounce without writing modulo math.
palette(t, colors[])Sample a multi-stop gradient at t (0..1). Each stop is [r,g,b,a].
js
// Pulsing fire-palette glow at centre
const fireColors = [[0,0,0,255],[180,20,0,255],[255,120,0,255],[255,255,100,255]];
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
  const d = 1 - constrain(dist(x, y, W/2, H/2) / (H/2), 0, 1);
  const pulse = pingpong(t, 1.2);           // 0→1→0 every 1.2s
  set(x, y, palette(d * easeOut(pulse), fireColors));
}

Live UI controls — codeParams

TypeExtra fieldsUI rendered
"number"min, max, stepSlider
"color"default is "#rrggbb"Color picker
"boolean"—Toggle
"select"options: string[]Dropdown
js
codeParams: [
  { key: "speed", type: "number", label: "Speed", default: 1, min: 0.1, max: 5, step: 0.1 },
  { key: "color", type: "color",  label: "Color", default: "#ff6600" },
]
// In code:
const spd = params.speed;
const hex = params.color.replace('#','');
const cr = parseInt(hex.slice(0,2),16), cg = parseInt(hex.slice(2,4),16), cb = parseInt(hex.slice(4,6),16);

Universal controls (details panel)

ControlWhat it does
Speed (0.1–4×)Scales the engine FPS (30 fps base)
Glow Intensity (0–3)Multiplies the alpha of every glow() call
Glow Radius (0–3)Multiplies the radius of every glow() call
Trail (0–2)Scales the decay factor of every fade() call (0 = clear, 1 = original, 2 = very persistent)
Scale / Offset X-Y / RotationPixel-level backward-mapping transform applied after each frame

Built-in generators

Every built-in generator with its parameter names, types, ranges, and defaults, read live from the app.

Basic

Generator IDNameParameters (key: type = default)
solid-colorSolid Colorcolor: color = "#ffffff", brightness: number (0..1) = 1
gradient-sweepGradient SweepcolorA: color = "#ffffff", colorB: color = "#aaaaaa", colorC: color = "#555555", useThreeColors: boolean = false, angle: number (0..360) = 0, speed: number (0..3) = 0.5, loop: boolean = true
draw-frameDraw Frameframe: pixeldata = ""
flattened-animationFlattened Animationdata: flatteneddata = ""

Effects

Generator IDNameParameters (key: type = default)
rainRaincolor: color = "#ffffff", speed: number (0.05..1) = 0.3, density: number (0.1..1) = 0.6, direction: select = "down", trailLength: number (1..20) = 4
twinkleTwinkle / Starscolor: color = "#ffffff", density: number (0.01..1) = 0.3, speed: number (0.005..0.2) = 0.04, minBrightness: number (0..1) = 0.05, maxBrightness: number (0..1) = 1
fireFireintensity: number (0.1..1) = 0.7, wind: number (-2..2) = 0, palette: select = "classic", cooling: number (0.1..1) = 0.4
matrix-rainMatrix Raincolor: color = "#ffffff", speed: number (0.05..2) = 0.4, density: number (0.1..1) = 0.7, trailLength: number (2..32) = 8, glitch: number (0..0.5) = 0.05
pulsePulse / Ripplecolor: color = "#ffffff", bgColor: color = "#000000", speed: number (0.1..5) = 1, rings: number (1..10) = 3, decay: number (0.05..1) = 0.5, originX: number (0..1) = 0.5, originY: number (0..1) = 0.5
bouncing-ballsBouncing Ballscount: number (1..20) = 3, radius: number (0.5..8) = 1.5, speed: number (0.05..3) = 0.5, trailLength: number (0..30) = 5, rainbow: boolean = true, color: color = "#ffffff"
physics-ballsPhysics Ballscount: number (1..15) = 4, radius: number (1..8) = 2, restitution: number (0.1..1) = 0.9, gravity: number (-2..3) = 0.5, trailLength: number (0..20) = 4

Procedural

Generator IDNameParameters (key: type = default)
plasmaPlasmaspeed: number (0.1..5) = 1, scale: number (0.2..5) = 1, palette: select = "rainbow", phase: number (0..6.28) = 0
perlin-flowPerlin Flowscale: number (0.2..6) = 1.5, speed: number (0.05..3) = 0.5, colorA: color = "#1a0533", colorB: color = "#ff6bff", contrast: number (0.1..3) = 1
waveformWaveformamplitude: number (0.05..1) = 0.7, frequency: number (0.1..10) = 2, speed: number (0.1..5) = 1, layers: number (1..5) = 2, palette: select = "viridis", direction: select = "horizontal", filled: boolean = true
heatmapHeatmap / Cellularpalette: select = "hot", scale: number (0.2..8) = 2, speed: number (0.01..2) = 0.3, spread: number (0.1..4) = 1

Cellular

Generator IDNameParameters (key: type = default)
game-of-lifeGame of Lifedensity: number (0.05..0.8) = 0.35, speed: number (0.02..1) = 0.15, liveColor: color = "#ffffff", deadColor: color = "#000000", trailColor: color = "#666666", trailLength: number (0..20) = 4

Creative

Generator IDNameParameters (key: type = default)
textTexttext: textarea = "HELLO", color: color = "#ffffff", bg: color = "#000000", scroll: boolean = true, speed: number (0.1..6) = 0.5, size: number (0.3..2) = 0.75, bold: boolean = true
emojiEmojiemoji: textarea = "🔥", size: number (0.2..2) = 0.85, anim: select = "none", speed: number (0.1..5) = 1, bg: color = "#000000"
pixel-iconPixel Iconicon: select = "heart", color: color = "#ffffff", size: number (0.1..2) = 0.8, anim: select = "none", speed: number (0.1..5) = 1, bg: color = "#000000"
keyframe-animKeyframe Animdata: flatteneddata = ""
formulaFormula (tixy-style)expression: textarea = "(t,i,x,y,a,b,c,d) => ...", a: number (-16..16) = 2, b: number (-16..16) = 8, c: number (-1..1) = 0, d: number (0..1) = 0.35, fgColor: color = "#ffffff", bgColor: color = "#000000", negColor: color = "#ff3333", scale: number (0.1..5) = 1
sprite-animSprite AnimationframesJson: spritedata = "[]", fps: number (1..30) = 6, loop: select = "loop"
sprite-sceneSprite SceneobjectsJson: textarea = "[ { "kind": "em...", bg: color = "#000000", speed: number (0.1..5) = 1
path-animPath AnimationpathJson: pathdata = "[]", pathsJson: textarea = "[]", element: select = "glow", elementColor: color = "#ffffff", elementSize: number (1..12) = 2, animationLengthMs: number (1000..20000) = 5000, trail: boolean = true, trailDecay: number (0.5..1) = 0.85, trailLength: number (0..1) = 0.25, trailIntensity: number (0..2) = 1, blur: number (0..12) = 3, speed: number (0.1..5) = 1, staggerMs: number (0..2000) = 0, timingMode: select = "distance", loop: select = "loop"

Geometric

Generator IDNameParameters (key: type = default)
scannerScanner Sweepcolor: color = "#22d3ee", bgColor: color = "#020617", direction: select = "horizontal", speed: number (0.1..5) = 1, beamWidth: number (0.01..0.35) = 0.08, glow: number (0..1.5) = 0.65, bounce: boolean = true
chaseChase Lightscolor: color = "#ffffff", bgColor: color = "#020617", track: select = "perimeter", lights: number (1..12) = 4, speed: number (0.1..5) = 1, glow: number (1..6) = 2, trail: number (0..1) = 0.35
wave-interferenceWave InterferencecolorA: color = "#38bdf8", colorB: color = "#f472b6", bgColor: color = "#020617", layout: select = "corners", sources: number (2..6) = 4, frequency: number (1..20) = 7, speed: number (0.1..5) = 1, contrast: number (0.5..4) = 1.4, falloff: number (0..6) = 1.6

Input

Generator IDNameParameters (key: type = default)
video-inputVideo InputvideoUrl: videoupload = "", speed: number (0.1..4) = 1, placementMode: select = "fit", fitMode: select = "stretch", scale: number (0.1..4) = 1, offsetX: number (-100..100) = 0, offsetY: number (-100..100) = 0, overlayScale: number (0.1..2) = 1, overlayX: number (-100..100) = 0, overlayY: number (-100..100) = 0, tintColor: color = "#ffffff", tintAmount: number (0..1) = 0, brightness: number (0..3) = 1, contrast: number (0..3) = 1, saturation: number (0..3) = 1, sampleRadius: number (0..4) = 1, grayscale: boolean = false, cropX: number (0..1) = 0, cropY: number (0..1) = 0, cropW: number (0.01..1) = 1, cropH: number (0.01..1) = 1
json-sequenceJSON Sequencejson: textarea = "", fps: number (0..120) = 0, loop: boolean = true