params
"params": { "paper": { "type": "color", "default": "#fbf6e6" }, "softness": { "type": "number", "default": 0, "min": 0, "max": 4, "description": "Blur over the finished tile, in px." }, "labels": { "type": "bool", "default": true }, "ink-mode": { "type": "string", "default": "multiply", "enum": ["multiply", "darken", "normal"] }}Reference a param with $name anywhere a scalar field lives:
"bg": { "op": "solid", "color": "$paper" },"out": { "op": "blur", "input": "@composite", "sigma": "$softness" }Declaration fields
Section titled “Declaration fields”| Field | Applies to | Meaning |
|---|---|---|
type |
all | color, number, bool, or string — required |
default |
all | used when the caller supplies nothing — required |
min / max |
number |
validated on every override; max also serves as the static bound for padding-determining fields |
enum |
string |
the values the param may take; the default must be one of them, and every override is checked against the list |
description |
all | surfaced in the generated schema and the editor’s params panel |
Colours are #rrggbb or #rrggbbaa. Bools are true / false. A string is
any text unless it declares an enum.
Overriding at render time
Section titled “Overriding at render time”# CLI — repeatable, validated against the declarationsezu tile --style watercolor.json --tile 13/7276/3225 \ --param 'paper=#ffe0f0' --param softness=2 --out tile.png
# Tile server — query-string overrides on the tile endpointcurl 'http://127.0.0.1:8080/tiles/13/7276/3225.png?paper=%23ffe0f0&softness=2'let mut params = ezu::graph::ParamValues::new();params.set("softness".into(), parse_param_value(&doc.params, "softness", "2")?);Every path validates: a number outside min/max, a malformed colour, a
non-boolean, or a string outside its enum fails before rendering. An unset
param falls back to its default.
One graph, every combination
Section titled “One graph, every combination”Params resolve at render time, so nothing rebuilds when a value changes, and the cache keys on the values each node actually reads — flipping one param re-evaluates only its subtree. That is what makes the editor’s params panel responsive on a style that takes hundreds of milliseconds cold.
The generated schema
Section titled “The generated schema”Document::params_schema() derives a JSON Schema of the declarations — types,
defaults, ranges, a string’s enum, descriptions:
curl http://127.0.0.1:8080/style/paramsThe same document is available wherever the style is loaded: the tile server
serves it at /style/params, and in the browser it is the paramsSchema getter
on the wasm Renderer.
Drive your own sliders and colour pickers off that rather than parsing the style.
The live editor’s panel is generated from exactly this: a string with an enum
becomes a menu of its values, and one without a text field.
Padding-determining fields need max
Section titled “Padding-determining fields need max”Canvas padding is fixed at build time, so a field that decides it — blur sigma and
friends — must carry an upper bound the build can see. A literal is its own; a
$param qualifies only if it declares max:
"softness": { "type": "number", "default": 0, "min": 0, "max": 4 } // ✅ usable as a blur sigma"softness": { "type": "number", "default": 0 } // ❌ build errorA @node port carries no bound, so the style states one beside it with a
<field>-max — see
padding and neighbours.
The bound is what the graph budgets padding for, so keep it as tight as the style
actually needs — an unnecessarily large max costs render time on every tile.
Fields with a fixed set of names
Section titled “Fields with a fixed set of names”A field that takes one of a few names — a blend mode, a stroke cap, a noise
type — takes a string param:
"params": { "ink-mode": { "type": "string", "default": "multiply", "enum": ["multiply", "darken", "normal"] }},"nodes": { "inked": { "op": "blend", "base": "@paper", "over": "@ink", "mode": "$ink-mode" }}The build checks the param against the field: its default, and every value its
enum allows, must be names the field accepts, so a param that could be set to
something the field has no meaning for fails there rather than at the render that
sets it. Without an enum any string can arrive, and a name the field does not
know fails that render. A @node cannot feed such a field, since no node produces
a string.
Most of these fields follow the param per render. A few decide what the graph is —
a node’s output kind, its ports, the padding it asks for, the neighbour tiles it
requests — and the graph is fixed before anything renders. Those are read when the
graph is built: a $param there gives its default, the build warns that an
override will not reach the field, and --strict refuses it.
| Read per render | Read when the graph is built |
|---|---|
blend mode, composite; stroke cap, join; buffer join; noise and warp type, anchor; warp and displace boundary; color-ramp interpolation, space; gradient-* anchor, space, interpolation; mix space; quantize and dither space, dither method; tiling anchor; hillshade mode; flow-field direction; flow-smear sides, taper; channel-shuffle r/g/b/a; pick-channel channel; place fit, anchor; stamp anchor; point-grid and point-scatter anchor; tile-bounds cover; graticule axes; boundary clip-edges; feature-boolean mode; label-boxes include; text and text-labels placement, anchor, justify, transform, overlap, polygon-anchor, icon-anchor, icon-text-fit; switch select, naming a branch |
kind of solid, circle, gradient-* and noise; math and field-math fn; expr type; mosaic anchor, mode; features stitch |
A name inside an array — one entry of anchor-offsets, of parts — is a literal.
A label layer whose placement follows a param can place points on one render
and lines on the next. Its point-only fields — the icon, the background box,
rotate-deg, dedupe-key-expr, anchor-expr — apply on the renders that place
points, and a literal placement: "line" still drops them with a warning when the
graph is built. A *-expr field such as justify-expr still takes the place of
its field for the features it names, whatever the param resolves the field to.
Inside a table
Section titled “Inside a table”Some fields are not scalars but tables — a colour ramp’s stops, a palette, a
dash pattern. A $param works in each entry too:
"tint": { "op": "color-ramp", "field": "@dem", "stops": [ { "value": 0, "color": "$lowland" }, { "value": "$treeline", "color": "$upland" } ] }Two differences from a scalar field. A table entry takes a literal or a
$param but not a @node: a port needs a fixed name and its own slot on
the node, and a table of unknown length has neither. And a ramp is sorted by
value on every render rather than at build time, since a param can reorder
it — so stops need not be declared in order.
Composing computed values
Section titled “Composing computed values”Params are inputs, not expressions. To combine them, wire scalars through the
graph — a $param argument keeps its runtime-override behaviour all the way
through:
"z": { "op": "zoom" },"zfrac": { "op": "math", "fn": "div", "a": "@z", "b": 16 },"lu_alpha": { "op": "math", "fn": "mul", "a": "$landuse-alpha", "b": "@zfrac" }watercolor.json
is a complete parametric style.
Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.