# nittygrid rendered animation 1.0

This specification is dedicated to the public domain under CC0-1.0. The schemas
are MIT licensed. These licenses do not apply to user content.

An animation is UTF-8 JSON (`application/json`, recommended download suffix `.ngrender.json`; older `.nganim.json` is equivalent).
Recognize its contents, not its filename. `format` is `nittygrid-animation` and
`version` is a string `major.minor`, initially `1.0`. Readers accept all `1.x`
versions and ignore unknown optional properties. Unsupported major versions,
layout types, ordering, timing modes and storage profiles must fail clearly.
Known contradictory or malformed properties remain errors in newer minors.
`$schema` is an optional authoring hint, never a network dependency.

## Layout

`layout.type = "grid"` requires positive integer `rows`, `cols`, and
`order = "row-major"`; it forbids `pixels`. Every cell exists. Block i belongs to
row `floor(i / cols)`, column `i % cols`, ID `r{row}c{col}` (zero-based unpadded
decimal). Top-left is the logical origin; rows increase downward, columns right.
LED count is rows × cols. Black and transparent LEDs still occupy a block.

`layout.type = "explicit"` requires a nonempty `pixels` array and forbids grid
`rows`, `cols`, `order`. Each entry has a unique nonempty string `id`, optionally
nonnegative integer `row`, `col`, `part`. `part` requires row and col. Row-only
and column-only metadata are allowed; free layouts need neither. Block i belongs
to `pixels[i]`, in exactly that order. Never sort IDs or infer addresses by parsing
them. Distinct parts may share a cell. Missing LEDs have no entry. Identity,
logical address, sampling address, spatial placement and wiring are distinct.

Either layout may carry `reference: {path: "layout.svg", sha256: "…"}`. The digest
is 64 lowercase hexadecimal SHA-256 digits of the exact UTF-8 companion bytes.
It is a local relative path, never a fetch instruction. SVG packages require it.
Standalone JSON is valid without SVG but free layouts then offer only named
colors/timing, not spatial placement. External maps must match the complete ID
set explicitly, check supplied digests, and reject missing/duplicate identities.
Geometry, positions, bounds and units belong to SVG, not animation JSON.

## Storage and timing

`frames` requires `pixelFormat: "rgba8"`, `colorSpace: "srgb"`,
`alphaMode: "straight"`, `encoding: "base64"`, and nonempty string array `data`.
Each string is one complete frame of exactly LED-count × 4 bytes. Block i has
unsigned R, G, B, A bytes. Preserve all bytes, including RGB at alpha zero.
No premultiplication, compositing, gamma conversion or hardware policy applies.
Base64 is canonical RFC 4648: standard +/ alphabet, required padding, zero pad
bits, no whitespace, URL-safe alphabet or data URL prefixes.

`timing` requires `mode: "fixed"`, positive finite `fps`, boolean `loop`.
Count is `frames.data.length`; duration is count × 1000 / fps milliseconds.
Do not serialize `frameCount` or `totalDurationMs`. Frame i starts at i × 1000 /
fps and is held a complete interval. Looping repeats from zero; one-shot playback
holds the last output until explicitly changed. No interpolation or duplicate
periodic endpoint is implied. Equal frames are intentional and never trimmed.

Optional `meta` contains `name`, `exportedAt` (RFC 3339 date-time with a real
calendar date, hours 00–23, seconds 00–59 and an offset of at most ±23:59), and
`exporter: {name, version}`. App, project, package and animation versions are
independent. Metadata does not change interpretation.

Readers check sizes and safe integer arithmetic before allocating. This
implementation limits clips to 1,000,000 LEDs, 36,000 frames and 64 MiB decoded
RGBA; JSON to 96 MiB. Resource-budget errors are distinct from malformed files.
These are implementation limits, not a promise that the editor renders that size.
JSON Schema checks structure; semantic validation additionally checks unique
identities, canonical Base64, safe products and exact frame sizes.

## Archival package

Ordinary ZIP (`application/zip`, `.zip`) contains `README.txt`,
`animation.nganim.json`, `preview.apng`, `FORMAT.md`, `animation.schema.json`,
`manifest.schema.json`, `LICENSE-MIT.txt`, `LICENSE-CC0.txt`, and `layout.svg`
for SVG matrices. No recipes or executable code. `.ngproj` remains the editable
project format. Both complete examples are in `examples/` alongside
this public reference; `explicit.svg` is exactly 243 UTF-8 bytes including its
final LF and hashes to
`ede530d5b43f5d15bdde4b7b99af5559a9c753971ae98048db88effbba1a4ba4`.

`manifest.json` has `format: "nittygrid-animation-package"`, `version: "1.0"`,
and `files: [{path, mediaType, bytes, sha256}]` listing every other file. Hash
final uncompressed bytes; exclude the manifest itself. Paths are unique, relative,
forward-slash separated, with no empty, dot or parent components. Readers reject
duplicate ZIP entries, unsafe paths, missing/unlisted files and size/digest mismatch.
Readers check declared sizes before allocation and enforce expansion limits on
actual decompressed bytes while reading. SVG's digest must agree with JSON and
manifest; its selected LED identities must match JSON. Checksums are not signatures.
This reader permits 32 files, 128 MiB total expanded data, 2048 pixels per preview
dimension and 64 MiB cumulative decoded preview RGBA.
The app limits packaged SVG source to 8 MiB of UTF-8 bytes. This portable-export
limit does not prevent opening larger projects or using legacy video/JSON exports.

APNG uses full RGBA frames, SOURCE replacement, NONE disposal, sRGB encoding,
delay_num / delay_den = 1 / fps, num_plays = 0 for loops or 1 for one-shot. Rates
not exactly representable in 16-bit delay fields fail rather than drift silently.
The transparent preview is a presentation: browser SVG/canvas rasterization may
composite/quantize colors, unlike authoritative raw RGBA. Grid scale/shape/gap and
SVG raster dimensions are recorded in README. No product/background image is
included. SVG previews use the original document's transforms and definitions,
recoloring existing LED fills/strokes while preserving unpainted surfaces and
stroke geometry.

## Supported SVG import

LED shapes are path, circle, rect, ellipse, polygon, polyline and line outside
defs, clipPath, mask, marker, pattern and symbol. If eligible shapes or their
groups have identity/address metadata, only those tagged shapes are LEDs;
otherwise all eligible shapes are selected in document order before sampling
resolution. Metadata includes data-led-id, data-source-id, data-row/data-grid-row,
data-col/data-column/data-grid-col/data-grid-column, px-ROW-COL, the import alias
led-rNN-cNNN (bottom-up rows), and data-part/data-subpixel/data-tri/data-triangle/
data-half. Sampling aliases are data-x/data-matrix-x/data-address-x and the y
equivalents. Part names a/first/left/upper/top and b/second/right/lower/bottom
mean 0 and 1. Import removes metadata it cannot use instead of rejecting the file:
addresses that are not non-negative integers, other part labels, parts without
a row and col, a lone data-x or data-y, sampling addresses already used by an
earlier LED, and references to missing IDs (url() values become none). Incomplete logical metadata remains optional; sampling must resolve
completely. Groups may supply metadata. Render-tree use instances are unsupported:
expand them to independent shapes before importing.

Import persists unique data-led-id values, preferring established LED identities,
then unique data-source-id, then unique document id, then collision-free led-N.
Document IDs and local references are preserved. Legacy SVG and cache migration
must prove element correspondence and preserve cached order/addresses/metadata;
ambiguous layouts remain editable but portable export requires repair.
Export never rewrites stored SVG. It validates offline safety and ID correspondence
and hashes/copies precisely those stored bytes. Import permits only supported SVG
elements and presentation CSS, local resolved references and embedded raster PNG,
JPEG, GIF or WebP. Scripts, event handlers, network resources, nested SVG images,
CSS imports and unsupported CSS are removed with an import notice or rejected.
The editor's older flattened SVG renderer can drop ancestor transforms/definitions;
the packaged original SVG and raster preview follow standard document rendering.
DrawFrame playback currently turns nonzero stored alpha into 255. Export faithfully
stores the resulting rendered buffer; it cannot restore alpha already lost by a
source generator. This existing playback behavior is outside the new storage codec.

## Offline decoding example (Python 3 standard library)

This teaching example reads every frame as named LED RGBA values without network
access. It omits the full resource/semantic checks, package integrity and exact
SVG byte checks described above.

```python
import base64, json
with open('animation.nganim.json', encoding='utf-8') as f:
    a = json.load(f)
assert a['format'] == 'nittygrid-animation'
assert a['version'].split('.')[0] == '1'
l, t, s = a['layout'], a['timing'], a['frames']
assert t['mode'] == 'fixed' and t['fps'] > 0 and type(t['loop']) is bool
assert (s['pixelFormat'], s['colorSpace'], s['alphaMode'], s['encoding']) == (
    'rgba8', 'srgb', 'straight', 'base64')
if l['type'] == 'grid':
    assert l['order'] == 'row-major'
    ids = [f'r{r}c{c}' for r in range(l['rows']) for c in range(l['cols'])]
else:
    assert l['type'] == 'explicit'
    ids = [p['id'] for p in l['pixels']]
assert ids and len(set(ids)) == len(ids)
for i, text in enumerate(s['data']):
    raw = base64.b64decode(text, validate=True)
    assert base64.b64encode(raw).decode() == text
    assert len(raw) == len(ids) * 4
    colors = {name: list(raw[j*4:j*4+4]) for j, name in enumerate(ids)}
    print(i, i * 1000 / t['fps'], colors)
```

Public reference: /formats/animation/1.0/FORMAT.md (also included offline).
Legacy nittygrid-flat, at3000-kf and row-brightness contracts are unchanged;
AE consumers must keep using those until migrated. No AE importer is maintained
in this repository. Hardware/device adapters and an editor importer are separate.
