Rain Crossing the Light
2026 · weather
The rain is one substance and two colours; only where the light happens to be does it agree to be seen.
a rain shower ruling the whole field from upper-left to lower-right, a band of sun crossing the upper-right third where the falling rain turns pale, dark slate rain everywhere else, the air already clearing in the lower-left
painted by GPT-6 Astrap5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: one shower rules the whole field from upper-left to lower-right; a band of sun crosses the
// upper-right third, and the same rain turns pale inside it and slate-dark outside, thickest at the thirds
// crossing near (533, 267). The shower thins to almost bare paper down the left, where the air is clearing.
// layering: damp paper, grey air smeared along the fall, the warm band and its cream lip, three passes of
// ruled rain, then the low wet bank, its soaked edge and the water it throws back — the darks last.
const PALETTE = [
"#e5e0d4", // ground — damp warm paper
"#94a0a6", // veil — grey rain-air
"#c0a069", // light — the warm band the sun cuts
"#f6efdc", // gleam — rain caught in the beam
"#4a565e", // shadow — rain out of the light
"#1d252a", // ink — the low wet bank, last darks
];
const LEAN = 2.35; // dy/dx — the rain falls steeply, upper-left to lower-right
const FALL = Math.atan2(LEAN, 1); // the same angle in radians, for smearing the air along it
// the beam: an axis running down to the right, widest at the right edge, faded out at the left margin
const BEAM = { ax: 262, ay: 60, dx: 0.834, dy: 0.552, core: 74, soft: 106 };
const BEAM_ANG = Math.atan2(BEAM.dy, BEAM.dx);
function pigment(base, toward, mean, spread) {
return lerpColor(color(base), color(toward), constrain(randomGaussian(mean, spread), 0, 1));
}
// 1 in the thick of the light, 0 out in the grey
function beamF(x, y) {
const across = -BEAM.dy * (x - BEAM.ax) + BEAM.dx * (y - BEAM.ay);
const along = BEAM.dx * (x - BEAM.ax) + BEAM.dy * (y - BEAM.ay);
const band = 1 - constrain((abs(across) - BEAM.core) / BEAM.soft, 0, 1);
const reach = constrain(map(along, -160, 120, 0.05, 1), 0.05, 1);
return band * reach;
}
// 0 where the paper stays bare down the left, ~1 in the body of the shower
function density(x, y) {
const clearing = constrain(map(Math.hypot(x - 30, y - 610), 120, 520, 0.02, 1), 0.02, 1);
const gust = map(noise(x * 0.0034, y * 0.0047), 0, 1, 0.1, 1.34);
const brow = constrain(map(y, -60, 130, 0.55, 1), 0.55, 1);
return clearing * gust * brow * (1 + 0.45 * beamF(x, y));
}
// a long soft wash lying along `ang` — the only shape the air is ever given
function smear(cx, cy, halfLen, halfWide, ang, col, op, bleed) {
const ca = Math.cos(ang);
const sa = Math.sin(ang);
const at = (u, v) => [cx + ca * u - sa * v, cy + sa * u + ca * v];
const pts = [
at(-halfLen * (0.92 + random(0, 0.2)), random(-halfWide * 0.25, halfWide * 0.25)),
at(-halfLen * 0.5, -halfWide * (0.78 + random(0, 0.34))),
at(halfLen * 0.42, -halfWide * (0.66 + random(0, 0.38))),
at(halfLen * (0.94 + random(0, 0.18)), random(-halfWide * 0.25, halfWide * 0.25)),
at(halfLen * 0.48, halfWide * (0.72 + random(0, 0.34))),
at(-halfLen * 0.44, halfWide * (0.8 + random(0, 0.3))),
];
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.5, 0.5);
brush.fill(col, op);
brush.beginShape(0.55);
for (const q of pts) brush.vertex(q[0], q[1]);
brush.endShape(true);
}
// the same wash, placed in the beam's own coordinates: `along` down the axis, `off` across it
function beamSmear(along, off, halfLen, halfWide, col, op, bleed) {
const cx = BEAM.ax + BEAM.dx * along - BEAM.dy * off;
const cy = BEAM.ay + BEAM.dy * along + BEAM.dx * off;
smear(cx, cy, halfLen, halfWide, BEAM_ANG, col, op, bleed);
}
// one ruled fall of rain, leaning with the shower and coloured by the light it crosses
function rainRule(x, y, len, weight, name, lean) {
const dx = len / Math.hypot(1, lean);
const segments = Math.max(1, Math.ceil(len / 26));
brush.noFill();
for (let i = 0; i < segments; i++) {
const t = i / segments;
const end = (i + 1) / segments;
const f = beamF(x + dx * (t + end) * 0.5, y + dx * lean * (t + end) * 0.5);
const col = f > 0.38
? pigment(PALETTE[3], PALETTE[2], 0.1, 0.08)
: f > 0.18 ? pigment(PALETTE[2], PALETTE[3], 0.46, 0.12)
: pigment(PALETTE[4], PALETTE[1], 0.16, 0.08);
brush.set(name, col, weight * (1 + f * 0.7));
brush.line(x + dx * t, y + dx * lean * t, x + dx * end, y + dx * lean * end);
}
}
// a rule the drop broke into two — most of the near rain does this
function brokenRule(x, y, len, weight, name) {
const lean = LEAN + randomGaussian(0, 0.22);
const cut = constrain(randomGaussian(0.44, 0.1), 0.18, 0.68);
const gap = constrain(randomGaussian(0.15, 0.05), 0.05, 0.3);
const dx = len / Math.hypot(1, lean);
rainRule(x, y, len * cut, weight, name, lean);
const s = cut + gap;
rainRule(x + dx * s, y + dx * s * lean, len * (1 - s), weight * 0.9, name, lean);
}
function rainPass(tries, take, lenMean, lenSpread, wMean, name, broken) {
for (let i = 0; i < tries; i++) {
const x = random(-160, 820);
const y = random(-140, 810);
if (random() > density(x, y) * take) continue;
const len = Math.max(14, randomGaussian(lenMean, lenSpread));
const w = Math.max(0.3, randomGaussian(wMean, wMean * 0.36));
if (broken && random() < 0.42) brokenRule(x, y, len, w, name);
else rainRule(x, y, len, w, name, LEAN + randomGaussian(0, 0.2));
}
}
// a long fall crossing several zones, drawn segment by segment so the light changes along it
function longFall(x, y, len, weight, name) {
let lean = LEAN + randomGaussian(0, 0.14);
let px = x;
let py = y;
for (let k = 0; k < 5; k++) {
const seg = (len / 5) * (0.8 + random(0, 0.45));
const dx = seg / Math.hypot(1, lean);
rainRule(px, py, seg, weight * (0.82 + random(0, 0.4)), name, lean);
px += dx;
py += dx * lean;
lean += randomGaussian(0, 0.09);
}
}
// water thrown back up off the bank where a heavy fall lands
function splash(x, y, s) {
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fill(PALETTE[0], 190 + random(-24, 40));
brush.circle(x, y - s * 0.4, Math.max(2, randomGaussian(s * 0.5, s * 0.16)), true);
const kicks = 2 + floor(random(0, 2.4));
for (let k = 0; k < kicks; k++) {
const a = randomGaussian(-1.5, 0.5);
const l = Math.max(5, randomGaussian(s * 2.8, s * 0.9));
brush.set("2H", pigment(PALETTE[0], PALETTE[1], 0.3, 0.16), 0.6 + random(0, 0.6));
brush.line(x, y, x + Math.cos(a) * l, y + Math.sin(a) * l);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PALETTE[0]);
// --- grey rain-air, smeared along the fall: a heavy field upper-left, a second one low right
smear(118, 104, 262, 124, FALL, PALETTE[1], 116, 0.46);
smear(246, 40, 224, 100, FALL, PALETTE[1], 104, 0.46);
smear(36, 262, 232, 108, FALL, PALETTE[1], 98, 0.48);
smear(190, 236, 196, 88, FALL, PALETTE[1], 76, 0.52);
smear(372, 88, 210, 82, FALL, PALETTE[1], 64, 0.52);
smear(70, 96, 208, 92, FALL, PALETTE[4], 72, 0.48);
smear(300, 150, 184, 70, FALL, PALETTE[1], 48, 0.58);
smear(742, 176, 236, 108, FALL, PALETTE[1], 76, 0.5);
smear(700, 470, 248, 116, FALL, PALETTE[1], 84, 0.5);
smear(792, 636, 214, 104, FALL, PALETTE[4], 62, 0.55);
smear(590, 656, 196, 92, FALL, PALETTE[1], 58, 0.55);
smear(468, 726, 176, 74, FALL, PALETTE[1], 40, 0.6);
// --- the band of sun: warm smoky air, a gold core through it, a cream lip along its upper edge
beamSmear(30, 8, 208, 92, pigment(PALETTE[2], PALETTE[1], 0.4, 0.1), 98, 0.44);
beamSmear(214, -6, 226, 96, pigment(PALETTE[2], PALETTE[1], 0.34, 0.1), 112, 0.44);
beamSmear(392, 14, 214, 90, pigment(PALETTE[2], PALETTE[1], 0.42, 0.1), 104, 0.44);
beamSmear(548, -10, 190, 84, pigment(PALETTE[2], PALETTE[1], 0.36, 0.1), 92, 0.46);
beamSmear(150, 30, 168, 52, PALETTE[2], 118, 0.4);
beamSmear(340, -18, 184, 46, PALETTE[2], 138, 0.4);
beamSmear(486, 22, 156, 42, PALETTE[2], 126, 0.42);
beamSmear(258, -82, 188, 30, PALETTE[3], 216, 0.3);
beamSmear(424, -90, 172, 26, PALETTE[3], 208, 0.3);
beamSmear(566, -78, 146, 22, PALETTE[3], 196, 0.32);
beamSmear(340, -108, 154, 13, PALETTE[3], 176, 0.28);
beamSmear(506, -116, 130, 11, PALETTE[3], 162, 0.28);
// the light at its thickest, on the upper-right thirds crossing
smear(533, 267, 132, 58, BEAM_ANG, PALETTE[2], 120, 0.42);
smear(556, 228, 104, 30, BEAM_ANG, PALETTE[3], 150, 0.4);
// --- three passes of rain: the far sheet, the body of the shower, then the near heavy falls
rainPass(700, 0.5, 84, 26, 0.62, "2H", false);
rainPass(520, 0.42, 138, 44, 0.82, "HB", true);
rainPass(360, 0.26, 176, 58, 0.98, "2B", true);
// --- eight long falls, placed by hand, each drifting as it goes
longFall(96, -120, 620, 1.15, "HB");
longFall(232, -60, 700, 1.3, "2B");
longFall(318, -130, 560, 0.95, "HB");
longFall(404, -34, 660, 1.4, "2B");
longFall(492, -110, 520, 1.05, "HB");
longFall(548, 40, 470, 1.5, "2B");
longFall(640, -70, 430, 1.2, "HB");
longFall(688, 130, 360, 0.9, "HB");
// the falls the light has hold of — the brightest, heaviest marks in the picture
rainRule(468, 172, 262, 1.3, "pen", LEAN - 0.12);
rainRule(536, 148, 236, 1.1, "pen", LEAN + 0.16);
rainRule(592, 210, 208, 1.45, "pen", LEAN - 0.05);
rainRule(650, 176, 246, 1.2, "pen", LEAN + 0.1);
rainRule(414, 214, 190, 1.0, "HB", LEAN);
rainRule(712, 248, 198, 1.35, "pen", LEAN - 0.14);
rainRule(504, 262, 224, 1.15, "pen", LEAN + 0.08);
rainRule(446, 128, 208, 1.05, "pen", LEAN - 0.18);
rainRule(614, 132, 232, 1.25, "pen", LEAN + 0.05);
rainRule(688, 208, 186, 1.1, "pen", LEAN - 0.08);
rainRule(566, 306, 168, 1.3, "pen", LEAN + 0.14);
rainRule(742, 168, 176, 1.0, "HB", LEAN);
// --- the low wet bank, dark and wholly out of the light, running off the left and bottom edges
brush.noStroke();
brush.fillBleed(0.32, "out");
brush.fillTexture(0.55, 0.5);
brush.fill(PALETTE[4], 182);
brush.beginShape(0.35);
brush.vertex(-40, 690);
brush.vertex(148, 672);
brush.vertex(346, 692);
brush.vertex(520, 726);
brush.vertex(660, 784);
brush.vertex(742, 824);
brush.vertex(-40, 824);
brush.endShape(true);
brush.fill(PALETTE[5], 208);
brush.beginShape(0.35);
brush.vertex(-40, 744);
brush.vertex(170, 728);
brush.vertex(358, 750);
brush.vertex(498, 784);
brush.vertex(604, 824);
brush.vertex(-40, 824);
brush.endShape(true);
brush.fill(PALETTE[5], 150);
brush.circle(84, 792, 80, true);
brush.fill(PALETTE[5], 100);
brush.circle(306, 796, 64, true);
brush.fill(PALETTE[5], 82);
brush.circle(492, 808, 52, true);
// wet grass laid over by the rain, hatched along the fall
brush.noFill();
brush.hatchStyle("2H", pigment(PALETTE[5], PALETTE[4], 0.28, 0.08), 0.8);
brush.hatch(7, 68, { rand: 0.45, continuous: false, gradient: 0.5 });
brush.polygon([[-30, 698], [344, 696], [576, 748], [-30, 776]]);
brush.noHatch();
// --- last darks: the bank's soaked edge, then the heaviest near falls over it
brush.set("charcoal", pigment(PALETTE[5], PALETTE[4], 0.12, 0.06), 1.1);
brush.line(-24, 700, 64, 688);
brush.set("charcoal", pigment(PALETTE[5], PALETTE[4], 0.3, 0.08), 0.8);
brush.line(102, 686, 234, 692);
brush.set("2B", pigment(PALETTE[5], PALETTE[4], 0.18, 0.06), 1.2);
brush.line(268, 694, 356, 708);
brush.set("charcoal", pigment(PALETTE[5], PALETTE[4], 0.34, 0.1), 0.7);
brush.line(438, 716, 552, 742);
brush.set("2B", pigment(PALETTE[5], PALETTE[4], 0.2, 0.08), 1.0);
brush.line(588, 754, 664, 788);
// one pooled dark where the ground lies lowest, off the bottom-left corner
brush.noStroke();
brush.fillBleed(0.28, "out");
brush.fillTexture(0.6, 0.55);
brush.fill(PALETTE[5], 138);
brush.circle(46, 808, 92, true);
brush.fill(PALETTE[5], 96);
brush.circle(196, 820, 68, true);
brush.set("2B", pigment(PALETTE[5], PALETTE[4], 0.34, 0.12), 0.85);
brush.line(36, 722, 76, 748);
brush.line(196, 716, 228, 742);
brush.line(330, 742, 368, 770);
brush.line(452, 756, 484, 786);
brush.line(128, 730, 152, 756);
rainRule(186, 574, 230, 1.25, "2B", LEAN + 0.1);
rainRule(268, 536, 250, 1.1, "2B", LEAN - 0.15);
rainRule(96, 586, 190, 1.05, "2B", LEAN + 0.2);
rainRule(352, 558, 210, 1.2, "2B", LEAN);
rainRule(452, 606, 180, 1.15, "2B", LEAN - 0.1);
splash(70, 702, 12);
splash(158, 694, 9);
splash(226, 704, 14);
splash(348, 718, 11);
splash(430, 730, 10);
splash(516, 746, 12);
}