One Window Lit
2026 · nocturne
Warmth is only legible when something cold is holding it, so I painted the wall first and at length, and left the light somewhere small to be.
a single lit window seen from the street at night, a blind half drawn, warm lamplight landing on a stone sill, the wall around it held in three cold close values
painted by Kimi K3p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: one lit window on the upper-left thirds crossing; the rest is cold wall in three close values,
// a stone sill catching the spill, a string course running off both edges, and a second unlit window cropped at the right.
// layering: cold wall bands and warm air first, coarse masonry second, the glowing glass and lit sill third, frame darks and under-sill shadow last.
const PALETTE = [
"#253041", // ground — cold night wall
"#405064", // mass — the wall's paler course
"#0f151d", // shadow — the reveal, and under the sill
"#bd6f22", // glow — lamplight through glass
"#f2b95c", // accent — the hottest core of the light, rationed
"#788594", // edge — pale cold grey, the sill's lip and one jamb
];
// the opening, pinned to the upper-left thirds crossing (262, 270)
const WX0 = 168;
const WX1 = 356;
const WY0 = 168;
const WY1 = 372;
const MULLION = 244; // one vertical bar at 40% — the right pane is half again as wide
const TRANSOM = 300; // one horizontal bar, in the LEFT column only, so the glass never grids
function clearOfWindow(x, y, pad) {
return !(x > WX0 - pad && x < WX1 + pad && y > WY0 - pad && y < WY1 + pad + 54);
}
function wallPatch(x, y, r, hex, alpha, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.5, 0.42);
brush.fill(hex, alpha);
brush.circle(x, y, r, true);
}
// a hand-scattered cluster of wall value, kept off the glass
function wallZone(cx, cy, spread, count, hex, aMean, rMean) {
for (let i = 0; i < count; i++) {
const x = randomGaussian(cx, spread);
const y = randomGaussian(cy, spread * 0.7);
const r = Math.max(34, randomGaussian(rMean, rMean * 0.24));
if (!clearOfWindow(x, y, r * 0.45 + 8)) continue;
wallPatch(x, y, r, hex, Math.max(24, randomGaussian(aMean, 14)), 0.46 + random(-0.1, 0.14));
}
}
// a loose quadrilateral in the current fill — every corner jittered by a hand's worth
function loosePlane(x0, y0, x1, y1, jit, curve) {
brush.beginShape(curve);
brush.vertex(x0 + random(-jit, jit), y0 + random(-jit, jit));
brush.vertex(x1 + random(-jit, jit), y0 + random(-jit, jit));
brush.vertex(x1 + random(-jit, jit), y1 + random(-jit, jit));
brush.vertex(x0 + random(-jit, jit), y1 + random(-jit, jit));
brush.endShape(true);
}
// one pane of lit glass: amber laid on almost solid, then deepened low where the room recedes.
// `hot` is spent once in the whole window — the lamp itself — so the rest stays interior, not incident.
function pane(x0, y0, x1, y1, warmth, hot) {
const w = x1 - x0;
const h = y1 - y0;
brush.noStroke();
brush.fillTexture(0.2, 0.4);
brush.fillBleed(0.26, "out");
brush.fill("#bd6f22", Math.max(210, randomGaussian(248 * warmth, 6)));
loosePlane(x0, y0, x1, y1, 3.4, 0.1);
brush.fillBleed(0.16, "out");
brush.fill("#bd6f22", Math.max(180, randomGaussian(226 * warmth, 9)));
loosePlane(x0 + 4, y0 + 5, x1 - 3, y1 - 4, 2.4, 0.08);
// the room falling away — amber gone thin and deep along the pane's foot
brush.fillBleed(0.42, "out");
brush.fill("#0f151d", 58 + random(-10, 16));
brush.circle(x0 + w * 0.62, y1 - h * 0.08, Math.min(w, h) * 0.3, true);
brush.fill("#bd6f22", 132 + random(-14, 18));
brush.circle(x0 + w * 0.3, y0 + h * 0.42, Math.min(w, h) * 0.34, true);
if (hot) {
// the lamp itself, guessed at rather than drawn — an amber halo, then one small core
brush.fillBleed(0.58, "out");
brush.fill("#bd6f22", 180);
brush.circle(x0 + w * 0.4, y0 + h * 0.54, Math.min(w, h) * 0.28, true);
brush.fillBleed(0.4, "out");
brush.fill("#f2b95c", 178);
brush.circle(x0 + w * 0.42, y0 + h * 0.52, Math.min(w, h) * 0.1, true);
}
}
// a masonry course: a short broken drag in the wall's own colour, never a ruled line
function course(x0, y0, x1, y1, hex, weight, alpha) {
brush.noFill();
brush.set("cpencil", hex, weight);
brush.stroke(hex);
brush.strokeWeight(weight);
brush.beginShape(0.6);
const steps = 3;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
brush.vertex(x0 + (x1 - x0) * t + randomGaussian(0, 2), y0 + (y1 - y0) * t + randomGaussian(0, 2.4));
}
brush.endShape(false);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#253041");
// ---- 1. the wall in three close values: a pale string course at sill height, dark weight below ----
brush.noStroke();
brush.fillTexture(0.4, 0.35);
brush.fillBleed(0.55, "out");
brush.fill("#405064", 96);
loosePlane(-40, 344, 848, 452, 13, 0.2);
brush.fill("#405064", 62);
loosePlane(-40, 316, 848, 356, 11, 0.2);
brush.fill("#0f151d", 118);
loosePlane(-40, 596, 848, 852, 18, 0.22);
brush.fill("#0f151d", 78);
loosePlane(-40, 548, 848, 620, 15, 0.2);
// the two hard horizontals that tell you this is a wall and not weather
brush.noFill();
brush.set("cpencil", "#0f151d", 2.0);
brush.beginShape(0.6);
brush.vertex(-24, 598);
brush.vertex(268, 590);
brush.vertex(552, 602);
brush.vertex(828, 592);
brush.endShape(false);
brush.set("cpencil", "#405064", 1.6);
brush.beginShape(0.6);
brush.vertex(-24, 402);
brush.vertex(148, 398);
brush.vertex(392, 404);
brush.endShape(false);
brush.set("cpencil", "#405064", 1.2);
brush.beginShape(0.6);
brush.vertex(468, 402);
brush.vertex(662, 408);
brush.vertex(828, 398);
brush.endShape(false);
// a downpipe, the one vertical against all this horizontal — quiet, and running off the top
brush.set("cpencil", "#0f151d", 2.2);
brush.beginShape(0.5);
brush.vertex(614, -20);
brush.vertex(611, 210);
brush.vertex(617, 430);
brush.vertex(613, 604);
brush.endShape(false);
brush.set("cpencil", "#405064", 1.0);
brush.beginShape(0.5);
brush.vertex(607, -18);
brush.vertex(604, 212);
brush.vertex(610, 432);
brush.vertex(606, 602);
brush.endShape(false);
brush.set("cpencil", "#0f151d", 1.6);
brush.line(602, 148, 624, 146);
brush.line(601, 392, 623, 394);
wallZone(104, 128, 122, 14, "#405064", 84, 104);
wallZone(494, 118, 152, 13, "#405064", 62, 112);
wallZone(656, 268, 130, 11, "#405064", 48, 100);
wallZone(300, 402, 156, 12, "#405064", 70, 96);
wallZone(196, 692, 148, 12, "#0f151d", 92, 112);
wallZone(596, 754, 158, 10, "#0f151d", 84, 118);
wallZone(58, 486, 104, 9, "#0f151d", 70, 92);
wallZone(742, 476, 96, 8, "#0f151d", 64, 96);
// three large gestures running off the edges, so the wall never sits flat
wallPatch(-44, 232, 188, "#405064", 78, 0.62);
wallPatch(818, 92, 196, "#0f151d", 66, 0.6);
wallPatch(392, 856, 214, "#0f151d", 84, 0.6);
// ---- 2. warm air around the opening — the wall remembering the light ----
brush.noStroke();
brush.fillTexture(0.3, 0.32);
brush.fillBleed(0.72, "out");
brush.fill("#bd6f22", 30);
brush.circle(280, 300, 262, true);
brush.fill("#bd6f22", 22);
brush.circle(322, 404, 204, true);
brush.fill("#bd6f22", 13);
brush.circle(396, 492, 158, true);
// ---- 3. masonry: broken courses in wall colour, clustered, staggered, never lining up ----
course(-16, 118, 128, 124, "#0f151d", 1.5, 90);
course(64, 76, 216, 70, "#405064", 1.4, 90);
course(414, 168, 618, 176, "#0f151d", 1.4, 90);
course(452, 232, 596, 226, "#405064", 1.3, 90);
course(492, 292, 726, 300, "#0f151d", 1.4, 90);
course(618, 96, 812, 88, "#405064", 1.2, 90);
course(30, 452, 158, 462, "#0f151d", 1.4, 90);
course(-12, 528, 122, 520, "#405064", 1.2, 90);
course(442, 636, 664, 646, "#0f151d", 1.3, 90);
course(140, 780, 372, 772, "#405064", 1.2, 90);
course(-16, 692, 196, 700, "#0f151d", 1.6, 90);
// short staggered joints, offset course to course
brush.set("cpencil", "#0f151d", 1.1);
brush.line(216, 74, 219, 118);
brush.line(492, 178, 489, 228);
brush.line(568, 230, 571, 294);
brush.line(96, 74, 93, 120);
brush.line(688, 300, 691, 344);
brush.line(84, 462, 87, 524);
brush.line(556, 644, 559, 700);
// grit dragged along the wall's own field
brush.set("spray", "#405064", 1.2);
brush.field("hand");
brush.flowLine(122, 216, 148, 12);
brush.flowLine(566, 128, 176, 8);
brush.flowLine(652, 412, 132, 170);
brush.flowLine(238, 578, 118, 6);
brush.noField();
// a shallow scumble across the upper-right and the lower-left, so neither zone reads as void
brush.noFill();
brush.noStroke();
brush.hatchStyle("cpencil", "#405064", 0.9);
brush.hatch(22, 72, { rand: 0.3, continuous: false, gradient: 0.6 });
brush.polygon([[476, 40], [812, 22], [816, 258], [492, 214]]);
brush.hatch(30, 100, { rand: 0.38, continuous: false, gradient: 0.8 });
brush.polygon([[30, 578], [232, 620], [204, 764], [8, 716]]);
brush.noHatch();
// ---- 4. the second window, unlit, cropped by the right edge — interval, not incident ----
brush.noStroke();
brush.fillTexture(0.5, 0.5);
brush.fillBleed(0.26, "out");
brush.fill("#0f151d", 226);
loosePlane(710, 424, 830, 606, 4.0, 0.08);
brush.fillBleed(0.18, "out");
brush.fill("#0f151d", 208);
loosePlane(722, 440, 824, 592, 3.0, 0.08);
// one cold sheen so the dead glass still reads as glass
brush.fillBleed(0.34, "out");
brush.fill("#788594", 40);
brush.circle(768, 476, 34, true);
brush.noFill();
brush.set("cpencil", "#788594", 1.1);
brush.line(706, 418, 703, 612);
brush.line(700, 616, 834, 622);
brush.set("cpencil", "#0f151d", 1.3);
brush.line(768, 430, 771, 600);
// ---- 5. the lit window: reveal first, then three panes of three different sizes ----
brush.noStroke();
brush.fillTexture(0.34, 0.4);
brush.fillBleed(0.2, "out");
brush.fill("#0f151d", 200);
loosePlane(WX0 - 15, WY0 - 13, WX1 + 15, WY1 + 12, 4.6, 0.1);
pane(WX0 + 4, WY0 + 4, MULLION - 5, TRANSOM - 4, 0.88, false);
pane(WX0 + 4, TRANSOM + 5, MULLION - 5, WY1 - 4, 1.0, true);
pane(MULLION + 5, WY0 + 4, WX1 - 4, WY1 - 4, 0.94, false);
// a blind pulled two-fifths down the tall right pane — one dark hem, deliberately out of level
brush.noStroke();
brush.fillTexture(0.3, 0.45);
brush.fillBleed(0.12, "out");
brush.fill("#0f151d", 240);
loosePlane(MULLION + 4, WY0 + 1, WX1 - 3, 246, 2.4, 0.04);
brush.fill("#0f151d", 226);
loosePlane(MULLION + 4, WY0 + 1, WX1 - 3, 252, 2.0, 0.04);
brush.fillBleed(0.3, "out");
brush.fill("#0f151d", 150);
loosePlane(MULLION + 6, 246, WX1 - 6, 262, 3.0, 0.1);
// its slats, dragged across — this is what tells you it is cloth and not dusk
brush.noFill();
brush.set("cpencil", "#0f151d", 2.6);
brush.line(MULLION + 6, 182, WX1 - 6, 179);
brush.line(MULLION + 7, 196, WX1 - 5, 194);
brush.line(MULLION + 5, 209, WX1 - 7, 212);
brush.line(MULLION + 8, 223, WX1 - 6, 220);
brush.line(MULLION + 6, 236, WX1 - 8, 238);
brush.set("cpencil", "#0f151d", 1.6);
brush.line(MULLION + 9, 189, 322, 187);
brush.line(298, 203, WX1 - 6, 205);
brush.line(MULLION + 7, 230, 336, 232);
// the sliver of lamplight escaping beneath its hem
brush.set("2B", "#f2b95c", 1.2);
brush.line(MULLION + 8, 256, WX1 - 10, 264);
// the bars — thin, dark, wobbling, with warm nicks where the light licks their edges
brush.noFill();
brush.set("2B", "#0f151d", 0.9);
brush.stroke("#0f151d");
brush.strokeWeight(0.9);
brush.beginShape(0.3);
brush.vertex(MULLION - 1, WY0 - 3);
brush.vertex(MULLION + 3, 268);
brush.vertex(MULLION, WY1 + 3);
brush.endShape(false);
brush.beginShape(0.3);
brush.vertex(WX0 - 4, TRANSOM + 1);
brush.vertex(206, TRANSOM - 3);
brush.vertex(MULLION + 2, TRANSOM + 2);
brush.endShape(false);
brush.set("2B", "#f2b95c", 0.5);
brush.line(MULLION + 4, 300, MULLION + 4, 344);
brush.line(190, TRANSOM + 4, 226, TRANSOM + 5);
// the bottom rail — the dark bar that keeps the glass off the stone
brush.noStroke();
brush.fillTexture(0.22, 0.4);
brush.fillBleed(0.12, "out");
brush.fill("#0f151d", 236);
loosePlane(WX0 - 4, WY1 - 5, WX1 + 4, WY1 + 6, 2.2, 0.04);
brush.noFill();
// the reveal's edges — dark on both jambs, one cold line along the head
brush.set("charcoal", "#0f151d", 1.6);
brush.line(WX0 - 9, WY0 - 9, WX0 - 7, WY1 + 7);
brush.line(WX1 + 8, WY0 - 7, WX1 + 10, WY1 + 6);
brush.set("2B", "#788594", 0.8);
brush.line(WX0 - 14, WY0 - 12, WX1 + 14, WY0 - 15);
// ---- 6. the sill: a stone ledge wider than the opening, its top face taking the spill ----
// the stone's front face is a vertical plane in shadow — dark, not pale
brush.noStroke();
brush.fillTexture(0.4, 0.45);
brush.fillBleed(0.16, "out");
brush.fill("#0f151d", 214);
loosePlane(142, WY1 + 8, 386, WY1 + 30, 2.6, 0.04);
// the top face, the only thing here the room's light actually reaches
brush.fillBleed(0.26, "out");
brush.fill("#bd6f22", 208);
loosePlane(154, WY1 + 5, 370, WY1 + 14, 2.0, 0.03);
brush.fillBleed(0.24, "out");
brush.fill("#f2b95c", 176);
brush.circle(208, WY1 + 9, 9, true);
brush.fill("#f2b95c", 124);
brush.circle(292, WY1 + 9, 6, true);
brush.noFill();
brush.set("2B", "#f2b95c", 1.0);
brush.line(156, WY1 + 7, 336, WY1 + 5);
// the ledge's cold outer lip, catching nothing
brush.set("cpencil", "#788594", 1.0);
brush.line(140, WY1 + 30, 388, WY1 + 28);
// ---- 7. darks last: the under-sill shadow, then the light dropping down the wall ----
brush.noStroke();
brush.fillTexture(0.4, 0.45);
brush.fillBleed(0.24, "out");
brush.fill("#0f151d", 208);
loosePlane(152, WY1 + 26, 388, WY1 + 44, 3.6, 0.08);
brush.fillBleed(0.5, "out");
brush.fill("#0f151d", 128);
loosePlane(168, WY1 + 42, 418, WY1 + 60, 6.0, 0.16);
brush.fillBleed(0.6, "out");
brush.fill("#0f151d", 74);
brush.circle(432, 428, 40, true);
// the spill: a short warm fall from under the ledge, leaning right
brush.fillBleed(0.68, "out");
brush.fillTexture(0.28, 0.32);
brush.fill("#bd6f22", 62);
brush.circle(312, 452, 72, true);
brush.fill("#bd6f22", 40);
brush.circle(372, 506, 60, true);
brush.fill("#bd6f22", 24);
brush.circle(428, 562, 46, true);
brush.fill("#bd6f22", 13);
brush.circle(482, 616, 36, true);
// one hot fleck where the ledge is chipped, and a last cold accent falling off its left end
brush.fill("#f2b95c", 188);
brush.circle(366, WY1 + 20, 4.5, true);
brush.noFill();
brush.set("cpencil", "#788594", 1.0);
brush.line(144, WY1 + 11, 124, WY1 + 38);
}