Marks While Tilted
2026 · gesture
I decided where the marks went; gravity decided how long each one lasted, and it kept working after I stopped.
a packed band of swung oxide-red swipes across the top third of warm paper, every one shedding a run of pigment straight down into empty sheet
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: a packed band of swung, arcing swipes across the top third, densest and darkest
// at the third-point near (548, 250), running off both side edges; each wet stroke sheds a run
// straight down into bare sheet. the lower-left half of the paper is never touched.
// layering: sizing stains, wet swipes back-to-front, overlap bruises, the runs, then dry
// near-black drags and the last long hairlines on top.
const PALETTE = [
"#eae2d3", // ground — warm sized paper, propped at a tilt
"#7e2617", // mass — oxide red, the wet body of every stroke
"#4d1c18", // shadow — where two loaded strokes crossed and stayed wet
"#3a1109", // ink — dry warm near-black, laid last
"#8a7a63", // drip — exhausted pigment at the tail of a run
"#cf5f26", // accent — one hot orange, rationed
];
// quadratic through a start, a pulled midpoint and an end — the shape of a swung arm
function arcPoint(p, t) {
const cx = 2 * p[2] - 0.5 * (p[0] + p[4]);
const cy = 2 * p[3] - 0.5 * (p[1] + p[5]);
const u = 1 - t;
return [
u * u * p[0] + 2 * u * t * cx + t * t * p[4],
u * u * p[1] + 2 * u * t * cy + t * t * p[5],
];
}
// a soft ground stain: pigment that soaked in before it could travel anywhere
function stain(x, y, r, col, op) {
brush.noStroke();
brush.fillBleed(0.64, "out");
brush.fillTexture(0.32, 0.4);
brush.fill(col, op);
brush.circle(x, y, Math.max(24, randomGaussian(r, r * 0.1)), true);
}
// one swipe: loaded at the start, drying out along the arc, with bristle drag over the top
function swipe(p, w0, w1, col, op) {
brush.noStroke();
brush.fillBleed(0.42 + random(-0.06, 0.12), "out");
brush.fillTexture(0.58, 0.42);
for (let pass = 0; pass < 2; pass++) {
brush.fill(col, pass === 0 ? op : op * 0.52);
brush.beginShape(0.24);
for (let side = 0; side < 2; side++) {
for (let i = 0; i <= 5; i++) {
const t = side === 0 ? i / 5 : 1 - i / 5;
const a = arcPoint(p, t);
const b = arcPoint(p, Math.min(0.999, t + 0.02));
const ang = Math.atan2(b[1] - a[1], b[0] - a[0]);
const hw =
lerp(w0, w1, pow(t, 0.72)) *
(0.46 + 0.54 * Math.sin(Math.PI * (0.2 + 0.76 * t))) *
(1 + random(-0.14, 0.1));
const s = side === 0 ? -1 : 1;
brush.vertex(a[0] - Math.sin(ang) * hw * s, a[1] + Math.cos(ang) * hw * s);
}
}
brush.endShape(true);
}
brush.noFill();
const bristles = 3 + floor(random(0, 3));
for (let b = 0; b < bristles; b++) {
const off = randomGaussian(0, w0 * 0.36);
brush.set(random() < 0.5 ? "charcoal" : "2B", random() < 0.3 ? "#4d1c18" : col, 1.3 + random(0, 1.2));
brush.beginShape(0.3);
for (let i = 0; i <= 4; i++) {
const t = i / 4;
const a = arcPoint(p, t);
const bb = arcPoint(p, Math.min(0.999, t + 0.02));
const ang = Math.atan2(bb[1] - a[1], bb[0] - a[0]);
const d = off * (1 - 0.4 * t) + randomGaussian(0, 3);
brush.vertex(a[0] - Math.sin(ang) * d, a[1] + Math.cos(ang) * d, 1 - 0.5 * t);
}
brush.endShape(false);
}
}
// the bead that gathers and stops at the bottom of a run
function bead(x, y, r, col, op) {
brush.noStroke();
brush.fillBleed(0.38, "out");
brush.fillTexture(0.5, 0.6);
brush.fill(col, op);
brush.circle(x, y, Math.max(2, randomGaussian(r, r * 0.2)), true);
}
// a run: pigment leaving the stroke and travelling straight down, thinning as it goes.
// beads its own tail so nothing is ever left floating in the empty paper.
function run(x, y, len, col, w, drift, beadChance) {
brush.noFill();
brush.set(w > 2 ? "charcoal" : random() < 0.5 ? "2B" : "pen", col, w);
brush.beginShape(0.5);
let ex = x;
for (let i = 0; i <= 7; i++) {
const t = i / 7;
const wob = (noise(x * 0.015, y * 0.009 + t * 2.4) - 0.5) * drift * (0.35 + t);
ex = x + wob + drift * 0.22 * t;
brush.vertex(ex, y + len * t, Math.max(0.24, 1 - 0.62 * pow(t, 0.8)));
}
brush.endShape(false);
if (random() < beadChance) {
bead(ex, y + len + random(1, 4), Math.max(2, w * 1.25 + random(0, 1.1)), col, 138 + random(-22, 42));
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#eae2d3");
// sizing stains: pigment that soaked in before gravity took it
stain(552, 202, 168, "#7e2617", 26);
stain(672, 254, 132, "#4d1c18", 22);
stain(326, 162, 118, "#7e2617", 18);
stain(152, 224, 88, "#8a7a63", 14);
stain(600, 392, 106, "#7e2617", 13);
stain(452, 320, 78, "#4d1c18", 12);
// ---- the band: swung arcs, thin at the left, packed and dark at the right third ----
swipe([-48, 148, 92, 190, 232, 154], 15, 6, "#7e2617", 116);
swipe([44, 268, 156, 250, 274, 262], 12, 5, "#8a7a63", 92);
swipe([102, 196, 254, 232, 408, 168], 20, 8, "#7e2617", 148);
swipe([186, 74, 320, 106, 462, 66], 17, 6, "#4d1c18", 152);
swipe([234, 158, 380, 212, 520, 208], 23, 8, "#7e2617", 168);
swipe([322, 42, 434, 66, 552, 38], 13, 5, "#7e2617", 132);
swipe([278, 96, 442, 152, 608, 138], 30, 11, "#7e2617", 186);
swipe([336, 246, 512, 226, 700, 190], 38, 13, "#7e2617", 206);
swipe([398, 118, 522, 152, 654, 88], 22, 8, "#4d1c18", 178);
swipe([424, 284, 566, 300, 712, 254], 21, 8, "#4d1c18", 190);
swipe([482, 96, 620, 148, 758, 172], 26, 9, "#7e2617", 194);
swipe([508, 178, 656, 220, 812, 226], 34, 12, "#7e2617", 212);
swipe([456, 256, 640, 286, 846, 282], 24, 9, "#7e2617", 172);
swipe([596, 108, 712, 130, 830, 96], 16, 6, "#4d1c18", 158);
swipe([550, 214, 700, 232, 848, 188], 18, 7, "#4d1c18", 182);
swipe([614, 300, 706, 268, 796, 306], 14, 6, "#4d1c18", 146);
// two short heavy ones straight through the core, so the focal reads as one weight
swipe([492, 230, 576, 258, 664, 238], 21, 8, "#4d1c18", 204);
swipe([528, 252, 608, 278, 692, 262], 17, 7, "#7e2617", 200);
// flung flecks — the tail end of each swing, atomised
brush.noStroke();
brush.fillBleed(0.32, "out");
brush.fillTexture(0.66, 0.55);
for (let i = 0; i < 26; i++) {
const x = constrain(randomGaussian(536, 186), 60, 792);
const y = constrain(randomGaussian(190, 76), 24, 312);
const r = Math.max(2.4, randomGaussian(7.2, 3.2));
const hot = random() < 0.08;
brush.fill(hot ? "#cf5f26" : random() < 0.66 ? "#7e2617" : "#4d1c18", 132 + random(-34, 62));
brush.circle(x, y, r, true);
}
// overlap bruises: two loaded strokes crossing, pooling darker
brush.fillBleed(0.46, "out");
brush.fillTexture(0.4, 0.45);
brush.fill("#4d1c18", 128);
brush.circle(572, 232, 40, true);
brush.fill("#4d1c18", 112);
brush.circle(646, 246, 33, true);
brush.fill("#4d1c18", 96);
brush.circle(486, 208, 29, true);
brush.fill("#3a1109", 148);
brush.circle(608, 262, 24, true);
brush.fill("#3a1109", 122);
brush.circle(568, 250, 20, true);
brush.fill("#4d1c18", 74);
brush.circle(374, 186, 22, true);
brush.fill("#4d1c18", 62);
brush.circle(714, 216, 19, true);
brush.fill("#3a1109", 58);
brush.circle(538, 268, 17, true);
// a few runs that started high inside the band and travelled through it
run(536, 150, 430, "#7e2617", 1.8, 7, 1);
run(606, 136, 342, "#4d1c18", 1.5, -6, 1);
run(462, 174, 274, "#7e2617", 1.4, 6, 1);
run(702, 162, 212, "#4d1c18", 1.2, -5, 1);
run(354, 120, 156, "#8a7a63", 1.0, 4, 0.6);
// ---- the runs: eleven heavy ones in clumps, hand-placed under the wettest crossings ----
run(498, 262, 118, "#7e2617", 2.7, 9, 1); // a short one, stopped almost at once
run(510, 268, 372, "#7e2617", 2.2, 11, 1);
run(520, 264, 214, "#4d1c18", 1.9, -6, 1);
run(562, 272, 428, "#7e2617", 3.7, 12, 1); // the heaviest three, right at the focal crossing
run(572, 282, 536, "#7e2617", 3.0, -9, 1);
run(590, 288, 316, "#4d1c18", 3.2, 7, 1);
run(648, 276, 258, "#7e2617", 2.6, -8, 1);
run(660, 282, 466, "#4d1c18", 2.1, 6, 1);
run(722, 302, 176, "#7e2617", 2.0, -7, 1);
run(736, 296, 344, "#7e2617", 1.6, -11, 1);
run(412, 240, 202, "#7e2617", 2.3, 8, 1); // one alone, out to the left of the clumps
// the field of lesser runs — most of the band leaks a little, the left end barely at all
for (let i = 0; i < 38; i++) {
const x = constrain(randomGaussian(556, 168), 104, 792);
const y = constrain(randomGaussian(240, 60), 112, 306);
const reach = constrain(1 - Math.abs(x - 588) / 470, 0.18, 1);
const len = Math.max(22, randomGaussian(132 * reach + 30, 68));
const pale = random() < 0.3;
run(
x,
y,
len,
pale ? "#8a7a63" : random() < 0.72 ? "#7e2617" : "#4d1c18",
Math.max(0.7, randomGaussian(1.5, 0.5)),
randomGaussian(0, 7),
0.28,
);
}
// two strays out at the thin left end, so the band is not a wall
run(226, 214, 88, "#8a7a63", 1.1, 5, 0.5);
run(168, 258, 56, "#7e2617", 1.0, -4, 0.4);
// ---- accent: one hot orange stroke and the two runs it sheds ----
swipe([590, 224, 632, 240, 676, 232], 9, 4, "#cf5f26", 218);
run(624, 246, 244, "#cf5f26", 2.4, 8, 1);
run(656, 240, 126, "#cf5f26", 1.4, -5, 1);
// spray thrown clear of the band on the follow-through
brush.noFill();
brush.set("spray", "#7e2617", 1.6);
brush.line(620, 308, 672, 396);
brush.set("spray", "#4d1c18", 1.3);
brush.line(526, 300, 484, 380);
brush.set("spray", "#7e2617", 1.1);
brush.line(716, 318, 764, 378);
brush.set("spray", "#8a7a63", 0.9);
brush.line(412, 262, 374, 322);
// ---- last, the dry darks: hard short drags that hold the focal crossing down ----
brush.set("marker", "#3a1109", 3.6);
brush.beginShape(0.28);
brush.vertex(468, 236, 1);
brush.vertex(560, 258, 0.85);
brush.vertex(636, 246, 0.35);
brush.endShape(false);
brush.set("marker", "#3a1109", 2.7);
brush.beginShape(0);
brush.vertex(676, 214, 0.9);
brush.vertex(788, 236, 0.4);
brush.endShape(false);
brush.set("2B", "#3a1109", 2.4);
brush.beginShape(0);
brush.vertex(374, 150, 0.9);
brush.vertex(464, 176, 0.4);
brush.endShape(false);
brush.set("marker", "#3a1109", 3.0);
brush.beginShape(0);
brush.vertex(556, 282, 1);
brush.vertex(636, 292, 0.6);
brush.endShape(false);
run(578, 290, 274, "#3a1109", 2.6, -8, 1);
brush.set("rotring", "#3a1109", 1.4);
brush.line(596, 292, 664, 282);
brush.line(292, 210, 350, 200);
brush.set("2H", "#3a1109", 1.1);
brush.line(212, 128, 164, 138);
// three hairline runs, near-black and very long, drawn over everything
run(570, 272, 496, "#3a1109", 1.2, 6, 1);
run(628, 292, 396, "#3a1109", 0.95, -8, 1);
run(466, 250, 302, "#3a1109", 0.85, 5, 1);
}