The Hand Keeps Going
2026 · gesture
Handwriting with the language taken out: what is left is pressure, hesitation, and the refusal to stop, and the knot is where the hand admits it has nothing to say.
a single long graphite line drawn slowly by an unsteady hand across the middle of a warm sheet, doubling back on itself and tangling once near the right edge
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: one unsteady line crosses the middle band from left to right and tangles once on the right thirds line, at (533, 452)
// layering: pale smudge and erased attempts first, the trembling graphite line second, the knot's darks and one oxide smear last
// the top third and the bottom quarter stay bare paper — the line is the only event on the sheet
const PALETTE = [
"#efe9de", // ground, warm paper
"#b6b0a3", // smudge, the erased attempts
"#6c675e", // graphite, the mass of the line
"#211e1a", // ink, the darks that land last
"#a4503f", // oxide, accent rationed to the knot
];
// hand-placed waypoints, written the way a sentence is written: left to right,
// curling on itself three small times, reversing four times (x goes backwards),
// running off both edges of the sheet.
const WAYPOINTS = [
[-34, 412], [26, 400], [72, 422],
[104, 396], [120, 364], [140, 386], [110, 406], [140, 416], // first curl
[182, 446], [218, 424], [194, 384], [234, 356], [288, 382],
[318, 428], [352, 458], [330, 494], [294, 464], [340, 428], // second, wider curl
[392, 416], [434, 390], [472, 406], [506, 440], [532, 464],
[556, 428], [520, 414], [548, 474], [584, 454], [616, 426],
[642, 452], [604, 484], [654, 498],
[692, 474], [710, 500], [678, 514], [666, 488], // third curl, the exit stammer
[730, 494], [770, 454], [834, 470],
];
// four erased attempts: short pale paths that lie under the line and disagree with it
const ATTEMPTS = [
[[62, 448], [124, 462], [176, 440], [216, 458]],
[[264, 402], [318, 386], [362, 402]],
[[430, 468], [478, 486], [528, 480], [566, 494]],
[[598, 420], [648, 408], [688, 424], [716, 414]],
];
// walk the waypoints, emitting a trembling point every `step` pixels: a gaussian
// twitch of the wrist plus a slow noise drift, both thrown sideways to the travel.
function tremor(pts, step, twitch, drift) {
const out = [];
for (let i = 0; i < pts.length - 1; i++) {
const x0 = pts[i][0];
const y0 = pts[i][1];
const dx = pts[i + 1][0] - x0;
const dy = pts[i + 1][1] - y0;
const seg = Math.hypot(dx, dy) || 1;
const nx = -dy / seg;
const ny = dx / seg;
const n = max(2, floor(seg / step));
for (let k = 0; k < n; k++) {
const t = k / n;
const x = x0 + dx * t;
const y = y0 + dy * t;
const w = randomGaussian(0, twitch) + (noise(x * 0.014, y * 0.014) - 0.5) * drift;
out.push([x + nx * w, y + ny * w]);
}
}
out.push(pts[pts.length - 1]);
return out;
}
// one pass of the line, from index a to index b, pressure wavering as it goes
function pass(pts, a, b, name, col, weight, press, curve) {
const hi = min(b, pts.length - 1);
if (hi - a < 2) return;
brush.noFill();
brush.set(name, col, weight);
brush.beginShape(curve);
for (let i = a; i <= hi; i++) {
const t = (i - a) / max(1, hi - a);
const p = press * (0.78 + 0.34 * sin(t * 3.1)) + randomGaussian(0, 0.09);
brush.vertex(pts[i][0], pts[i][1], constrain(p, 0.25, 1.5));
}
brush.endShape(false);
}
function span(pts, fa, fb) {
return [floor(pts.length * fa), floor(pts.length * fb)];
}
// a loop of the tangle: slightly more than one turn, radius breathing, ends left open
function whorl(cx, cy, rx, ry, rot, name, col, weight, turn) {
brush.noFill();
brush.set(name, col, weight);
brush.beginShape(0.62);
const n = 13;
for (let i = 0; i <= n; i++) {
const a = rot + (i / n) * turn;
const g = 1 + 0.14 * sin(a * 1.7) + randomGaussian(0, 0.045);
const px = cx + Math.cos(a) * rx * g + randomGaussian(0, 2.2);
const py = cy + Math.sin(a) * ry * g + randomGaussian(0, 2.2);
brush.vertex(px, py, constrain(randomGaussian(0.95, 0.16), 0.4, 1.4));
}
brush.endShape(false);
}
// where the hand stopped and the pigment pooled
function clot(x, y, r, col, op) {
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.6, 0.55);
brush.fill(col, op);
brush.circle(x + randomGaussian(0, 2), y + randomGaussian(0, 2), r, true);
}
function smudge(x, y, r, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.42, 0.4);
brush.fill(col, op);
brush.circle(x, y, max(18, randomGaussian(r, r * 0.09)), true);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#efe9de");
// ---- 1. smudge: the dirty band the heel of the hand dragged along, heaviest
// under the coming tangle, thinning to nothing at the left edge
smudge(492, 452, 120, "#b6b0a3", 46, 0.34);
smudge(556, 448, 96, "#b6b0a3", 52, 0.3);
smudge(602, 472, 74, "#b6b0a3", 38, 0.3);
smudge(392, 436, 84, "#b6b0a3", 32, 0.32);
smudge(300, 412, 76, "#b6b0a3", 24, 0.36);
smudge(668, 464, 68, "#b6b0a3", 28, 0.3);
smudge(162, 406, 60, "#b6b0a3", 17, 0.38);
smudge(744, 466, 52, "#b6b0a3", 18, 0.32);
smudge(532, 444, 58, "#6c675e", 30, 0.26);
smudge(560, 484, 42, "#6c675e", 22, 0.24);
smudge(414, 426, 38, "#6c675e", 15, 0.26);
// ---- 2. erased attempts, laid down pale and then abandoned
for (let a = 0; a < ATTEMPTS.length; a++) {
const gh = tremor(ATTEMPTS[a], 9, 4.4, 22);
pass(gh, 0, gh.length - 1, "cpencil", "#b6b0a3", 2.2, 0.6, 0.6);
pass(gh, 1, gh.length - 2, "2H", "#b6b0a3", 1.2, 0.5, 0.62);
}
// ---- 3. the line itself: three passes over the same wandering path, each one
// a fresh tremor, so the graphite thickens where the passes happen to agree
const ghost = tremor(WAYPOINTS, 9, 5.2, 26);
const body = tremor(WAYPOINTS, 8, 3.1, 15);
const near = tremor(WAYPOINTS, 7, 1.7, 8);
pass(ghost, 0, ghost.length - 1, "cpencil", "#b6b0a3", 2.6, 0.8, 0.52);
pass(body, 0, body.length - 1, "charcoal", "#6c675e", 1.9, 0.9, 0.55);
pass(near, 0, near.length - 1, "2B", "#6c675e", 1.5, 0.9, 0.58);
// the pressure passes: the hand leans in over the second half and lets go on the
// left, so the graphite is a thread at the start and a rope by the tangle
const dry = tremor(WAYPOINTS, 8, 2.4, 12);
const wet = tremor(WAYPOINTS, 7, 1.5, 7);
const r1 = span(dry, 0.02, 0.09);
const r2 = span(dry, 0.20, 0.29);
const r3 = span(dry, 0.38, 0.47);
const r4 = span(dry, 0.52, 0.74);
const r5 = span(dry, 0.80, 0.94);
pass(dry, r1[0], r1[1], "charcoal", "#6c675e", 1.6, 0.7, 0.5);
pass(dry, r2[0], r2[1], "charcoal", "#211e1a", 1.6, 0.85, 0.5);
pass(dry, r3[0], r3[1], "2B", "#211e1a", 1.9, 0.95, 0.54);
pass(dry, r4[0], r4[1], "charcoal", "#211e1a", 2.3, 1.05, 0.52);
pass(dry, r5[0], r5[1], "2B", "#211e1a", 2.0, 0.9, 0.56);
pass(wet, span(wet, 0.44, 0.62)[0], span(wet, 0.44, 0.62)[1], "rotring", "#211e1a", 1.5, 0.9, 0.55);
pass(wet, span(wet, 0.78, 0.88)[0], span(wet, 0.78, 0.88)[1], "rotring", "#211e1a", 1.2, 0.75, 0.58);
// ---- 4. the tangle on the right thirds line: the line refuses to leave and
// circles itself seven times, each turn a different size and lean
whorl(538, 456, 86, 60, 3.42, "cpencil", "#b6b0a3", 2.6, 5.6);
whorl(526, 444, 70, 46, 0.86, "cpencil", "#6c675e", 2.0, 6.2);
whorl(536, 454, 52, 36, 0.31, "2B", "#6c675e", 1.6, 7.0);
whorl(548, 470, 38, 46, 2.23, "charcoal", "#6c675e", 1.7, 6.8);
whorl(518, 436, 60, 30, 4.26, "2B", "#211e1a", 1.4, 7.4);
whorl(552, 434, 29, 38, 1.08, "charcoal", "#211e1a", 1.8, 7.9);
whorl(522, 474, 34, 25, 5.34, "2B", "#211e1a", 1.5, 6.4);
whorl(560, 456, 20, 24, 1.47, "rotring", "#211e1a", 1.3, 8.2);
whorl(510, 458, 25, 19, 2.94, "rotring", "#211e1a", 1.1, 7.6);
whorl(540, 450, 14, 11, 4.71, "2B", "#211e1a", 1.2, 8.6);
// two long loops swinging out of the knot, one up-left, one down-right
brush.noFill();
brush.set("2B", "#211e1a", 1.5);
brush.beginShape(0.6);
brush.vertex(534, 434, 0.7);
brush.vertex(498, 396, 0.9);
brush.vertex(478, 418, 1.1);
brush.vertex(506, 448, 0.8);
brush.vertex(540, 456, 0.6);
brush.endShape(false);
brush.set("charcoal", "#211e1a", 1.7);
brush.beginShape(0.6);
brush.vertex(546, 472, 0.7);
brush.vertex(572, 512, 1.0);
brush.vertex(600, 494, 0.9);
brush.vertex(578, 462, 0.7);
brush.vertex(552, 458, 0.5);
brush.endShape(false);
// ---- 5. hesitations: where the hand paused, pigment pooled and went dark
clot(556, 472, 3.4, "#211e1a", 130);
clot(514, 430, 3.2, "#211e1a", 118);
clot(352, 458, 5.0, "#211e1a", 140);
clot(194, 384, 4.2, "#211e1a", 126);
clot(604, 484, 4.4, "#211e1a", 130);
clot(120, 364, 3.4, "#6c675e", 120);
clot(678, 514, 3.6, "#6c675e", 116);
clot(730, 494, 3.0, "#6c675e", 104);
// ---- 6. the oxide: one dragged smear beneath the knot and two short crossings
brush.noStroke();
brush.fillBleed(0.22, "out");
brush.fillTexture(0.62, 0.6);
brush.fill("#a4503f", 132);
brush.circle(566, 484, 19, true);
brush.fill("#a4503f", 104);
brush.circle(510, 470, 13, true);
brush.fill("#a4503f", 88);
brush.circle(588, 458, 9, true);
brush.set("2B", "#a4503f", 1.5);
brush.line(508, 424, 542, 452);
brush.set("2B", "#a4503f", 1.2);
brush.line(560, 418, 578, 448);
brush.line(228, 400, 248, 416);
// ---- 7. last darks: three short scratches that tighten the right half
const s1 = tremor([[658, 456], [676, 472], [696, 474]], 6, 1.5, 7);
const s2 = tremor([[738, 474], [754, 460], [772, 462]], 6, 1.3, 6);
const s3 = tremor([[464, 414], [480, 402], [494, 406]], 6, 1.2, 5);
pass(s1, 0, s1.length - 1, "2B", "#211e1a", 1.5, 0.9, 0.62);
pass(s2, 0, s2.length - 1, "2B", "#211e1a", 1.3, 0.8, 0.62);
pass(s3, 0, s3.length - 1, "rotring", "#211e1a", 1.2, 0.75, 0.62);
// and one stray tick high on the left, so the bare paper has a scale
const s4 = tremor([[266, 212], [278, 222], [288, 218]], 5, 1.1, 4);
pass(s4, 0, s4.length - 1, "2H", "#6c675e", 1.0, 0.6, 0.6);
}