# 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]`).

> [!TIP] **How brightness works.** An LED is **off** when red, green, and blue are all `0`, or when alpha is `0`. Otherwise `a = 255` is full brightness and lower values dim it (`a / 255`). Exports keep all four values exactly.

## 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:

| Way | Good for | Stored as |
| :--- | :--- | :--- |
| **Built-in generator** | Fast effects such as Fire, Plasma, Rain, or Text, with ready-made sliders. | `type: "generator"` + `generatorId` + `params` |
| **JavaScript `render_code`** | Custom scenes, or anything the AI writes for you. | `type: "code"` + `code` + optional `codeParams` |
| **Drawn frames and sprites** | Hand-painted pixel art and short loops. | `generatorId: "draw-frame"` or `"sprite-anim"` |
| **Vector path** | Light sweeps and glows travelling along a line. | `generatorId: "path_animation"` |
| **Video** | Sampling 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);
```

> [!TIP] **Want more control? Ask the AI for sliders.** Say something like *"add a slider for the radius and a color picker for the glow"*. The AI turns those numbers into live controls in the sidebar (`codeParams`), so you can fine-tune without generating again.

### 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
> [!NOTE] Always available — no import or prefix needed.

| Name | Type | Description |
| :--- | :--- | :--- |
| `matrix` | PixelMatrix | Full matrix object — use top-level helpers instead where possible |
| `frame` | number | Frame counter, increments ~30×/s (normalized to 60 fps ticks during sequence export) |
| `t` | number | Elapsed seconds |
| `W, H` | number | Matrix width and height in pixels |
| `state` | object | Plain object persisting between frames — store particles, flags, etc. here |
| `params` | object | Live values of any codeParams you declared |

### Drawing
> [!WARNING] Frame buffer is not auto-cleared each frame. Call clear() or fade() yourself.

| Call | Description |
| :--- | :--- |
| `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

| Call | Description |
| :--- | :--- |
| `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
> [!NOTE] Input and output are both 0–1. Use map() to scale to any range.

| Function | Behaviour |
| :--- | :--- |
| `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
> [!NOTE] Smooth elastic follow. Call once per frame; store {val, vel} in state.

| Parameter | Default | Effect |
| :--- | :--- | :--- |
| `stiffness` | 0.1 | How strongly it pulls toward target — higher = snappier |
| `damping` | 0.8 | How 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

| Call | Description |
| :--- | :--- |
| `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

| Call | Description |
| :--- | :--- |
| `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

| Call | Description |
| :--- | :--- |
| `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
> [!NOTE] Declare tweakable parameters. They appear as sliders/pickers in the details panel and are always accessible via params.key.

| Type | Extra fields | UI rendered |
| :--- | :--- | :--- |
| `"number"` | min, max, step | Slider |
| `"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)
> [!NOTE] These are always available for any code animation — no codeParams needed.

| Control | What 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 / Rotation` | Pixel-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 ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `solid-color` | **Solid Color** | `color`: color = `"#ffffff"`, `brightness`: number (0..1) = `1` |
| `gradient-sweep` | **Gradient Sweep** | `colorA`: 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-frame` | **Draw Frame** | `frame`: pixeldata = `""` |
| `flattened-animation` | **Flattened Animation** | `data`: flatteneddata = `""` |

### Effects

| Generator ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `rain` | **Rain** | `color`: color = `"#ffffff"`, `speed`: number (0.05..1) = `0.3`, `density`: number (0.1..1) = `0.6`, `direction`: select = `"down"`, `trailLength`: number (1..20) = `4` |
| `twinkle` | **Twinkle / Stars** | `color`: 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` |
| `fire` | **Fire** | `intensity`: number (0.1..1) = `0.7`, `wind`: number (-2..2) = `0`, `palette`: select = `"classic"`, `cooling`: number (0.1..1) = `0.4` |
| `matrix-rain` | **Matrix Rain** | `color`: 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` |
| `pulse` | **Pulse / Ripple** | `color`: 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-balls` | **Bouncing Balls** | `count`: 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-balls` | **Physics Balls** | `count`: 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 ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `plasma` | **Plasma** | `speed`: number (0.1..5) = `1`, `scale`: number (0.2..5) = `1`, `palette`: select = `"rainbow"`, `phase`: number (0..6.28) = `0` |
| `perlin-flow` | **Perlin Flow** | `scale`: 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` |
| `waveform` | **Waveform** | `amplitude`: 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` |
| `heatmap` | **Heatmap / Cellular** | `palette`: select = `"hot"`, `scale`: number (0.2..8) = `2`, `speed`: number (0.01..2) = `0.3`, `spread`: number (0.1..4) = `1` |

### Cellular

| Generator ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `game-of-life` | **Game of Life** | `density`: 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 ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `text` | **Text** | `text`: 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` |
| `emoji` | **Emoji** | `emoji`: textarea = `"🔥"`, `size`: number (0.2..2) = `0.85`, `anim`: select = `"none"`, `speed`: number (0.1..5) = `1`, `bg`: color = `"#000000"` |
| `pixel-icon` | **Pixel Icon** | `icon`: 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-anim` | **Keyframe Anim** | `data`: flatteneddata = `""` |
| `formula` | **Formula (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-anim` | **Sprite Animation** | `framesJson`: spritedata = `"[]"`, `fps`: number (1..30) = `6`, `loop`: select = `"loop"` |
| `sprite-scene` | **Sprite Scene** | `objectsJson`: textarea = `"[   {     "kind": "em..."`, `bg`: color = `"#000000"`, `speed`: number (0.1..5) = `1` |
| `path-anim` | **Path Animation** | `pathJson`: 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 ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `scanner` | **Scanner Sweep** | `color`: 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` |
| `chase` | **Chase Lights** | `color`: 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-interference` | **Wave Interference** | `colorA`: 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 ID | Name | Parameters (`key`: type = default) |
| :--- | :--- | :--- |
| `video-input` | **Video Input** | `videoUrl`: 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-sequence` | **JSON Sequence** | `json`: textarea = `""`, `fps`: number (0..120) = `0`, `loop`: boolean = `true` |
