Geometry
Features -> Features transforms. They rewrite geometry before anything is painted, so downstream paint ops stay simple.
22 ops: bbox, boundary, buffer, centroid, contour, convex-hull, dash, densify, dot-density, feature-boolean, hatch, junctions, medial-axis, resample, segment-to, simplify, smooth, transform, triangulate, voronoi, voronoi-fracture, wave.
Ops with no demo image below produce no visible result on their own — they feed other nodes rather than paint.
Axis-aligned bounding box of every input vertex (pooled across polygons, lines, and points), emitted as a single rectangular polygon.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
boundary
Section titled “boundary”Convert each polygon to its boundary rings (exterior + holes) as polylines. Existing polylines and points pass through.

| Field | Type | Required | Description |
|---|---|---|---|
clip-edges |
keep | drop |
no | What to do with the straight edges a vector tile adds where it clips a polygon at the tile buffer, just outside the tile. keep outlines them like any other edge; drop leaves them out, splitting a ring into open polylines where they were. Use drop when a wide stroke on the outline (a coastal band, say) shows a band along every tile seam: the stroke on those edges reaches into the tile. drop removes every segment that lies entirely past one side of the tile and runs within 15° of that side; the tolerance is what lets it work after a buffer, which can bend the clip line slightly. A real shoreline outside the tile running nearly parallel to its edge is dropped too, but nothing inside the tile goes missing, and a rectangle exactly on the tile border is kept. |
features |
@node |
yes | Reference to another node (@name). |
buffer
Section titled “buffer”Minkowski sum / offset with a disk. Polygons accept negative distance for erosion; lines and points are inflated by |distance|.

| Field | Type | Required | Description |
|---|---|---|---|
distance |
number | $param | @node |
yes | Offset distance in tile pixels. Positive inflates, negative erodes (polygons only). |
features |
@node |
yes | Reference to another node (@name). |
join |
miter | round | bevel |
no | — |
centroid
Section titled “centroid”Replace input geometry with one point per polygon / polyline centroid.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
include-lines |
boolean | no | — |
include-polygons |
boolean | no | — |
contour
Section titled “contour”Isolines from a ScalarField (marching squares with linear sub-cell interpolation), chained into polylines. One feature group per level with properties {"level": <number>}, so data-driven paint can style by level. Levels are base + k·interval, or the explicit levels array (which overrides both), clamped by min/max. Chain dem → contour → stroke for elevation contours, or density → contour → stroke for point-density isolines.
| Field | Type | Required | Description |
|---|---|---|---|
base |
number | $param | @node |
no | Offset the interval grid: levels sit at base + k·interval. |
field |
@node |
yes | Reference to another node (@name). |
interval |
number | $param | @node |
no | Spacing between levels, in field units. Required unless levels is given. |
levels |
array of number | string | no | Explicit levels to extract; overrides interval/base. A level may be a $param; the list is sorted and de-duplicated per render. |
max |
number | no | Emit no level above this value. |
min |
number | no | Emit no level below this value. |
convex-hull
Section titled “convex-hull”Convex hull of every input vertex as a single polygon.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
Cut polylines into dash/gap segments. Lengths are in canvas pixels (consistent with *-px brush parameters). Polygons and points pass through unchanged.

| Field | Type | Required | Description |
|---|---|---|---|
dash-px |
number (0…∞) | $param | @node |
yes | — |
features |
@node |
yes | Reference to another node (@name). |
gap-px |
number (0…∞) | $param | @node |
yes | — |
phase-px |
number | $param | @node |
no | Initial offset into the dash/gap pattern, in pixels. May be negative. |
densify
Section titled “densify”Insert intermediate vertices so no segment of any polyline or polygon ring exceeds target-px. Original vertices are preserved.
| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
target-px |
number (0…∞) | yes | — |
dot-density
Section titled “dot-density”Scatter points inside polygons at a per-feature areal density (dot density map).

density-expr instead of stating one constant. style| Field | Type | Required | Description |
|---|---|---|---|
density |
number (0…∞) | $param | @node |
no | Constant density in units per square kilometre, used where density-expr is absent. |
density-expr |
— | no | Per-feature MapLibre expression giving density in units per square kilometre, e.g. [“/”, [“get”, “POP”], [“get”, “AREA_KM2”]]. |
dot-value |
number | $param | @node |
no | Units one dot stands for. 100 means one dot per 100 people. |
features |
@node |
yes | Reference to another node (@name). |
jitter |
number (0…1) | $param | @node |
no | How far a dot may stray from its cell centre, as a fraction of the cell. 0 leaves a visible grid. |
seed |
integer (0…∞) | no | Lattice salt; change it to reshuffle the dots without changing their density. |
spacing-px |
number | $param | @node |
no | Lattice cell size in screen pixels. One dot per cell at most, so this caps how dense the scatter can get. |
feature-boolean
Section titled “feature-boolean”Polygon boolean operation between two Features inputs. Subject = a, clip = b. Lines and points on either input are dropped — booleans are defined on polygons only.
| Field | Type | Required | Description |
|---|---|---|---|
a |
@node |
yes | Reference to another node (@name). |
b |
@node |
yes | Reference to another node (@name). |
mode |
union | intersection | difference | symmetric-difference | xor |
no | — |
Parallel-line hatching of input polygons.

| Field | Type | Required | Description |
|---|---|---|---|
angle-deg |
number | $param | @node |
no | Hatch direction (degrees, CCW from +X). |
features |
@node |
yes | Reference to another node (@name). |
phase |
number | $param | @node |
no | Per-line offset in spacing units (0..1 cycles through one period). |
spacing |
number (0…∞) | $param | @node |
yes | Perpendicular spacing between consecutive lines, in tile pixels. |
junctions
Section titled “junctions”One point wherever polylines end or meet, for marking a line network split into sections (a block perimeter cut into frontages, a route cut into stages) with a cross tick at each section boundary. Both ends of every polyline count; polygons and points are ignored. Each junction is its own feature carrying axis-deg and arms and none of the source properties, since it may join several features. axis-deg is the tick’s axis in degrees clockwise from the x axis, in [0, 180), so stamp with a horizontal bar sprite and "rotation-deg-expr": ["get", "axis-deg"] draws the tick: across the line at a loose end or where the line runs straight on, along the bisector at a corner, and through the middle of the widest gap between arms where the bisector would lie on or near an arm (a T-junction) or the corner is sharper than 40°. arms counts the line ends meeting there (1 for a loose end), for filtering. Loose ends a vector tile makes by cutting a line at its buffer are dropped: an end outside the tile, as far out as any geometry on that side, whose line runs back inward. A junction near a tile border comes out the same, with the same axis, from the tiles on either side. Feed it unstitched features: stitch cuts lines at the tile border, and the cut ends would meet as junctions there.

axis-deg. style| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
snap-px |
number (0…∞) | $param | @node |
no | Line ends closer than this, in tile pixels, merge into one junction (chained: ends each within reach of a third merge too). Adjacent sections’ shared ends do not always coincide exactly: separate digitising, or rounding before the data reached the tile, can leave them a fraction of a pixel apart. 1 px merges those without joining ends a reader would see apart. Raise it for data digitised less carefully. |
medial-axis
Section titled “medial-axis”Approximate medial axis of each input polygon, emitted as polylines. Smaller densify-px → more accurate but slower; min-branch-px prunes short side branches after stitching.

| Field | Type | Required | Description |
|---|---|---|---|
densify-px |
number (0.5…∞) | $param | @node |
no | — |
features |
@node |
yes | Reference to another node (@name). |
min-branch-px |
number (0…∞) | $param | @node |
no | — |
resample
Section titled “resample”Resample each polyline / polygon ring to evenly-spaced vertices at spacing-px along arc length. Original vertices are not preserved.
| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
spacing-px |
number (0…∞) | yes | — |
segment-to
Section titled “segment-to”Turn each point into a two-vertex polyline from a position its feature’s properties give in longitude and latitude to the point itself: a leader from a label anchor to what it labels, a connector from a building’s representative point to its entrance. A polygon gets a line from that position towards its centroid that stops where it first meets the polygon’s outer edge, so a leader to a label box from label-boxes ends at the box instead of running under the text; one line per polygon, and none when the position lies inside or on the polygon (holes count as inside). Each line keeps its feature’s properties; dash it with dash and paint it with stroke. Lines are ignored, as are features whose fields are missing, not numbers (or numeric strings), or out of range, and points that coincide with their other end. A connector can cross a tile from a point outside it, beyond the tile’s thin buffer; set stitch: "pieces" on the upstream features node to bring in the neighbouring tiles’ points too.
| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
gap-px |
number (0…∞) | $param | @node |
no | Pixels taken off the feature end of each line, at the point or the polygon’s edge, to leave air between the line and what it reaches. The other end stays put; a line no longer than the gap is dropped. |
lat-field |
string | yes | Property holding the other end’s latitude, in WGS84 degrees. |
lng-field |
string | yes | Property holding the other end’s longitude, in WGS84 degrees. |
simplify
Section titled “simplify”Douglas-Peucker simplify polylines and polygon rings; points pass through.

| Field | Type | Required | Description |
|---|---|---|---|
epsilon |
number (0…∞) | $param | @node |
yes | Max perpendicular distance a vertex may be from the simplified line, in tile pixels. |
features |
@node |
yes | Reference to another node (@name). |
smooth
Section titled “smooth”Chaikin corner-smoothing over each input polyline and polygon ring. Each iteration roughly doubles the vertex count; 2–3 iterations are usually enough.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
iterations |
integer (0…8) | no | — |
transform
Section titled “transform”Translate / rotate / scale every input vertex. Rotation happens around pivot (defaults to the origin) before the final translation. The scalar fields take a $param or an @node port and follow it per render; the [x, y] arrays are literal, and seed the matching scalars. Uniform scale multiplies the per-axis factors.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
pivot |
array of number | no | Literal [x, y] rotation centre in feature space. |
rotation-deg |
number | $param | @node |
no | — |
scale |
number | $param | @node |
no | — |
scale-x |
number | $param | @node |
no | — |
scale-xy |
array of number | no | Literal per-axis scale. scale-x / scale-y override it; uniform scale multiplies it. |
scale-y |
number | $param | @node |
no | — |
translate |
array of number | no | Literal [x, y] shift. translate-x / translate-y override it. |
translate-x |
number | $param | @node |
no | — |
translate-y |
number | $param | @node |
no | — |
triangulate
Section titled “triangulate”Delaunay triangulation of the input’s point set. Each triangle becomes a closed polygon. Polygons and lines on input are ignored.

centroid first. style| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
voronoi
Section titled “voronoi”Voronoi diagram of the input’s point set. Output is a Features value carrying the Voronoi edges as polylines (each polyline is a single 2-point edge). Polygons and lines in the input are ignored — use centroid to derive points from them.

| Field | Type | Required | Description |
|---|---|---|---|
features |
@node |
yes | Reference to another node (@name). |
voronoi-fracture
Section titled “voronoi-fracture”Fracture each polygon in features into Voronoi sub-cells using the points in seeds as Voronoi sites. Cells are clipped against the source polygon. Each cell is its own feature, carrying the source polygon’s properties plus random: a value in [0, 1) drawn from its seed’s world position, the same in every tile, for per-cell colour through a fill-expr such as ["interpolate", ["linear"], ["get", "random"], 0, "#8c3a24", 1, "#b65a38"]. Lines/points on features and lines/polygons on seeds are ignored.

| Field | Type | Required | Description |
|---|---|---|---|
aspect |
number | $param | @node |
no | Weight on vertical distance when choosing each point’s nearest seed. > 1 stretches cells along X, < 1 along Y — seeds on a staggered point-grid with aspect well above 1 give the rectangles of a brick bond rather than hexagons. |
features |
@node |
yes | Reference to another node (@name). |
seeds |
@node |
yes | Reference to another node (@name). |
Displace polylines laterally with a sine wave. Lengths in canvas pixels. Sharp corners may kink (no tangent smoothing). Polygons/points pass through.

| Field | Type | Required | Description |
|---|---|---|---|
amplitude-px |
number | $param | @node |
yes | Peak lateral deviation in pixels. May be negative to flip phase. |
features |
@node |
yes | Reference to another node (@name). |
noise-amp-px |
number | $param | @node |
no | Peak 1D-value-noise jitter added on top of the sine, in pixels. 0 = pure sine (default). |
noise-scale-px |
number (0.5…∞) | $param | @node |
no | Cell length of the noise jitter along arc length, in pixels. Defaults to wavelength-px. |
phase-px |
number | $param | @node |
no | Offset into the wave at the polyline start, in pixels. |
samples-per-wavelength |
number (2…256) | no | Subdivisions per wavelength. Higher = smoother but more vertices. Default 16. |
seed |
integer (0…∞) | no | Optional explicit u32 seed for the world-anchored 2D value noise. Default: a fixed constant so adjacent tiles agree across the seam. |
wavelength-px |
number (0.5…∞) | $param | @node |
yes | — |
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.