Where the Wrist Turns
2026 · gesture
Speed is invisible until the wrist changes its mind: the pigment gathers exactly where the hand hesitated and thins out wherever it had already decided.
three abstract strokes stepping down and to the right, each swelling where the wrist pivots and leaving as a dry splitting hair, the left third of the paper kept empty
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: three turning strokes step down a diagonal right of centre — the middle pivot lands near the lower-right thirds point; the whole left third stays bare paper
// layering: a slate breath along the diagonal, damp haloes under each spine, the body swept as brush-hair passes whose spread follows the local width, a crisp rim on the outside of each turn, the loaded dark core, pigment pooled in the corner, and last the skips, the dry hairs and the ligatures
// each stroke arrives as a hair from the upper left, takes on weight where the wrist rotates, and frays out to the right as the brush runs dry — a quick one, a long open one, a flat one leaving the paper
const PALETTE = [
"#e4dfd2", // ground — warm stone paper
"#48555e", // mass — mid slate ink, the wet body
"#26313a", // shadow — deep ink, cores and pooling
"#8d8b7e", // dry — grey-olive residue, fringes and ligatures
"#9d5a34", // accent — rust, only where the pigment burns through
];
// spines and width profiles, hand-set per stroke; anchor 4 is the wrist turn.
// A is quick and steep, B is the long open gesture, C runs flat off the right edge.
const A_PTS = [[404, 130], [428, 192], [450, 246], [476, 286], [518, 296], [554, 288], [584, 276]];
const A_W = [6, 22, 34, 45, 26, 9, 2.5];
const B_PTS = [[476, 358], [506, 416], [532, 464], [562, 502], [618, 516], [678, 500], [736, 472]];
const B_W = [7, 28, 44, 60, 37, 13, 3];
const C_PTS = [[588, 596], [622, 640], [652, 678], [686, 706], [736, 716], [784, 712], [828, 704]];
const C_W = [6, 19, 29, 40, 23, 9, 2.5];
const STEPS = 5;
const TURN = 3 * STEPS; // sample index of the pivot
// weighted pick so no two passes carry quite the same pigment
function inkOf() {
const r = random();
if (r < 0.6) return "#48555e";
if (r < 0.88) return "#26313a";
return "#8d8b7e";
}
function samplesOf(pts, ws, wobAmp, phase) {
const raw = [];
for (let i = 0; i < pts.length - 1; i++) {
for (let k = 0; k < STEPS; k++) {
const t = k / STEPS;
raw.push([
lerp(pts[i][0], pts[i + 1][0], t),
lerp(pts[i][1], pts[i + 1][1], t),
lerp(ws[i], ws[i + 1], t),
]);
}
}
raw.push([pts[pts.length - 1][0], pts[pts.length - 1][1], ws[ws.length - 1]]);
const out = [];
for (let i = 0; i < raw.length; i++) {
const p = raw[max(0, i - 1)];
const n = raw[min(raw.length - 1, i + 1)];
const dx = n[0] - p[0];
const dy = n[1] - p[1];
const m = Math.hypot(dx, dy) || 1;
const nx = -dy / m;
const ny = dx / m;
const off = map(noise(i * 0.16, phase), 0, 1, -wobAmp, wobAmp);
out.push({
x: raw[i][0] + nx * off,
y: raw[i][1] + ny * off,
w: max(3, raw[i][2] * (1 + randomGaussian(0, 0.045))),
nx,
ny,
dx: dx / m,
dy: dy / m,
});
}
return out;
}
// the paper drinking ahead of the brush — wide, pale, no edge of its own
function halo(s, op) {
brush.noStroke();
brush.fillBleed(0.72, "out");
brush.fillTexture(0.24, 0.24);
for (let i = 5; i < s.length - 4; i += 6) {
const q = s[i];
brush.fill("#48555e", max(16, randomGaussian(op, 6)));
brush.circle(
q.x + q.nx * randomGaussian(0, 7),
q.y + q.ny * randomGaussian(0, 7),
max(20, q.w * randomGaussian(0.72, 0.11)),
true,
);
}
}
// the body: parallel hair passes whose spread is the local width, so the brush
// opens at the turn and closes to nothing at either end
function sweep(s, passes, weightBase, spread, headTrim, tailTrim) {
brush.noFill();
for (let k = 0; k < passes; k++) {
const u = map(k, 0, passes - 1, -0.46, 0.46) + randomGaussian(0, 0.03);
const edge = abs(u) / 0.46;
const a = constrain(floor(randomGaussian(headTrim * edge, 1.4)), 0, 8);
const b = s.length - 1 - constrain(floor(randomGaussian(tailTrim * (0.4 + edge), 2.4)), 0, 13);
brush.set("cpencil", inkOf(), max(0.5, weightBase * (1 + randomGaussian(0, 0.22))));
brush.beginShape(0.45);
for (let i = a; i <= b; i++) {
const q = s[i];
const d = u * q.w * spread;
brush.vertex(q.x + q.nx * d + randomGaussian(0, 1.1), q.y + q.ny * d + randomGaussian(0, 1.1));
}
brush.endShape(false);
}
}
// the belly of the brush at the turn: the full width, laid on wet
function belly(s, from, to, passes, weightBase) {
brush.noFill();
for (let k = 0; k < passes; k++) {
const u = map(k, 0, passes - 1, -0.42, 0.42) + randomGaussian(0, 0.035);
const a = from + constrain(floor(randomGaussian(1.5, 1.6)), 0, 5);
const b = to - constrain(floor(randomGaussian(1.5, 1.6)), 0, 5);
brush.set("cpencil", random() < 0.5 ? "#26313a" : "#48555e", max(1, weightBase * (1 + randomGaussian(0, 0.2))));
brush.beginShape(0.45);
for (let i = a; i <= b; i++) {
const q = s[i];
const d = u * q.w;
brush.vertex(q.x + q.nx * d + randomGaussian(0, 1), q.y + q.ny * d + randomGaussian(0, 1));
}
brush.endShape(false);
}
}
// the outside of the turn, where the brush belly pressed and the edge stays crisp
function rim(s, from, to, side, weight) {
brush.noFill();
brush.set("rotring", "#26313a", weight);
brush.beginShape(0.5);
for (let i = from; i <= to; i++) {
const q = s[i];
const d = side * q.w * (0.4 + randomGaussian(0, 0.03));
brush.vertex(q.x + q.nx * d, q.y + q.ny * d);
}
brush.endShape(false);
}
// the loaded core, riding the outside of the pivot where the hand pressed hardest
function core(s, from, to, passes, weightBase, drift) {
brush.noFill();
for (let k = 0; k < passes; k++) {
const u = map(k, 0, passes - 1, -0.16, 0.16) + randomGaussian(0, 0.02);
brush.set("2B", "#26313a", max(1, weightBase * (1 + randomGaussian(0, 0.22))));
brush.beginShape(0.45);
for (let i = from; i <= to; i++) {
const q = s[i];
const d = u * q.w + drift;
brush.vertex(q.x + q.nx * d + randomGaussian(0, 0.8), q.y + q.ny * d + randomGaussian(0, 0.8));
}
brush.endShape(false);
}
}
// the chiselled corner: three short crisp drags across the pivot
function chisel(s, at, len, weight) {
brush.noFill();
for (let k = -1; k <= 1; k++) {
const q = s[at + k * 2];
const d = randomGaussian(0.12, 0.05) * q.w;
brush.set("rotring", "#26313a", max(0.8, weight * (1 + randomGaussian(0, 0.2))));
brush.line(
q.x - q.dx * len * 0.5 + q.nx * d,
q.y - q.dy * len * 0.5 + q.ny * d,
q.x + q.dx * len * 0.5 + q.nx * d,
q.y + q.dy * len * 0.5 + q.ny * d,
);
}
}
// where the hairs of the brush skipped and left the paper showing
function skip(s, from, to, n) {
brush.noFill();
for (let k = 0; k < n; k++) {
const i = constrain(from + floor(randomGaussian((to - from) * 0.55, (to - from) * 0.34)), from, to - 3);
const len = constrain(floor(randomGaussian(4, 1.8)), 2, 8);
const u = randomGaussian(0, 0.18);
brush.set("2H", "#e4dfd2", max(0.5, randomGaussian(1, 0.28)));
brush.beginShape(0.45);
for (let j = i; j <= min(to, i + len); j++) {
const q = s[j];
brush.vertex(q.x + q.nx * u * q.w, q.y + q.ny * u * q.w);
}
brush.endShape(false);
}
}
// pigment standing in the corner of the turn, where the wrist paused
function pool(s, at, n, rust) {
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.58, 0.52);
for (let k = 0; k < n; k++) {
const i = constrain(at + floor(randomGaussian(1, 2.6)), 2, s.length - 3);
const q = s[i];
const side = k % 3 === 0 ? -1 : 1;
brush.fill("#26313a", constrain(randomGaussian(196, 20), 130, 240));
brush.circle(
q.x + q.nx * side * randomGaussian(q.w * 0.2, 2.8),
q.y + q.ny * side * randomGaussian(q.w * 0.2, 2.8) + randomGaussian(2, 2),
max(7, q.w * randomGaussian(0.3, 0.05)),
true,
);
}
if (rust) {
const q = s[at + 1];
brush.fill("#9d5a34", 208);
brush.circle(q.x + q.nx * 7, q.y + q.ny * 7 + 4, max(9, q.w * 0.24), true);
brush.fill("#9d5a34", 128);
brush.circle(q.x - q.dx * 22 + q.nx * 9, q.y - q.dy * 22 + q.ny * 9, max(5, q.w * 0.13), true);
}
}
// the brush running out: the tip splits and lifts off the paper
function dryExit(s, hairs, reach) {
const q = s[s.length - 4];
const base = (Math.atan2(q.dy, q.dx) * 180) / Math.PI;
brush.noFill();
for (let k = 0; k < hairs; k++) {
brush.set(k < 2 ? "2B" : "2H", k < 2 ? "#48555e" : "#8d8b7e", max(0.4, randomGaussian(0.85, 0.24)));
brush.flowLine(
q.x + q.nx * randomGaussian(0, q.w * 0.45),
q.y + q.ny * randomGaussian(0, q.w * 0.45),
max(12, randomGaussian(reach, reach * 0.3)),
base + randomGaussian(0, 5),
);
}
}
// the hair that starts the stroke, before the brush has any weight in it
function dryEntry(s, hairs, reach) {
const q = s[2];
brush.noFill();
for (let k = 0; k < hairs; k++) {
const a = Math.atan2(-q.dy, -q.dx) + randomGaussian(0, 0.08);
const r = max(9, randomGaussian(reach, 6));
brush.set("2H", "#8d8b7e", max(0.35, randomGaussian(0.7, 0.16)));
brush.line(
q.x + q.nx * randomGaussian(0, 3),
q.y + q.ny * randomGaussian(0, 3),
q.x + Math.cos(a) * r,
q.y + Math.sin(a) * r,
);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e4dfd2");
const A = samplesOf(A_PTS, A_W, 3.2, 11.2);
const B = samplesOf(B_PTS, B_W, 4.2, 47.6);
const C = samplesOf(C_PTS, C_W, 2.4, 83.9);
// a slate breath lying under the diagonal, heaviest behind the middle turn
brush.noStroke();
brush.fillBleed(0.82, "out");
brush.fillTexture(0.2, 0.18);
brush.fill("#48555e", 14);
brush.circle(486, 246, 152, true);
brush.fill("#48555e", 19);
brush.circle(578, 476, 194, true);
brush.fill("#48555e", 11);
brush.circle(716, 668, 128, true);
// damp paper under each stroke
halo(A, 38);
halo(B, 44);
halo(C, 30);
// the bodies — a streaky wide sweep, then a closer one inside it
sweep(A, 6, 5, 1.02, 3, 7);
sweep(A, 4, 3.6, 0.6, 2, 5);
sweep(B, 7, 5.6, 1.04, 3, 8);
sweep(B, 5, 4.2, 0.62, 2, 6);
sweep(C, 5, 4.6, 1, 3, 6);
sweep(C, 4, 3.4, 0.58, 2, 4);
// the loaded bellies, then a crisp edge on the outside of each turn
belly(A, 8, 21, 5, 4.6);
belly(B, 8, 22, 6, 5.2);
belly(C, 9, 21, 4, 4.2);
rim(A, 6, 22, 1, 2);
rim(B, 6, 23, 1, 2.6);
rim(C, 8, 22, 1, 1.8);
// the darks land last and hold the picture
core(A, 5, 21, 4, 2.6, 2.2);
core(B, 5, 22, 5, 3, 2.8);
core(C, 7, 21, 3, 2.2, 1.8);
chisel(A, TURN - 1, 26, 1.4);
chisel(B, TURN, 32, 1.8);
chisel(C, TURN, 20, 1.1);
// each pivot pools; only the middle turn burns through to rust
pool(A, TURN - 1, 6, false);
pool(B, TURN, 8, true);
pool(C, TURN, 5, false);
// and the hairs skip: bare paper scraped through the loaded stretch
skip(A, 8, 20, 3);
skip(B, 8, 21, 4);
skip(C, 9, 20, 2);
// entries arrive as hairs, exits leave as split hairs
dryEntry(A, 3, 26);
dryEntry(B, 3, 22);
dryEntry(C, 2, 17);
dryExit(A, 5, 42);
dryExit(B, 6, 54);
dryExit(C, 4, 30);
// the ligatures — where the brush travelled between strokes without touching down
brush.noFill();
brush.set("2H", "#8d8b7e", 0.5);
brush.beginShape(0.6);
brush.vertex(586, 288);
brush.vertex(536, 300);
brush.vertex(492, 328);
brush.vertex(474, 356);
brush.endShape(false);
brush.set("2H", "#8d8b7e", 0.45);
brush.beginShape(0.6);
brush.vertex(730, 480);
brush.vertex(686, 508);
brush.vertex(630, 552);
brush.vertex(592, 590);
brush.endShape(false);
// two specks flicked off the fastest turn
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fill("#26313a", 172);
brush.circle(700, 552, 6, true);
brush.fill("#9d5a34", 120);
brush.circle(658, 570, 5, true);
}