CabinFever
In developmentPaper marbling in the browser, built on the one fact that makes marbling work: ink doesn't cover ink, it shoves it out of the way.
At a glance
- Outcome: A dependency-free browser library that reproduces the traditional marbling canon — battal, gel-git, nonpareil, feather, şal, hatip, suminagashi, Italian hair vein and eight more — by treating every tool as an area-preserving map of the plane rather than as something drawn.
- Status: In development at 0.1, and this is the second engine. The first one, and the twelve prototypes it came from, were thrown away.
- Role: Solo.
- Stack & libraries: Plain ESM, zero dependencies, no build step, one canvas.
The texture pass is a fragment shader in all but name, run on the CPU over an
ImageData, so there’s no WebGL and it works headless. Tooling is Node driving headless Firefox over WebDriver BiDi. - Source: Public, MIT-licensed: github.com/Griffinhale/cabinFever.
- Validation: A
node --testsuite over the engine, including the property the whole interaction rests on: injecting radiusr₁thenr₂lands every point exactly where a single injection of√(r₁²+r₂²)would, asserted to 1e-6. Every figure on this page is generated rather than captured, and reproduces from a seed because the engine takes an explicit timestep and never reads a clock. - Limitations: Not a fluid solver and doesn’t want to be — no velocity field, no
timestepped physics. Colour never mixes, because pigment displaces rather than
blends. Paper is never laid down, so Spanish ripple shears the floating film
instead of lifting a sheet; the banding matches, the mechanism doesn’t. A finite
poured line has no closed-form map, so it walks the path laying discs above an
enforced spacing floor. The stylus is the one tool that isn’t exactly
area-preserving. And
italianVeincosts about 2.4 seconds against 380ms for a feather, for a reason that isn’t going away.
Ink displaces ink
Marbling floats pigment on a thickened size, and each colour is cut with ox gall. The gall is the whole mechanism: a new drop spreads, and because the floating film can’t compress, spreading means physically shoving every earlier colour outward. Drop into an existing drop and it doesn’t cover it. It squeezes it into a ring.
So every tool is an area-preserving map applied to every boundary at once. Adding a
disc of radius r at C sends each point to C + (P−C)·√(1 + r²/‖P−C‖²) — in
polar terms ρ′ = √(ρ² + r²), so ρ′dρ′ = ρdρ and area is conserved exactly rather
than approximately. A comb tooth displaces parallel to its own line by an amount
depending only on perpendicular distance, which is a shear: Jacobian 1, conserved
again.
Two things follow that are hard to get any other way. Boundaries stay exact under any amount of stretching, so a filament pulled to a hair is still a polygon edge rather than a sampled field, and that’s why the comb patterns hold up. And a held press is exactly one big drop, because radii compose in quadrature — which means pigment can be injected one small increment per frame while you hold, and the growth is an animation of the arithmetic instead of an approximation of it.
What the bake-off couldn’t find
The version of this project before it was a bake-off: twelve standalone sketches across two waves, each modelling pigment a different way, scored against a written rubric on beauty, feel, collision correctness and room to add displacement later.
All twelve modelled pigment as regions that stop at each other. Real marbling is pigment that displaces other pigment. Nothing in the rubric tested for that, so nothing in the bake-off found it — including the criterion that named displacement directly, because it asked whether a model left room to add displacement later rather than whether displacement was the thing being modelled.
Two of the twelve did become a grid-based engine. That engine was then thrown away too. The honest account of both rounds is in the repo, with a contact sheet of all twelve given identical input, because the second rebuild is the more useful story and it only reads as one if the first is still visible.
That’s the part I’d defend: a rubric written before any code existed, scored on the qualities that were easy to name, and the fact it was chasing turned out not to be on it.
The patterns are the operations
Each traditional pattern is the one before it plus a pass of a tool, which is exactly
how it works in a tray — so the library composes the same way. stone is drops.
gelGit is a needle down then up, working across. nonpareil is one fine comb
straight across. feather is that plus a wide comb at right angles.
Three are worth naming because in each case the pattern is what the arithmetic does
rather than something drawn on top of it. Hatip, the drawn flower, is colours
dropped one inside another and then a needle pulled out through the rings; the petals
are what a radial pull does to nested circles, and because equal drops compose in
quadrature the rings thin as √k, which is why a real rosette’s outer bands are its
finest. Suminagashi is that nesting taken to two dozen alternating rings and then
nudged, where shearing rather than pushing is what draws rings out into contours.
Italian hair vein is a last colour carrying far more gall than the rest — and
there’s no gall in the model and there needs to be none, because a colour that
spreads harder is just a colour dropped more often.
The rule that’s easiest to get wrong
Every comb in the catalogue obeys one bound: the decay length has to stay well under the tooth spacing. Let the two approach and neighbouring teeth merge into a single uniform translation — the surface slides sideways and the comb leaves no lanes at all. The same bound governs how far a wavy tooth may wander, since at an amplitude of a full spacing each tooth swings into its neighbour’s lane, the two disagree about which way that lane runs, and waves become cross-hatch.
There’s a related fix worth recording. The classic tooth is geometric in distance
from the tine, λ^(|s|/z), and it has a corner at s = 0 where the derivative flips
sign. Every pass leaves a crease down it, and teeth pulling against each other leave
hard cusps where they meet: paper folded rather than marbled. Squaring the distance,
λ^((s/z)²), removes the corner without moving the tooth — same two knobs, same
displacement one decay length out, smooth across the peak. Both depend only on
perpendicular distance, so both are shears and both stay exactly area-preserving. The
choice is about the tooth, not the physics.
Prior art, and being clear about it
The area-preserving formulation isn’t mine. It’s from Lu, Jaffer, Jin, Gao and Mao’s Mathematical Marbling (IEEE CG&A, 2012) and Aubrey Jaffer’s marbling notes, and the repo cites both, along with a fluid-solver alternative for contrast and the Turkish Cultural Foundation’s monograph on the craft itself. What’s mine is the engine, the gesture layer, the rake catalogue, the rounded tooth, the texture pass, and the account of getting there the long way.
What it makes




