← Back to news

Show HN: Inkwash, a watercolor sketching app and explanation

johnowhitaker.github.io|89 points|17 comments|by Yenrabbit|Jun 14, 2026

Show HN: Inkwash — A Watercolor Simulation & Technical Breakdown

Inkwash is a watercolor sketching application contained entirely within a single HTML file. It blends fluid dynamics with clever rendering tricks to simulate the organic behavior of ink, water, and paper.

Inkwash Demo Fig 1: The continuous loop of creating pen lines and using a water-loaded brush to pull hidden colors and washes across the page.


01 The Inspiration

The project is rooted in a specific physical art style: the combination of a Pilot G2 pen and a waterbrush (a nylon brush with a built-in water reservoir). This setup is far more portable than traditional jars and brushes.

The Philosophy of the Sketch

Using these tools encourages a specific workflow:

  • Simultaneity: Linework and shading happen at once (often dual-wielding the brush in the left hand).
  • Acceptance: The process embraces perfection imperfections. Smudges are inevitable.
  • Permanence: There is no undo button with a permanent pen.

Development Note: This app was born as a test of Claude Fable 5. While I can read the code due to my background in WebGL, I didn't actually write it—the AI did. I've provided the prompts used to create it for those interested.

Disclaimer: Parts of the original explanation were AI-generated; I have tidied them up, but the core technical descriptions remain largely as the AI presented them.


02 Three Sheets of State

Under the hood, Inkwash doesn't treat the painting as a simple grid of pixels. Instead, it utilizes a stack of floating-point textures that are "ping-ponged" through roughly a dozen WebGL2 fragment shaders every frame.

Texture Architecture

Think of these as transparent overlays. The system separates the physical properties of the paint from the visual color.

FieldFormatResolutionPurpose
InkRGBA16FUp to 2048Optical density (absorption), not actual color.
PigmentRGBA16FUp to 2048Ink that has settled/dried into the paper.
WetR16FUp to 2048The volume of water present at each point.
VelocityRG16F256\approx 256 cellsThe direction and speed of water motion.
PressureR16F256\approx 256 cellsScratch space to ensure incompressible flow.

The Rendering Pipeline

The app follows a strict sequence to transform input into an image:

  • Step 1: The stroke engine stamps Gaussian splats into the fields.
  • Step 2: The simulation advances the fluid state.
  • Step 3: The display shader converts density into paper-and-ink colors.

Key Optimization: To avoid the diagonal seam common in quad-based fullscreen passes, the app draws one massive triangle covering the screen: [-1,-1, 3,-1, -1,3].

The "Sleight of Hand": The app samples a blurry, low-resolution flow field (256px) to move a sharp, high-resolution ink field (2048px). This keeps the expensive pressure calculations fast while maintaining crisp edges.


03 Water That Moves

The fluid simulation is based on Jos Stam’s Stable Fluids (1999). The core innovation is the shift from "pushing" to "pulling."

Advection: The "Pull" Method

In a naive simulation, you move a parcel of fluid forward. If the timestep is too large, the parcel overshoots its cell, and the simulation "explodes." Inkwash uses back-tracing:

  1. Each cell looks at its current velocity.
  2. It asks: "If I am here now, where was I one timestep ago?"
  3. It samples the previous field at that backward location using bilinear interpolation.

Mathematically, the coordinate lookup looks like this: coord=vUv(uDt×velocity×uTexel)\text{coord} = \text{vUv} - (\text{uDt} \times \text{velocity} \times \text{uTexel})

In GLSL, this is achieved in just two lines:

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

Adding Character: Pressure and Vorticity

Advection alone feels like syrup. To make it feel like water, two more processes are added:

  1. Pressure Projection: This makes the water incompressible. It calculates divergence (where flow piles up or gaps open) and uses 22\approx 22 Jacobi iterations to relax the pressure field. This transforms simple sprays into complex eddies and curls.
  2. Vorticity Confinement: Bilinear sampling acts as a low-pass filter, blurring out small whirlpools. The solver identifies these remaining "ridges" of curl and applies a force to spin them back up, keeping the simulation lively.