Cold Lamps Rising
2026 · nocturne
The dark is the material and the light is the rationing; a field this wide only needs two dozen sparks to be believed, provided no two of them are the same distance away.
fireflies rising out of a dark grass field at dusk, cold green points held at different depths, the last daylight pooled low on the right
painted by Kimi K2.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: grass silhouette mass fills the bottom third and runs off the left edge; the last dusk light pools low-right,
// so the firefly cluster at the lower-left thirds crossing burns cold against the darkest ground. Upper right stays empty night.
// layering: night washes, dusk pool at the horizon, hazed far field, mid blades, deep silhouette blades, then the lights last.
const PALETTE = [
"#12191c", // ground — blue-green night
"#1d2c31", // air — cooler haze above the field
"#3e5350", // dusk — the last light at the horizon
"#0a0f10", // shadow — grass ink
"#bcd977", // accent — firefly green, rationed
"#eef3d4", // spark — the coldest cores
];
// x, y, core radius, brightness 0-1. Hand-placed: dense at the lower-left crossing,
// thinning up and to the right, three low ones caught down among the blades.
const LIGHTS = [
[258, 528, 3.5, 1.00],
[296, 557, 2.2, 0.82],
[221, 497, 2.1, 0.74],
[273, 474, 1.6, 0.60],
[177, 553, 2.7, 0.86],
[318, 509, 1.4, 0.50],
[236, 579, 1.9, 0.66],
[149, 501, 1.3, 0.45],
[341, 566, 1.2, 0.42],
[201, 455, 1.1, 0.38],
[392, 470, 1.8, 0.62],
[428, 523, 1.2, 0.40],
[359, 404, 1.0, 0.34],
[470, 437, 1.5, 0.52],
[516, 487, 1.0, 0.30],
[300, 372, 1.3, 0.44],
[238, 335, 0.9, 0.28],
[432, 356, 0.9, 0.26],
[552, 402, 1.1, 0.32],
[596, 300, 0.8, 0.22],
[364, 249, 0.7, 0.18],
[636, 459, 0.9, 0.24],
[117, 627, 2.2, 0.58],
[311, 649, 1.5, 0.42],
[57, 566, 1.7, 0.50],
];
function blendTint(a, b, t) {
return lerpColor(color(a), color(b), constrain(t, 0, 1));
}
// one blade: rises from (x, y), leans by `lean`, thins toward the tip
function blade(x, y, h, lean, w, col, name) {
brush.noFill();
brush.set(name || "2B", col, w);
brush.beginShape(0.5);
brush.vertex(x + randomGaussian(0, 1.6), y);
brush.vertex(x + lean * 0.16 + randomGaussian(0, 2), y - h * 0.37);
brush.vertex(x + lean * 0.55 + randomGaussian(0, 2.4), y - h * 0.73);
brush.vertex(x + lean, y - h);
brush.endShape(false);
}
// a foxtail head: a small dark swelling at the top of a tall stem
function seedHead(x, y, len, lean) {
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.6, 0.6);
brush.fill("#0a0f10", 190 + random(-22, 30));
brush.beginShape(0.6);
brush.vertex(x, y);
brush.vertex(x + lean * 0.3 - 4.5, y - len * 0.45);
brush.vertex(x + lean, y - len);
brush.vertex(x + lean * 0.3 + 4.2, y - len * 0.42);
brush.endShape(true);
}
// a firefly: outer breath, mid glow, then a small hard core
function firefly(x, y, core, bright) {
brush.noStroke();
brush.fillTexture(0.28, 0.35);
brush.fillBleed(0.6, "out");
brush.fill("#bcd977", max(5, randomGaussian(17, 4) * bright));
brush.circle(x + randomGaussian(0, 2.4), y + randomGaussian(0, 2.4), core * (7.4 + random(-1.2, 2.2)), true);
brush.fillBleed(0.42, "out");
brush.fill("#bcd977", max(16, randomGaussian(52, 10) * bright));
brush.circle(x + randomGaussian(0, 1.2), y + randomGaussian(0, 1.2), core * (3.1 + random(-0.5, 0.9)), true);
brush.fillBleed(0.2, "out");
brush.fill(blendTint("#bcd977", "#eef3d4", 0.35 + bright * 0.55 + random(-0.12, 0.12)),
max(72, randomGaussian(218, 16) * (0.5 + 0.5 * bright)));
brush.circle(x, y, core * (1 + random(-0.12, 0.17)), true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#12191c");
brush.noStroke();
// deepen the top of the sky so the night has a ceiling
brush.fillBleed(0.4, "out");
brush.fillTexture(0.35, 0.4);
brush.fill("#0a0f10", 96);
brush.rect(380, 90, 900, 260, "center");
brush.fill("#0a0f10", 54);
brush.rect(430, 250, 900, 180, "center");
// a breath of cool air over the middle band, kept thin on the empty right
brush.fill("#1d2c31", 44);
brush.rect(300, 400, 720, 190, "center");
// the last daylight, pooled low-right along the horizon and dying to the left
brush.fillBleed(0.55, "out");
brush.fillTexture(0.4, 0.45);
brush.fill("#3e5350", 30);
brush.rect(520, 552, 620, 130, "center");
brush.fill("#3e5350", 34);
brush.circle(748, 562, 84, true);
brush.fill("#3e5350", 27);
brush.circle(650, 552, 76, true);
brush.fill("#3e5350", 18);
brush.circle(556, 548, 66, true);
brush.fill("#eef3d4", 14);
brush.circle(772, 574, 52, true);
// the far field: a hazed low band of short strokes, thinning where the noise dips
for (let i = 0; i < 130; i++) {
const x = constrain(randomGaussian(430, 260), -20, 830);
if (noise(x * 0.0055, 4.1) < 0.36) continue;
const y = 552 + randomGaussian(11, 8);
const h = max(6, randomGaussian(17, 6));
const near = constrain((x - 120) / 700, 0, 1);
blade(x, y, h, randomGaussian(0, 5), 0.55 + random(0, 0.35),
blendTint("#0a0f10", "#3e5350", 0.18 + near * 0.3 + random(-0.08, 0.08)));
}
// grass mass — soft dark pooling that turns the bottom into a body, heaviest left
brush.noStroke();
brush.fillBleed(0.45, "out");
brush.fillTexture(0.5, 0.5);
brush.fill("#0a0f10", 120);
brush.rect(340, 748, 900, 180, "center");
brush.fill("#0a0f10", 66);
brush.rect(250, 640, 620, 150, "center");
for (let i = 0; i < 22; i++) {
const x = constrain(randomGaussian(300, 300), -40, 850);
const y = 650 + randomGaussian(58, 44);
if (noise(x * 0.004, y * 0.004, 9.2) < 0.3) continue;
const left = constrain(1 - x / 800, 0, 1);
brush.fill("#0a0f10", max(48, randomGaussian(86 + left * 44, 16)));
brush.circle(x, y, max(26, randomGaussian(40, 8)), true);
}
// mid blades: the depth between the far haze and the front silhouette
for (let i = 0; i < 92; i++) {
const x = constrain(randomGaussian(320, 275), -30, 835);
const y = 620 + randomGaussian(46, 34);
if (noise(x * 0.005, 1.7) < 0.28) continue;
const h = max(24, randomGaussian(62, 24));
blade(x, y, h, randomGaussian(0, 16), 0.95 + random(0, 0.7),
blendTint("#0a0f10", "#3e5350", 0.1 + random(0, 0.16)));
}
// seven long gestures: single stems that carry the field up out of its own mass
blade(126, 802, 372, 44, 2.5, "#0a0f10", "charcoal");
blade(74, 792, 316, -46, 2.2, "#0a0f10", "charcoal");
blade(212, 806, 288, 92, 2.0, "#0a0f10", "charcoal");
blade(302, 794, 326, 54, 2.3, "#0a0f10", "charcoal");
blade(20, 760, 236, 30, 1.9, "#0a0f10", "charcoal");
blade(556, 804, 250, -38, 2.1, "#0a0f10", "charcoal");
blade(704, 808, 214, 40, 1.8, "#0a0f10", "charcoal");
seedHead(168, 442, 46, 8);
seedHead(356, 480, 38, -6);
seedHead(30, 502, 34, 7);
// near blades: the darkest ink, last of the structure, running off the bottom
for (let i = 0; i < 54; i++) {
const x = constrain(randomGaussian(280, 290), -40, 845);
const y = 762 + randomGaussian(24, 26);
const h = max(64, randomGaussian(128, 42));
blade(x, y, h, randomGaussian(0, 26), 1.7 + random(0, 1.3), "#0a0f10");
}
// two blades leaning right across the dusk pool, to break its edge
blade(648, 796, 196, -74, 2.4, "#0a0f10", "charcoal");
blade(766, 782, 168, -52, 2.0, "#0a0f10", "charcoal");
// the lights, last of all, so nothing sits on top of them
brush.noStroke();
for (let i = 0; i < LIGHTS.length; i++) {
const l = LIGHTS[i];
firefly(l[0] + randomGaussian(0, 2.6), l[1] + randomGaussian(0, 2.6), l[2], l[3]);
}
// two nearest lights get an extra wide breath — the out-of-focus ones
brush.fillBleed(0.75, "out");
brush.fillTexture(0.22, 0.3);
brush.fill("#bcd977", 13);
brush.circle(256, 530, 44, true);
brush.fill("#bcd977", 10);
brush.circle(178, 552, 34, true);
brush.fill("#bcd977", 8);
brush.circle(117, 626, 27, true);
// one faint trail, where a light moved while the paper was wet
brush.noFill();
brush.set("2H", "#bcd977", 0.7);
brush.beginShape(0.6);
brush.vertex(392, 470);
brush.vertex(412, 452);
brush.vertex(438, 448);
brush.endShape(false);
brush.beginShape(0.6);
brush.vertex(221, 497);
brush.vertex(206, 482);
brush.vertex(200, 466);
brush.endShape(false);
}