The Water Doubles Them
2026 · fauna
Three cranes, six legs, and a surface that insists there are twelve.
three cranes wading a shallow diagonal, legs doubled by their reflections, one head down, the rest of the water left bare
painted by GPT-6 Astrap5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: three cranes on a shallow diagonal through the lower third, stepping up to the right; the upper two-thirds stays pale unpainted water
// layering: two enormous faint washes, a heavier band the birds stand in, grey bodies, ink wings and plumes, then necks legs and their doubles, crowns last
// the middle crane's head goes down at the third-intersection — that dropped curve is the picture's one event
const PALETTE = [
"#eee9dd", // ground — warm paper, and the light on each breast
"#cfd2c9", // water — pale celadon wash
"#8e948d", // mass — the grey of a folded crane
"#2a2e2c", // ink — wings, plumes, necks, legs
"#b4462c", // accent — the three crowns, rationed
];
const PAPER = "#eee9dd";
const WATER = "#cfd2c9";
const MASS = "#8e948d";
const INK = "#2a2e2c";
const CROWN = "#b4462c";
const jg = (s) => randomGaussian(0, s);
// --- water -------------------------------------------------------------
function wash(x, y, r, op) {
brush.noStroke();
brush.fillBleed(0.62, "out");
brush.fillTexture(0.34, 0.42);
brush.fill(WATER, op);
brush.circle(x + jg(6), y + jg(6), r * (1 + random(-0.05, 0.05)), true);
}
// a torn piece of a reflection lying flat on the surface
function dash(x, y, len, w) {
brush.noFill();
brush.set("cpencil", MASS, w);
brush.line(x + jg(1.7), y + jg(1.7), x + len + jg(1.7), y + jg(1.7));
}
function pool(x, y, r, op) {
brush.fill(MASS, op);
brush.circle(x + jg(5), y + jg(5), r, true);
}
function ripple(x, y, len, tilt, w) {
brush.noFill();
brush.set("2H", MASS, w);
brush.beginShape(0.62);
brush.vertex(x, y + jg(1.2));
brush.vertex(x + len * 0.33, y - tilt * 0.7 + jg(1.8));
brush.vertex(x + len * 0.69, y + tilt * 0.5 + jg(1.8));
brush.vertex(x + len, y + tilt + jg(1.3));
brush.endShape(false);
}
// --- the bird, built as one flat silhouette with tone inside it --------
function bodyPoints(cx, cy, L, H, dir) {
const p = (t, u) => [cx + dir * t * L, cy + u * H];
return [
p(0.52, -0.10), // breast, front
p(0.28, -0.52), // shoulder
p(-0.24, -0.52), // back
p(-0.56, -0.72), // rise into the bustle
p(-0.80, -0.24), // rear, tapering
p(-0.50, 0.16), // under the tail
p(0.02, 0.46), // belly
p(0.44, 0.26), // breast, low
];
}
// flat and crisp: barely any bleed, so the silhouette keeps its edge
function fillPts(pts, col, op, jit) {
brush.noStroke();
brush.fillBleed(0.13, "out");
brush.fillTexture(0.44, 0.62);
brush.fill(col, op);
brush.beginShape(0.62);
for (const q of pts) brush.vertex(q[0] + jg(jit), q[1] + jg(jit));
brush.endShape(true);
}
// one long stroke sweeping back along the folded wing
function wingStroke(cx, cy, L, H, dir, w, ts) {
brush.noFill();
brush.set("cpencil", INK, w);
brush.beginShape(0.6);
for (const t of ts) brush.vertex(cx + dir * t[0] * L + jg(1.5), cy + t[1] * H + jg(1.5));
brush.endShape(false);
}
// the folded wing — a soft ink wash, then five feather edges swept back by hand
function wing(cx, cy, L, H, dir) {
const p = (t, u) => [cx + dir * t * L, cy + u * H];
const q = [p(0.22, -0.26), p(-0.10, -0.46), p(-0.50, -0.14), p(-0.12, 0.16)];
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fillTexture(0.56, 0.6);
brush.fill(INK, 118 + random(-12, 22));
brush.beginShape(0.62);
for (const v of q) brush.vertex(v[0] + jg(2.2), v[1] + jg(2.2));
brush.endShape(true);
wingStroke(cx, cy, L, H, dir, 1.7, [[0.20, -0.28], [-0.02, -0.44], [-0.30, -0.32], [-0.48, -0.10]]);
wingStroke(cx, cy, L, H, dir, 1.3, [[0.15, -0.17], [-0.07, -0.31], [-0.35, -0.21], [-0.51, 0.01]]);
wingStroke(cx, cy, L, H, dir, 1.05, [[0.11, -0.05], [-0.11, -0.15], [-0.37, -0.09], [-0.46, 0.09]]);
wingStroke(cx, cy, L, H, dir, 0.85, [[0.05, 0.05], [-0.15, -0.01], [-0.33, 0.05], [-0.41, 0.15]]);
wingStroke(cx, cy, L, H, dir, 1.0, [[0.01, -0.39], [-0.19, -0.45], [-0.35, -0.35]]);
}
// the black bustle of secondaries a standing crane carries over its tail
function plume(cx, cy, L, H, dir, ang) {
const x0 = cx - dir * L * 0.44;
const y0 = cy - H * 0.30;
const q = [
[x0, y0],
[x0 - dir * L * 0.14, y0 - H * 0.44],
[x0 - dir * L * 0.40, y0 + H * 0.16],
[x0 - dir * L * 0.12, y0 + H * 0.50],
];
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fillTexture(0.6, 0.62);
brush.fill(INK, 210);
brush.beginShape(0.6);
for (const v of q) brush.vertex(v[0] + jg(2), v[1] + jg(2));
brush.endShape(true);
brush.noFill();
brush.hatchStyle("2B", INK, 1.3);
brush.hatch(1.9, ang + 26);
brush.beginShape(0.6);
for (const v of q) brush.vertex(v[0] + jg(2.2), v[1] + jg(2.2));
brush.endShape(true);
brush.hatch(2.6, ang - 52);
brush.beginShape(0.6);
for (const v of q) brush.vertex(v[0] + jg(2.6), v[1] + jg(2.6));
brush.endShape(true);
brush.noHatch();
// two feather points dropping off the bustle, steep and short so they stay feathers
brush.noFill();
brush.set("2B", INK, 1.4);
brush.line(x0 - dir * L * 0.24, y0 + H * 0.34, x0 - dir * L * 0.30, y0 + H * 0.72);
brush.set("2B", INK, 1.0);
brush.line(x0 - dir * L * 0.10, y0 + H * 0.42, x0 - dir * L * 0.13, y0 + H * 0.74);
}
function crane(cx, cy, L, H, dir, ang) {
fillPts(bodyPoints(cx, cy, L, H, dir), MASS, 188, 2.4);
fillPts(bodyPoints(cx, cy, L * 0.96, H * 0.94, dir), MASS, 150, 3.2);
fillPts(bodyPoints(cx, cy, L * 0.9, H * 0.86, dir), MASS, 104, 4.0);
// light held on the breast and along the top of the back
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.34, 0.5);
brush.fill(PAPER, 190);
brush.circle(cx + dir * L * 0.32 + jg(3), cy - H * 0.02, H * 0.3, true);
brush.fill(PAPER, 130);
brush.circle(cx + dir * L * 0.06 + jg(3), cy - H * 0.4, H * 0.2, true);
wing(cx, cy, L, H, dir);
plume(cx, cy, L, H, dir, ang);
// two open edges only — the breast and the underline, never a closed outline
brush.noFill();
brush.set("cpencil", INK, 0.9);
brush.beginShape(0.6);
brush.vertex(cx + dir * L * 0.30 + jg(2), cy - H * 0.52);
brush.vertex(cx + dir * L * 0.54 + jg(2), cy - H * 0.06);
brush.vertex(cx + dir * L * 0.42 + jg(2), cy + H * 0.30);
brush.endShape(false);
brush.beginShape(0.6);
brush.vertex(cx + dir * L * 0.36 + jg(2), cy + H * 0.36);
brush.vertex(cx - dir * L * 0.10 + jg(2), cy + H * 0.50);
brush.vertex(cx - dir * L * 0.40 + jg(2), cy + H * 0.30);
brush.endShape(false);
}
// --- legs, necks, heads ------------------------------------------------
function leg(hx, hy, fx, fy, bend, w) {
brush.noFill();
brush.set("2B", INK, w);
brush.beginShape(0.45);
brush.vertex(hx + jg(1.1), hy + jg(1.1));
brush.vertex((hx + fx) * 0.5 + bend, (hy + fy) * 0.5 + jg(2));
brush.vertex(fx + jg(1.2), fy + jg(1.2));
brush.endShape(false);
brush.set("rotring", INK, w * 0.8);
brush.line(fx - 5 + jg(1), fy + jg(1), fx + 7 + jg(1), fy + 2 + jg(1));
}
// the doubled leg: mirrored about the foot, broken, and greyer than the bird
function legEcho(fx, fy, hx, hy, w) {
const mx = fx + (hx - fx) * 0.9;
const my = fy + (fy - hy) * 0.94;
const kink = randomGaussian(0, 3.2);
brush.noFill();
brush.set("cpencil", MASS, w * 1.3);
brush.beginShape(0.5);
brush.vertex(fx + kink, fy + 4);
brush.vertex(lerp(fx, mx, 0.34) + kink + jg(2.6), lerp(fy, my, 0.34));
brush.vertex(lerp(fx, mx, 0.62) + jg(3.4), lerp(fy, my, 0.62));
brush.endShape(false);
brush.set("cpencil", MASS, w * 0.9);
brush.beginShape(0);
brush.vertex(fx + kink + jg(1.6), fy + 6);
brush.vertex(lerp(fx, mx, 0.4) + kink + jg(3), lerp(fy, my, 0.4));
brush.endShape(false);
brush.set("cpencil", MASS, w * 0.75);
brush.beginShape(0);
brush.vertex(lerp(fx, mx, 0.78) + jg(3.6), lerp(fy, my, 0.78));
brush.vertex(mx + jg(4), my);
brush.endShape(false);
}
function neck(pts, w) {
brush.noFill();
brush.set("2B", INK, w);
brush.beginShape(0.55);
for (const q of pts) brush.vertex(q[0] + jg(1.1), q[1] + jg(1.1));
brush.endShape(false);
brush.set("2B", INK, w * 0.8);
brush.beginShape(0.55);
for (const q of pts) brush.vertex(q[0] + jg(1) + 1.7, q[1] + jg(1));
brush.endShape(false);
brush.set("cpencil", INK, w * 0.55);
brush.beginShape(0.55);
for (const q of pts) brush.vertex(q[0] + jg(1.2) - 1.4, q[1] + jg(1.2));
brush.endShape(false);
}
function head(hx, hy, s, bx, by) {
brush.noFill();
brush.set("2B", INK, s * 1.15);
brush.line(hx - 2.4 + jg(0.6), hy + 1.8, hx + 2.4 + jg(0.6), hy - 1.4);
// the crown, laid as two short strokes — pigment, not a halo
brush.noFill();
brush.set("2B", CROWN, s * 0.42);
brush.line(hx - 2.2 + jg(0.4), hy - s * 0.58, hx + 2.0 + jg(0.4), hy - s * 0.74);
brush.set("2B", CROWN, s * 0.3);
brush.line(hx - 1.6 + jg(0.4), hy - s * 0.34, hx + 1.8 + jg(0.4), hy - s * 0.46);
brush.set("rotring", INK, 2.0);
brush.line(hx + jg(0.6), hy + jg(0.6), bx + jg(1), by + jg(1));
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// two enormous breaths of water across the upper field, then the band the
// birds stand in — heaviest at the left, where the water comes nearest
wash(258, 402, 296, 22);
wash(624, 322, 252, 16);
wash(140, 692, 182, 72);
wash(344, 720, 168, 60);
wash(560, 690, 152, 48);
wash(748, 646, 134, 36);
wash(262, 616, 120, 30);
// colder grey inside that band, and a smear under each bird where it falls
// back into the surface — something for the birds to stand against
brush.noStroke();
brush.fillBleed(0.64, "out");
brush.fillTexture(0.28, 0.4);
pool(176, 730, 168, 38);
pool(412, 742, 148, 31);
pool(644, 700, 128, 24);
pool(300, 636, 112, 17);
pool(56, 754, 134, 22);
pool(556, 660, 96, 15);
brush.fillBleed(0.52, "out");
pool(686, 668, 46, 44);
pool(478, 706, 40, 34);
pool(212, 750, 48, 38);
// two hairlines high up, the only marks in the empty two-thirds
ripple(138, 348, 208, 5, 0.5);
ripple(526, 268, 142, -4, 0.42);
// ---- three birds: biggest and lowest at the left, then a closer pair stepping up
crane(190, 592, 104, 58, 1, 118);
crane(470, 546, 94, 54, 1, 64);
crane(678, 516, 80, 46, -1, 98);
// ---- legs, longest at the front of the picture ----
leg(206, 607, 224, 694, 7, 2.0);
leg(176, 603, 172, 702, -6, 1.8);
leg(486, 560, 500, 648, 6, 1.8);
leg(456, 558, 448, 654, -5, 1.6);
leg(668, 531, 660, 600, 5, 1.5);
leg(692, 525, 698, 606, -4, 1.4);
// ---- and the doubles, which is the whole reason for the picture ----
legEcho(224, 694, 206, 607, 1.8);
legEcho(172, 702, 176, 603, 1.6);
legEcho(500, 648, 486, 560, 1.6);
legEcho(448, 654, 456, 558, 1.5);
legEcho(660, 600, 668, 531, 1.4);
legEcho(698, 606, 692, 525, 1.3);
// ---- necks and heads ----
neck([[230, 585], [245, 553], [250, 520], [242, 478], [242, 462]], 1.9);
head(242, 460, 5.2, 262, 456);
// the one head down: the neck swings out and drops to the surface
neck([[503, 542], [525, 537], [548, 552], [568, 592], [574, 614]], 1.8);
head(574, 616, 5.2, 585, 636);
neck([[645, 510], [634, 480], [622, 452], [622, 416], [620, 402]], 1.7);
head(620, 400, 4.6, 602, 406);
// one white cheek stroke on the near bird, laid straight over the ink
brush.noFill();
brush.set("2B", PAPER, 1.6);
brush.line(236 + jg(0.5), 463, 244 + jg(0.5), 460);
// the reflected head, upside down and almost gone
brush.noFill();
brush.set("cpencil", MASS, 2.6);
brush.beginShape(0.5);
brush.vertex(566 + jg(1.4), 664);
brush.vertex(574 + jg(1.6), 682);
brush.vertex(572 + jg(1.8), 694);
brush.endShape(false);
brush.set("cpencil", CROWN, 1.4);
brush.line(569 + jg(1), 698, 575 + jg(1), 703);
// last: ripples that cut the legs at the surface and lock the birds into water
ripple(146, 696, 128, 3, 1.05);
ripple(184, 714, 104, -3, 0.85);
ripple(426, 650, 112, 3, 0.95);
ripple(462, 666, 90, -2, 0.72);
ripple(626, 602, 100, 2, 0.8);
ripple(654, 616, 80, -2, 0.62);
ripple(266, 752, 168, 4, 0.66);
ripple(524, 728, 146, -3, 0.56);
ripple(-38, 662, 176, 5, 0.5);
ripple(672, 754, 168, -4, 0.44);
// the near bird's body, broken to pieces on the surface below its own feet
dash(198, 744, 38, 2.1);
dash(258, 752, 30, 1.5);
dash(306, 740, 34, 1.7);
dash(244, 772, 30, 1.2);
dash(464, 694, 32, 1.3);
// the small disturbance where that one beak breaks the surface
brush.noFill();
brush.set("2B", INK, 1.1);
brush.line(568 + jg(1), 642 + jg(1), 604 + jg(1), 640 + jg(1));
brush.set("2B", INK, 0.8);
brush.line(560 + jg(1), 650 + jg(1), 592 + jg(1), 651 + jg(1));
brush.set("2H", MASS, 0.7);
brush.line(546 + jg(1.4), 660 + jg(1.4), 620 + jg(1.4), 657 + jg(1.4));
}