Docs menu

Geometry and rendering

Artwork and metadata are generated onchain at call time. Shapes exposes the rendered outputs and the machine-readable geometry behind them, so another contract or an offchain renderer can work from the same data.

Rendered outputs

FunctionReturns
tokenURI(tokenId)data:application/json;base64,…
metadataJSON(tokenId)The decoded JSON body of tokenURI
svg(tokenId)The SVG document, a 0 0 250 350 viewBox, black and white only
unicodeCard(tokenId)The module grid as glyphs, cells separated by spaces, rows by newlines. Display only
contractURI()Collection metadata, data:application/json;base64,…

The image inside the metadata is a data:image/svg+xml;base64,… URI. No IPFS, no external image, no fonts, no server. A Black Shape renders the same geometry inverted.

Rendering reads the token's seed, denomination, ink gene, Black flag, origin count, compose depth, split provenance, owner-token status and stored modules, and passes them to renderer(). Until lockPresentation() the admin can replace the renderer, which changes how every token looks and nothing else. presentationLocked() reports the freeze.

Metadata attributes

Every token's JSON carries these attributes, in this order: ETH Value, Grid, Fill, Ink, Modules, Module Count, Primitive, Variety, Ink Tier, Formation, Independent Origins, Origin Density, Complete, Black, Compose Depth. A split child adds Split From and Split Origin. The owner token adds a value-only "Contract Owner" attribute with no trait_type and its name reads Shape N, Contract Owner. METADATA.md in the repository documents each field. Treat the strings as presentation; the numeric reads on Reading state are the API.

The grid

geometryOf(tokenId) returns (cols, rows, moduleCount). The grid is fixed per denomination:

IndexETHGridModules
00.015 × 525
10.054 × 520
20.14 × 416
30.53 × 412
413 × 39
552 × 36
6102 × 24
7501 × 22
81001 × 11

Modules

A module is one mark in one cell. Its identity is a kind, a solid flag and a rotation; its position, size and stroke weight derive from the grid and the card constants, so every module on a card shares one size and one weight.

KindPrimitiveRotations
0Circle1
1Square1
2Triangle4
3Half circle4
4Quarter circle4
5Diamond1
6Half square4
7Right triangle4
8Arc4, outline only
9Diagonal line2, outline only

ModuleCodec

modulesOf, effectiveModulesOf, ShapeState.modules, ComposeInputView.modules, ShapeChildPreview.modules and the ModulesSampled event all carry one byte per cell in row-major order:

bit 7      always 0
bits 6..5  rotation index, 0..3, meaning rotIndex * 90 degrees clockwise
bit 4      solid
bits 3..0  kind, 0..9

A byte is valid when bit 7 is clear, kind < 10 and rotIndex < rotations(kind).

Two geometry sources

An original mint stores no modules: modulesOf is empty and the grid is derived from the seed at render time (grammar v1). Compose and split store sampled modules for the survivor or each child, drawn from the inputs' effective modules, and decompose restores whatever was stored before. effectiveModulesOf(tokenId) hides the difference and returns the bytes that actually render. SAMPLING_SPEC.md in the repository specifies the sampling.

Per-module reads

function moduleAt(uint256 tokenId, uint256 index) external view returns (
    uint8 kind, bool solid, uint16 rotation, uint256 cx, uint256 cy, uint256 size, uint256 weight);

index runs over effectiveModulesOf order. rotation is in degrees. cx, cy, size and weight are 18-decimal fixed point (1e18 = 1.0) in SVG user units of the 250 × 350 viewBox.

IShapeGeometry on the renderer

The renderer answers IShapeGeometry, a pure interface that computes the same values from raw inputs, so a contract can reason about a card that no token exists for:

function grammarVersion() external pure returns (uint32);
function grammarHash() external pure returns (bytes32);
function cardGeometry(bytes32 seed, uint256 amountWei, uint8 inkGene) external pure returns (
    uint8 denominationIndex, uint256 cols, uint256 rows, uint256 cell, uint256 target,
    uint256 weight, uint256 solidProbability, uint256 moduleCount);
function cardGeometrySampled(bytes calldata modules, uint256 amountWei, uint8 inkGene) external pure returns (…same…);
function moduleAt(bytes32 seed, uint256 amountWei, uint8 inkGene, uint256 index) external pure returns (…as above…);
function moduleAtSampled(bytes calldata modules, uint256 amountWei, uint8 inkGene, uint256 index) external pure returns (…as above…);

Read the address from Shapes.renderer(). IShapeRenderer.renderSVG(seed, amountWei, inverted, inkGene) and renderSVGSampled(modules, …) draw a card from the same inputs. The collection contract at Shapes.collection() offers cardFor(seed, denomIndex) for a seeded preview with the ink gene derived the way a mint derives it.