Hard bands — palette_steps and palette_contour
Everything above treats the gradient as a smooth ramp: a scene computes a
palette coordinate, the LUT is sampled with linear filtering, and the result is a
continuous blend. palette_steps breaks that ramp into hard graphic bands, and
palette_contour draws a darkened hairline where the picture crosses from one band
to the next (ADR-0078).
This is the difference between a field that reads as a gradient and one that reads as designed — flat areas of colour with drawn edges, the way a printed poster or a topographic map does.
| Param | Default | Range that reads | Accepted |
|---|---|---|---|
palette_steps | 0 (off) | 4–12 | 0 = smooth, integers up to 64 |
palette_contour | 0 (off) | 0–0.5 | 0 = none, up to 1 |
[params]palette_steps = "6" # six flat bandspalette_contour = "0.35" # a drawn edge between themcolor_span = "1.4" # walk enough gradient that the bands differWhat it does to the coordinate
The palette coordinate is quantized, not the baked LUT:
t' = (floor(t * N) + 0.5) / Nimmediately before the sample, landing on each band’s centre — so a band takes the
colour the smooth ramp had in the middle of it, not at its edge. The [palette]
bake is untouched, which is the whole point: the band count has to be bindable to
audio, and re-baking a 256-entry LUT every frame is exactly the work the bake
exists to remove. Bind palette_steps to bar or latch it on a beat and the
picture re-quantizes per frame at no cost.
Two consequences worth having up front:
palette_stepsis stepped. It rounds to a whole number, likekaleido_order. A fractional band count does not step — it leaves every boundary crawling across the field one frame at a time, which reads as shimmer rather than as colour. An eased or boundpalette_stepsstill eases; it snaps at each half-integer.- Off is the exact identity.
palette_steps <= 1andpalette_contour = 0take the unquantized path bit-for-bit, so adding these to an existing preset changes nothing until you actually turn them on.
The ranges, and what is outside them
These were picked off a rendered sweep. Outside them nothing breaks — it stops being the graphic look:
4–12bands is where the field reads as flat areas with real edges.16and up approaches the smooth ramp again. The bands become narrower than the eye separates them at, so the picture converges back on the unbanded gradient. A legitimate destination if you want almost continuous with a hint of structure — not what to bind if you want bands.palette_contourup to about0.5is an edge between two colours.0.8is a deliberate topographic look, not an error: past roughly0.5the dark line stops being an edge and becomes the dominant mark, and the field reads as a contour map with colour fill. Arrive there on purpose, not by easing.
color_span decides whether banding is visible at all
palette_steps quantizes the coordinate, so it can only separate colours the
coordinate actually reaches. A fragment_field at its default color_span of 0.6
walks barely half the gradient, and six bands there land on six neighbouring
shades — technically banded, visually a texture. Widen the window and the same six
bands become six distinct colours. Since the LUT is repeat-addressed, a
color_span above 1 wraps and walks the whole gradient (see the
cyclic-wrap caution — on the four stop-list palettes the wrap
is the sharpest transition there is, which under banding becomes a visible seam
rather than a soft one).
So tune color_span and palette_steps together. If banding “isn’t doing
anything”, the span is the first thing to check.
Where the contour falls — it reads the palette
[!IMPORTANT] The contour draws only where the two bands it separates are actually different colours. It samples the palette at the two band centres either side of the nearest edge, and draws nothing when they come back the same (ADR-0133).
This is what makes the parameter usable on a limited-ink look. A two-ink
print is written as plateaus — runs of bands holding one colour, the only way
to get flat ink out of palette_steps — and a contour drawn at every boundary
inside those runs is a grey hairline across flat colour: exactly the shading
such a look is defined by not having. shape_contourmono’s twenty steps sit in
five runs, so fifteen of its twenty band edges are white-meets-white or
black-meets-black; without the palette read, its only usable setting would be 0.
Nothing on a smooth palette is suppressed. The test is equality, not
similarity: the line is dropped only when the two samples agree to within half
an 8-bit code value, which is below the LUT’s own quantization. Two distinct band
centres on a ramp always differ by at least one code value, so on a ramp every
edge draws, at any palette_steps and any color_span.
Two edges of the rule are worth knowing, because both will read as a surprise:
- A near-plateau is not a plateau. A “flat” run built from two stops that differ by one code value still contours inside the run. Correct by the stated rule, and avoidable: write a plateau as the same colour on both stops.
- A run boundary that does not land on a band boundary still draws at the nearest band edge, not at the stop. The contour lives on the band grid; a custom-stop palette whose hard transition falls mid-band gets its line at whichever band edge is closest. Place a run boundary on a band edge — the way the shipped mono presets write their stops wide of the seam — if you want the line where you drew it.
The scene scoping — banding reaches every scene, contours do not
[!IMPORTANT]
palette_contouris inert onattractor,swarm,emitterand the four line scenes, and nothing warns you. The parameter is accepted there because it is a known name, so no unknown-parameter warning fires. This section is the warning.
A contour needs a gradient across a fragment to sit in — its width comes from
fwidth, which measures how fast a value changes between neighbouring pixels and
exists only in a fragment shader. That is what keeps the hairline a constant
width on screen instead of thick where the field is flat and invisible where it is
steep.
Where the LUT is sampled decides whether that derivative exists at all:
| Scene | Where it samples the LUT | palette_steps | palette_contour |
|---|---|---|---|
fragment_field | per pixel, fragment stage | ✅ | ✅ |
reaction_diffusion | per pixel, fragment stage | ✅ | ✅ |
shape_field | per pixel, fragment stage | ✅ | ✅ |
warp_mesh (the native deposit; a [milk] preset draws its own colours) | per pixel, fragment stage | ✅ | ✅ |
attractor | per particle, vertex stage | ✅ | ❌ inert |
swarm | per particle, on the CPU | ✅ | ❌ inert |
emitter | per particle, on the CPU | ✅ | ❌ inert |
spectrum, parametric_curve, lsystem, star_pattern | per segment, on the CPU | ✅ | ❌ inert |
A point sprite or a stroke segment carries one palette coordinate for its whole extent, so there is no crossing from one band to the next within it to draw a line at. This is a fact about the pipeline, not a policy someone chose, and it is not fixable by turning the parameter up.
So: banding reaches every scene; contours reach the continuous-field scenes. If
you want drawn edges on a particle or line look, the mark’s own geometry is where
they come from — the swarm’s shape, a line scene’s thickness — not from
palette_contour.
palette_contour under shape_field’s two coordinates
shape_field can hand the palette either of two figure coordinates
(ADR-0111): the
normalized distance (coord_mode = "0", the default), whose contours are
offsets of the outline, or r / r_boundary(theta) ("1"), whose contours are
scaled copies of it. Both are 0 at the figure’s centre and 1 on its
outline, and palette_contour works on both.
The hairline keeps its weight across the switch, and that is worth stating
because the arithmetic suggests otherwise. The two fields have genuinely
different gradients — the distance rises at 1/inradius everywhere, while the
radius rises at 1/r_boundary(theta), which varies with direction by the shape’s
own circumradius-to-inradius ratio. What absorbs that is the contour’s own
construction: it is drawn within one pixel of a band edge, because the width
comes from fwidth of the banded coordinate rather than from a fixed value in
coordinate space. That normalization is exactly what a changing gradient runs
into, so the line comes out the same. Measured on a nine-ring heart at
palette_contour = "0.75", as the darkening the parameter adds:
| inner rings | outer rings | |
|---|---|---|
coord_mode = "0" | 27.3 mean over 492 px | 32.6 mean over 2131 px |
coord_mode = "1" | 29.8 mean over 466 px | 31.3 mean over 1929 px |
The two modes differ by less than the inner and outer rings differ within
either one. So do not re-tune palette_contour when you switch modes.
What does change is where the rings are, which is the whole point of the
second coordinate. Under "0" they are evenly spaced in distance, so they hug
the outline’s offsets and the innermost ones round off any reflex corner; under
"1" they are evenly spaced as fractions of the boundary radius, so they fan out
in proportion — further apart toward a tip, closer toward a valley — and every
one of them is the same figure at a smaller size.
Banding fights bloom
The bright pass blurs exactly the hard edges banding creates
(ADR-0046), so a preset cannot have crisp
bands and heavy bloom_amount at full strength. Pick one, or keep the bloom low
enough that it haloes the bright bands without softening their edges.
This is not how you get a cyclic look
The cyclic-hue character of a banded reference image is already reachable without
any of this: the LUT is repeat-addressed, so a color_span above 1 wraps it and
repeats the whole gradient across the field. palette_steps adds only the hard
edge between one cycle’s colours and the next.
Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.