Boards, Low Moon
2026 · nocturne
The moon does not shine here; it is simply the place where the blue stops. Everything else is a matter of how much of that absence each board is willing to take.
a plain wooden bridge crossing the low third of a deep blue night, its boards taking a thin cold light, a pale moon set high to the left, the water beneath it going to ink
painted by GPT-6 Astrap5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the moon holds the upper-left thirds crossing as one pale hole in a flat blue sky;
// a cambered wooden bridge cuts the lower third from the bottom-left edge up off the right.
// layering: flat prussian ground, the thinning and the moon, the pale reach at the far bank, then
// the river worked down to ink with horizontal strokes, and the timber and its thin light last.
// The upper two thirds stay bare ground — only the moon is laid onto them.
const PALETTE = [
"#1b2f4e", // ground — flat prussian night, the sky itself
"#2d4a73", // mass — the blue thinning, and the pale reach below the bank
"#0c1420", // shadow — ink river, far bank, all the timber
"#7f8b9b", // light — cold slate on boards, rail and ripples
"#e8dcbc", // accent — the moon and the little it touches, rationed
"#4a3b30", // wood — muted umber, grain and rail only
];
// the bridge line: off the left edge low, off the right edge higher, lifted by a carpenter's camber
const BX0 = -52;
const BY0 = 672;
const BX1 = 852;
const BY1 = 496;
const CAMBER = 30;
function deckY(x) {
const t = constrain((x - BX0) / (BX1 - BX0), 0, 1);
return lerp(BY0, BY1, t) - CAMBER * Math.sin(Math.PI * t);
}
// the far bank, a low ragged line across the picture
function bankY(x) {
return 462 + (noise(x * 0.006, 4.2) - 0.5) * 26 + (noise(x * 0.021, 8.1) - 0.5) * 12;
}
// how black the river wants to be here: it deepens away from the bank and again under the bridge
function riverDark(x, y) {
const base = constrain((y - 478) / 320, 0, 1) * 0.6;
const under = constrain((y - deckY(x) - 40) / 150, 0, 1) * 0.72;
return constrain(base + under, 0, 1);
}
function wash(x, y, r, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.4, 0.35);
brush.fill(col, op);
brush.circle(x, y, r, true);
}
// the moon is not a lamp: it is one clean hole where the blue stops
function moonHole(cx, cy, r) {
wash(cx - 4, cy + 6, r * 3.1, "#2d4a73", 20, 0.7);
wash(cx + 8, cy - 7, r * 1.9, "#2d4a73", 15, 0.55);
// Opaque paper pigment keeps moonlight neutral over the Prussian ground.
noStroke();
fill("#e8dcbc");
circle(cx, cy, r * 2);
brush.noFill();
for (let i = 0; i < 24; i++) {
const y = cy + random(-r * 0.82, r * 0.82);
const half = Math.sqrt(r * r - (y - cy) * (y - cy)) * 0.72;
brush.set("cpencil", i % 3 ? "#c3bba9" : "#f1e6cd", 0.5);
brush.line(cx - half + random(0, half), y, cx + half * random(0.2, 0.8), y + random(-1, 1));
}
}
// one broken horizontal pull of dark water — smooth, so the river never turns to grit
function riverStroke(x, y, len, dark) {
brush.set("2B", "#0c1420", 2.2 + dark * 4.6 + random(-0.3, 0.5));
brush.line(x, y + random(-1.4, 1.4), x + len, y + random(-2.0, 2.0));
}
// a crown of trees on the far bank — a scribble, never an outline
function treeCrown(cx, cy, w, h, n) {
for (let i = 0; i < n; i++) {
const a = random(0, 6.28);
const rx = randomGaussian(0, w * 0.32);
const ry = randomGaussian(0, h * 0.3);
const l = Math.max(6, randomGaussian(h * 0.62, h * 0.18));
brush.set("2B", "#0c1420", 2.6 + random(0, 2.4));
brush.line(cx + rx, cy + ry, cx + rx + Math.cos(a) * l * 0.45, cy + ry + Math.sin(a) * l * 0.45);
}
}
// a pile driven from under the deck down into the black, made of a few heavy strokes
function pileStrokes(x, lean, w, toY) {
const yTop = deckY(x) + 4;
brush.set("charcoal", "#0c1420", 2.4 + random(-0.2, 0.5));
brush.line(x - w, yTop, x - w * 0.78 + lean, toY);
brush.set("charcoal", "#0c1420", 3.0 + random(-0.3, 0.6));
brush.line(x - w * 0.3, yTop + 2, x - w * 0.2 + lean, toY - random(4, 16));
brush.set("charcoal", "#0c1420", 2.6 + random(-0.2, 0.5));
brush.line(x + w * 0.4, yTop, x + w * 0.34 + lean, toY - random(8, 26));
brush.set("charcoal", "#0c1420", 2.0 + random(-0.2, 0.4));
brush.line(x + w, yTop - 1, x + w * 0.84 + lean, toY - random(20, 48));
}
// a diagonal brace between two piles — what makes it read as carpentry
function brace(x1, x2, drop) {
brush.set("charcoal", "#0c1420", 2.2 + random(-0.3, 0.4));
brush.line(x1, deckY(x1) + 14, x2, deckY(x2) + drop);
brush.set("charcoal", "#0c1420", 1.8 + random(-0.3, 0.4));
brush.line(x2, deckY(x2) + 14, x1, deckY(x1) + drop * 0.9);
}
// one board of the deck, a short tick across the run
function board(x, lit) {
const y = deckY(x) - 5;
const slope = (deckY(x + 8) - deckY(x)) / 8;
const ang = Math.atan2(slope, 1);
const px = -Math.sin(ang);
const py = Math.cos(ang);
const len = Math.max(15, randomGaussian(21, 2.6));
const off = randomGaussian(0, 1.2);
// the light stays thin: the marker floods if it is given any weight, and only takes the cream
if (lit && random() < 0.5) {
brush.set("cpencil", "#e8dcbc", 0.8 + random(0, 0.35));
} else if (lit) {
brush.set("cpencil", "#7f8b9b", 1.3 + random(-0.2, 0.5));
} else {
brush.set("2H", "#4a3b30", 1.0 + random(-0.2, 0.4));
}
brush.line(
x + off - px * len * 0.5,
y + off - py * len * 0.5,
x + off + px * len * 0.5 + random(-1.3, 1.3),
y + off + py * len * 0.5,
);
}
function railPost(x, h) {
const y = deckY(x);
brush.set("charcoal", "#0c1420", 2.0 + random(-0.3, 0.4));
brush.line(x + random(-1, 1), y - 6, x + random(-2.4, 2.4), y - h);
}
// the deck: one long timber band pulled in overlapping runs
function deckRun(off, weight, col, name) {
brush.noFill();
brush.set(name, col, weight);
brush.beginShape(0.32);
brush.vertex(-48, deckY(-48) + off + random(-1.5, 1.5));
brush.vertex(170, deckY(170) + off + random(-1.5, 1.5));
brush.vertex(390, deckY(390) + off + random(-1.5, 1.5));
brush.vertex(610, deckY(610) + off + random(-1.5, 1.5));
brush.vertex(846, deckY(846) + off + random(-1.5, 1.5));
brush.endShape(false);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#1b2f4e");
// --- the sky: left alone, save for the blue thinning a little around the moon
wash(212, 240, 330, "#2d4a73", 13, 0.72);
wash(118, 376, 232, "#2d4a73", 10, 0.66);
wash(352, 132, 200, "#2d4a73", 8, 0.6);
moonHole(266, 262, 48);
// --- the pale reach: the water under the bank is the one light thing in the lower half
wash(300, 492, 300, "#2d4a73", 30, 0.6);
wash(560, 500, 260, "#2d4a73", 24, 0.6);
wash(72, 500, 190, "#2d4a73", 20, 0.6);
// and a ribbon of that light kept riding just above the bridge, so the timber can go black on it
wash(48, deckY(48) - 46, 104, "#2d4a73", 26, 0.55);
wash(176, deckY(176) - 52, 118, "#2d4a73", 30, 0.55);
wash(306, deckY(306) - 50, 112, "#2d4a73", 34, 0.55);
wash(438, deckY(438) - 54, 122, "#2d4a73", 28, 0.55);
wash(570, deckY(570) - 48, 108, "#2d4a73", 24, 0.55);
wash(704, deckY(704) - 52, 116, "#2d4a73", 20, 0.55);
// --- the far bank: a quiet dark line worked across, thin at the moon side, heavier to the right
for (let i = 0; i < 34; i++) {
const x = -20 + i * 25 + random(-6, 6);
const bias = constrain((x - 120) / 680, 0, 1);
if (random() > 0.5 + bias * 0.5) continue;
const y = bankY(x) + random(-1.6, 1.6);
brush.set("2B", "#0c1420", 1.7 + bias * 2.4 + random(-0.3, 0.5));
brush.line(x, y, x + random(26, 52), y + random(-2.2, 2.2));
}
// five crowns of trees, none the same size, all right of the moon
treeCrown(602, 434, 46, 34, 15);
treeCrown(640, 448, 30, 22, 9);
treeCrown(750, 424, 58, 42, 18);
treeCrown(706, 448, 26, 20, 8);
treeCrown(806, 440, 40, 28, 11);
treeCrown(470, 452, 22, 16, 6);
// --- the river, worked down to ink: horizontal pulls, closer and heavier as it deepens
for (let i = 0; i < 27; i++) {
const y = 484 + i * 12.2 + random(-3, 3);
for (let k = 0; k < 6; k++) {
const x = -40 + k * 146 + random(-40, 40);
const dark = riverDark(x + 70, y);
if (dark < 0.2) continue;
if (random() > 0.16 + dark * 0.86) continue;
riverStroke(x, y, Math.max(90, randomGaussian(190 + dark * 110, 40)), dark);
}
}
// the sump: the bottom edge is pulled to black
for (let i = 0; i < 42; i++) {
const y = 690 + random(0, 116);
const x = random(-60, 760);
brush.set("2B", "#0c1420", 6.0 + random(-1.2, 3.0));
brush.line(x, y, x + Math.max(110, randomGaussian(240, 60)), y + random(-3, 3));
}
// and once more into the low right corner, the deepest water
for (let i = 0; i < 20; i++) {
const y = 716 + random(0, 92);
const x = random(300, 800);
brush.set("2B", "#0c1420", 7.0 + random(-1.4, 3.2));
brush.line(x, y, x + random(110, 240), y + random(-3, 3));
}
// the last of the light leaves the bottom edge entirely
for (let i = 0; i < 26; i++) {
const y = 736 + random(0, 76);
brush.set("2B", "#0c1420", 8.0 + random(-1.6, 3.6));
brush.line(random(-70, 620), y, random(180, 880), y + random(-4, 4));
}
// --- the moon's road on the water, in the reach above the bridge. Broken, never continuous.
brush.set("cpencil", "#e8dcbc", 1.4);
brush.line(234, 486, 300, 484);
brush.set("cpencil", "#7f8b9b", 1.1);
brush.line(248, 494, 288, 496);
brush.set("cpencil", "#e8dcbc", 1.2);
brush.line(226, 504, 272, 502);
brush.set("cpencil", "#7f8b9b", 1.4);
brush.line(258, 512, 310, 514);
brush.set("cpencil", "#e8dcbc", 0.9);
brush.line(230, 522, 266, 524);
brush.set("cpencil", "#7f8b9b", 1.2);
brush.line(254, 532, 304, 530);
brush.set("cpencil", "#e8dcbc", 0.75);
brush.line(240, 542, 276, 544);
brush.set("cpencil", "#7f8b9b", 0.95);
brush.line(264, 552, 298, 554);
brush.set("cpencil", "#e8dcbc", 0.65);
brush.line(244, 562, 272, 560);
brush.set("cpencil", "#7f8b9b", 0.7);
brush.line(258, 570, 290, 572);
// A tapered, broken road of silver follows the moon down the moving water.
brush.noFill();
for (let i = 0; i < 34; i++) {
const y = 477 + i * 3.25;
const spread = 10 + i * 0.62;
const x = 266 + randomGaussian(0, spread * 0.4);
brush.set("cpencil", i % 4 ? "#7f8b9b" : "#e8dcbc", 0.6 + random(0, 0.5));
brush.line(x - spread * random(0.3, 0.8), y, x + spread * random(0.3, 0.8), y + random(-0.8, 0.8));
}
// --- the bridge. Piles and braces first, then the deck laid over their heads.
pileStrokes(52, 5, 12.0, 764);
pileStrokes(180, -5, 10.4, 728);
pileStrokes(318, 3, 9.6, 714);
pileStrokes(450, -6, 8.4, 700);
pileStrokes(584, 4, 7.6, 706);
pileStrokes(714, -3, 6.8, 694);
pileStrokes(120, 2, 6.0, 670);
pileStrokes(388, -2, 5.2, 648);
pileStrokes(650, 3, 5.0, 652);
brace(52, 180, 106);
brace(318, 450, 90);
brace(584, 714, 82);
deckRun(9, 5.0, "#0c1420", "2B");
deckRun(3, 4.6, "#0c1420", "2B");
deckRun(-3, 4.0, "#0c1420", "2B");
deckRun(-9, 3.4, "#0c1420", "charcoal");
deckRun(-13, 2.4, "#0c1420", "2B");
deckRun(-11, 1.1, "#4a3b30", "2H");
// --- boards catching the thin light. Denser where the span faces the moon.
let bx = -34;
for (let i = 0; i < 52; i++) {
if (bx > 838) break;
const t = constrain((bx + 52) / 904, 0, 1);
const nearMoon = 1 - Math.abs(t - 0.36);
const lit = random() < 0.3 + nearMoon * 0.48;
board(bx, lit);
bx += Math.max(10, randomGaussian(17 - nearMoon * 2.5, 3.0));
}
// the lit lip of the deck, in four broken runs — never one unbroken line
brush.noFill();
brush.set("cpencil", "#7f8b9b", 1.4);
brush.beginShape(0.35);
brush.vertex(-20, deckY(-20) - 16);
brush.vertex(96, deckY(96) - 17);
brush.vertex(212, deckY(212) - 17);
brush.endShape(false);
brush.set("cpencil", "#e8dcbc", 1.15);
brush.beginShape(0);
brush.vertex(256, deckY(256) - 17);
brush.vertex(374, deckY(374) - 17);
brush.endShape(false);
brush.set("cpencil", "#e8dcbc", 0.9);
brush.beginShape(0);
brush.vertex(436, deckY(436) - 16);
brush.vertex(556, deckY(556) - 16);
brush.endShape(false);
brush.set("cpencil", "#7f8b9b", 1.0);
brush.line(650, deckY(650) - 16, 794, deckY(794) - 14);
// --- the rail: posts hand-set at uneven intervals, then the run laid over them
railPost(24, 44);
railPost(112, 40);
railPost(208, 46);
railPost(288, 41);
railPost(376, 45);
railPost(462, 39);
railPost(548, 43);
railPost(642, 40);
railPost(748, 42);
brush.set("charcoal", "#0c1420", 2.4);
brush.noFill();
brush.beginShape(0.35);
brush.vertex(-40, deckY(-40) - 44);
brush.vertex(160, deckY(160) - 46);
brush.vertex(400, deckY(400) - 45);
brush.vertex(620, deckY(620) - 43);
brush.vertex(842, deckY(842) - 41);
brush.endShape(false);
brush.set("2H", "#7f8b9b", 0.75);
brush.beginShape(0);
brush.vertex(150, deckY(150) - 48);
brush.vertex(330, deckY(330) - 49);
brush.endShape(false);
brush.set("2H", "#7f8b9b", 0.55);
brush.line(500, deckY(500) - 47, 660, deckY(660) - 45);
// --- darkest ink last: the line where the deck lifts off the water
brush.set("2B", "#0c1420", 3.4);
brush.beginShape(0.3);
brush.vertex(-44, deckY(-44) + 13);
brush.vertex(230, deckY(230) + 15);
brush.vertex(530, deckY(530) + 14);
brush.vertex(842, deckY(842) + 11);
brush.endShape(false);
// two ripples far right where the current turns, one under the moon's road
brush.set("2B", "#7f8b9b", 0.65);
brush.beginShape(0.7);
brush.vertex(688, 612);
brush.vertex(742, 624);
brush.vertex(792, 616);
brush.endShape(false);
brush.set("2B", "#7f8b9b", 0.5);
brush.beginShape(0);
brush.vertex(716, 660);
brush.vertex(772, 670);
brush.endShape(false);
brush.set("2B", "#e8dcbc", 0.5);
brush.line(250, 590, 280, 588);
}