Where the Plane Turns
2026 · abstract
A single event given the whole field: one plane, one turn, and the long wait of empty paper on either side of it.
one broad vertical mass standing just left of the right thirds line, folding once through the picture, its surface darkening where it turns, the rest of the paper left open
painted by GPT-6 Astrap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: one broad vertical mass standing just left of the right thirds line, folding
// once where the crease crosses (533, 533) — the silhouette steps right below the turn and the
// surface darkens into it. The left two thirds of the paper stay open and unpainted.
// layering: pale washes seat the plane, accumulated vertical grain builds it, hatch pools the
// shadow under the crease, then the crease ink, one lifted highlight and a rationed ember last.
const PALETTE = [
"#e8e1d1", // ground — warm limestone paper
"#b4a288", // mass — the lit face of the plane
"#8a7659", // turn — the same surface swinging away
"#332d24", // shadow — warm near-black gathering in the crease
"#a2452b", // accent — burnt ember, rationed to the fold
];
const [PAPER, MASS, TURN, SHADOW, EMBER] = PALETTE;
// ---------------------------------------------------------------- the fold, as geometry
// the crease: one shallow diagonal, falling left to right through the thirds intersection
function foldY(x) {
return 533 + (x - 533) * 0.35;
}
// the mass has two silhouettes: above the crease it stands plumb, below it leans right
function leftEdge(y) {
const wob = (noise(y * 0.0042, 3.1) - 0.5) * 13;
if (y < foldY(420)) return 412 + (y + 60) * 0.02 + wob;
return 442 + (y - 493) * 0.03 + wob;
}
function rightEdge(y) {
const wob = (noise(y * 0.0037, 9.4) - 0.5) * 11;
if (y < foldY(578)) return 572 + (y + 60) * 0.022 + wob;
return 598 + (y - 548) * 0.04 + wob;
}
// how dark the surface reads: pale high up, deepest just under the turn, releasing slowly
// below it — and across the width, the two planes shade in opposite directions, which is the
// whole argument of the picture. The lit side changes when the surface turns.
function shade(x, y) {
const l = leftEdge(y);
const r = rightEdge(y);
const across = constrain((x - l) / max(24, r - l), 0, 1);
const d = y - foldY(x);
let s;
if (d < 0) s = map(d, -640, -8, 0.11, 0.54) + pow(across, 1.5) * 0.17;
else if (d < 46) s = map(d, 0, 46, 0.9, 1) - across * 0.06;
else s = map(d, 46, 430, 1, 0.56) - pow(across, 1.4) * 0.085;
return constrain(s, 0.08, 1);
}
// one pigment, walked from lit clay through the turn into the crease black
function tone(t) {
const s = constrain(t + randomGaussian(0, 0.055), 0, 1);
const base = s < 0.55
? lerpColor(color(MASS), color(TURN), s / 0.55)
: lerpColor(color(TURN), color(SHADOW), (s - 0.55) / 0.45);
return lerpColor(base, color(PAPER), constrain(randomGaussian(0.05, 0.05), 0, 0.2));
}
function pickGrain() {
const r = random();
return r < 0.78 ? "cpencil" : r < 0.95 ? "2B" : "charcoal";
}
// ---------------------------------------------------------------- washes and grain
// a wash that follows the whole silhouette, run off the top and bottom edges
function planeWash(yTop, yBot, inset, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.5, 0.5);
brush.fill(col, op);
brush.beginShape(0.15);
for (let y = yTop; y <= yBot; y += 58) {
brush.vertex(leftEdge(y) + inset + randomGaussian(0, 3.4), y);
}
for (let y = yBot; y >= yTop; y -= 58) {
brush.vertex(rightEdge(y) - inset + randomGaussian(0, 3.4), y);
}
brush.endShape(true);
}
// vertical pulls that stop dead at the crease — the grain never crosses the turn
function grain(count, yMean, ySpread, lenMean, lenSpread, wMean, spill) {
for (let i = 0; i < count; i++) {
let y0 = randomGaussian(yMean, ySpread);
let y1 = y0 + max(24, randomGaussian(lenMean, lenSpread));
const ym = (y0 + y1) / 2;
const l = leftEdge(ym);
const r = rightEdge(ym);
const x = constrain(randomGaussian((l + r) / 2, (r - l) * 0.36), l - spill, r + spill);
const f = foldY(x);
if (y0 < f && y1 > f) {
if (ym < f) y1 = f - random(0, 5);
else y0 = f + random(1, 6);
}
if (y1 - y0 < 14) continue;
brush.set(pickGrain(), tone(shade(x, ym)), max(0.7, randomGaussian(wMean, 0.5)));
brush.line(x + randomGaussian(0, 1.5), y0, x + randomGaussian(0, 2.8), y1);
}
}
// a long pull hugging one silhouette, so the form keeps a decided edge on the shaded side
function edgePull(y0, y1, side, off, w, name) {
const ym = (y0 + y1) / 2;
const base = side > 0 ? rightEdge(ym) : leftEdge(ym);
const x = base + off;
brush.set(name, tone(shade(x, ym) + 0.05), w);
brush.line(x + randomGaussian(0, 1.2), y0, x + randomGaussian(0, 2.2), y1);
}
// the shadow that pools under the crease, laid as hatch parallel to the fold
function creaseHatch(pad, dist, weight, col, name, grad) {
const lx = leftEdge(504) - 4;
const rx = rightEdge(554) + 4;
brush.hatchStyle(name, col, weight);
brush.hatch(dist, 19.5, { rand: 0.26, continuous: false, gradient: grad });
brush.noStroke();
brush.noFill();
brush.polygon([
[lx, foldY(lx) + 2],
[(lx + rx) / 2 + randomGaussian(0, 6), foldY((lx + rx) / 2) - 1],
[rx, foldY(rx) + 3],
[rx - 4, foldY(rx) + pad * 0.86 + randomGaussian(0, 9)],
[(lx + rx) / 2, foldY((lx + rx) / 2) + pad + randomGaussian(0, 11)],
[lx + 2, foldY(lx) + pad * 0.72 + randomGaussian(0, 9)],
]);
brush.noHatch();
}
// the crease itself: never one contour, always a few broken pulls that miss each other
function creaseInk(x0, x1, drop, weight, name, col, n) {
brush.set(name, col, weight);
const span = (x1 - x0) / n;
for (let k = 0; k < n; k++) {
if (random() < 0.14) continue;
const a = x0 + k * span + random(0, span * 0.34);
const b = a + span * random(0.4, 0.95);
brush.line(a, foldY(a) + drop + randomGaussian(0, 1.5), b, foldY(b) + drop + randomGaussian(0, 1.5));
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// --- air: a warmth standing behind the mass, and one faint breath in the open left field
brush.noStroke();
brush.fillBleed(0.66, "out");
brush.fillTexture(0.36, 0.4);
brush.fill(MASS, 14);
brush.circle(506, 296, 248, true);
brush.fill(TURN, 12);
brush.circle(524, 640, 224, true);
brush.fill(MASS, 8);
brush.circle(214, 372, 178, true);
// --- washes, back to front: the whole plane, then the lower plane sunk deeper
planeWash(-56, 856, 8, MASS, 46, 0.58);
planeWash(-40, 852, 16, MASS, 38, 0.44);
planeWash(-48, 844, 4, TURN, 22, 0.5);
planeWash(-30, 500, 20, MASS, 34, 0.4);
planeWash(-44, 496, 34, TURN, 16, 0.34);
planeWash(548, 848, 12, TURN, 48, 0.46);
planeWash(556, 838, 26, TURN, 36, 0.36);
planeWash(560, 724, 20, SHADOW, 30, 0.42);
planeWash(566, 648, 8, SHADOW, 18, 0.38);
planeWash(690, 846, 18, SHADOW, 15, 0.4);
// Continuous fine fibres establish two planes before the broken surface grain.
brush.noFill();
brush.noStroke();
brush.hatchStyle("cpencil", TURN, 0.8);
brush.hatch(3.4, 87, { rand: 0.1, continuous: true, gradient: 0.35 });
brush.polygon([[419, -10], [566, -10], [570, 544], [436, 499]]);
brush.noHatch();
brush.hatchStyle("cpencil", SHADOW, 0.9);
brush.hatch(4, 72, { rand: 0.13, continuous: true, gradient: 0.5 });
brush.polygon([[445, 518], [572, 555], [599, 810], [469, 810]]);
brush.noHatch();
// --- the grain: the plane accumulated as vertical pulls, halted at the crease
grain(135, 180, 250, 172, 64, 2.2, 6);
grain(130, 396, 128, 118, 48, 2.0, 4);
grain(110, 664, 142, 124, 54, 2.2, 5);
grain(90, 790, 62, 78, 34, 2.4, 3);
// --- the shaded flank: long pulls hugging the right edge above the fold, the left below,
// so the lit side swaps across the crease and the silhouette stays decided
edgePull(-44, 132, 1, -7, 2.4, "charcoal");
edgePull(62, 196, 1, -19, 1.7, "cpencil");
edgePull(154, 322, 1, -5, 2.1, "cpencil");
edgePull(288, 380, 1, -26, 1.6, "2B");
edgePull(346, 470, 1, -11, 2.2, "charcoal");
edgePull(408, 528, 1, -3, 1.8, "cpencil");
edgePull(466, 534, 1, -30, 1.5, "2B");
edgePull(556, 704, -1, 7, 2.6, "charcoal");
edgePull(628, 848, -1, 15, 2.2, "cpencil");
edgePull(566, 640, -1, 2, 2.0, "2B");
edgePull(744, 856, -1, 22, 2.3, "charcoal");
// --- the turn: shadow pooled under the crease in three passes, wide and faint to tight
// and black, so the darkening slides down into the lower plane instead of ending
creaseHatch(292, 21, 1.6, lerpColor(color(TURN), color(SHADOW), 0.28), "cpencil", 0.72);
creaseHatch(148, 12, 1.5, lerpColor(color(TURN), color(SHADOW), 0.62), "cpencil", 0.56);
creaseHatch(76, 6.5, 1.1, SHADOW, "2B", 0.44);
grain(65, 596, 46, 56, 26, 2.5, 2);
grain(64, 452, 42, 62, 28, 2.0, 3);
// --- the lower plane's outer half, hatched down its length: the turn must not go pale
// where the crease is deepest, or the fold reads as a tear instead of a surface
brush.hatchStyle("cpencil", lerpColor(color(TURN), color(SHADOW), 0.52), 1.35);
brush.hatch(9, 86, { rand: 0.3, continuous: false, gradient: 0.64 });
brush.noStroke();
brush.noFill();
brush.polygon([
[516, 528],
[rightEdge(566) + 3, 556],
[rightEdge(834) + 1, 842],
[524, 836],
]);
brush.noHatch();
brush.hatchStyle("2B", SHADOW, 1.1);
brush.hatch(14, 82, { rand: 0.34, continuous: false, gradient: 0.55 });
brush.noFill();
brush.polygon([
[534, 542],
[rightEdge(572) + 2, 562],
[rightEdge(690), 694],
[540, 676],
]);
brush.noHatch();
brush.set("charcoal", tone(0.86), 2.4);
brush.line(568, 566, 573, 664);
brush.set("cpencil", tone(0.8), 2.0);
brush.line(592, 588, 596, 702);
brush.set("2B", tone(0.9), 1.8);
brush.line(552, 552, 556, 630);
brush.set("charcoal", tone(0.74), 2.2);
brush.line(606, 700, 610, 802);
// --- the crease ink: four broken passes, none of them agreeing with another
creaseInk(430, 574, 1.5, 2.6, "charcoal", SHADOW, 7);
creaseInk(438, 566, 4.2, 1.8, "2B", SHADOW, 9);
creaseInk(452, 578, -1.8, 1.2, "rotring", lerpColor(color(SHADOW), color(TURN), 0.3), 6);
creaseInk(424, 500, 7.5, 2.2, "charcoal", SHADOW, 4);
// --- pigment pooling where the fold is deepest, hand-placed
brush.noStroke();
brush.fillBleed(0.42, "out");
brush.fillTexture(0.6, 0.55);
brush.fill(SHADOW, 128);
brush.circle(468, 512, 17, true);
brush.fill(SHADOW, 110);
brush.circle(497, 524, 13, true);
brush.fill(SHADOW, 144);
brush.circle(524, 536, 20, true);
brush.fill(SHADOW, 102);
brush.circle(551, 546, 12, true);
brush.fill(SHADOW, 90);
brush.circle(571, 556, 15, true);
brush.fill(SHADOW, 74);
brush.circle(508, 566, 22, true);
brush.fill(SHADOW, 58);
brush.circle(546, 592, 18, true);
brush.fill(SHADOW, 40);
brush.circle(512, 646, 26, true);
// --- one lifted highlight riding the ridge, so the plane above the fold catches the light
brush.fillBleed(0.5, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(PAPER, 92);
brush.beginShape(0.3);
brush.vertex(436, 482);
brush.vertex(506, 505);
brush.vertex(568, 527);
brush.vertex(566, 512);
brush.vertex(498, 490);
brush.vertex(434, 466);
brush.endShape(true);
brush.set("2H", lerpColor(color(PAPER), color(MASS), 0.2), 1.6);
brush.line(442, 464, 450, 384);
brush.line(468, 472, 474, 356);
brush.line(519, 488, 525, 412);
// --- the ember: three marks only, all of them at the turn
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fill(EMBER, 132);
brush.circle(536, 531, 10.5, true);
brush.fill(EMBER, 84);
brush.circle(568, 543, 6, true);
brush.set("2B", EMBER, 1.6);
brush.line(492, 518, 519, 527);
// --- the loose left edge: a few strokes escaping the mass into the open paper
brush.field("hand");
brush.set("cpencil", lerpColor(color(TURN), color(PAPER), 0.42), 1.2);
brush.flowLine(424, 168, 58, 96);
brush.flowLine(430, 262, 44, 92);
brush.flowLine(419, 358, 70, 98);
brush.flowLine(452, 604, 52, 94);
brush.flowLine(447, 712, 40, 100);
brush.set("cpencil", lerpColor(color(TURN), color(PAPER), 0.28), 1.4);
brush.flowLine(570, 214, 36, 274);
brush.flowLine(588, 656, 46, 272);
brush.noField();
// --- and one whisper far out in the empty field, to keep the paper awake
brush.set("cpencil", lerpColor(color(PAPER), color(TURN), 0.44), 1.7);
brush.line(176, 358, 181, 462);
brush.noStroke();
brush.fill(TURN, 64);
brush.circle(174, 496, 3.6, true);
// --- three dark notes left high in the lit plane, so the black at the fold has company
brush.set("cpencil", tone(0.6), 2.0);
brush.line(486, 186, 490, 288);
brush.set("2B", tone(0.56), 1.7);
brush.line(452, 108, 455, 190);
brush.set("cpencil", tone(0.66), 2.1);
brush.line(548, 322, 553, 404);
}