functions
Ten stroke layers that differ only in their source layer and colour should be one
thing declared once. The functions block declares a subgraph with typed input
ports and an output kind; op: "func" calls it.
{ "functions": { "sketchy-line": { "inputs": { "features": { "kind": "features" }, "brush": { "kind": "brush" }, "color": { "kind": "scalar" }, "radius": { "kind": "scalar", "default": 1.0 } }, "output": "@draw", "output-kind": "raster", "nodes": { "wob": { "op": "wave", "features": "@features", "amplitude-px": 0.8 }, "draw": { "op": "line", "features": "@wob", "brush": "@brush", "color": "@color", "radius-px": "@radius" } } } }, "nodes": { "roads": { "op": "func", "fn": "sketchy-line", "features": "@roads_f", "brush": "@pencil", "color": "$ink" }, "rail": { "op": "func", "fn": "sketchy-line", "features": "@rail_f", "brush": "@pencil", "color": "#555", "radius": 0.7 } }}Declaration
Section titled “Declaration”| Field | Meaning |
|---|---|
inputs |
map of port name → { "kind": …, "default": … }. Kinds: features, raster, sprite, brush, scalar, scalar-field |
nodes |
the body — same syntax as the document’s nodes |
output |
body node whose value the call produces |
output-kind |
the port kind of that output, checked against the call site |
An input with a default is optional at the call site. A default of null
means “remove the substituted field entirely”, which matters for op fields whose
absence is meaningful.
Calling
Section titled “Calling”{ "op": "func", "fn": "<name>", …arguments }. Each remaining field is an
argument named after an input port. The call node’s id names the result, so
@roads above refers to the function’s output.
Expansion semantics
Section titled “Expansion semantics”Functions expand inline at graph-build time, like hygienic macros.
- Closed over inputs. Inside a body,
@nameresolves to a function input, another body node, or a document-scoped source — nothing else. A typo cannot silently capture a caller’s node. - Structural substitution. Literals stay literals.
$paramreferences keep their runtime-override behaviour.@nodearguments become port connections. Scalar arguments substitute verbatim, so they reach places a plain$paramcannot — gradient stops, stroke-curve arrays. - Nesting allowed. Functions may call functions. Cyclic calls are a build
error, reported with the path (
a → b → a). - Namespaced ids. Body nodes become
<call>/<node>, with the output node taking the call id — so build errors and--verboselogs read naturally (roads/wob). - No duplicated work. The intermediate cache is content-addressed, so two calls with identical arguments share cache entries. Inlining costs nothing at evaluation time.
Functions or params?
Section titled “Functions or params?”They solve different problems and compose well:
- The structure repeats → function.
- A value should change per render → param.
- Usually both: declare params for the knobs, then pass them as function arguments.
pencil-sketch.json
is the live demo — ten wave-then-line stroke layers are one sketchy-line
function, and the water hatching is a water-hatch function that calls it.
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.