Snow Finds the Flame
2026 · nocturne
Snow is falling everywhere, but it is only snow where something is burning; the rest of the field is weather we are told about rather than shown.
a stone lantern off to the lower right in deep blue night, snow drifting diagonally across the whole field, the flakes turning warm only where the lamp can reach them
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: a stone lantern off-centre right, the lit window of its fire box on the lower-right thirds
// crossing; snow drifts on one diagonal from the upper left and banks into a crest along the bottom right
// layering: night, cold sky washes, warm halo, far snow and three squalls, ink stone twice over, snow caps, drift, flame last
const PALETTE = [
"#152740", // ground — night blue, and most of the picture is left as it is
"#060c17", // ink — the lantern's stone
"#2f5680", // cold — sky haze and the snow underfoot
"#a8c2d6", // flake — snow crisp enough to catch the sky
"#f0cb8b", // glow — lamplight, the accent
"#d07a35", // ember — the flame itself, rationed hardest
];
const LAMP_X = 535;
const LAMP_Y = 531;
const FALL = 1.15; // radians: the whole storm leans this way, down and to the right
// a loose vertex — nothing in stone stays straight once snow has been sitting on it
function jv(x, y) {
brush.vertex(x + random(-2.2, 2.2), y + random(-1.9, 1.9), 1);
}
function wash(x, y, r, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.42, 0.38);
brush.fill(col, op);
brush.circle(x, y, r, true);
}
// a near-black only holds if the shape stays tight; bled wide it blooms pale.
// so every piece of stone is laid twice, the second pass a little inside the first.
function ink(pts, curve) {
brush.noStroke();
brush.fillTexture(0.44, 0.58);
for (const pass of [0, 1]) {
brush.fillBleed(pass ? 0.06 : 0.09, "out");
brush.fill("#060c17", pass ? 240 : 210);
brush.beginShape(curve);
for (const p of pts) jv(p[0], p[1]);
brush.endShape(true);
}
}
function cap(pts, op) {
brush.noStroke();
brush.fillBleed(0.26, "out");
brush.fillTexture(0.5, 0.6);
brush.fill("#a8c2d6", op);
brush.beginShape(0.2);
for (const p of pts) jv(p[0], p[1]);
brush.endShape(true);
}
// one flake — mostly a speck, sometimes long enough to show it is falling,
// and warmer the closer it passes to the flame
function flake(x, y, cold, pale, warm) {
const d = Math.hypot(x - LAMP_X, y - LAMP_Y);
const t = constrain(1 - d / 210, 0, 1);
const c = t > 0.5
? lerpColor(pale, warm, (t - 0.5) / 0.5)
: lerpColor(cold, pale, constrain(randomGaussian(0.3 + t, 0.24), 0, 1));
const speck = random() < 0.62;
const len = speck ? max(1.6, randomGaussian(2.8, 1.1)) : max(5, randomGaussian(13 + t * 8, 5));
const w = speck
? max(0.6, randomGaussian(1.15 + t * 0.7, 0.34))
: max(0.3, randomGaussian(0.5 + t * 0.5, 0.2));
const a = FALL + randomGaussian(0, 0.15);
brush.set(speck ? "pen" : "2H", c, w);
brush.line(x, y, x + Math.cos(a) * len, y + Math.sin(a) * len);
}
// a squall: flakes crowded along the fall axis, thinning out across it
function squall(cx, cy, along, across, n, cold, pale, warm) {
const ux = Math.cos(FALL);
const uy = Math.sin(FALL);
for (let i = 0; i < n; i++) {
const s = randomGaussian(0, along);
const q = randomGaussian(0, across);
flake(cx + ux * s + uy * -q, cy + uy * s + ux * q, cold, pale, warm);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#152740");
// --- weather in the sky, heaviest where the snow is coming from ---
wash(108, 58, 276, "#2f5680", 30, 0.74);
wash(306, 216, 254, "#2f5680", 22, 0.72);
wash(44, 264, 198, "#2f5680", 18, 0.72);
wash(224, 8, 182, "#2f5680", 17, 0.7);
// a slightly colder pool behind the lantern, so the stone has something to be dark against
wash(566, 490, 214, "#2f5680", 26, 0.72);
wash(646, 604, 166, "#2f5680", 18, 0.7);
wash(452, 424, 148, "#2f5680", 15, 0.7);
// --- the lamp's reach, painted as air before the stone that casts it ---
wash(LAMP_X + 6, LAMP_Y + 10, 212, "#f0cb8b", 26, 0.8);
wash(LAMP_X - 4, LAMP_Y + 2, 146, "#f0cb8b", 40, 0.76);
wash(LAMP_X + 12, LAMP_Y + 18, 92, "#f0cb8b", 54, 0.72);
wash(LAMP_X + 2, LAMP_Y + 6, 56, "#f0cb8b", 66, 0.66);
wash(LAMP_X + 36, LAMP_Y + 112, 118, "#f0cb8b", 22, 0.74);
wash(LAMP_X - 50, LAMP_Y + 158, 88, "#f0cb8b", 14, 0.72);
const cold = color("#2f5680");
const pale = color("#a8c2d6");
const warm = color("#f0cb8b");
// --- the far field: a jittered lattice, thinned hard toward the two quiet corners ---
for (let gy = 0; gy < 24; gy++) {
for (let gx = 0; gx < 24; gx++) {
const x = -16 + gx * 35 + randomGaussian(0, 10);
const y = -16 + gy * 35 + randomGaussian(0, 10);
const across = Math.abs((x - y) / 800);
const n = noise(x * 0.0048, y * 0.0048);
if (random() > -0.06 + n * 1.2 - across * 0.82) continue;
flake(x, y, cold, pale, warm);
}
}
// four squalls: the dense weather, sweeping in from the upper left, passing under the lamp
squall(352, 254, 300, 74, 220, cold, pale, warm);
squall(486, 632, 172, 82, 140, cold, pale, warm);
squall(136, 100, 130, 44, 74, cold, pale, warm);
squall(688, 176, 168, 52, 88, cold, pale, warm);
// --- the lantern: roof, fire box, platform, shaft, all in the one dark ---
ink([
[534, 426], [590, 458], [646, 494], [664, 508], [632, 498],
[570, 488], [496, 488], [434, 498], [404, 508], [422, 496],
[478, 458],
], 0.1);
ink([[494, 488], [578, 491], [582, 572], [490, 569]], 0.04);
ink([[496, 492], [576, 495], [579, 568], [493, 565]], 0.04);
ink([[480, 569], [592, 572], [586, 599], [484, 596]], 0.04);
ink([[512, 599], [560, 601], [578, 812], [500, 808]], 0.04);
brush.noStroke();
brush.fillBleed(0.08, "out");
brush.fill("#060c17", 242);
brush.circle(533, 422, 11, true);
brush.fill("#060c17", 226);
brush.circle(534, 408, 6, true);
// --- snow lying along the roof, joined over the ridge, heavier on the windward left ---
cap([[548, 416], [498, 442], [436, 484], [416, 496], [438, 500], [498, 464], [552, 432]], 196);
cap([[522, 418], [572, 442], [630, 486], [648, 498], [626, 500], [570, 468], [518, 434]], 156);
cap([[483, 566], [591, 569], [588, 576], [485, 573]], 176);
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fill("#a8c2d6", 118);
brush.circle(422, 500, 7, true);
brush.fill("#a8c2d6", 92);
brush.circle(652, 502, 5.5, true);
brush.fill("#a8c2d6", 100);
brush.circle(513, 603, 5.5, true);
brush.fill("#a8c2d6", 76);
brush.circle(566, 605, 4.5, true);
brush.fill("#a8c2d6", 60);
brush.circle(509, 668, 4, true);
// --- the drift: banked along the bottom right, rising, burying the foot of the shaft ---
wash(342, 802, 152, "#2f5680", 84, 0.52);
wash(474, 790, 170, "#2f5680", 96, 0.5);
wash(604, 772, 178, "#2f5680", 108, 0.5);
wash(744, 756, 172, "#2f5680", 96, 0.5);
wash(812, 794, 142, "#2f5680", 82, 0.5);
wash(214, 810, 134, "#2f5680", 50, 0.52);
// the crest, pale where it turns up to the sky
wash(492, 786, 112, "#a8c2d6", 30, 0.58);
wash(630, 764, 120, "#a8c2d6", 36, 0.56);
wash(764, 748, 106, "#a8c2d6", 30, 0.56);
wash(362, 800, 92, "#a8c2d6", 17, 0.58);
// and warm where the lamp still reaches it
wash(552, 716, 104, "#f0cb8b", 34, 0.62);
wash(650, 732, 84, "#f0cb8b", 22, 0.6);
wash(466, 736, 76, "#f0cb8b", 16, 0.6);
// --- the window, and the flame behind it: the reason for the picture ---
// the pane, built up in three passes so the blue underneath stops showing through
brush.noStroke();
brush.fillBleed(0.05, "out");
brush.fillTexture(0.05, 0.18);
for (const op of [200, 232, 250]) {
brush.fill("#d07a35", op);
brush.beginShape(0.06);
jv(515, 509);
jv(557, 512);
jv(555, 553);
jv(513, 550);
brush.endShape(true);
}
brush.fill("#f0cb8b", 210);
brush.circle(535, 530, 17, true);
brush.fill("#f0cb8b", 246);
brush.circle(535, 530, 13, true);
brush.fillBleed(0.2, "out");
brush.fill("#d07a35", 224);
brush.circle(533, 540, 7.5, true);
brush.fill("#f0cb8b", 255);
brush.circle(537, 526, 6, true);
brush.fill("#f0cb8b", 255);
brush.circle(533, 533, 4.4, true);
// warm light caught on the roof's underside, the box edges and the platform lip
brush.noFill();
brush.set("2H", "#f0cb8b", 0.95);
brush.line(500, 490, 572, 493);
brush.set("2H", "#f0cb8b", 0.65);
brush.line(490, 571, 584, 574);
brush.set("2H", "#d07a35", 0.6);
brush.line(514, 600, 562, 602);
brush.set("2H", "#f0cb8b", 0.55);
brush.line(497, 511, 495, 556);
brush.set("2H", "#f0cb8b", 0.5);
brush.line(577, 514, 577, 558);
// --- near flakes, scattered not ringed: the only marks allowed to run warm ---
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.34, 0.45);
for (let i = 0; i < 74; i++) {
const x = LAMP_X + randomGaussian(-14, 122);
const y = LAMP_Y + randomGaussian(-18, 134);
const d = Math.hypot(x - LAMP_X, y - LAMP_Y);
if (d < 56 && random() < 0.82) continue;
const t = constrain(1 - d / 200, 0, 1);
brush.fill(random() < 0.55 * t + 0.05 ? "#f0cb8b" : "#a8c2d6", max(48, randomGaussian(66 + t * 152, 30)));
brush.circle(x, y, max(1.2, randomGaussian(1.9 + t * 3.2, 1.1)), true);
}
// three flakes passing close enough to blur
brush.fillBleed(0.56, "out");
brush.fill("#f0cb8b", 128);
brush.circle(478, 646, 8, true);
brush.fill("#f0cb8b", 98);
brush.circle(626, 612, 6.6, true);
brush.fill("#a8c2d6", 100);
brush.circle(608, 396, 5.6, true);
// the lamp reaching the snow it has to shine through: warm on the near ledges
brush.fillBleed(0.3, "out");
brush.fillTexture(0.3, 0.45);
brush.fill("#f0cb8b", 54);
brush.circle(520, 574, 38, true);
brush.fill("#f0cb8b", 34);
brush.circle(576, 578, 26, true);
brush.fill("#f0cb8b", 30);
brush.circle(506, 482, 40, true);
brush.fill("#f0cb8b", 22);
brush.circle(572, 480, 32, true);
// --- the darks land last: eave tips, and the shadow under the platform ---
brush.fillBleed(0.07, "out");
brush.fillTexture(0.4, 0.6);
brush.fill("#060c17", 246);
brush.circle(428, 501, 6, true);
brush.fill("#060c17", 244);
brush.circle(642, 500, 5.5, true);
brush.fill("#060c17", 236);
brush.circle(530, 596, 8, true);
brush.fill("#060c17", 222);
brush.circle(548, 662, 6, true);
// short streaks crossing in front of the glow, so the snow reads as passing through it
brush.noFill();
brush.set("pen", "#f0cb8b", 0.9);
brush.line(602, 536, 590, 566);
brush.set("pen", "#a8c2d6", 0.75);
brush.line(462, 546, 450, 576);
brush.set("2H", "#f0cb8b", 0.65);
brush.line(644, 516, 630, 548);
brush.set("2H", "#a8c2d6", 0.55);
brush.line(496, 644, 484, 672);
}