Skip to content
HN On Hacker News ↗

inkwash · how it works

▲ 246 points 28 comments by Yenrabbit 6d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is primarily AI-generated with some human-written content

82 %

AI likelihood · overall

AI
18% human-written 82% AI-generated
SEGMENTS · HUMAN 1 of 6
SEGMENTS · AI 5 of 6
WORD COUNT 1,784
PEAK AI % 99% · §4
Analyzed
Jun 17
backend: pangram/v3.3
Segments scanned
6 windows
avg 297 words each
Distribution
18 / 82%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,784 words · 6 segments analyzed

Human AI-generated
§1 Human · 12%

01inspiration

I love nature journaling. Over time I've developed a style and approach that I like for capturing sketches quickly, using a Pilot G2 pen in combination with a waterbrush. This lets me add linework and shading simultaneously (I dual wield with the brush in my left hand) and forces me not to be too precious about the final result - there will inevitably be smudges and imperfections, and there is no undo with pen!

This project began as a test of Anthropic's new model Claude Fable 5, and grew once I saw the potential to actually recreate that experience in a browser. I love the final result!

Example sketches from my notebooks, from fast flamingo figure-drawings to more finessed fan-art fun.

Of course, I'm left in a rather funny position: I've 'created' this app, but I haven't actually touched the code! I can read it (it's a rather nice, self-contained single HTML file) since I have experience with the underlying technologies. But I'm hoping that this app is interesting to many people who *aren't* webGL nerds, and so for the sake of all of our collective understanding I've had Fable spin up some interactive demos to illustrate the concepts. You are also welcome to check out the prompts I used to conjure this app into being.

Disclaimer: While I've tidied up a bit, the rest of this article contains plenty of AI-witten prose.

§2 AI · 89%

I don't like AI writing as a rule, especially undisclosed! Hopefully you can forgive me in this case, since (with some iteration) the AI actually did a pretty good job showing all the key pieces. Over time I might refactor and reorganise it to be more in line with my personal sensibilities, but no promises :)

02three sheets of state

Under the canvas, the painting is not pixels — it’s a small stack of floating-point textures, ping-ponged through about a dozen WebGL2 fragment shaders every frame. Think of them as transparent sheets laid over each other:

fieldformatresolutionmeaning inkRGBA16Fup to 2048, matches screen mobile pigment. RGB is optical density (how much each light channel is absorbed), not color. Alpha is white gouache. fixedRGBA16Fsame as ink pigment that has settled into the paper and no longer moves (section 07). wetR16Fsame as ink how much water is sitting on each point of the paper. velocityRG16F~256 cells on the short side the water’s motion. Coarse on purpose — flow is smooth, pigment is not. pressureR16Fsame as velocity scratch space for keeping the flow incompressible.

Each frame: the stroke engine stamps gaussian splats into these fields (section 05), the simulation advances them (03–04), and a final display shader turns density into paper-and-ink color (08). Nothing in the pipeline ever stores “a color” — color only exists for the one shader that draws the screen.

You can see the sheets directly. The demo below paints a stroke and washes through it; the buttons switch which field you’re looking at.

fig 2An x-ray of the engine. painting is the composed result; pigment is raw ink density; water is the wetness field (note how it spreads past the brush and slowly evaporates); flow shows velocity, direction as hue. The flow only exists where the paper is wet.

§3 AI · 99%

The two resolutions matter. Velocity lives on a coarse ~256-cell grid because fluid motion is inherently smooth, and the pressure solve (the expensive part) scales with cell count. Pigment and wetness live at up to 2048 — near screen resolution — because that’s where edges, granulation and fine linework live. The sleight of hand of the whole app is sampling a blurry, cheap flow field to push around a sharp, expensive ink field.

03water that moves

The flow is Jos Stam’s Stable Fluids (1999), the algorithm behind nearly every realtime smoke, ink and fire toy on the GPU. It earns the “stable” in its name from one idea: don’t push, pull.

A naive simulation moves each parcel of fluid forward along its velocity — and explodes the moment a parcel overshoots a grid cell. Stam’s semi-Lagrangian advection flips the question. Each grid cell asks: if the fluid here arrived from somewhere, where was it one timestep ago? It traces backward along the velocity, samples the old field at that point (bilinearly, between the four nearest cells), and adopts the value. No overshoot is possible, because every cell ends up with a weighted average of values that already existed. Big timestep, lazy frame rate, doesn’t matter — it cannot blow up.

fig 3Semi-Lagrangian advection. The highlighted cell traces backward along the local velocity (dashed), samples the field between the four nearest cells, and carries that value home. Every cell does this, every frame, in one fragment shader.

In GLSL the whole maneuver is two lines:

vec2 coord = vUv - uDt * texture(uVelocity, vUv).xy * uTexel; vec2 vel = texture(uVelocity, coord).xy * uDissipation;

Advection alone gives you syrup, not water. Two more passes give it character:

Pressure projection makes the water incompressible. After advection the velocity field has places where flow piles up (positive divergence) or tears apart (negative). A real liquid refuses both — push it and it must go around.

§4 AI · 99%

The solver computes the divergence, relaxes a pressure field against it with ~22 Jacobi iterations, and subtracts the pressure gradient from the velocity. What that buys, visibly, is swirl: pushes turn into eddies and curls instead of sprays.

Vorticity confinement fights numerical mush. All that bilinear sampling acts like a low-pass filter — little whirlpools blur away within seconds. So the solver measures the curl that remains, finds its ridges, and applies a small force that spins them back up. It’s a knob for liveliness: inkwash ties it (along with the push strength and how slowly velocity decays) to the flow slider.

flow · low flow · high fig 4The same scripted strokes — an ink blob, then a circling water brush — at the two ends of the flow slider. Low flow is a damp, obedient wash; high flow has momentum, vorticity, and opinions.

04paper that decides

A fluid solver on its own makes smoke — everything drifts forever. What makes this feel like paper is that the wetness field acts as a permission system over the whole simulation. Three gates, all reading the same little texture:

Velocity is confined to wet paper. After advecting, the velocity is multiplied by smoothstep(0.005, 0.2, wet) — flow simply cannot exist on dry ground. This is why a wash stops at its own boundary instead of smearing across the page.

Pigment mobility is earned, not assumed. The ink pass computes mob = smoothstep(0.02, 0.45, wet) and scales both its advection distance and its bleed rate by it. Damp paper lets ink creep; soaked paper lets it run. Bone-dry paper is a museum — the shader returns the old value untouched and the pixel costs almost nothing.

Water itself moves reluctantly. The wet field is advected at only 0.6× the flow speed, blurred a little into its neighbors each frame (capillary creep — a puddle’s edge slowly widens), and decays exponentially.

§5 AI · 99%

The dry slider sets that time constant, from about 2 to about 18 seconds. Drying is what turns a fluid sim into a painting: every wash is a closing window.

fig 5A pen line, then water brushed over its left half. Only the wetted half moves — and only until the paper dries. Slide dry and let the loop replay to feel the working window change.

Drying in inkwash is honest in one more way: water doesn’t take the pigment with it when it goes. Wherever ink happens to be when its puddle evaporates, that’s where it stays — mid-bloom, mid-streak, mid-swirl. Most of the textures that read as “watercolor” are just the flow field’s last words, frozen.

05making marks

Between your hand and the fields sits a small stroke engine, and it draws everything with a single primitive: a gaussian splat — a fuzzy radial stamp, exp(-d²/r²), blended into a field. A pen stroke is a chain of ink splats; a brush stroke is a chain of water splats plus velocity impulses pointing along the motion. The stamps are spaced at 0.6 of the radius so their overlap sums to a smooth ribbon:

fig 6Gaussian stamps along a stroke. Each curve is one splat; the line above is their sum, and the strip is how it renders. At the app’s spacing (0.6×r) the seams vanish; drag the slider apart to see the beads a stroke is secretly made of.

How the stamps are blended matters as much as their shape. Ink is additive — densities sum, which (as section 08 will make precise) is exactly how real glazes deepen. Water uses MAX blending instead: wetness saturates rather than accumulates, so scrubbing the brush in place makes paper wet, not impossibly flooded. One blend-equation flag, and it’s the difference between a watercolor and a swamp.

The hand data feeding those splats gets shaped, too:

Pressure and speed set the nib.

§6 AI · 99%

For the pen, radius and density both grow with stylus pressure and shrink with speed — a fast flick gives a thin, dry line; a slow, heavy drag gives a dark, swelling one. On a trackpad, Force Touch stands in for pressure; with a mouse or finger, the engine fakes pressure from speed (slow ≈ deliberate ≈ heavy), which is wrong in theory and convincing in practice.

The cursor is chased, not obeyed. The brush position relaxes toward the pointer exponentially (k = 1 - exp(-14·dt)). That few-millisecond lag is the cheapest line-quality trick in graphics: jitter is absorbed, corners round off, and strokes get the slight follow-through of a real brush.

Stillness is a mark. If the pen dwells in place, ink keeps feeding in at a trickle and the spot blooms — pooling, like resting a real nib on damp paper. A dwelling brush gently stirs the water beneath it instead.

fig 7The pen’s vocabulary: a slow, pressure-tapered stroke; a fast light one; and a dwell that pools. Try it yourself — with a stylus you get real pressure, with anything else the speed fake.

06black that isn’t black

Here is the trick the whole app was built around. Put a drop of water on a line of cheap black ink and watch the edge: the black stays close, but a blue-violet ghost walks out ahead of it. Black inks are dye cocktails, and on wet paper they chromatograph — each dye travels at its own speed.

Inkwash gets this almost for free because of a decision from section 02: pigment is stored as per-channel optical density, and the bleed step — which each frame nudges ink toward the average of its neighbors, where wet — runs at a different