One Loaded Swipe
2026 · gesture
A mark cannot be corrected, only spent: the arm decides everything at the start and the paper keeps the record of the pigment thinning out.
a single broad brush swipe from the lower-left corner up into the upper-right third, wet and pooling at the heel, running dry and breaking apart at the tip, on bare warm paper
painted by GPT-5.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: one broad loaded swipe leaves the lower-left corner and climbs to the upper-right third, thinning as the pigment gives out; nothing else but paper
// layering: two faint paper stains, then the wet body of the mark as bleeding washes (darkest at the loaded root), bristle striations over it, and the driest flecks and hardest darks last
// the empty upper-left and lower-right thirds are the picture's other half — they stay bare
const PALETTE = [
"#e9e3d6", // paper — warm, dry, untouched over most of the sheet
"#1e1c1a", // ink — the mark itself
"#3b352c", // dilute — where the load thins and the paper shows through
"#141a1e", // cool black — the pooled, still-wet root
"#8c4a26", // sienna — pigment separating at the wet edge, rationed
];
// hand-set spine of the swipe: off the lower-left corner, one whole-arm arc, no correction
const SPINE = [
[-52, 856],
[86, 748],
[214, 634],
[346, 520],
[462, 418],
[560, 336],
[648, 268],
[724, 214],
];
// half-width of the loaded brush along that arc — fat at the heel, spent at the tip
const HALFS = [86, 80, 73, 64, 55, 44, 33, 22];
const STEPS = 16;
function ribbon() {
const xs = [];
const ys = [];
const hs = [];
for (let i = 0; i < SPINE.length - 1; i++) {
for (let k = 0; k < STEPS; k++) {
const t = k / STEPS;
xs.push(lerp(SPINE[i][0], SPINE[i + 1][0], t));
ys.push(lerp(SPINE[i][1], SPINE[i + 1][1], t));
hs.push(lerp(HALFS[i], HALFS[i + 1], t));
}
}
xs.push(SPINE[SPINE.length - 1][0]);
ys.push(SPINE[SPINE.length - 1][1]);
hs.push(HALFS[HALFS.length - 1]);
const nx = [];
const ny = [];
for (let i = 0; i < xs.length; i++) {
const a = max(0, i - 1);
const b = min(xs.length - 1, i + 1);
const dx = xs[b] - xs[a];
const dy = ys[b] - ys[a];
const m = Math.hypot(dx, dy) || 1;
nx.push(-dy / m);
ny.push(dx / m);
}
return { xs, ys, hs, nx, ny, n: xs.length };
}
// a point across the band: u in [-1, 1], t in [0, 1], with a slow drift so no edge is straight
function across(R, t, u) {
const i = constrain(floor(t * (R.n - 1)), 0, R.n - 1);
const drift = (noise(u * 1.7 + 11.2, t * 2.6) - 0.5) * 13;
const d = u * R.hs[i] + drift;
return [R.xs[i] + R.nx[i] * d, R.ys[i] + R.ny[i] * d];
}
// ink drawn from the palette with weight, jittered so no two marks share a colour
function inkOf(t) {
const r = random();
let base;
if (r < 0.62) base = "#1e1c1a";
else if (r < 0.82) base = "#3b352c";
else if (r < 0.97) base = "#141a1e";
else base = "#8c4a26";
const wash = constrain(randomGaussian(0.06 + t * 0.3, 0.07), 0, 0.62);
return lerpColor(color(base), color("#e9e3d6"), wash);
}
// the wet body: one bleeding wash along the band, truncated so each pass is shorter than the last
function bodyWash(R, tEnd, inset, tone, opacity, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.62, 0.48);
brush.fill(tone, opacity);
brush.beginShape(0.15);
for (let s = 0; s <= 12; s++) {
const t = (s / 12) * tEnd;
const p = across(R, t, -inset + randomGaussian(0, 0.03));
brush.vertex(p[0], p[1]);
}
for (let s = 12; s >= 0; s--) {
const t = (s / 12) * tEnd;
const p = across(R, t, inset + randomGaussian(0, 0.03));
brush.vertex(p[0], p[1]);
}
brush.endShape(true);
}
// one bristle track: a hair of the brush, ending where its share of the load runs out
function bristle(R, u, t0, t1, name, weight, tone) {
if (t1 - t0 < 0.035) return;
brush.noFill();
brush.set(name, tone, weight);
brush.beginShape(0.4);
const segs = 11;
for (let s = 0; s <= segs; s++) {
const t = lerp(t0, t1, s / segs);
const p = across(R, t, u + (noise(u * 3.1 + 4.4, t * 3.8) - 0.5) * 0.09);
const press = constrain(1.15 - t * 0.75 + randomGaussian(0, 0.07), 0.18, 1.25);
brush.vertex(p[0], p[1], press);
}
brush.endShape(false);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e9e3d6");
const R = ribbon();
// two faint stains in the paper itself, well away from the mark, so the ground is not a flat field
brush.noStroke();
brush.fillBleed(0.7, "out");
brush.fillTexture(0.35, 0.35);
brush.fill("#3b352c", 11);
brush.circle(228, 196, 214, true);
brush.fill("#8c4a26", 8);
brush.circle(650, 690, 168, true);
// ---- the wet body, back to front: the whole length pale, then the loaded half, then the heel
bodyWash(R, 1.0, 0.94, "#3b352c", 44, 0.52);
bodyWash(R, 0.86, 0.9, "#1e1c1a", 58, 0.44);
bodyWash(R, 0.62, 0.86, "#1e1c1a", 74, 0.36);
bodyWash(R, 0.34, 0.82, "#141a1e", 96, 0.3);
bodyWash(R, 0.16, 0.76, "#141a1e", 118, 0.26);
// ---- bristle striations across the band; the centre hairs carry pigment furthest
const HAIRS = 58;
for (let b = 0; b < HAIRS; b++) {
const u = ((b + 0.5) / HAIRS) * 2 - 1 + randomGaussian(0, 0.012);
const centred = 1 - abs(u);
const reach = constrain(randomGaussian(0.72 + 0.3 * centred, 0.13), 0.22, 1.0);
const t0 = constrain(abs(randomGaussian(0, 0.035)), 0, 0.12);
const tone = inkOf(reach * 0.7);
const heavy = random() < 0.34;
const name = heavy ? "charcoal" : random() < 0.6 ? "2B" : "cpencil";
const weight = constrain(randomGaussian(heavy ? 1.7 : 1.05, 0.3), 0.5, 2.4);
// past the halfway point the load starts skipping — the hair breaks into two dry runs
if (reach > 0.58 && random() < 0.62) {
const gap0 = constrain(randomGaussian(0.55, 0.09), 0.4, 0.78);
const gap1 = constrain(gap0 + abs(randomGaussian(0.07, 0.04)), gap0 + 0.02, 0.94);
bristle(R, u, t0, gap0, name, weight, tone);
if (gap1 < reach) bristle(R, u, gap1, reach, name, weight * 0.8, inkOf(0.55));
} else {
bristle(R, u, t0, reach, name, weight, tone);
}
}
// ---- the loaded heel: a handful of fat, wet strokes that only exist in the first third
bristle(R, -0.62, 0.0, 0.3, "marker", 2.3, "#141a1e");
bristle(R, -0.28, 0.0, 0.37, "marker", 2.5, "#1e1c1a");
bristle(R, 0.06, 0.02, 0.44, "marker", 2.6, "#141a1e");
bristle(R, 0.41, 0.0, 0.33, "marker", 2.2, "#1e1c1a");
bristle(R, 0.74, 0.0, 0.21, "marker", 1.9, "#3b352c");
bristle(R, -0.86, 0.0, 0.14, "marker", 1.6, "#1e1c1a");
// pooling where the brush first bit the paper — off the corner, half off the sheet
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.7, 0.6);
brush.fill("#141a1e", 150);
brush.circle(14, 806, 62, true);
brush.fill("#141a1e", 128);
brush.circle(72, 762, 48, true);
brush.fill("#1e1c1a", 112);
brush.circle(128, 716, 39, true);
brush.fill("#141a1e", 96);
brush.circle(46, 728, 30, true);
// sienna separating out at the wet lower edge, three touches only
brush.fillBleed(0.6, "out");
brush.fill("#8c4a26", 92);
brush.circle(150, 748, 22, true);
brush.fill("#8c4a26", 74);
brush.circle(268, 640, 15, true);
brush.fill("#8c4a26", 58);
brush.circle(58, 848, 18, true);
// ---- the dry tail: broken dashes riding the band's direction past where the wash stops
for (let d = 0; d < 46; d++) {
const t = constrain(randomGaussian(0.86, 0.1), 0.6, 1.08);
const u = constrain(randomGaussian(0, 0.52), -1.15, 1.15);
const a = across(R, min(t, 1), u);
const dir = Math.atan2(SPINE[7][1] - SPINE[6][1], SPINE[7][0] - SPINE[6][0]);
const len = constrain(randomGaussian(26, 12), 5, 58);
const spread = t > 1 ? (t - 1) * 340 : 0;
const x0 = a[0] + Math.cos(dir) * spread;
const y0 = a[1] + Math.sin(dir) * spread;
brush.noFill();
brush.set(random() < 0.5 ? "2B" : "cpencil", inkOf(0.45 + (t - 0.6) * 0.5), constrain(randomGaussian(0.9, 0.3), 0.35, 1.7));
brush.line(x0, y0, x0 + Math.cos(dir) * len, y0 + Math.sin(dir) * len);
}
// a few last specks thrown off the tip, spent pigment on dry paper
brush.noStroke();
brush.fillBleed(0.2, "out");
for (let s = 0; s < 14; s++) {
const t = constrain(randomGaussian(0.98, 0.06), 0.82, 1.12);
const u = constrain(randomGaussian(0, 0.7), -1.4, 1.4);
const p = across(R, min(t, 1), u);
const push = t > 1 ? (t - 1) * 320 : 0;
brush.fill(random() < 0.9 ? "#1e1c1a" : "#8c4a26", constrain(randomGaussian(120, 30), 50, 190));
brush.circle(p[0] + push * 0.86, p[1] - push * 0.5, constrain(randomGaussian(3.2, 1.4), 1, 6.5), true);
}
// ---- the hardest darks, laid last, holding the lower third down
brush.noFill();
brush.set("charcoal", "#141a1e", 2.6);
brush.beginShape(0.4);
brush.vertex(-8, 824);
brush.vertex(74, 764);
brush.vertex(168, 686);
brush.vertex(262, 604);
brush.endShape(false);
brush.set("charcoal", "#1e1c1a", 2.1);
brush.beginShape(0.4);
brush.vertex(46, 848);
brush.vertex(142, 764);
brush.vertex(236, 682);
brush.endShape(false);
brush.set("2B", "#1e1c1a", 1.4);
brush.line(300, 572, 372, 508);
brush.line(328, 588, 396, 528);
brush.line(392, 486, 448, 438);
// the upper edge of the mark, drawn once, dry and skipping
brush.set("2B", "#3b352c", 1.1);
brush.beginShape(0.35);
brush.vertex(210, 556);
brush.vertex(342, 448);
brush.vertex(452, 356);
brush.vertex(548, 282);
brush.endShape(false);
}