The Ink Is Still Wet
2026 · still life
The tools get painted last and least, so I painted them first: everything here exists to make marks, and the marks it has made so far are the ones you are looking at.
an inkstone with a pool of freshly ground ink, a bamboo brush laid across it and a small water dish, all three on one diagonal through the lower right, the rest left as bare paper
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: inkstone, laid brush and water dish ride one diagonal through the lower right; the whole upper-left half is left as bare paper
// layering: warm ground and cast shade first, then stone and porcelain built from many overlapping loads, wet ink and the brush head last
// the only accent is the bamboo handle — one warm rod crossing the slab and running off the bottom-left corner
const PALETTE = [
"#ece4d4", // ground — warm sized paper
"#9c9184", // mass — the slab, dry stone
"#5e5a52", // shadow — cast shade and stone grain
"#15151a", // ink — the wet pool, the loaded head
"#b26a34", // accent — bamboo handle, rationed
"#8a9995", // water — the cool note in the dish
];
// the slab lies almost flat, tipped six degrees so it is parallel to nothing
const ST = { x: 516, y: 574, c: 0.9945, s: -0.1045, hw: 176, hh: 96 };
const S_BR = [701, 651];
const S_BL = [351, 688];
// the brush axis: point in the ink, butt off the bottom-left corner
const TIP = [556, 545];
const FER = [489, 590];
const BUT = [140, 825];
const PER = [0.558, 0.829]; // the shaded side, turned away from the light
function local(dx, dy) {
return [ST.x + dx * ST.c - dy * ST.s, ST.y + dx * ST.s + dy * ST.c];
}
function blob(cx, cy, rx, ry, ang, n, jitter, curve) {
const c = Math.cos(ang);
const s = Math.sin(ang);
brush.beginShape(curve);
for (let i = 0; i < n; i++) {
const t = (i / n) * Math.PI * 2;
const k = 1 + randomGaussian(0, jitter);
const dx = Math.cos(t) * rx * k;
const dy = Math.sin(t) * ry * k;
brush.vertex(cx + dx * c - dy * s, cy + dx * s + dy * c);
}
brush.endShape(true);
}
// a run of overlapping pigment along a line — how every solid here gets its body
function rod(x0, y0, x1, y1, n, r0, r1, col, op, spread) {
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
brush.fill(col, Math.max(30, randomGaussian(op, 16)));
brush.circle(
x0 + (x1 - x0) * t + randomGaussian(0, spread),
y0 + (y1 - y0) * t + randomGaussian(0, spread),
Math.max(1, (r0 + (r1 - r0) * t) * (1 + randomGaussian(0, 0.09))),
true
);
}
}
// ---- the ground: warm air pooling along the diagonal, nothing above it
function groundWash() {
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.4, 0.4);
const drift = [
[560, 726], [664, 750], [752, 676], [462, 758], [716, 578],
[776, 484], [372, 786], [628, 790], [792, 648], [672, 300],
[520, 796], [788, 380],
];
for (let i = 0; i < 12; i++) {
brush.fill("#9c9184", Math.max(14, randomGaussian(28, 7)));
brush.circle(
drift[i][0] + randomGaussian(0, 10),
drift[i][1] + randomGaussian(0, 10),
Math.max(40, randomGaussian(64, 9)),
true
);
}
brush.fill("#8a9995", 20);
brush.circle(700, 296, 56, true);
}
function castShadows() {
brush.noStroke();
brush.fillTexture(0.5, 0.45);
brush.fillBleed(0.38, "out");
// the slab throws its weight down and to the left
for (let i = 0; i < 11; i++) {
const t = i / 10;
brush.fill("#5e5a52", Math.max(34, randomGaussian(62, 12)));
brush.circle(
S_BL[0] + (S_BR[0] - S_BL[0]) * t - 17 + randomGaussian(0, 5),
S_BL[1] + (S_BR[1] - S_BL[1]) * t + 27 + randomGaussian(0, 6),
Math.max(20, randomGaussian(32, 5)),
true
);
}
brush.fill("#5e5a52", 34);
brush.circle(392, 726, 46, true);
brush.fill("#5e5a52", 26);
brush.circle(456, 736, 36, true);
// the dish, a crescent of shade at its lower left
brush.fill("#5e5a52", 48);
brush.circle(676, 424, 34, true);
brush.fill("#5e5a52", 38);
brush.circle(712, 446, 30, true);
brush.fill("#5e5a52", 26);
brush.circle(652, 400, 22, true);
}
// ---- the slab: pigment laid across the grinding face until it has body
function inkstone() {
brush.noStroke();
brush.fillTexture(0.6, 0.55);
brush.fillBleed(0.2, "out");
for (let gy = 0; gy < 5; gy++) {
for (let gx = 0; gx < 10; gx++) {
const dx = -ST.hw * 0.9 + (gx / 9) * ST.hw * 1.8 + randomGaussian(0, 9);
const dy = -ST.hh * 0.86 + (gy / 4) * ST.hh * 1.72 + randomGaussian(0, 7);
const p = local(dx, dy);
const near = dy / ST.hh; // the near half of the face turns down into shade
const r = random();
const col = r < 0.62 ? "#9c9184" : r < 0.88 ? "#5e5a52" : "#ece4d4";
brush.fill(col, Math.max(40, randomGaussian(86 + near * 34, 15)));
brush.circle(p[0], p[1], Math.max(26, randomGaussian(40, 6)), true);
}
}
// the far edge catches the light: a pale flare skimming the top
brush.fillBleed(0.3, "out");
for (let i = 0; i < 7; i++) {
const p = local(-140 + i * 48 + randomGaussian(0, 7), -78 + randomGaussian(0, 6));
brush.fill("#ece4d4", Math.max(60, randomGaussian(120, 26)));
brush.circle(p[0], p[1], Math.max(14, randomGaussian(23, 4)), true);
}
// grain, dragged the long way across the face — broken, never ruled
brush.noFill();
brush.set("2H", "#5e5a52", 0.9);
brush.line(368, 522, 592, 498);
brush.line(430, 546, 636, 524);
brush.line(356, 574, 498, 560);
brush.line(540, 566, 664, 552);
brush.line(402, 606, 656, 578);
brush.set("2H", "#9c9184", 0.75);
brush.line(462, 630, 620, 612);
brush.line(348, 648, 452, 638);
brush.line(600, 486, 668, 480);
// the front face — the stone has real depth, and this is its darkest side
brush.noStroke();
brush.fillTexture(0.5, 0.5);
brush.fillBleed(0.14, "out");
rod(348, 700, 704, 662, 22, 17, 15, "#5e5a52", 132, 2.6);
rod(352, 706, 702, 668, 16, 10, 9, "#15151a", 78, 2.4);
// edges, only where the eye needs them
brush.noFill();
brush.set("2B", "#5e5a52", 1.5);
brush.beginShape(0.1);
brush.vertex(334, 498);
brush.vertex(492, 481);
brush.vertex(678, 461);
brush.endShape(false);
brush.set("charcoal", "#15151a", 1.6);
brush.beginShape(0.1);
brush.vertex(356, 688);
brush.vertex(520, 671);
brush.vertex(698, 652);
brush.endShape(false);
brush.set("2B", "#5e5a52", 1.1);
brush.line(699, 654, 706, 676);
brush.line(352, 690, 356, 710);
brush.line(680, 462, 700, 650);
}
// ---- the well and the ink standing in it
function inkPool() {
brush.noStroke();
brush.fillTexture(0.45, 0.5);
// the ground depression, a ring of shade around the water line
brush.fillBleed(0.34, "out");
brush.fill("#5e5a52", 86);
blob(596, 526, 70, 44, -0.13, 12, 0.09, 0.5);
brush.fill("#5e5a52", 60);
blob(600, 520, 60, 37, -0.13, 11, 0.11, 0.5);
// the ink: laid in overlapping loads until it stops being grey
brush.fillBleed(0.22, "out");
const ring = [
[556, 522], [572, 512], [594, 508], [618, 510], [636, 518],
[640, 532], [626, 542], [604, 546], [578, 544], [560, 536],
];
for (let i = 0; i < 10; i++) {
brush.fill("#15151a", Math.max(120, randomGaussian(168, 24)));
brush.circle(
ring[i][0] + randomGaussian(0, 3),
ring[i][1] + randomGaussian(0, 3),
Math.max(14, randomGaussian(20, 3)),
true
);
}
brush.fillBleed(0.1, "in");
const core = [
[586, 522], [604, 520], [618, 526], [598, 532], [578, 528], [612, 514],
];
for (let i = 0; i < 6; i++) {
brush.fill("#15151a", Math.max(170, randomGaussian(216, 20)));
brush.circle(
core[i][0] + randomGaussian(0, 2),
core[i][1] + randomGaussian(0, 2),
Math.max(11, randomGaussian(16, 2)),
true
);
}
// the one thing wet ink does that dry stone cannot — hold a window of sky
brush.fillBleed(0.2, "out");
brush.fill("#8a9995", 132);
blob(576, 512, 17, 5, -0.4, 8, 0.13, 0.5);
brush.fill("#ece4d4", 138);
blob(571, 509, 10, 3, -0.44, 8, 0.15, 0.5);
brush.fill("#8a9995", 76);
blob(628, 536, 9, 3, -0.2, 8, 0.18, 0.5);
// a thread of ink dragged out where the brush was lifted
brush.noFill();
brush.set("2B", "#15151a", 1.4);
brush.beginShape(0.5);
brush.vertex(562, 536);
brush.vertex(548, 544);
brush.vertex(540, 554);
brush.endShape(false);
}
// ---- the water dish, cropped by the right edge
function waterDish() {
brush.noStroke();
brush.fillTexture(0.5, 0.5);
brush.fillBleed(0.24, "out");
// porcelain: a ring of overlapping pigment, brighter above, weighted below
for (let i = 0; i < 24; i++) {
const a = (i / 24) * Math.PI * 2 + randomGaussian(0, 0.05);
const x = 730 + Math.cos(a) * 76 * (1 + randomGaussian(0, 0.03));
const y = 376 + Math.sin(a) * 68 * (1 + randomGaussian(0, 0.03));
const low = Math.sin(a) > 0.2;
brush.fill(low ? "#9c9184" : "#ece4d4", Math.max(80, randomGaussian(low ? 122 : 158, 22)));
brush.circle(x, y, Math.max(11, randomGaussian(17, 3)), true);
}
// the water, shallow and cool, sitting off-centre in the bowl
for (let i = 0; i < 9; i++) {
brush.fill("#8a9995", Math.max(40, randomGaussian(72, 16)));
brush.circle(738 + randomGaussian(0, 20), 380 + randomGaussian(0, 16), Math.max(18, randomGaussian(28, 5)), true);
}
brush.fill("#5e5a52", 54);
blob(722, 404, 34, 15, 0.2, 10, 0.11, 0.5);
brush.fill("#ece4d4", 96);
blob(752, 348, 26, 10, -0.16, 9, 0.13, 0.5);
// a single drop of ink still unravelling in clean water
brush.fill("#15151a", 108);
blob(752, 372, 13, 7, 0.5, 9, 0.15, 0.5);
brush.fill("#15151a", 58);
blob(768, 388, 8, 4, 0.3, 8, 0.18, 0.5);
// rim
brush.noFill();
brush.set("pen", "#5e5a52", 1.2);
brush.beginShape(0.6);
brush.vertex(658, 358);
brush.vertex(694, 312);
brush.vertex(766, 304);
brush.endShape(false);
brush.set("charcoal", "#15151a", 1.5);
brush.beginShape(0.6);
brush.vertex(656, 376);
brush.vertex(680, 432);
brush.vertex(738, 452);
brush.vertex(800, 442);
brush.endShape(false);
brush.set("pen", "#5e5a52", 0.95);
brush.beginShape(0.6);
brush.vertex(676, 380);
brush.vertex(694, 418);
brush.vertex(742, 432);
brush.endShape(false);
}
// ---- the brush, laid across the slab: bamboo, ferrule, loaded head
function laidBrush() {
const px = PER[0];
const py = PER[1];
brush.noStroke();
brush.fillTexture(0.55, 0.5);
brush.fillBleed(0.12, "out");
// bamboo, one warm rod running off the corner
rod(FER[0], FER[1], BUT[0], BUT[1], 30, 10, 7.5, "#b26a34", 150, 1.4);
rod(FER[0] - px * 3.4, FER[1] - py * 3.4, BUT[0] - px * 2.6, BUT[1] - py * 2.6, 18, 4, 3, "#ece4d4", 92, 1.1);
rod(FER[0] + px * 4.6, FER[1] + py * 4.6, BUT[0] + px * 3.6, BUT[1] + py * 3.6, 20, 3.6, 3, "#5e5a52", 118, 1.1);
// two bamboo nodes, and the shaded edge under them
brush.noFill();
brush.set("2B", "#5e5a52", 1.6);
brush.line(374 - px * 10, 668 - py * 10, 374 + px * 10, 668 + py * 10);
brush.line(248 - px * 9, 754 - py * 9, 248 + px * 9, 754 + py * 9);
brush.set("pen", "#b26a34", 1.0);
brush.line(382 - px * 8, 662 - py * 8, 382 + px * 8, 662 + py * 8);
brush.set("2B", "#15151a", 1.2);
brush.line(FER[0] + px * 8.4, FER[1] + py * 8.4, 156 + px * 6.4, 812 + py * 6.4);
// ferrule
brush.noStroke();
brush.fill("#5e5a52", 176);
blob(494, 587, 11, 8, -0.59, 9, 0.08, 0.3);
brush.noFill();
brush.set("rotring", "#15151a", 1.2);
brush.line(501 - px * 9, 582 - py * 9, 501 + px * 9, 582 + py * 9);
brush.line(486 - px * 9.5, 592 - py * 9.5, 486 + px * 9.5, 592 + py * 9.5);
// the head: loaded, belly full, point still sharp
brush.noStroke();
brush.fillBleed(0.18, "out");
rod(492, 588, TIP[0], TIP[1], 14, 10, 1.4, "#15151a", 178, 1.2);
rod(496, 585, TIP[0] - 3, TIP[1] + 2, 9, 6, 1.2, "#15151a", 210, 1.0);
// three hairs that never came back into line
brush.noFill();
brush.set("rotring", "#15151a", 0.85);
brush.beginShape(0.5);
brush.vertex(516, 572);
brush.vertex(542, 556);
brush.vertex(568, 548);
brush.endShape(false);
brush.beginShape(0.5);
brush.vertex(510, 568);
brush.vertex(538, 550);
brush.vertex(562, 538);
brush.endShape(false);
brush.beginShape(0.5);
brush.vertex(500, 592);
brush.vertex(528, 574);
brush.vertex(550, 558);
brush.endShape(false);
}
// ---- what the grinding threw off, kept out of the empty half
function spatter() {
brush.noStroke();
brush.fillBleed(0.18, "out");
const seeds = [
[470, 742], [508, 766], [438, 776], [566, 730], [612, 706],
[392, 756], [656, 736], [536, 792], [594, 780], [712, 620],
];
for (let i = 0; i < 10; i++) {
brush.fill(random() < 0.75 ? "#15151a" : "#5e5a52", Math.max(120, randomGaussian(190, 34)));
brush.circle(
seeds[i][0] + randomGaussian(0, 7),
seeds[i][1] + randomGaussian(0, 7),
Math.max(1.4, randomGaussian(3.4, 1.3)),
true
);
}
brush.fill("#15151a", 210);
brush.circle(424, 786, 5.2, true);
brush.fill("#15151a", 150);
brush.circle(330, 744, 2.6, true);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#ece4d4");
groundWash();
castShadows();
inkstone();
waterDish();
inkPool();
laidBrush();
spatter();
}