The Front Arrives
2026 · weather
Weather has a direction, and you can read it in one edge: everything left of that line is still yesterday.
a squall line advancing across an open field, the shelf cloud's hard edge cutting a diagonal with rain shafts trailing behind it and low gold light still holding at the left
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the hard underside of a shelf cloud cuts one diagonal from the upper left down to the right edge;
// the loaded mass fills everything above it, rain shafts trail off it into the lower right, the lower left stays open lit paper.
// layering: paper, gold light at the left, wash passes then combed hatch to build the mass, belly lobes, the broken hard edge, rain shafts and the wet foot last.
const PALETTE = [
"#e9e2d0", // paper — the clear light holding the left
"#9a9c8f", // olive-grey cloud body
"#6b6a5c", // bruised underbelly
"#3b3a33", // the hard shelf edge, near-black olive
"#7e8590", // cool slate rain
"#c9a45c", // low gold light, the accent
];
// the leading edge, left to right — lumpy, and steeper once it passes the middle
const SHELF = [
[-40, 132], [78, 170], [176, 176], [268, 212], [352, 230],
[438, 272], [516, 302], [590, 336], [672, 382], [752, 430], [846, 496],
];
function edgeAt(x) {
if (x <= SHELF[0][0]) return SHELF[0][1];
for (let i = 0; i < SHELF.length - 1; i++) {
const a = SHELF[i];
const b = SHELF[i + 1];
if (x <= b[0]) return lerp(a[1], b[1], (x - a[0]) / (b[0] - a[0]));
}
return SHELF[SHELF.length - 1][1];
}
// a strip of cloud between some upper boundary and the shelf edge itself.
// topLift measures up from the edge; pass topY instead for a strip hung off the top of the sheet.
function shelfBand(xa, xb, topY, topLift, lift) {
const pts = [];
for (let i = 0; i <= 6; i++) {
const x = lerp(xa, xb, i / 6);
pts.push([x, topY === null ? edgeAt(x) - topLift + randomGaussian(0, 9) : topY + randomGaussian(0, 11)]);
}
for (let i = 6; i >= 0; i--) {
const x = lerp(xa, xb, i / 6);
pts.push([x, edgeAt(x) - lift + randomGaussian(0, 7)]);
}
return pts;
}
function washBand(pts, col, opacity, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.5, 0.55);
brush.fill(col, opacity);
brush.beginShape(0.5);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(true);
}
function combBand(pts, dist, angle, col, name, weight) {
brush.noFill();
brush.hatchStyle(name, col, weight);
brush.hatch(dist, angle, { rand: 0.26, continuous: false });
brush.polygon(pts);
brush.noHatch();
}
function lobe(x, y, r, col, opacity, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.6, 0.55);
brush.fill(col, opacity);
brush.circle(x, y, r, true);
}
// one bitten-off length of the leading edge — never the same weight twice
function edgeStroke(x1, y1, x2, y2, w, name) {
brush.noFill();
brush.set(name, PALETTE[3], w);
brush.beginShape(0.4);
brush.vertex(x1 + random(-3, 3), y1 + random(-2, 3));
brush.vertex(lerp(x1, x2, 0.5) + random(-6, 6), lerp(y1, y2, 0.5) + random(-5, 4));
brush.vertex(x2 + random(-3, 3), y2 + random(-2, 3));
brush.endShape(false);
}
// a torn tuft dragged down out of the underside
function shelfNub(x, drop, wide, dark) {
const y = edgeAt(x);
lobe(x + random(-4, 4), y + drop * 0.45, wide * (0.78 + random(-0.14, 0.2)),
dark ? PALETTE[3] : PALETTE[2], (dark ? 132 : 96) + random(-20, 24), 0.44);
if (random() < 0.7) {
brush.noFill();
brush.set(random() < 0.5 ? "charcoal" : "2B", PALETTE[3], 0.9 + random(-0.25, 0.7));
brush.beginShape(0);
brush.vertex(x - wide * (0.5 + random(0, 0.5)), y + random(-4, 4));
brush.vertex(x + random(-9, 7), y + drop * (0.7 + random(0, 0.6)));
brush.endShape(false);
}
}
// a shaft of rain let down from the edge, dragged behind the front as it falls
function rainShaft(x0, len, lean, n, wide, weight) {
const y0 = edgeAt(x0) + random(2, 14);
const sn = Math.sin((lean * Math.PI) / 180);
brush.noStroke();
brush.fillTexture(0.5, 0.55);
for (let k = 0; k < 3; k++) {
const w = wide * (1 - k * 0.26);
const l = len * (1 - k * 0.2);
brush.fillBleed(0.26 - k * 0.05, "down");
brush.fill(k === 1 ? PALETTE[2] : PALETTE[4], (84 - k * 18) + random(-12, 14));
brush.beginShape(0.3);
brush.vertex(x0 - w * 0.5 + random(-4, 4), y0 + random(-6, 6));
brush.vertex(x0 + w * 0.52 + random(-4, 4), y0 + random(-8, 4));
brush.vertex(x0 + l * sn + w * 0.8, y0 + l + random(-30, 8));
brush.vertex(x0 + l * sn - w * 0.7, y0 + l * 0.9 + random(-22, 12));
brush.endShape(true);
}
for (let i = 0; i < n; i++) {
const sx = x0 + randomGaussian(0, wide * 0.42);
const sy = y0 + random(-4, 52);
const l = max(48, randomGaussian(len * 0.6, len * 0.3));
const ex = sx + l * Math.sin((lean + randomGaussian(0, 4)) * Math.PI / 180);
const r = random();
const name = r < 0.56 ? "2H" : r < 0.84 ? "HB" : r < 0.95 ? "cpencil" : "charcoal";
brush.set(name, r < 0.56 ? PALETTE[4] : r < 0.95 ? PALETTE[2] : PALETTE[3],
max(0.28, randomGaussian(weight * (r < 0.56 ? 0.6 : 1), 0.3)));
brush.line(sx, sy, ex, min(sy + l, 768));
}
}
// one heavy strand standing out of a shaft, bending as it falls and thinning at the foot
function heavyStrand(x1, y1, x2, y2, name, col, weight) {
const bend = (x2 - x1) * 0.35 + random(-14, 14);
brush.noFill();
brush.set(name, col, weight);
brush.beginShape(0.42);
brush.vertex(x1 + random(-3, 3), y1 + random(-4, 4));
brush.vertex(lerp(x1, x2, 0.42) + bend * 0.3, lerp(y1, y2, 0.45) + random(-10, 10));
brush.vertex(x2 + random(-4, 4), y2 + random(-8, 6));
brush.endShape(false);
// a lighter ghost beside it, so the strand reads as water and not wire
brush.set(name, PALETTE[4], max(0.3, weight * 0.45));
brush.beginShape(0.42);
brush.vertex(x1 + random(2, 9), y1 + random(0, 8));
brush.vertex(lerp(x1, x2, 0.5) + bend * 0.24 + random(2, 10), lerp(y1, y2, 0.5));
brush.vertex(x2 + random(3, 12), y2 - random(8, 30));
brush.endShape(false);
}
// the rain flattening out where it lands: a low smear, wider than it is deep
function footSmear(x, y, w, h, col, opacity) {
brush.fill(col, opacity + random(-12, 14));
brush.beginShape(0.5);
brush.vertex(x - w * 0.5 + random(-8, 8), y + random(-4, 4));
brush.vertex(x - w * 0.18 + random(-10, 10), y - h * (0.6 + random(0, 0.5)));
brush.vertex(x + w * 0.26 + random(-10, 10), y - h * (0.4 + random(0, 0.6)));
brush.vertex(x + w * 0.5 + random(-8, 8), y + random(-3, 6));
brush.vertex(x + w * 0.2 + random(-9, 9), y + h * (0.3 + random(0, 0.4)));
brush.vertex(x - w * 0.24 + random(-9, 9), y + h * (0.24 + random(0, 0.4)));
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PALETTE[0]);
// low gold light surviving at the left, under the advancing edge
brush.noStroke();
brush.fillBleed(0.7, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(PALETTE[5], 46);
brush.circle(146, 452, 122, true);
brush.fill(PALETTE[5], 30);
brush.circle(280, 372, 96, true);
brush.fill(PALETTE[5], 24);
brush.circle(66, 340, 82, true);
// the mass: wash passes first, top of the sheet down to the edge, weight gathering right
washBand(shelfBand(-40, 846, -40, 0, 6), PALETTE[1], 168, 0.28);
washBand(shelfBand(240, 846, -40, 0, 40), PALETTE[1], 132, 0.32);
washBand(shelfBand(-40, 420, -40, 0, 30), PALETTE[1], 96, 0.36);
washBand(shelfBand(300, 846, null, 190, 8), PALETTE[2], 128, 0.3);
washBand(shelfBand(-40, 360, null, 120, 10), PALETTE[2], 86, 0.36);
washBand(shelfBand(430, 846, null, 96, 4), PALETTE[3], 92, 0.32);
washBand(shelfBand(560, 846, null, 230, 6), PALETTE[2], 104, 0.3);
// combed in overlapping patches, each with its own grain, so the mass never reads as one texture
combBand(shelfBand(-40, 846, -40, 0, 10), 15, 12, PALETTE[2], "2H", 0.8);
combBand(shelfBand(-40, 300, -40, 0, 96), 9, 7, PALETTE[2], "cpencil", 0.9);
combBand(shelfBand(120, 470, -40, 0, 26), 7.5, 21, PALETTE[2], "2H", 0.95);
combBand(shelfBand(300, 620, -40, 0, 128), 11, -50, PALETTE[1], "cpencil", 1.0);
combBand(shelfBand(430, 846, -40, 0, 44), 6, 15, PALETTE[2], "cpencil", 1.0);
combBand(shelfBand(560, 846, 40, 0, 6), 5, 27, PALETTE[3], "2B", 0.8);
combBand(shelfBand(-40, 250, 24, 0, 8), 6.5, 31, PALETTE[3], "2B", 0.75);
combBand(shelfBand(250, 530, null, 72, 4), 5.5, 11, PALETTE[3], "charcoal", 0.7);
combBand(shelfBand(600, 846, null, 150, 4), 7, -42, PALETTE[2], "2H", 0.8);
combBand(shelfBand(60, 390, null, 124, 44), 13, -70, PALETTE[1], "2H", 0.7);
// bruised lobes crowding the underside — densest just left of the rain, the focal knuckle
for (let i = 0; i < 36; i++) {
const t = i / 35;
const x = lerp(-30, 846, t) + randomGaussian(0, 18);
if (random() < 0.14) continue;
const near = 1 - constrain(abs(x - 524) / 400, 0, 1);
const y = edgeAt(x) - max(4, randomGaussian(20 + near * 10, 13));
const r = max(12, randomGaussian(26 + near * 10, 8));
const deep = random() < 0.2 + near * 0.34;
lobe(x, y, r, deep ? PALETTE[3] : PALETTE[2], deep ? 130 + random(-24, 28) : 96 + random(-22, 26), 0.34);
}
// the knuckle: five heavier gestures stacked over the right-hand thirds point
lobe(506, 254, 56, PALETTE[3], 108, 0.3);
lobe(568, 292, 46, PALETTE[3], 138, 0.32);
lobe(458, 232, 40, PALETTE[2], 116, 0.36);
lobe(632, 328, 42, PALETTE[3], 100, 0.32);
lobe(716, 366, 34, PALETTE[2], 104, 0.34);
// pale breaths of light on the shoulders of the mass — they cut the comb open
lobe(126, 84, 66, PALETTE[0], 78, 0.5);
lobe(212, 44, 50, PALETTE[0], 58, 0.5);
lobe(402, 96, 56, PALETTE[0], 52, 0.52);
lobe(300, 136, 32, PALETTE[0], 44, 0.5);
// and the loaded core answers them, low on the right
lobe(760, 246, 64, PALETTE[3], 96, 0.32);
lobe(658, 266, 50, PALETTE[3], 78, 0.34);
lobe(830, 330, 56, PALETTE[3], 88, 0.32);
lobe(700, 178, 44, PALETTE[3], 62, 0.36);
lobe(590, 236, 38, PALETTE[2], 84, 0.34);
// the hard edge, drawn last over the wet mass — bitten lengths, one soft gap near the left
edgeStroke(-40, 132, 84, 172, 2.4, "charcoal");
edgeStroke(72, 168, 186, 178, 3.1, "charcoal");
edgeStroke(258, 208, 356, 232, 3.5, "charcoal");
edgeStroke(344, 228, 448, 276, 2.3, "2B");
edgeStroke(436, 270, 528, 306, 4.0, "charcoal");
edgeStroke(520, 302, 600, 340, 3.0, "charcoal");
edgeStroke(594, 336, 684, 386, 2.1, "2B");
edgeStroke(676, 382, 762, 434, 2.9, "charcoal");
edgeStroke(754, 430, 848, 496, 1.9, "2B");
brush.set("2B", PALETTE[3], 1.2);
brush.line(198, 180, 246, 198);
brush.line(662, 372, 700, 396);
// lit lip: the leading edge catching the low light where it is still thin
brush.set("2H", PALETTE[0], 1.9);
brush.line(28, 150, 98, 182);
brush.line(114, 188, 194, 192);
brush.set("2H", PALETTE[5], 1.3);
brush.line(150, 194, 234, 216);
// nubs hanging off the edge, torn and unequal
shelfNub(118, 26, 20, false);
shelfNub(206, 15, 13, false);
shelfNub(296, 40, 26, true);
shelfNub(372, 24, 16, false);
shelfNub(452, 54, 31, true);
shelfNub(538, 42, 25, true);
shelfNub(604, 31, 19, false);
shelfNub(694, 47, 28, true);
shelfNub(782, 29, 17, false);
// rain, trailing behind the front into the lower right
rainShaft(534, 442, 16, 72, 84, 1.05);
rainShaft(616, 396, 19, 56, 70, 0.92);
rainShaft(466, 312, 14, 38, 56, 0.74);
rainShaft(696, 344, 22, 44, 66, 0.82);
rainShaft(772, 286, 25, 28, 58, 0.66);
rainShaft(408, 176, 12, 16, 40, 0.52);
// four heavier strands standing out of the shafts, each bending a little as it falls
heavyStrand(524, 320, 640, 716, "charcoal", PALETTE[2], 1.8);
heavyStrand(576, 346, 674, 670, "charcoal", PALETTE[2], 1.3);
heavyStrand(618, 368, 710, 646, "HB", PALETTE[3], 1.1);
heavyStrand(492, 298, 560, 552, "HB", PALETTE[2], 0.9);
// the wet foot: the rain flattening out where it meets the land
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.45, 0.5);
footSmear(672, 720, 128, 30, PALETTE[4], 74);
footSmear(776, 702, 104, 24, PALETTE[4], 58);
footSmear(598, 734, 90, 22, PALETTE[2], 46);
footSmear(830, 664, 96, 20, PALETTE[4], 36);
// warm light pooling out on the ground at the left, where nothing has reached yet
lobe(206, 692, 50, PALETTE[5], 48, 0.62);
lobe(112, 704, 34, PALETTE[5], 34, 0.62);
lobe(316, 686, 26, PALETTE[5], 24, 0.62);
}