ezu-paint 0.8.0

Paint GIS features onto a hokusai surface for ezu
Documentation

ezu-paint

Rendering primitives + built-in node implementations for the ezu workspace.

This crate sits between the low-level brush engine (hokusai) / 2D rasterizer (tiny-skia) / blur (libblur) and the graph evaluator (ezu-graph).

Three things live here:

  1. Paint primitives — functions that take a Canvas and feature data and produce pixels. Reusable on their own.
  2. nodes moduleNodeFactory implementations for each built-in op, grouped into raster, source, paint, geometry, scalar, util submodules. Each op self-registers via ezu_graph::submit_node!; default_registry() just collects everything via NodeRegistry::from_inventory().
  3. host module — host-side glue: ready-made AssetLoader implementations (BrushBankLoader for document-scoped images / brushes, TileLoader for per-tile feature overlays), and conversions from RasterBuf to PNG / straight RGBA.

How a style paints

An ezu style is a typed node DAG, not an ordered layer list. Every operation below is a node, and ports are statically type-checked across seven kinds — Features, Raster, Sprite, Brush, Scalar, Labels, ScalarField (see ezu-graph for what each carries). Ports list the kinds they accept, so polymorphic ops (e.g. blur over Raster/Sprite) pass the input kind straight through, and intermediate buffers are cached and reusable across tiles.

Tile-pyramid inputs go beyond vectors: dem sources feed elevation as a ScalarField, and raster sources feed RGBA imagery (XYZ / TileJSON / PMTiles; satellite photos, pre-rendered basemaps) as a seam-free padded Raster — so any filter chain can post-process a photo basemap (photo-pop.json posterizes 国土地理院 aerial imagery). External inputs — images, brushes, fonts, per-tile MVT/GeoJSON feature layers — enter through one uniform AssetLoader trait (see Host glue); the schemes a style names them with are documented in the ezu-style README.

Example: a watercolor water layer with a brushed road on top of an earth-tone background, composited bottom-to-top with stack.

{
  "name": "demo",
  "pad": 24,
  "sources": {
    "glazing":  { "type": "brush", "src": "file:brushes/watercolor_glazing.myb" },
    "basemap":  { "type": "mvt", "url": "https://papers.reearth.land/protomaps/tilejson.json" }
  },
  "nodes": {
    "bg":      { "op": "solid", "color": "#fbf6e6" },
    "earth":   { "op": "features", "layer": "earth" },
    "earth_p": { "op": "fill-solid", "features": "@earth", "fill": "#e8d9b0" },
    "water":   { "op": "features", "layer": "water" },
    "water_p": { "op": "fill-dabs", "features": "@water",
                 "color": "#5876a0", "opacity": 0.22,
                 "radius-px": 7, "spacing-px": 3 },
    "roads":   { "op": "features", "layer": "roads",
                 "filter-expr": ["==", ["get", "kind_detail"], "motorway"] },
    "brush":   { "op": "brush-file", "src": "@glazing" },
    "roads_p": { "op": "line", "features": "@roads", "brush": "@brush",
                 "color": "#4a3424", "radius-px": 2.6 },
    "out":     { "op": "stack", "layers": ["@bg", "@earth_p", "@water_p", "@roads_p"] }
  },
  "output": "@out"
}

stack composites its layers with plain source-over; reach for blend when you want a specific blend mode, a composite operator, or a clipping mask:

"paper":  { "op": "solid", "color": "#fbf6e6" },
"shaded": { "op": "blend", "base": "@paper", "over": "@ink", "mode": "multiply" }

The full reference watercolor style is in crates/ezu/examples/styles/watercolor.json.

Paint primitives

Function Op name What it does
paint_polygons fill-solid tiny-skia solid fill + optional outline + libblur gaussian blur
paint_polygons_dabs fill-dabs hokusai scatter-dab fill with world-deterministic position / size / opacity jitter — same world coord → same dab regardless of tile
paint_lines line hokusai::Brush::stroke_to per polyline vertex with world-seeded pressure jitter

For fill-dabs the polygon is rasterized to a binary mask, then a regular grid of candidate positions is iterated; no brush trajectory is constructed, which is what keeps fills seamless across tile boundaries.

Stroke curves on line

line exposes four optional stroke curves that vary brush behavior along each polyline, so strokes can simulate taper-in / taper-out and speed dynamics rather than running at constant pressure and rhythm:

Field Drives y semantics
radius-stroke-curve brush radius_logarithmic (stroke input) log-space offset added to base radius. y = -2.3 ≈ ×0.1, y = +0.69 ≈ ×2
opacity-stroke-curve brush opaque (stroke input) linear offset added to base opaque
hardness-stroke-curve brush hardness (stroke input) linear offset added to base hardness
dtime-stroke-curve per-vertex dtime multiplier on the base dtime. y = 3 slows the hand 3×, y = 0.3 speeds it up

Each curve is a piecewise-linear [[t, y], ...] where t is normalized progress along the polyline (t = 0 at the first vertex, t = 1 at the last). t values must be non-decreasing; at least two points are required. Evaluation matches libmypaint's InputMapping::eval (clamps below the first knot, extrapolates from the last segment).

When any of the brush-side curves (radius / opacity / hardness) is set, paint_lines clones the brush per polyline and auto-sets stroke_duration_logarithmic = ln(line_length_px) so the brush's internal stroke input ramps from 0 → 1 over the full polyline length on the rendered canvas. dtime-stroke-curve doesn't need a clone — it scales the per-vertex dtime directly.

Example: ink-style taper (thin → fat → thin, faster in the middle):

"roads_primary": {
  "op": "line", "features": "@roads_primary_f", "brush": "@glazing_brush",
  "color": "#3a2a18",
  "radius-stroke-curve":  [[0.0, -1.5], [0.15, 0.0], [0.85, 0.0], [1.0, -2.0]],
  "opacity-stroke-curve": [[0.0, -0.3], [0.1,  0.0], [0.9,  0.0], [1.0, -0.4]],
  "dtime-stroke-curve":   [[0.0, 3.0],  [0.15, 1.0], [0.85, 1.0], [1.0, 4.0]]
}

Built-in nodes

ezu_paint::nodes::default_registry() returns a NodeRegistry preloaded with:

Raster utility (nodes::raster)

Op Inputs → Output Notes
solid () → Raster|Sprite Constant-color fill. kind: raster (default) fills the canvas; kind: sprite emits a Sprite at width-px × height-px
circle () → Raster|Sprite Centered disk with optional edge falloff. Sprite mode anchors radius to the shorter sprite side
noise () → Raster|ScalarField Procedural noise: type (white/value/perlin/simplex/worley), scale-px (number for isotropic, [x, y] for anisotropic — wood grain / wave streaks), fBm via octaves/lacunarity/gain, optional domain warp (warp-amp/warp-freq), anchor (world default — seamless across tile borders). kind: raster (default) maps the noise to RGBA via low-color/high-color/opacity; kind: scalar emits the raw fBm value as a ScalarField for downstream map-range / hillshade / color-ramp
blur Raster|Sprite → same kind Gaussian (libblur); pass-through over Raster/Sprite — the output kind mirrors the input. Grows upstream pad by 3σ
displace Raster|Sprite + Raster|Sprite → mirrors main input Photoshop-style displacement map. displacement raster's R/G channels (0.5 = no offset) drive per-pixel offsets up to amp-px. Output kind mirrors the main input. Grows upstream pad by amp-px; boundary (clamp/transparent/mirror) handles edge sampling
warp Raster|Sprite → same kind Domain warp via internal noise (same dial as noise: type, scale-px, octaves, lacunarity, gain, seed) plus amp-px. Pass-through over Raster/Sprite. anchor: world default → seamless across tile borders; grows upstream pad by amp-px
blend Raster|Sprite base + over [+ mask] → mirrors base W3C blend modes (normal/multiply/screen/overlay/darken/lighten/color-dodge/color-burn/hard-light/soft-light/difference/exclusion/hue/saturation/color/luminosity), composite operator (over default / destination-out for brush-eraser), clip (source-atop, PS clipping mask), optional alpha mask, opacity. All three inputs accept Raster or Sprite; output kind mirrors base
stack [Raster|Sprite] → mirrors first Composite an ordered layers list bottom-to-top with plain source-over — the n-ary form of a blend chain, and the usual document output
mix Raster|Sprite ×2 → mirrors first Tween two rasters by a scalar t in a selectable colour space — a straight colour blend, not a composite
brightness-contrast Raster|Sprite → same kind Linear brightness shift + contrast slope around mid-gray; pass-through over Raster/Sprite
levels Raster|Sprite → same kind Photoshop-style levels: remap [in-black, in-white] through gamma onto [out-black, out-white]; generalises brightness-contrast with a midtone curve
erode / dilate Raster|Sprite → same kind Per-channel morphological min / max over a square kernel of radius-px. Classic mask cleanup after color-to-alpha. Grows upstream pad by radius-px
edge-detect Raster|Sprite → same kind Sobel gradient magnitude per channel, scaled by strength and clamped. Grows upstream pad by 1
hsl Raster|Sprite → same kind Hue rotation (degrees) + saturation/lightness shift in [-1, 1]; pass-through over Raster/Sprite
invert Raster|Sprite → same kind Negate RGB (alpha preserved); pass-through over Raster/Sprite
color-to-alpha Raster|Sprite → same kind Chroma-key: pixels near color (Chebyshev distance) become transparent with threshold/softness ramp; pass-through over Raster/Sprite
saturate / vibrance Raster|Sprite → same kind Scale CIELAB chroma preserving hue + lightness — saturate uniformly, vibrance adaptively boosting low-chroma pixels
posterize Raster|Sprite → same kind Quantise each RGB channel into steps evenly-spaced levels (non-premultiplied sRGB). Alpha preserved
quantize Raster|Sprite → same kind Snap every pixel to the nearest entry of a fixed palette, in perceptual CIELAB (default) or RGB — limited-palette / poster / pixel-art looks
dither Raster → Raster Palette reduction with error diffusion (Floyd–Steinberg) or an ordered Bayer matrix — retro / print looks
mosaic Raster → Raster Quantise into uniform block × block squares. mode: "average" (default) blends covered pixels into a mean colour — classic mosaic filter. mode: "nearest" samples each block's centre pixel verbatim, giving hard block edges without inter-colour blending (compose with posterize for indexed-palette / pixel-art looks). anchor: "world" (default) keeps the block grid seamless across tile borders by growing the upstream pad; anchor: "tile" restarts the grid per-tile
place Raster|Sprite → Raster Composite one image at fixed canvas coordinates with fit: none / cover / contain / stretch
tiling Raster|Sprite → Raster Repeat an image across the canvas, world-anchored so the pattern is seamless across tiles
channel-shuffle Raster|Sprite → same kind Rearrange RGBA channels: each output r/g/b/a names which input channel (or constant 0/1) feeds it. Operates in non-premultiplied sRGB
sharpen Raster|Sprite → same kind 4-neighbour Laplacian sharpen with strength amount. Grows upstream pad by 1
gradient-linear () → Raster|Sprite Linear gradient between two points. start/end as [x, y] fractions, stops: [[t, "#hex"], …], optional anchor: "tile" | "world". kind: sprite switches to sprite-local [0, 1] coords at width-px × height-px
gradient-radial () → Raster|Sprite Radial / elliptical gradient. center, radius, optional aspect. Sprite mode same as linear
gradient-conic () → Raster|Sprite Sweep gradient around center starting at start-angle (degrees). Sprite mode same as linear
gradient-diamond () → Raster|Sprite Manhattan-distance gradient. center, radius. Sprite mode same as linear. All four gradients interpolate their stops in a selectable space (rgb default, plus hsl / hsv / hcl / lab; hue-based spaces take the shortest path around the wheel), and take an anchor: "tile" | "world"world keeps the pattern seamless across tile borders
hillshade ScalarField → Raster Horn-method analytical hillshade. azimuth-deg / altitude-deg light angle, z-factor / exaggeration, optional ESRI multidirectional. mode: shade (grayscale) or mode: relief (transparent black for multiply-blend over a base map). Geographically accurate only when the input's geo_scale is populated (DEM source); otherwise produces pixel-space gradients (fine for stylization)
slope ScalarField → Raster Per-pixel slope angle as grayscale, normalised to 0..1 against max-deg; optional invert. Same geo_scale caveat as hillshade
color-ramp ScalarField → Raster Map scalar values to colour via a stops: [{value, color}] table; linear interp, end colours clamp out-of-range. Canonical use is hypsometric tinting over an elevation ScalarField (stops[i].value = metres) but works on any scalar field
map-range ScalarField → ScalarField Linearly remap from [in-min, in-max] to [out-min, out-max] with optional clamp. Normalise a DEM or distance field into [0, 1] before color-ramp
density Features → ScalarField Kernel-density estimate over point features — the MapLibre heatmap kernel. Pair with color-ramp for a heatmap
threshold ScalarField → ScalarField Binarise against value: emit low for samples ≤ value, high otherwise; softness gives a linear ramp instead of a hard step

Sources (nodes::source)

Op Inputs → Output Notes
features () → Features Samples a host-bound vector tile layer. source (optional, matches a mvt/pmtiles entry in the document's sources block; defaults to the single such entry) + layer (the MVT layer name). Looked up as <source>.<layer> on the AssetLoader
dem () → ScalarField Samples a host-bound DEM mosaic. source (optional, matches a dem entry in sources; defaults to the single such entry) — looked up by bare source name. The host fetches + decodes raster-DEM tiles (terrarium / mapbox-rgb) and binds the stitched scalar field (with geo_scale populated) per render
raster () → Raster Samples a host-bound RGBA imagery mosaic (satellite photos, pre-rendered basemaps). source (optional, matches a raster entry in sources; defaults to the single such entry) — looked up by bare source name. The host fetches PNG/WebP/JPEG tiles (XYZ / TileJSON / PMTiles), stitches the 3×3 neighbourhood onto the padded canvas, and binds it per render; unbound tiles (on-missing: empty) emit transparent pixels
image () → Sprite Load a PNG / WebP asset from the document's sources block at its native dimensions
icon () → Sprite Crop one named icon out of a sprite atlas — the feed for stamp (symbol icons), tiling (fill-pattern) and line-stamp (line-pattern)
literal-geometry () → Features Inline points / lines / polygons from style fields
tile-bounds () → Features Polygon covering the current tile
point-grid () → Features Regular grid of points across the tile
point-scatter () → Features Random points at a given mean spacing across the tile — a variable count per cell, so no lattice frequency survives

Feature paint (nodes::paint)

Op Inputs → Output Notes
fill-solid Features → Raster wraps paint_polygons
fill-dabs Features → Raster wraps paint_polygons_dabs
line Features + Brush → Raster wraps paint_lines
stroke Features → Raster Crisp constant-width tiny-skia vector stroke with cap / join, optional dasharray, and a gap-width that renders MapLibre's line-gap-width casing annulus — clean cartographic lines rather than brushwork
line-stamp Features + Raster|Sprite → Raster Repeat a sprite along each polyline, tangent-rotated and fit to the line width — MapLibre line-pattern
circles Features → Raster Crisp filled disks at feature points with per-feature radius / colour / stroke — the vector counterpart to MapLibre's circle
stamp Features + Raster|Sprite → Raster Paint a sprite at every feature point, with world-deterministic jitter
text Features → Raster SDF glyph labels with self-contained collision — see Text labels
text-labels / label-placement / text-draw Features → Labels → Raster The shared-placement trio: every label layer's candidates collide in one index — see Text labels
brush-file () → Brush Load a MyPaint .myb brush, resolved by the host's AssetLoader
brush-solid () → Brush Synthesize a crisp constant-width brush without a .myb file

Geometry ops (nodes::geometry) — turf.js-flavored transforms, mostly Features → Features

Op Inputs → Output Notes
centroid Features → Features Polygon / line centroids as points
boundary Features → Features Polygon rings as lines
simplify Features → Features Douglas–Peucker
convex-hull Features → Features Convex hull over all input vertices
buffer Features → Features Offset / Minkowski-style buffer
hatch Features → Features Hatch-line fill of polygons
voronoi Features → Features Voronoi diagram of input points → edge polylines (2-point each). Polygons/lines ignored — pipe centroid upstream to derive seeds
voronoi-fracture (Features, Features) → Features Fracture each polygon in features into Voronoi sub-cells seeded by seeds' points; cells clipped to the source polygon
medial-axis Features → Features Approximate medial axis (skeleton) of each input polygon as polylines. densify-px controls boundary sampling, min-branch-px prunes short branches. Useful for river / lake centrelines
bbox Features → Features Axis-aligned bounding box of every input vertex as a single rectangular polygon
transform Features → Features Translate / rotate / scale every vertex. Rotation around an optional pivot
smooth Features → Features Chaikin corner-smoothing on polylines and polygon rings; iterations controls passes
densify Features → Features Insert intermediate vertices so no segment exceeds target-px. Originals preserved
resample Features → Features Evenly-spaced vertices at spacing-px along arc length on each polyline / ring
feature-boolean (Features, Features) → Features Polygon set ops: mode: union/intersection/difference/symmetric-difference. Lines / points on either input are dropped
triangulate Features → Features Delaunay triangulation of input points → triangles as polygons
contour ScalarField → Features Isolines from a scalar field via marching squares — contour lines over a DEM, edges of a noise field
dash Features → Features Cut polylines into dash / gap segments
wave Features → Features Lateral sine displacement of polylines, for hand-drawn wobble

Scalars (nodes::scalar) — computed values for any scalar field

Op Inputs → Output Notes
zoom () → Scalar The tile's zoom level, for zoom-dependent styling
math Scalar… → Scalar Arithmetic over literals, $params, and @node scalar ports
expr () → Scalar Evaluate a MapLibre expression once per tile (the tile's zoom in context) and emit the result as a Scalar

Utility (nodes::util)

Op Inputs → Output Notes
switch (any, any) → mirrors selected Build-time pick between a and b via select ("a" / "b", or bool / 0/1). Both inputs accept any port kind; output mirrors the selected input's kind. Use for A/B variants and param-driven branching
pick-channel Raster → ScalarField Extract one of r/g/b/a/luminance as a [0, 1] ScalarField (non-premultiplied RGB; Rec. 601 luma). Bridges the raster pipeline into map-range / threshold / color-ramp

Each factory implements NodeFactory::schema() so editors picking up the registry-derived JSON Schema get per-op autocomplete. Adding a new op means dropping a file under the right category and ending it with ezu_graph::submit_node!(MyFactory); — no central list to edit. Downstream crates can register custom ops on top of default_registry().

Text labels

The text node renders labels the way MapLibre's symbol layer does, from vector features:

  • Shapingrustybuzz (a pure-Rust HarfBuzz port) shapes each label; placement: point labels each feature point, placement: line / line-center walks each polyline with tangent-rotated glyphs. Layout knobs mirror MapLibre — justify, anchor / anchor-variants (variable anchor), offset-em, max-width-em (wrapping), letter-spacing-em, spacing-px, max-angle-deg, keep-upright.
  • Two glyph backends — the font fallback stack names font and/or glyphs sources:
    • a font source supplies outline font bytes (TTF / OTF / TTC), which ezu shapes and rasterises into an SDF itself. Its url is a font file (file:, http(s)://, data:) or an installed-font reference (system:, below).
    • a glyphs source is a MapLibre glyph-PBF endpoint — a {fontstack} / {range} URL template serving pre-rendered 24 px SDF glyphs in 256-codepoint ranges, fetched lazily per range. This is the exact glyph data maplibre-gl-js itself draws, so a translated style can label with zero font files.
  • SDF drawing — glyphs are composited from signed-distance fields, so the size, color, and halo (halo-color, halo-width) are all cheap runtime parameters. Every paint property has an optional *-expr sibling (color-expr, size-expr, halo-width-expr, …) evaluated per feature.
  • Deterministic collision — collision is on by default and is deterministic across tile boundaries: candidates come from this tile plus its 8 neighbours (host-bound under <source>.<layer>@dx,dy), deduped and placed greedily by symbol-sort-key, so a label straddling a tile edge is placed or dropped identically in both tiles. Set the node's source / layer (the upstream feature source) to enable neighbour gathering; without them collision is centre-tile-only. allow-overlap / ignore-placement / padding-px mirror MapLibre.
  • Shared cross-layer placement & icons — label layers can split into text-labels (candidates) feeding one label-placement node, with a text-draw per layer painting its winners: every layer's labels then collide in one index, placed top layer first with ties broken by tile feature order, as MapLibre does. A point symbol's icon places with its text as one unit — icon-size/-anchor/-offset/ -padding, overlap flags, text-optional / icon-optional, and icon-text-fit with nine-slice sprite stretching are all honoured.

ezu translate emits exactly this shape from a MapLibre symbol layer; see the ezu-translate README for the property mapping and the known divergences.

The system: font scheme

A font source can resolve a face from the machine's installed fonts by family name instead of shipping bytes:

"sans": { "type": "font",
          "url": "system:Arial Unicode MS?weight=700&style=italic" }

The family may contain literal spaces or be percent-encoded; weight (100–900, default 400) and style (normal / italic / oblique) are optional query params. A system: reference makes the recipe machine-dependent — the same family resolves to whatever face that machine has installed, so glyph shapes and character coverage can differ across environments, and it is unavailable in the browser/wasm host (supply font bytes there). Reference a font file for a fully portable, reproducible recipe.

{
  "name": "labels",
  "sources": {
    "basemap": { "type": "mvt", "url": "https://papers.reearth.land/protomaps/tilejson.json" },
    "sans":    { "type": "font", "url": "system:Helvetica" }
  },
  "nodes": {
    "bg":        { "op": "solid", "color": "#f7f4ee" },
    "places":    { "op": "features", "source": "basemap", "layer": "places" },
    "place_lbl": { "op": "text", "features": "@places",
                   "source": "basemap", "layer": "places",
                   "font": ["sans"],
                   "text": ["get", "name"],
                   "size": 14,
                   "color": "#333333",
                   "halo-color": "#ffffff", "halo-width": 1.2,
                   "color-expr": ["match", ["get", "kind"], "city", "#111111", "#555555"] },
    "out":       { "op": "stack", "layers": ["@bg", "@place_lbl"] }
  },
  "output": "@out"
}

Brushes

Nothing is bundled into the library — a style references every brush it uses through a src in its sources block, and the host loads it from disk, HTTP, or an inline data: payload. Any MyPaint .myb brush works; brush-solid synthesizes a crisp constant-width one when no file is wanted.

The example styles ship their brushes alongside the style JSON in crates/ezu/examples/styles/brushes/ and reference them by relative file: path (resolved against the style file's directory); those are CC0 brushes by David Revoy from mypaint/mypaint-brushes (attribution in brushes/CREDITS.md).

Canvas

pub struct Canvas { /**/ }
impl Canvas {
    pub fn new_padded(tile_w: u32, tile_h: u32, pad: u32) -> Self;
    pub fn pixmap(&self) -> &tiny_skia::Pixmap;
    pub fn pixmap_mut(&mut self) -> &mut tiny_skia::Pixmap;
    pub fn into_pixmap(self) -> tiny_skia::Pixmap;          // zero-copy handoff
    // accessors for width / height / tile_width / tile_height / pad
}

The canvas paints into a padded buffer (tile + 2 * pad) so blurs extend cleanly through the tile edge and MVT buffer geometry that overflows [0, extent] lands inside the buffer. Internal node impls construct a Canvas, paint into it, then into_pixmap().take() to hand the pixel Vec<u8> to the graph layer without a memcpy.

Host glue

use ezu_paint::host::{BrushBankLoader, TileLoader, raster_to_png, raster_to_webp, raster_to_rgba8};

let mut assets = BrushBankLoader::new().with_dir("assets/brushes".into());
assets.insert("watercolor_glazing", hokusai::myb::from_str(&myb_json)?);

// Per render, overlay tile-scoped feature layers on top of the base
// loader. `bind_mvt` registers every layer under
// `<source>.<layer-name>` so the style's `features` nodes
// (`source: "basemap", layer: "earth"`) resolve to it.
let mut tile_loader = TileLoader::new(&assets, tile_id);
tile_loader.bind_mvt("basemap", ezu_features::mvt::decode(&bytes)?);

let ev = Evaluator::new(&graph, &cache, &tile_loader);
let raster = ev.render(tile_id, canvas, &params, seed)?;
let png  = raster_to_png(&raster, tile_size, pad)?;       // cropped + PNG
let webp = raster_to_webp(&raster, tile_size, pad)?;      // cropped + lossless WebP
let rgba = raster_to_rgba8(&raster, tile_size, pad);      // cropped, straight RGBA
// `crop_to_png` / `crop_to_webp` / `crop_to_rgba8` take a width and a
// height, for a canvas that is not a square tile (a legend swatch).

BrushBankLoader implements AssetLoader for document-scoped images and brushes (in-memory + disk fallback). TileLoader is a per-render overlay that adds tile-scoped feature bindings on top of any base loader. Both compose freely with custom AssetLoader impls.

Tile binding convention

Anything the style's features / dem node references is expected to be bound by the host once per tile. TileLoader::bind_mvt("<source>", decoded) walks every layer in a decoded MVT and registers each one under <source>.<layer-name>, matching the features node's source + layer fields. DEM bindings use the bare source name to match the dem node's source field. Custom bindings (GeoJSON, in- memory synthesized data, …) go through bind_features(<key>, layer) where <key> is whatever string the style references.

Names that look like asset srcs — those with a scheme: prefix (builtin:, file:, http(s)://) — bypass the per-tile bindings and flow through to the base loader, which is where document-scoped image / brush assets live. Unbound names without a scheme surface as NotFound; the features op treats that as an empty layer so sparse / partial-layer tiles render cleanly.

raster_to_png / raster_to_webp / raster_to_rgba8 all crop the padded buffer down to the central tile region before encoding / demultiplying. WebP uses the pure-Rust image-webp codec (lossless only) — no native deps. A pixmap_to_webp(&tiny_skia::Pixmap) helper covers non-tile-sized outputs (e.g. CLI bbox mosaics).

DEM sources (feature http)

host::dem ports the same sources-driven pattern to raster-DEM tiles. build_dem_sources(doc) walks the style's sources block, building one fetcher (terrarium or mapbox-rgb, PNG or WebP) per declared source; bind_dem_sources(&mut tile_loader, &registry, tile, canvas) fetches the 3×3 neighbourhood (date-line-wrapping in X, edge-clamping in Y), bilinear-resamples it onto the padded canvas, and binds the resulting ScalarField under the bare source name so the style's dem node (source: "<name>") picks it up. Requests beyond the source's max-zoom upsample from the appropriate ancestor tile. Decoded tiles are cached unboundedly per source — well-suited to single-tile and modest-pyramid renders; swap in an LRU bound if working sets ever outgrow memory.

Features

  • parallel — pull-through to ezu-graph/parallel (Rayon within-tile evaluation). No effect on the paint primitives themselves; the hot loops inside hokusai are still single-threaded.
  • http — enable host::prefetch_doc_assets (walks a parsed Document's assets block, fetches every http(s):// src with reqwest, and stages the decoded brush / image into a BrushBankLoader) and the host::dem module (raster-DEM tile fetcher + 3×3 stitch + overzoom upsampling that feeds the ScalarField port). Off by default so wasm32 keeps its dep graph minimal (the JS host fetches assets directly there).

License

MIT or Apache-2.0, at your option.