Leaving Through the Top
2026 · abstract
The heavy thing is already halfway gone; what stays on the paper is only the argument it had with the air.
a dense mass of ink drifting out of the frame, thinner and thinner marks left behind it
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a dense ink mass climbs out through the top edge left of centre, its heaviest
// dark parked on the upper-left third; the wake thins downward and breaks into a low scatter.
// layering: pale air washes, then stacked bodies of ink along the drift axis, frayed flanks,
// paper laid back in to carve the light, and last the streaks, the wake and the small marks.
// The lower-left, the right edge and the bottom corners stay bare paper.
const PALETTE = [
"#e9e2d1", // ground
"#6b7069", // mist
"#3a352e", // mass
"#141311", // core
"#b2542a", // accent
];
const PAPER = PALETTE[0];
const MIST = PALETTE[1];
const MASS = PALETTE[2];
const CORE = PALETTE[3];
const CINNABAR = PALETTE[4];
// the drift axis. It runs off the top of the canvas, so the mass has somewhere to be going.
const SPINE = [
[358, 414], [336, 336], [312, 258], [292, 178], [274, 94], [254, 6], [234, -80],
];
const GIRTH = [24, 54, 88, 114, 126, 116, 98];
function alongSpine(t) {
const u = constrain(t, 0, 1) * (SPINE.length - 1);
const i = min(floor(u), SPINE.length - 2);
const f = u - i;
return {
x: lerp(SPINE[i][0], SPINE[i + 1][0], f),
y: lerp(SPINE[i][1], SPINE[i + 1][1], f),
r: lerp(GIRTH[i], GIRTH[i + 1], f),
};
}
// no two marks share a pigment: bias toward the mass tone, pull each one slightly to paper or mist
function inkTone(deepBias) {
const r = random();
const base = r < deepBias ? CORE : r < 0.94 ? MASS : MIST;
const towards = random() < 0.5 ? PAPER : MIST;
return lerpColor(color(base), color(towards), constrain(randomGaussian(0.06, 0.06), 0, 0.26));
}
function blob(x, y, r, tone, alpha, bleed, tex) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(tex, 0.55);
brush.fill(tone, constrain(alpha + randomGaussian(0, 14), 8, 235));
brush.circle(x, y, r, true);
}
// one whole body of ink along the axis, laid twice so the pigment actually builds
function massBody(t0, t1, widthF, tone, alpha, bleed, jit, curv, passes) {
const n = 9;
for (let p = 0; p < passes; p++) {
const pts = [];
for (let i = 0; i <= n; i++) {
const s = alongSpine(lerp(t0, t1, i / n));
pts.push([s.x + s.r * widthF * (1 + randomGaussian(0, jit)), s.y + randomGaussian(0, 8)]);
}
for (let i = n; i >= 0; i--) {
const s = alongSpine(lerp(t0, t1, i / n));
pts.push([s.x - s.r * widthF * (1 + randomGaussian(0, jit)), s.y + randomGaussian(0, 8)]);
}
brush.noStroke();
brush.fillBleed(bleed + p * 0.05, "out");
brush.fillTexture(constrain(randomGaussian(0.3, 0.1), 0.1, 0.6), 0.6);
brush.fill(tone, constrain(alpha - p * 14 + randomGaussian(0, 10), 16, 235));
brush.beginShape(curv);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(true);
}
}
// a loaded brush dragged from one point to another, thick end to thin end
function streak(x0, y0, x1, y1, w0, w1, tone, alpha, passes) {
const n = 6;
const ang = Math.atan2(y1 - y0, x1 - x0) + Math.PI / 2;
const px = Math.cos(ang);
const py = Math.sin(ang);
for (let p = 0; p < passes; p++) {
const pts = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const w = lerp(w0, w1, t) * (1 + randomGaussian(0, 0.2));
pts.push([lerp(x0, x1, t) + px * w + randomGaussian(0, 2.2), lerp(y0, y1, t) + py * w + randomGaussian(0, 2.2)]);
}
for (let i = n; i >= 0; i--) {
const t = i / n;
const w = lerp(w0, w1, t) * (1 + randomGaussian(0, 0.2));
pts.push([lerp(x0, x1, t) - px * w + randomGaussian(0, 2.2), lerp(y0, y1, t) - py * w + randomGaussian(0, 2.2)]);
}
brush.noStroke();
brush.fillBleed(0.07 + p * 0.06, "out");
brush.fillTexture(0.26, 0.62);
brush.fill(tone, constrain(alpha - p * 20 + randomGaussian(0, 10), 20, 235));
brush.beginShape(0.5);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(true);
}
}
// the mass frays where it meets the paper instead of stopping; noise reserves veins of light
function edgeRag(count, tMean, tSpread, widthF, sizeF, alpha, deepBias, bleed, lightCut) {
for (let i = 0; i < count; i++) {
const t = constrain(randomGaussian(tMean, tSpread), 0.02, 1);
const s = alongSpine(t);
const side = random() < 0.5 ? -1 : 1;
const x = s.x + side * s.r * (widthF + randomGaussian(0, 0.16));
const y = s.y + randomGaussian(0, 15);
if (noise(x * 0.012, y * 0.012) > lightCut) continue;
const r = constrain(randomGaussian(s.r * sizeF, s.r * sizeF * 0.34), 4, 70);
blob(x, y, r, inkTone(deepBias), alpha, bleed, 0.4);
}
}
function stroke2(x1, y1, x2, y2, name, col, w) {
brush.noFill();
brush.set(name, col, w);
brush.line(x1 + randomGaussian(0, 2), y1 + randomGaussian(0, 2), x2, y2);
}
// the marks the mass leaves behind: specks and short hairs, all falling the same way
function fineMark(x, y, s) {
const r = random();
if (r < 0.3) {
brush.noFill();
brush.set(random() < 0.5 ? "rotring" : "pen", inkTone(0.6), constrain(randomGaussian(0.6, 0.2), 0.2, 1.1));
brush.flowLine(x, y, constrain(randomGaussian(13 * s, 5), 4, 30), randomGaussian(66, 20));
} else if (r < 0.86) {
blob(x, y, constrain(randomGaussian(3.2 * s, 1.3), 0.9, 9), inkTone(0.7),
constrain(randomGaussian(200, 26), 90, 235), 0.14, 0.3);
} else {
const a = randomGaussian(1.2, 0.34);
const l = constrain(randomGaussian(10 * s, 4), 3, 21);
stroke2(x, y, x + Math.cos(a) * l, y + Math.sin(a) * l, "2B", inkTone(0.7), constrain(randomGaussian(1.1, 0.3), 0.35, 2.0));
}
}
function scatter(cx, cy, sx, sy, count, s) {
for (let i = 0; i < count; i++) {
const x = randomGaussian(cx, sx);
const y = randomGaussian(cy, sy);
if (x < 130 || x > 726 || y < 452 || y > 712) continue;
fineMark(x, y, s * (0.7 + random(0, 0.75)));
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// --- the air the mass is moving through: a few enormous pale breaths, top-heavy
blob(276, 108, 250, lerpColor(color(MIST), color(PAPER), 0.34), 24, 0.78, 0.45);
blob(378, 236, 176, lerpColor(color(MIST), color(PAPER), 0.5), 17, 0.82, 0.45);
blob(174, 196, 158, lerpColor(color(MIST), color(PAPER), 0.42), 20, 0.72, 0.5);
blob(330, 402, 118, lerpColor(color(MIST), color(PAPER), 0.54), 15, 0.7, 0.5);
blob(462, 566, 138, lerpColor(color(MIST), color(PAPER), 0.76), 10, 0.84, 0.5);
// --- the mass, back to front: whole bodies of ink, each smaller and heavier than the last
massBody(0.14, 1.0, 1.04, MASS, 108, 0.26, 0.16, 0.5, 2);
massBody(0.2, 1.0, 0.8, MASS, 152, 0.18, 0.14, 0.45, 2);
massBody(0.1, 0.88, 0.58, lerpColor(color(MASS), color(CORE), 0.55), 192, 0.12, 0.15, 0.5, 2);
massBody(0.26, 0.98, 0.44, CORE, 208, 0.09, 0.16, 0.42, 2);
massBody(0.3, 0.64, 0.34, CORE, 226, 0.06, 0.14, 0.5, 2);
massBody(0.46, 0.94, 0.22, CORE, 218, 0.07, 0.18, 0.4, 2);
massBody(0.06, 0.42, 0.3, lerpColor(color(MASS), color(CORE), 0.6), 186, 0.14, 0.2, 0.55, 2);
// --- the frayed skirt: pigment thrown off both flanks, heaviest low where the weight is
edgeRag(26, 0.56, 0.3, 1.0, 0.3, 128, 0.25, 0.34, 0.68);
edgeRag(18, 0.4, 0.2, 0.8, 0.24, 176, 0.55, 0.24, 0.64);
edgeRag(12, 0.3, 0.14, 0.58, 0.2, 208, 0.8, 0.16, 0.6);
// --- dense streaks leaving through the top edge: the mass is already half gone
streak(232, 300, 200, -60, 13, 30, CORE, 224, 2);
streak(300, 250, 322, -50, 8, 21, CORE, 214, 2);
streak(190, 214, 152, -30, 6, 15, MASS, 196, 2);
streak(348, 208, 372, -40, 5, 13, CORE, 188, 2);
// --- paper laid back over the ink: the light lives inside the mass, not around it
brush.noStroke();
brush.fillBleed(0.36, "out");
brush.fillTexture(0.45, 0.5);
brush.fill(PAPER, 132);
brush.beginShape(0.6);
brush.vertex(266, 132);
brush.vertex(246, 186);
brush.vertex(266, 224);
brush.vertex(296, 190);
brush.endShape(true);
brush.fill(PAPER, 108);
brush.beginShape(0.6);
brush.vertex(216, 54);
brush.vertex(196, 112);
brush.vertex(236, 132);
brush.endShape(true);
brush.fill(PAPER, 96);
brush.beginShape(0.55);
brush.vertex(344, 268);
brush.vertex(316, 302);
brush.vertex(350, 318);
brush.endShape(true);
brush.fill(PAPER, 90);
brush.circle(288, 66, 17, true);
// --- brush logic inside the mass: short decisive marks, not a haze
stroke2(272, 214, 300, 270, "2B", CORE, 3.0);
stroke2(240, 172, 262, 232, "2B", CORE, 2.2);
stroke2(306, 188, 288, 248, "pen", CORE, 1.8);
stroke2(320, 246, 350, 292, "2B", MASS, 1.5);
stroke2(214, 126, 246, 174, "2B", MASS, 2.0);
stroke2(198, 248, 238, 276, "pen", CORE, 1.6);
stroke2(292, 104, 304, 158, "2B", MASS, 2.4);
stroke2(344, 154, 328, 208, "pen", MASS, 1.3);
stroke2(256, 292, 294, 312, "2B", CORE, 2.0);
stroke2(178, 188, 210, 216, "pen", MASS, 1.2);
// --- the wake: streaks off the underside, each shorter and thinner than the one above
streak(354, 402, 402, 500, 9, 3.4, CORE, 210, 2);
streak(330, 396, 356, 470, 6, 2.4, CORE, 190, 2);
streak(386, 438, 442, 528, 5, 1.8, MASS, 176, 2);
streak(404, 480, 462, 552, 3.4, 1.3, CORE, 158, 1);
streak(432, 512, 486, 574, 2.6, 1.0, MASS, 140, 1);
streak(300, 384, 318, 442, 3.6, 1.2, MASS, 152, 1);
// --- and the last of it, as thin pen: the wake fading into single hairs
brush.field("hand");
for (let i = 0; i < 16; i++) {
const t = i / 15;
brush.noFill();
brush.set(random() < 0.5 ? "pen" : "rotring", inkTone(0.5),
constrain(randomGaussian(0.85 - 0.5 * t, 0.18), 0.2, 1.3));
brush.flowLine(
randomGaussian(346 + 152 * t, 26),
randomGaussian(414 + 148 * t, 22),
constrain(randomGaussian(62 - 34 * t, 14), 10, 92),
randomGaussian(70 - 16 * t, 14)
);
}
brush.noField();
// --- the scatter in the bottom third: one real cluster, then thinning outward
scatter(468, 574, 74, 36, 32, 1.05);
scatter(586, 636, 46, 26, 15, 0.85);
scatter(354, 600, 50, 32, 11, 0.7);
scatter(636, 686, 28, 18, 5, 0.55);
// --- the last darks, laid on top so they hold the picture down
blob(280, 248, 24, CORE, 232, 0.12, 0.35);
blob(256, 200, 17, CORE, 216, 0.12, 0.35);
blob(304, 278, 13, CORE, 196, 0.14, 0.4);
blob(234, 268, 10, CORE, 176, 0.16, 0.4);
stroke2(266, 234, 298, 264, "2B", CORE, 3.6);
stroke2(246, 260, 268, 226, "2B", CORE, 2.8);
stroke2(288, 296, 264, 322, "pen", CORE, 1.6);
// --- cinnabar, rationed: one warm ember in the core, two carried off in the wake
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.4, 0.5);
brush.fill(CINNABAR, 186);
brush.circle(300, 220, 9, true);
brush.fill(CINNABAR, 148);
brush.circle(452, 558, 4.4, true);
brush.fill(CINNABAR, 112);
brush.circle(590, 646, 2.9, true);
}