Radiant Off the Page
2026 · celestial
Draw enough of them and the sky confesses where they come from — a point you cannot see, somewhere past the edge of the paper.
a meteor shower over a very low dark horizon, every streak leaning out of the upper right, one head dragging a pale green train
painted by Kimi K3p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: every streak leans out of a radiant that sits off the page beyond the upper-right corner; the
// brightest head lands on the upper-right thirds crossing and drags a green train, and the land is one very low band.
// layering: warm brown-plum sky, radiant haze and horizon airglow, star specks, streaks faintest-to-brightest, land last.
const PALETTE = [
"#241a1b", // ground — warm brown-plum night
"#3d2e2f", // mass — radiant haze
"#ece2d2", // light — the burning streak
"#b9a893", // mid — dim streaks, airglow
"#a9b489", // accent — the persistent train
"#120e11", // shadow — the land band
];
const SKY = PALETTE[0];
const HAZE = PALETTE[1];
const LIGHT = PALETTE[2];
const DIM = PALETTE[3];
const TRAIN = PALETTE[4];
const LAND = PALETTE[5];
// the shower's radiant, off the page past the upper-right corner
const RX = 1010;
const RY = -150;
// a point d along the ray at angle ang, pushed off perpendicular by off
function rayPt(ang, d, off) {
const px = cos(ang);
const py = sin(ang);
return [RX + px * d - py * off, RY + py * d + px * off];
}
function glow(t) {
return lerpColor(color(SKY), color(LIGHT), constrain(t, 0, 1));
}
function dimGlow(t) {
return lerpColor(color(SKY), color(DIM), constrain(t, 0, 1));
}
// one meteor: soft halo, then a core that brightens toward the head, then the head itself
function streak(ang, d0, d1, w, peak, warm) {
const n = 9;
const a = ang + randomGaussian(0, 0.4);
const grit = random();
const hotBrush = peak > 0.6 ? (grit < 0.7 ? "pen" : "rotring") : grit < 0.45 ? "2B" : "pen";
const pts = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
pts.push(rayPt(a, lerp(d0, d1, t), randomGaussian(0, 1.2)));
}
brush.noFill();
brush.set("spray", warm ? dimGlow(peak * 0.3) : glow(peak * 0.22), w * (2.6 + peak));
brush.beginShape(0.05);
for (let i = 0; i <= n; i++) brush.vertex(pts[i][0], pts[i][1], 0.35 + 0.65 * (i / n));
brush.endShape(false);
for (let i = 0; i < n; i++) {
const t = (i + 0.5) / n;
const g = pow(t, 1.5) * peak;
const c = warm && t < 0.6 ? dimGlow(g * 0.95) : glow(g);
brush.set(t > 0.7 ? hotBrush : "2H", c, w * (0.3 + 1.1 * t));
brush.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
}
const h = pts[n];
if (peak > 0.42) {
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.35, 0.4);
brush.fill(LIGHT, constrain(randomGaussian(44, 9), 16, 76) * peak);
brush.circle(h[0], h[1], w * (2.2 + random(0, 0.9)), true);
}
return h;
}
// the train: glowing grain left hanging where the head has been, wandering off the line as it cools
function train(ang, dA, dB, count) {
brush.noFill();
for (let i = 0; i < count; i++) {
const t = i / (count - 1);
// the smoke lies to one side of the line it was left on, and leans further off as it ages
const drift = 9 + (1 - t) * 22 + randomGaussian(0, 3.6);
const p = rayPt(ang, lerp(dA, dB, t), drift);
const g = constrain(randomGaussian(0.4 + 0.34 * t, 0.11), 0.18, 0.86);
brush.set("spray", lerpColor(color(SKY), color(random() < 0.66 ? TRAIN : DIM), g), constrain(randomGaussian(17 - 6 * t, 3), 5, 24));
const a = ang + randomGaussian(0, 24);
const r = constrain(randomGaussian(16 - 6 * t, 4), 4, 26);
brush.line(p[0], p[1], p[0] + cos(a) * r, p[1] + sin(a) * r);
}
// three pools where the smoke has gathered, and one thread still holding the line
brush.noStroke();
brush.fillTexture(0.4, 0.4);
for (let i = 0; i < 3; i++) {
const t = 0.24 + i * 0.3;
const p = rayPt(ang, lerp(dA, dB, t), 12 + (1 - t) * 22 + randomGaussian(0, 3));
brush.fillBleed(0.62, "out");
brush.fill(TRAIN, constrain(randomGaussian(34, 7), 16, 52));
brush.circle(p[0], p[1], constrain(randomGaussian(19 - 5 * t, 3), 9, 26), true);
}
brush.noFill();
brush.set("charcoal", lerpColor(color(SKY), color(TRAIN), 0.7), 1.6);
brush.beginShape(0.55);
for (let i = 0; i <= 6; i++) {
const t = i / 6;
const p = rayPt(ang, lerp(dA - 26, dB, t), 8 + (1 - t) * 16 + randomGaussian(0, 3.4));
brush.vertex(p[0], p[1], 0.25 + 0.7 * t);
}
brush.endShape(false);
}
// engraver's hairlines: streaks so faint they are only interval
function hairline(ang, d0, d1) {
const a = ang + randomGaussian(0, 0.3);
brush.noFill();
brush.set("2H", dimGlow(constrain(randomGaussian(0.19, 0.05), 0.08, 0.3)), constrain(randomGaussian(0.85, 0.2), 0.5, 1.3));
brush.beginShape(0.1);
for (let i = 0; i <= 4; i++) {
const t = i / 4;
const p = rayPt(a, lerp(d0, d1, t), randomGaussian(0, 1.6));
brush.vertex(p[0], p[1], constrain(0.2 + 0.8 * pow(t, 1.7), 0.12, 1));
}
brush.endShape(false);
}
function starfield() {
brush.noFill();
for (let i = 0; i < 96; i++) {
const x = constrain(randomGaussian(510, 235), 6, 794);
const y = constrain(randomGaussian(330, 215), 8, 716);
if (dist(x, y, 533, 267) < 84) continue;
const b = constrain(randomGaussian(0.44, 0.16), 0.1, 0.78);
brush.set(random() < 0.32 ? "rotring" : "2H", random() < 0.34 ? dimGlow(b) : glow(b * 0.9), constrain(randomGaussian(0.75, 0.2), 0.4, 1.2));
const a = random(0, 360);
const r = constrain(randomGaussian(1.3, 0.6), 0.5, 2.6);
brush.line(x, y, x + cos(a) * r, y + sin(a) * r);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(SKY);
// haze gathering where the radiant would be, just off the corner
brush.noStroke();
brush.fillBleed(0.75, "out");
brush.fillTexture(0.3, 0.35);
brush.fill(HAZE, 62);
brush.circle(886, 34, 250, true);
brush.fill(HAZE, 40);
brush.circle(760, 118, 196, true);
brush.fill(HAZE, 26);
brush.circle(612, 232, 224, true);
brush.fill(HAZE, 22);
brush.circle(430, 372, 250, true);
brush.fill(HAZE, 13);
brush.circle(250, 520, 208, true);
// airglow lying along the horizon, warmest under the shower
brush.fillBleed(0.82, "out");
brush.fill(DIM, 26);
brush.circle(596, 776, 214, true);
brush.fill(DIM, 19);
brush.circle(352, 788, 186, true);
brush.fill(DIM, 12);
brush.circle(126, 798, 150, true);
brush.fill(HAZE, 34);
brush.circle(760, 770, 170, true);
starfield();
// a handful of stars bright enough to hold their own against the shower
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(LIGHT, 142);
brush.circle(196, 158, 2.8, true);
brush.fill(LIGHT, 112);
brush.circle(742, 464, 2.3, true);
brush.fill(DIM, 128);
brush.circle(312, 622, 2.5, true);
brush.fill(LIGHT, 98);
brush.circle(96, 404, 2.0, true);
brush.fill(LIGHT, 124);
brush.circle(468, 92, 2.4, true);
brush.fill(DIM, 104);
brush.circle(650, 596, 2.1, true);
brush.fill(LIGHT, 88);
brush.circle(548, 464, 1.9, true);
brush.fill(DIM, 96);
brush.circle(238, 336, 1.8, true);
brush.fill(LIGHT, 84);
brush.circle(404, 268, 1.7, true);
// hairlines first — the shower's quietest members
hairline(159, 880, 1074);
hairline(133, 916, 1022);
hairline(147, 812, 936);
hairline(124, 664, 742);
hairline(166, 620, 736);
// dim streaks, mostly short, warm-cored where they are almost spent
streak(155, 900, 1078, 1.0, 0.34, true);
streak(118, 598, 704, 1.1, 0.38, true);
streak(134, 902, 1012, 0.9, 0.32, true);
streak(145, 700, 818, 1.2, 0.44, true);
streak(126, 380, 472, 1.1, 0.4, true);
streak(163, 508, 618, 1.0, 0.36, true);
streak(143, 948, 1092, 1.1, 0.38, true);
streak(137, 986, 1078, 1.0, 0.3, true);
// mid streaks — the body of the fall
streak(160, 520, 836, 1.4, 0.54, false);
streak(131, 348, 692, 1.5, 0.58, true);
streak(128, 616, 764, 1.6, 0.62, false);
streak(141, 466, 566, 1.5, 0.66, false);
// two long gestures, run from near the corner deep into the lower left
streak(150, 424, 1014, 1.8, 0.7, false);
streak(122, 468, 908, 1.9, 0.74, true);
// the one that leaves a train: head on the upper-right thirds crossing
const head = streak(138.8, 288, 636, 3.1, 1.0, false);
// the train hangs back past the streak's own tail, still cooling where the head has already been
train(138.8, 174, 568, 16);
// light thrown off the head, sprayed rather than pooled, and each throw its own length
brush.noFill();
brush.set("spray", glow(0.34), 9);
brush.line(head[0] + 3, head[1] - 3, head[0] - 14, head[1] + 11);
brush.set("spray", glow(0.26), 7);
brush.line(head[0] - 4, head[1] - 9, head[0] + 9, head[1] + 6);
brush.set("spray", glow(0.22), 11);
brush.line(head[0] + 8, head[1] + 4, head[0] - 9, head[1] - 6);
brush.set("spray", glow(0.18), 6);
brush.line(head[0] - 11, head[1] + 4, head[0] - 21, head[1] + 14);
brush.noStroke();
brush.fillBleed(0.22, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(LIGHT, 176);
brush.circle(head[0] + 1, head[1] - 1, 4.6, true);
// two sparks shed off the bright one
brush.noFill();
brush.set("pen", glow(0.5), 0.9);
brush.line(head[0] - 26, head[1] + 12, head[0] - 41, head[1] + 26);
brush.set("pen", glow(0.36), 0.8);
brush.line(head[0] - 12, head[1] + 30, head[0] - 22, head[1] + 44);
// the land: one very low band, ragged at the top, running off both edges
brush.noStroke();
brush.fillTexture(0.34, 0.35);
brush.fillBleed(0.14, "out");
brush.fill(LAND, 255);
brush.beginShape(0.32);
brush.vertex(-34, 816);
brush.vertex(-34, 748);
brush.vertex(94, 734);
brush.vertex(212, 745);
brush.vertex(332, 731);
brush.vertex(454, 750);
brush.vertex(562, 742);
brush.vertex(686, 758);
brush.vertex(834, 744);
brush.vertex(834, 816);
brush.endShape(true);
// the near ground, deeper still, so the band does not read as dust
brush.fillTexture(0.2, 0.25);
brush.fillBleed(0.1, "out");
brush.fill(LAND, 255);
brush.beginShape(0.2);
brush.vertex(-34, 818);
brush.vertex(-34, 772);
brush.vertex(206, 766);
brush.vertex(430, 774);
brush.vertex(672, 768);
brush.vertex(834, 776);
brush.vertex(834, 818);
brush.endShape(true);
// a low ridge breaking the band's edge, left of centre
brush.fillBleed(0.2, "out");
brush.fillTexture(0.3, 0.3);
brush.fill(LAND, 255);
brush.beginShape(0.5);
brush.vertex(184, 768);
brush.vertex(246, 718);
brush.vertex(314, 738);
brush.vertex(296, 780);
brush.endShape(true);
brush.fill(LAND, 250);
brush.circle(618, 748, 15, true);
brush.fill(LAND, 244);
brush.circle(474, 752, 10, true);
brush.fill(LAND, 238);
brush.circle(722, 754, 8, true);
// the horizon caught in airglow — broken, never one ruled line
brush.noFill();
brush.set("cpencil", dimGlow(0.15), 1.0);
brush.beginShape(0);
brush.vertex(-12, 750);
brush.vertex(96, 741);
brush.endShape(false);
brush.set("cpencil", dimGlow(0.2), 1.1);
brush.beginShape(0.3);
brush.vertex(334, 735);
brush.vertex(452, 753);
brush.vertex(548, 747);
brush.endShape(false);
brush.set("cpencil", dimGlow(0.12), 0.9);
brush.beginShape(0);
brush.vertex(658, 760);
brush.vertex(774, 749);
brush.endShape(false);
}