After the Petals
2026 · botanical
What a plant does after it has nothing left to show: it stands, holds its seed, and keeps its posture. The line is anxious because standing is work.
a low band of dry poppy seed heads on bending stems across the bottom third, two climbing alone into empty paper, drawn with a nervous pencil line
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a low band of dry poppy stems crowds the bottom third, densest right of centre where
// the largest seed head sits on the thirds intersection; only two stalks climb into the upper air.
// layering: dust washes across the band, drifting grass ticks, then stalks and capsules back-to-front,
// ink contours and crown rays last. the top-left and top-right corners stay bare paper throughout.
const PALETTE = [
"#ece5d6", // ground — warm dry paper
"#9a9377", // mass — olive-grey of a stem gone dry
"#7b6a4c", // shadow — umber inside every capsule
"#2e2a24", // ink — nervous contour, laid last
"#b4462a", // accent — the rust memory of a petal, rationed
];
const PAPER = PALETTE[0];
const OLIVE = PALETTE[1];
const UMBER = PALETTE[2];
const INK = PALETTE[3];
const RUST = PALETTE[4];
// --- one seed-head shell: squat urn, narrow neck, flattened crown ---
function shell(cx, cy, r, tilt, scale, jitter) {
const ax = Math.sin(tilt);
const ay = -Math.cos(tilt);
const px = Math.cos(tilt);
const py = Math.sin(tilt);
const pts = [];
for (let i = 0; i < 16; i++) {
const t = (i / 16) * Math.PI * 2;
const s = Math.sin(t);
const w = r * scale * (1 + randomGaussian(0, jitter));
const h = r * scale * 0.82 * (1 + randomGaussian(0, jitter));
const u = Math.cos(t) * w * (1 - 0.2 * Math.max(0, s) - 0.46 * Math.max(0, -s));
const v = s * h * (s > 0 ? 0.9 : 1);
pts.push([cx + px * u + ax * v, cy + py * u + ay * v]);
}
return pts;
}
function laySoft(pts, col, op, curve) {
brush.noStroke();
brush.fill(col, op);
brush.beginShape(curve);
for (const p of pts) brush.vertex(p[0], p[1]);
brush.endShape(true);
}
// broken contour — the line searches, doubles back, and never closes the form
function nervousEdge(cx, cy, r, tilt, w, from, span) {
const pts = shell(cx, cy, r, tilt, 1.02, 0.05);
brush.noFill();
brush.set("rotring", INK, w);
brush.beginShape(0.78);
for (let i = 0; i <= span; i++) {
const p = pts[(from + i) % pts.length];
brush.vertex(p[0] + randomGaussian(0, 1.1), p[1] + randomGaussian(0, 1.1));
}
brush.endShape(false);
}
function crown(cx, cy, r, tilt, rays, w) {
const kx = cx + Math.sin(tilt) * r * 0.82;
const ky = cy - Math.cos(tilt) * r * 0.82;
brush.noFill();
brush.set("rotring", INK, w);
for (let i = 0; i < rays; i++) {
const a = tilt + Math.PI / 2 + randomGaussian(((i / (rays - 1)) - 0.5) * 2.6, 0.12);
const len = r * (0.5 + randomGaussian(0, 0.12));
brush.line(kx, ky, kx + Math.sin(a) * len, ky - Math.cos(a) * len);
}
brush.set("rotring", INK, w * 1.5);
brush.line(kx - Math.cos(tilt) * r * 0.62, ky - Math.sin(tilt) * r * 0.6,
kx + Math.cos(tilt) * r * 0.56, ky + Math.sin(tilt) * r * 0.56);
brush.noStroke();
brush.fill(INK, 132);
brush.circle(kx, ky, r * 0.09, true);
}
// the ribs running the height of the shell — a poppy keeps its pleats
function ribs(cx, cy, r, tilt, n, w) {
const ax = Math.sin(tilt);
const ay = -Math.cos(tilt);
const px = Math.cos(tilt);
const py = Math.sin(tilt);
brush.noFill();
brush.set("2H", UMBER, w);
for (let i = 0; i < n; i++) {
const u = r * randomGaussian((i / (n - 1) - 0.5) * 1.34, 0.05);
const k = 1 - abs(u / r) * 0.55;
brush.beginShape(0.5);
brush.vertex(cx + px * u * 0.42 + ax * -r * 0.74 * k, cy + py * u * 0.42 + ay * -r * 0.74 * k);
brush.vertex(cx + px * u + ax * r * 0.06, cy + py * u + ay * r * 0.06);
brush.vertex(cx + px * u * 0.5 + ax * r * 0.76 * k, cy + py * u * 0.5 + ay * r * 0.76 * k);
brush.endShape(false);
}
}
// weight 0 = far and pale, 1 = middle, 2 = front, carrying the darks
function seedHead(cx, cy, r, tilt, weight) {
const sx = Math.cos(tilt + 0.9);
const sy = Math.sin(tilt + 0.9);
brush.fillBleed(0.2 + random(0, 0.08), "out");
brush.fillTexture(0.3 + random(0, 0.14), 0.5);
laySoft(shell(cx, cy, r, tilt, 1.04, 0.07), UMBER, weight === 0 ? 160 : 215 + random(-14, 18), 0.9);
laySoft(shell(cx - sx * r * 0.24, cy - sy * r * 0.26, r, tilt, 0.8, 0.07), OLIVE,
weight === 0 ? 130 : 185 + random(-16, 22), 0.9);
laySoft(shell(cx + sx * r * 0.34, cy + sy * r * 0.32, r, tilt, 0.66, 0.08), UMBER,
weight === 0 ? 150 : 210 + random(-14, 20), 0.85);
if (weight === 2) {
brush.hatchStyle("2H", UMBER, 0.9);
brush.hatch(4.4 + random(0, 1.2), 56 + random(-16, 16), { rand: 0.16, continuous: false });
laySoft(shell(cx, cy, r, tilt, 0.96, 0.05), OLIVE, 40, 0.88);
brush.noHatch();
brush.hatchStyle("2B", INK, 1.15);
brush.hatch(3.2 + random(0, 0.9), 118 + random(-14, 14), { rand: 0.22, continuous: false });
laySoft(shell(cx + sx * r * 0.4, cy + sy * r * 0.42, r, tilt, 0.62, 0.1), UMBER, 118 + random(-12, 18), 0.8);
brush.noHatch();
laySoft(shell(cx + sx * r * 0.56, cy + sy * r * 0.54, r, tilt, 0.3, 0.12), INK, 186 + random(-12, 20), 0.75);
} else if (weight === 1) {
brush.hatchStyle("2H", UMBER, 0.8);
brush.hatch(5.2 + random(0, 1.4), 112 + random(-16, 16), { rand: 0.2, continuous: false });
laySoft(shell(cx + sx * r * 0.42, cy + sy * r * 0.42, r, tilt, 0.54, 0.1), UMBER, 96 + random(-12, 16), 0.8);
brush.noHatch();
laySoft(shell(cx + sx * r * 0.54, cy + sy * r * 0.52, r, tilt, 0.32, 0.1), INK, 128 + random(-12, 16), 0.75);
}
ribs(cx, cy, r, tilt, weight === 0 ? 3 : 5, weight === 2 ? 0.95 : 0.7);
if (weight === 0) {
nervousEdge(cx, cy, r, tilt, 0.85, floor(random(0, 16)), 7);
} else {
nervousEdge(cx, cy, r, tilt, weight === 2 ? 2.2 : 1.3, floor(random(0, 16)), 10);
nervousEdge(cx, cy, r, tilt, weight === 2 ? 1.4 : 0.9, floor(random(0, 16)), 6);
if (weight === 2) nervousEdge(cx, cy, r, tilt, 1.0, floor(random(0, 16)), 4);
}
crown(cx, cy, r, tilt, weight === 0 ? 6 : 9, weight === 2 ? 1.5 : 1.0);
// the collar where the shell chokes down onto the stem
const nx = cx - Math.sin(tilt) * r * 0.78;
const ny = cy + Math.cos(tilt) * r * 0.78;
brush.set("rotring", weight === 2 ? INK : UMBER, weight === 2 ? 2.0 : 1.3);
brush.line(nx - Math.cos(tilt) * r * 0.4, ny - Math.sin(tilt) * r * 0.38,
nx + Math.cos(tilt) * r * 0.34, ny + Math.sin(tilt) * r * 0.36);
if (weight === 2) {
brush.set("2B", INK, 1.5);
brush.line(nx - Math.cos(tilt) * r * 0.26 + random(-2, 2), ny + r * 0.1,
nx + Math.cos(tilt) * r * 0.2 + random(-2, 2), ny + r * 0.22);
brush.noStroke();
brush.fill(INK, 150);
brush.circle(nx + random(-2, 2), ny + r * 0.12, r * 0.1, true);
}
}
// a stalk bowed through one elbow — every pass re-finds the same line and misses it
function stalk(bx, by, mx, my, tx, ty, w, dark) {
for (let p = 0; p < (dark ? 3 : 2); p++) {
brush.noFill();
if (p === 0) brush.set("cpencil", random() < 0.6 ? UMBER : OLIVE, w * 1.7);
else if (p === 1) brush.set("rotring", random() < 0.55 ? UMBER : OLIVE, w * 0.9);
else brush.set("rotring", INK, w * 0.85);
brush.beginShape(0.5);
for (let i = 0; i <= 8; i++) {
const t = i / 8;
const u = 1 - t;
const x = u * u * bx + 2 * u * t * mx + t * t * tx;
const y = u * u * by + 2 * u * t * my + t * t * ty;
const j = p === 0 ? 2.0 : 1.3;
brush.vertex(x + randomGaussian(0, j), y + randomGaussian(0, j), 0.5 + 0.28 * u);
}
brush.endShape(false);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// --- dust settling along the band, thickest under the dense right of centre ---
brush.noStroke();
brush.fillBleed(0.62, "out");
brush.fillTexture(0.38, 0.35);
for (let i = 0; i < 26; i++) {
const x = constrain(randomGaussian(480, 250), -50, 850);
const y = constrain(randomGaussian(724, 66), 586, 840);
const r = max(52, randomGaussian(104, 26));
brush.fill(random() < 0.7 ? OLIVE : UMBER, max(22, randomGaussian(48, 11)));
brush.circle(x, y, r, true);
}
brush.fillBleed(0.68, "out");
brush.fill(OLIVE, 38);
brush.circle(556, 604, 194, true);
brush.fill(UMBER, 30);
brush.circle(618, 690, 150, true);
brush.fill(OLIVE, 13);
brush.circle(560, 348, 122, true);
// --- dry grass drifting in the field, low and thin ---
brush.noFill();
for (let i = 0; i < 30; i++) {
const x = constrain(randomGaussian(456, 250), -10, 810);
const y = constrain(randomGaussian(800, 24), 748, 834);
const len = max(32, randomGaussian(76, 28));
const lean = randomGaussian(0, 20);
brush.set("2H", random() < 0.7 ? OLIVE : UMBER, 0.8 + random(0, 0.5));
brush.beginShape(0.5);
brush.vertex(x, y);
brush.vertex(x + lean * 0.4 + randomGaussian(0, 3), y - len * 0.55);
brush.vertex(x + lean + randomGaussian(0, 4), y - len);
brush.endShape(false);
}
// --- back rank: pale, small, half-lost in the dust ---
stalk(148, 812, 158, 730, 176, 646, 1.5, false);
seedHead(176, 640, 14, -0.22, 0);
stalk(78, 812, 58, 742, 44, 672, 1.5, false);
seedHead(44, 666, 13, 0.3, 0);
stalk(596, 814, 578, 720, 566, 630, 1.6, false);
seedHead(566, 622, 16, 0.2, 0);
stalk(786, 812, 800, 742, 812, 674, 1.5, false);
seedHead(812, 668, 15, -0.18, 0);
stalk(694, 816, 664, 748, 640, 690, 1.4, false);
seedHead(640, 684, 12, 0.42, 0);
// --- three bare stalks, snapped, holding the band open ---
stalk(322, 814, 300, 758, 262, 726, 1.6, false);
stalk(408, 816, 430, 762, 468, 748, 1.4, false);
stalk(742, 818, 718, 766, 700, 736, 1.5, false);
// --- broken litter lying in the band, where the field is thickest ---
stalk(510, 812, 536, 792, 570, 798, 1.5, false);
stalk(664, 814, 638, 796, 614, 804, 1.4, false);
stalk(588, 806, 612, 786, 640, 792, 1.3, false);
stalk(454, 806, 424, 790, 400, 800, 1.3, false);
// --- middle rank ---
stalk(250, 814, 232, 712, 214, 608, 2.0, false);
seedHead(214, 600, 18, 0.3, 1);
stalk(442, 812, 424, 706, 414, 604, 2.4, true);
seedHead(414, 596, 20, 0.34, 1);
stalk(566, 816, 588, 700, 596, 592, 2.2, false);
seedHead(596, 584, 22, -0.28, 1);
stalk(720, 810, 744, 720, 756, 630, 2.0, false);
seedHead(756, 624, 19, -0.36, 1);
stalk(470, 814, 480, 686, 486, 560, 2.6, true);
seedHead(486, 552, 25, -0.14, 1);
// --- the tall left stalk, bending as it climbs out of the band ---
stalk(356, 812, 388, 590, 322, 380, 2.8, true);
seedHead(322, 370, 21, 0.58, 2);
// --- front rank, right of centre: the two heaviest heads ---
stalk(618, 812, 650, 682, 648, 556, 3.0, true);
seedHead(648, 548, 22, -0.78, 2);
stalk(520, 816, 496, 668, 542, 540, 3.6, true);
seedHead(542, 526, 35, -0.1, 2);
// --- the tall right stalk: one long nervous gesture into empty paper ---
stalk(536, 814, 498, 520, 566, 252, 3.0, true);
seedHead(566, 240, 26, 0.16, 2);
// --- last darks: split seed spilling from the front heads, and a rust fragment ---
brush.noStroke();
brush.fillBleed(0.24, "out");
for (let i = 0; i < 26; i++) {
const x = constrain(randomGaussian(552, 54), 430, 700);
const y = constrain(randomGaussian(628, 58), 540, 790);
brush.fill(random() < 0.82 ? INK : UMBER, max(70, randomGaussian(126, 26)));
brush.circle(x, y, max(1.1, randomGaussian(2.1, 0.7)), true);
}
brush.set("rotring", INK, 0.9);
brush.line(541, 552, 534, 578);
brush.line(647, 570, 652, 594);
brush.line(324, 392, 328, 414);
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.45, 0.55);
brush.fill(INK, 96);
brush.circle(534, 802, 13, true);
brush.fill(INK, 78);
brush.circle(556, 810, 10, true);
brush.fill(UMBER, 120);
brush.circle(514, 806, 9, true);
brush.fill(INK, 70);
brush.circle(632, 812, 8.5, true);
brush.fillBleed(0.44, "out");
brush.fillTexture(0.4, 0.5);
brush.fill(RUST, 98);
brush.beginShape(0.6);
brush.vertex(444, 776);
brush.vertex(472, 766);
brush.vertex(484, 782);
brush.vertex(456, 790);
brush.endShape(true);
brush.fill(RUST, 84);
brush.beginShape(0.6);
brush.vertex(528, 792);
brush.vertex(552, 784);
brush.vertex(556, 800);
brush.endShape(true);
brush.fill(RUST, 76);
brush.circle(368, 790, 4.0, true);
brush.fill(RUST, 80);
brush.circle(558, 508, 4.8, true);
brush.fill(RUST, 70);
brush.circle(574, 226, 3.8, true);
}