Wet media

Watercolour that behaves like watercolour. Paint pools, creeps along the tooth of the paper, darkens at its rim as it dries and settles into the valleys. Tilt the sheet and it runs.

import { PAPERS, WetGpu } from "shodo/paint";

const sheet = new WetGpu(canvas, 600, 400, {
  paper: PAPERS.coldPress,
  feel: { wetness: 0.8, edges: 0.7, gravity: 0.6 },
});

That sheet takes flooded strokes that dry with hard tidelines and drip down the page. Leave feel out and you get the sheet shodo has always made.

Paint on it. Let go of any dial below and the sheet starts again with that feel.open in the playground

Feel

Nine dials, each from 0 to 1. Before 2026-11-10 (record entry 62) there were six: strength, granulation and bleed came then. Every dial rests at 0.5 except gravity, which rests at 0. A dial you don’t set stays where it rests.

  • wetness
    dampflooded

    How much water a stroke carries: dry strokes sit still, wet ones run, bloom and pool.

  • drying
    stays opensets fast

    How fast the sheet gives its water up, and so how long a stroke stays open to work into.

  • edges
    softtidelines

    How dark the rim of a wash gets as it dries.

6 more dials
  • flow
    syrupracing

    How freely standing water moves over the paper.

  • staining
    liftsstains

    How hard dried paint holds on when a clear brush goes over it.

  • gravity
    flat tableupright easel

    Tilts the sheet so wet paint runs down it and drips off the bottom of shapes.

  • strength
    tintdeep

    How much colour a loaded brush lays down in one stroke.

  • granulation
    smoothspeckled

    How much pigment drops out into the grain of the paper, speckling a wash.

  • bleed
    crispfeathered

    How far a wet stroke feathers out into damp paper around it.

The same words live in WET_FEEL, so a controls panel can be built straight from it:

import { WET_FEEL } from "shodo/paint";

for (const [key, dial] of Object.entries(WET_FEEL)) {
  // dial.title, dial.summary, dial.ends ([low word, high word]), dial.value (where it rests)
}

A sheet’s feel is set when it is made. To change it, make a new sheet, and free the old one with dispose(). The wet lab in the playground has every dial on a panel to try.

Things worth knowing

Paper

The paper sets the tooth paint settles into, how thirsty the sheet is and its colour. Four to start from: laid, sanded, coldPress (the default for wet sheets) and toned. Any field of a Paper can be changed:

const paper = { ...PAPERS.coldPress, scale: 10, absorbency: 0.5 };

Pigments

Give a sheet three named pigments and their glazes mix the way transparent paint does, worked out wavelength by wavelength: hansa yellow over phthalo blue is a clean green, phthalo green over quinacridone rose goes grey.

import { PIGMENTS, WetGpu } from "shodo/paint";

const sheet = new WetGpu(canvas, 600, 400, {
  triad: [PIGMENTS.hansaYellow, PIGMENTS.quinacridoneRose, PIGMENTS.phthaloBlue],
});
const blue = sheet.ink(2, 0.6);

Resolution

Added 2026-11-10. scale is how many cells a sheet has per unit. Make the sheet scale times larger and multiply every length you pass (coordinates and radius) by the same number, and you get the same painting, sharper. Paint per length, edge darkening and the paper’s grain all follow the scale.

const k = Math.min(2, devicePixelRatio);
const sheet = new WetGpu(canvas, 640 * k, 480 * k, { scale: k });
sheet.brush(40 * k, 200 * k, 560 * k, 200 * k, { radius: 20 * k, pigment, load: 1 });

Blooms and runs still spread a set number of cells per step, so on a larger sheet they reach a little less far.

Gouache

Added 2026-11-10. Gouache is opaque watercolour: a light colour over a dark one covers it, a thick coat shows exactly the colour you mixed, it dries flat with hardly a rim and a wet brush lifts it again.

const sheet = new WetGpu(canvas, 600, 400, { medium: "gouache" });
sheet.brush(40, 200, 560, 200, { radius: 20, pigment: { color: [0.95, 0.85, 0.15], granulation: 0 }, load: 1 });

One medium per sheet. Gouache ignores granulation. feel and physics still apply on top of gouache’s own physics (GOUACHE_PHYSICS). In the lab, open wet.html?gouache for a gouache sheet with a white pan.

Underneath: physics

feel is a front for sixteen numbers from the simulation (after Curtis and others, 1997). Set them through physics when a dial is not enough; anything set there wins over feel.

new WetGpu(canvas, 600, 400, { feel: { wetness: 0.8 }, physics: { edge: 0.03, tilt: [0.02, 0.04] } });

WET_PHYSICS holds the numbers every sheet rests on. wetPhysics() fills in the rest of a partial set, holds each number inside a range the simulation stays steady in and refuses anything that is not a number. tilt is the one thing physics can do that feel cannot: point gravity sideways.

The presets WETNESS, DRYING and TILT (added 2026-11-10, record entry 50) are partial physics sets you can spread into physics. Since entry 51 the feel dials are the first thing to reach for; the presets stay.