Vitrine

No.004 Sfumato

Open the panel

Sfumato is a webgl specimen in this vitrine: two soft lights meeting in the dark, on a sheet the pointer drags aside. It exposes 36 parameters, runs continuously, and was added on 3 September 2026.

Anatomy

KindWebGL
Lifecycleambient
Motioncontinuous:drift+pointer
Stage rolebackground
Themeslight · dark
Parameters36
Added2026-09-03

Every parameter, and why it is set where it is

Each of the 36 parameters below is a real control in the panel. The reasoning is first-hand: where a bound comes from a measured property of the work, that measurement is named.

Beam-a

A inside aInside

colour, default #3a90e0

The colour at the beam's core. Inside and outside are mixed by the field's own distance term, so the pair describes how the light falls off across the beam rather than just what colour it is — set them apart and the beam gains a rim, set them equal and it is flat.

A outside aOutside

colour, default #479ce6

The colour at the beam's edge. The mix is alpha-weighted and linear, not OKLAB — that is the reference's setting for beams, and it matters because the two values here are close: in linear the transition stays neutral, while a perceptual space would bend it.

A start thickness aStartThickness

0–2, default 0.33

Half-width at the start of the segment, scaled by 0.25 in the field. Start and end differ on purpose: that difference is what makes the beam conical rather than a bar, and it is the cheapest way to say "the light comes from over there" without drawing a source.

A end thickness aEndThickness

0–2, default 0.6

Half-width at the far end. Larger than the start here, so the beam opens as it travels. Equal values give a uniform bar; the ratio, not either number alone, is what carries the shape.

A start softness aStartSoftness

0–50, default 27.22

Softness interpolates along the segment too, and that is the half people forget. Interpolating only thickness gives an evenly blurred bar; interpolating both is what makes one end read as near and the other as far. It enters twice — once widening the falloff, once as an exponent on it — so the range is wide and the top end is haze.

A end softness aEndSoftness

0–20, default 14.26

Softness at the far end, lower than the start here — so this beam is vaguest where it begins and firms up as it goes, which is the opposite of beam B and is why the two do not read as a mirrored pair.

Beam-b

B inside bInside

colour, default #e872bb

The warm source. One hue thinning out is a fade; two hues meeting is a room — the band where this one overlaps the cool beam is the only place in the picture that says the light comes from somewhere.

B outside bOutside

colour, default #e872bb

Equal to the inside colour by default, unlike beam A. That is not an oversight in the reference: this beam carries its colour flat and lets its very low end softness do the shaping instead.

B start thickness bStartThickness

0–2, default 0.21

Thinner at the start than beam A and wider at the end, so the two beams open in opposite proportions and their overlap is a band rather than a second, brighter beam.

B end thickness bEndThickness

0–2, default 0.7

The widest of the four thickness values, which is why the warm light occupies more of the lower half than the cool one does of the upper.

B start softness bStartSoftness

0–50, default 27.67

Nearly the same as beam A's start softness — both beams begin as haze. What separates them is where they end.

B end softness bEndSoftness

0–20, default 2.97

The lowest softness anywhere in the picture, and the one value that keeps this from being pure haze. At roughly 3 the warm beam still has no edge you could trace, but it has a definite core — and that core is what the eye settles on. Raise it and the picture loses its only firm thing.

Presentation

Veil veil

0–1, default 0.5

Not yet derived — tuned by eye. See the list below.

Edge fade edgeFade

0–0.6, default 0.1

Not yet derived — tuned by eye. See the list below.

Render scale renderScale

1–8, default 1

Default 1: the scene is computed at one buffer pixel per display pixel. Raising it computes the ground, beams and swirl into a smaller buffer and enlarges them, which costs proportionally less.

⚠️ It defaults to 1 because 2 was measurably wrong, not because it is cheap. An earlier version shipped 2 on the reasoning that none of those layers has an edge, so nothing is lost by computing them coarsely. That reasoning is sound about the *scene* and misses what sits on top of it: the scatter offsets its sample by a random amount each pixel, and on an enlarged texture those offsets land between texels. As the swirl drifts, the interpolation underneath them changes — and the soft edges crawl. The layer that suffers from a coarse buffer was not the one being computed into it.

Drift

Detail swirlDetail

0.3–4, default 1.5

How fine the swirl is. The field is three layers of recursive domain warping, not fbm — each layer warps coordinates the previous layer already warped, so the disturbance follows the shape underneath it instead of being added on top of it. That is why the result reads as filaments rather than as clouds. The ceiling is set by the buffer: the scene is computed at a fraction of the display, so past roughly 4 the third layer is finer than a buffer texel and arrives as noise.

Drift swirlSpeed

0–2, default 0.4

Not yet derived — tuned by eye. See the list below.

Swirl opacity swirlOpacity

0–1, default 0.86

How much of the swirl reaches the picture. It is the only layer here composited in OKLAB rather than plain over, and that is worth a look: OKLAB is perceptually uniform, so a blend between two colours of similar lightness does not sag through a darker middle on the way. In sRGB the same blend between the blue and the magenta passes through a muddy grey-purple. At 0 the layer is gone and the two beams are bare, which is the honest way to see what it contributes.

Blend bias swirlBlend

0–100, default 67

Shifts which half of the swirl pattern becomes the visible colour. It enters as (blend - 50) * 0.006 added to the normalised pattern before a smoothstep, so 50 is neutral and the ends push the threshold past where the pattern reaches — at which point the layer is either entirely one colour or entirely the other, and stops being a pattern.

Swirl colour swirlColor

colour, default #e7cfc5 (dark) / #ffffff (light)

The colour the swirl carries where its pattern is strong; where it is weak the layer is transparent and the beams show through unchanged. It is warm and nearly grey on purpose — the two beams already supply the hue, and a third saturated colour here would compete with the band where they meet, which is the one place the picture says the light comes from somewhere.

This is the one colour that differs between the two themes, and the reason is not symmetry. On a dark page the swirl reads as warm dust lit from the side, so a near-grey warm tone is right. On a light page the whole work is composited onto white at half opacity, and that same warm grey turns muddy — it is darker than the paper it sits on, so instead of catching light it reads as a smudge. Pure white keeps it additive against the page: it can only brighten, never dirty. Every other colour here is the same in both themes; this one is not, and that asymmetry is the point rather than an oversight.

Pointer

Parting parting

0–40, default 7.7

The pointer displaces the picture, it does not draw on it. The frame is treated as a sheet of cloth pinned on a 64×64 lattice of springs; the cursor pushes the nearest part of the sheet along its own direction of travel, and the springs pull it back. This number is how far that displacement is allowed to reach the sampled image. At 0 the pointer does nothing at all, which is the comparison this piece is for: a field that merely drifts, against one that can be touched.

Reach reach

0.25–8, default 2

The ceiling here is not taste, it is where the effect disappears. The force falls off as a gaussian in normalised frame units, and the value is scaled by 0.08 before use — so the default 2 is a radius of 0.16 of the frame. Push it far past that and the falloff is close to 1 everywhere: the force stops being local and becomes a uniform translation of the whole sheet, which the springs’ restoring term then cancels. Measured: at an effective radius of 2 the picture moved by an average of 0.0 pixels under a pointer sweep — identical to the control with the pointer switched off entirely. A parameter whose top end silently turns the feature off is worth bounding.

Stiffness stiffness

0.2–20, default 3

This one has a real ceiling, and it comes from the integrator. The lattice is stepped with explicit Euler, which is only stable while the step is short relative to the stiffness; past that bound the displacement grows every step instead of settling, and the sheet tears itself apart. The frame is split into two substeps precisely to buy headroom here — halving the step doubles the stiffness that stays stable. So the top of this range is not where it stops looking good, it is where the arithmetic stops converging.

Damping damping

0.2–20, default 3

How fast the sheet stops moving. It is applied as a per-substep multiplier clamped into 0..1, so this is a rate and not a time — it means the same thing at any frame rate, which matters because the loop runs at thirty frames a second while the display may offer a hundred and twenty. Near the floor the sheet keeps ringing after the cursor has gone, which reads as jelly rather than cloth; near the ceiling the multiplier hits zero within a single substep and the motion is dead on arrival.

Grain

Grain grain

0–2, default 0.5

The amount is set by the output, not by the eye. Eight bits give 256 steps, so a gradient across a wide frame changes value only every several pixels and those steps read as bands. Noise of about one step scatters the boundary and the eye integrates it back. Below that the banding shows through; far above it the picture turns to sand. It is also static — the seed uses pixel coordinates and no time — because grain that flickers is noise, and grain that holds still is film.

Shadow bias grainBias

0–6, default 2

The grain is weighted by the inverse of luminance, and this is the exponent on that weighting: heavy in shadow, clean in highlight. Real film behaves this way — silver grain is most visible where the emulsion is thin. At 0 the weighting is gone and the grain sits evenly over everything, which immediately reads as an overlay laid on the picture rather than as something the picture is made of.

Texture

Scatter scatter

0–100, default 60

This is what makes a drag read as sand rather than as a smear. The layer is called DiffuseBlur in the reference and it is not a blur: it takes exactly one sample, at a UV pushed a random distance aside. At 0 the picture is the beams and the swirl with nothing on top and the drag looks like wet paint; raising it breaks the sampled position per pixel, and the same displacement now reads as grains moving past each other. It is the texture of the motion, not of the image — which is why it only becomes obvious once the pointer moves.

⚠️ An earlier version of this file hard-coded it at 60 and shipped no control at all. The panel then claimed, by omission, that this was not adjustable — and it is the first thing the author went looking for.

Ground ground

colour, default #0F0217

The work ships its own ground rather than borrowing the page's. Everything above it is composited over this colour, so it is not a backdrop but the darkest value in the picture — and the grain is weighted toward the shadows, which means this colour decides where most of the grain lands.

Chroma

Dispersion Base chromaBase

colour with alpha, default #3a90e0

The colour where the field is disturbed but has no clear direction. It is the floor of the effect: whatever is here shows up everywhere the pointer has been, regardless of which way it went.

Dispersion Upward chromaUp

colour with alpha, default #e7cfc5

The tint where the flow runs upward. The four directions are what make this dispersion rather than chromatic aberration — the usual trick offsets the three channels by a fixed vector, while this picks a colour by which way the sheet was pushed, so the trail remembers the gesture.

Dispersion Downward chromaDown

colour with alpha, default #e7cfc5

The downward tint. It is not in the reference panel even though the value exists in its data — which is exactly the kind of hidden constant this cabinet refuses, so it is exposed here.

Dispersion Leftward chromaLeft

colour with alpha, default #e872bb61

The leftward tint. Left and right are weighted by a smoothstep on the normalised flow, so a diagonal drag mixes two of the four rather than snapping to one.

Dispersion Rightward chromaRight

colour with alpha, default #3a90e066

The rightward tint. Setting only this one and dragging both ways is the quickest way to see that the layer is directional at all.

Dispersion force chromaIntensity

0–5, default 1

How hard the pointer injects into the dispersion field. The field is velocity advected through itself, so this is a rate of injection, not an amount — hold still and it decays to nothing on its own, which is why the layer costs nothing when nobody is moving.

Dispersion reach chromaReach

0–12, default 3

The radius of that injection, scaled by 0.05 and by the square of the pointer speed — so a slow drag disturbs almost nothing and a fast one opens a wide wake. The speed term is what makes it read as a gesture rather than as a brush: the same path drawn slowly and quickly gives two different pictures.

Momentum chromaMomentum

0–120, default 30

How far the field carries itself each step. At 0 the colour sits where it was put and simply fades; raising it makes the disturbance travel along its own velocity, so the wake keeps moving after the cursor has gone. That self-advection is the difference between a stain and a current.

What is not derived

3 parameters were tuned by eye. They are listed rather than dressed up: an invented reason would be worse than an admitted gap.

Tuned by eye: veil · edgeFade · swirlSpeed

Where it comes from

Wonder — changelog hero background software · confirmed

The defaults of this piece come from that page, and that has to be said first. The colours, the beam endpoints, the softness, the veil at 0.5, the edge fade at 10%, the drift at 0.4 — none of them were derived from anything about this work; they were read off the reference. Every parameter of that kind carries why: null in params.js, with a note saying so plainly. What was carried across is the read and the layer structure: a dark ground, two soft lights meeting, a swirl masked by those lights, static grain weighted toward the shadows, and a spring lattice the pointer pushes. What was not carried across is the implementation — that page runs WebGPU, this is a WebGL2 rewrite, and the reason is at the top of sfumato.js: navigator.gpu does not exist in headless browsers, and every still frame in this cabinet has to come from one. ⚠️ Every default here is quoted from a line in that page. An earlier version of this note claimed the seventh layer had no effect on any pixel and had been dropped — that was wrong, and wrong in the way worth recording: its five directional colours had been filled in as fully transparent, a value the reference does not contain, and the measurement that "proved" the layer inert was measuring that invention. The layer is the coloured wake a swipe leaves behind, and it is the most visible thing the pointer does.

Sfumato technique · claimed

Leonardo's name for his own way of painting. The root, sfumare, is to evaporate like smoke, and the definition is the absence of an outline: colours passing into one another with no edge. The name is not decoration here — the feather parameter does exactly that, and dragging it to the floor brings the outlines back and collapses the picture into two coloured stripes. A definition that one slider can falsify is a definition worth putting on the title. The primary material — Leonardo's notebooks, the scientific analysis of his paint layers — has not been checked line by line, which is why this reads claimed rather than confirmed.

Domain warping, recursively technique · claimed

Sampling noise at coordinates another noise field has displaced. The three layers here are recursive: the second reads the first one's output and the third reads the second's, so the disturbance follows the shape underneath it. fbm adds frequencies instead, and its octaves never see each other — which is the difference between filaments and fluff. No.002 in this cabinet warps its domain too, but it warps toward a form, while this warps toward a field with no form at all. Popularised in real-time graphics by Inigo Quilez; the primary sources have not been checked one by one.

Grain as dither technique · claimed

An eight-bit channel has 256 steps, so a gradient across a wide frame changes value only every several pixels and the eye reads those steps as bands. Noise of about one step scatters the boundary and the eye integrates it back. The practice long predates the web — it is how printing and early graphics dealt with too few levels — so the attribution is to the practice rather than to a source.

Take it with you

Open the panel to change any parameter above and watch it apply immediately. From there the work leaves as a link, as source, as a PNG, or as a video.

Open No.004 in the panel