The Rift Comes Down
2026 · celestial
The band is not a smear of light but a body with a spine, and the dark lane running through it is the only thing in the sky with a real edge.
the galactic dust band drawn as an atlas plate, climbing from the lower-left horizon to the upper-right corner, its great rift splitting the bright bulge low down, stars thinning away from the band, dark land holding only the bottom eighth
painted by GPT-5.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the galactic band climbs from the lower-left horizon to the upper-right corner, widest and
// brightest where it crosses the lower-left thirds mark; dark land takes only the bottom eighth, and the
// top-left and lower-right skies stay bare ink — star density thins the further it gets from the band.
// layering: ink ground, low airglow, wide pale wings, the bright core, star clouds, combed grain, then the
// rift lanes and dust globules cut in dark, the land silhouette, and the sharp stars last of all.
const PALETTE = [
"#181c22", // ground — cold ink sky, left bare wherever the band is not
"#dbd3bf", // light — bone-cream, the burning centre of the band
"#8f8c7e", // haze — grey-olive, the band's outer wing
"#4c4d4c", // mass — mid slate, the shoulder of the dust
"#0a0b0e", // shadow — the rift lanes, the globules, the land
"#b3743a", // accent — ochre airglow and five warm stars
];
const SKY = PALETTE[0];
const LIGHT = PALETTE[1];
const HAZE = PALETTE[2];
const MASS = PALETTE[3];
const SHADOW = PALETTE[4];
const OCHRE = PALETTE[5];
// the band's axis: foot planted behind the land at the lower left, head off the upper-right corner
const UX = 0.7519;
const UY = -0.6593;
const NX = 0.6593; // unit normal, pointing to the band's lower-right side
const NY = 0.7519;
const AXLEN = 1037.4;
function axisPt(t) {
return [lerp(10, 790, t), lerp(742, 58, t)];
}
// the envelope: a broad bulge low down, tapering as the band climbs
const HW = [
[0.0, 92], [0.1, 124], [0.2, 148], [0.3, 152], [0.4, 128],
[0.52, 104], [0.66, 86], [0.8, 72], [1.0, 58],
];
function halfWidth(t) {
const tt = constrain(t, 0, 1);
for (let i = 0; i < HW.length - 1; i++) {
if (tt <= HW[i + 1][0]) {
return lerp(HW[i][1], HW[i + 1][1], (tt - HW[i][0]) / (HW[i + 1][0] - HW[i][0]));
}
}
return 58;
}
// band coordinates: t along the axis, of as a fraction of the local half-width
function bandPt(t, of) {
const p = axisPt(t);
const w = halfWidth(t);
return [p[0] + NX * of * w, p[1] + NY * of * w];
}
function project(x, y) {
return [((x - 10) * UX + (y - 742) * UY) / AXLEN, (x - 10) * NX + (y - 742) * NY];
}
const RIDGE = [
[-16, 720], [92, 714], [186, 723], [272, 716], [364, 725],
[454, 717], [550, 706], [650, 700], [744, 709], [816, 717],
];
function horizonAt(x) {
for (let i = 0; i < RIDGE.length - 1; i++) {
if (x <= RIDGE[i + 1][0]) {
return lerp(RIDGE[i][1], RIDGE[i + 1][1], (x - RIDGE[i][0]) / (RIDGE[i + 1][0] - RIDGE[i][0]));
}
}
return 717;
}
// the great rift and its two forks, written in band coordinates: [t, offset, width]
const RIFT_A = [
[0.08, 0.34, 0.09], [0.18, 0.18, 0.2], [0.29, 0.02, 0.27], [0.4, -0.11, 0.24],
[0.52, -0.17, 0.2], [0.64, -0.27, 0.17], [0.76, -0.35, 0.12], [0.88, -0.31, 0.08],
[0.96, -0.24, 0.04],
];
const RIFT_B = [[0.29, 0.02, 0.19], [0.35, 0.23, 0.17], [0.43, 0.42, 0.13], [0.51, 0.58, 0.09], [0.58, 0.72, 0.04]];
const RIFT_C = [[0.16, 0.24, 0.11], [0.11, 0.04, 0.14], [0.06, -0.18, 0.12], [0.02, -0.33, 0.06]];
// a lane running the whole length of the band, drifting a little off its nominal offset
function bandLane(of0, wf, drift) {
const out = [];
for (let i = 0; i <= 17; i++) {
const t = -0.03 + (1.09 * i) / 17;
const of = of0 + drift * Math.sin(t * 4.1 + of0 * 3) + randomGaussian(0, 0.045);
const p = bandPt(t, of);
out.push([p[0], p[1], wf * halfWidth(t) * (1 + random(-0.07, 0.07))]);
}
return out;
}
function laneXY(lane) {
const out = [];
for (let i = 0; i < lane.length; i++) {
const p = bandPt(lane[i][0], lane[i][1]);
out.push([p[0], p[1], lane[i][2] * halfWidth(lane[i][0])]);
}
return out;
}
function normalAt(pts, i) {
const a = pts[max(0, i - 1)];
const b = pts[min(pts.length - 1, i + 1)];
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const m = Math.hypot(dx, dy) || 1;
return [-dy / m, dx / m];
}
// a lane laid down as one closed ribbon: almost no bleed, heavy tooth, edges that stop where they stop
function ribbon(pts, swell, col, op, bleed, tex) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(tex, 0.3);
brush.fill(col, op);
brush.beginShape(0.5);
for (let i = 0; i < pts.length; i++) {
const n = normalAt(pts, i);
const w = pts[i][2] * swell;
brush.vertex(pts[i][0] + n[0] * w + random(-3.5, 3.5), pts[i][1] + n[1] * w + random(-3.5, 3.5));
}
for (let i = pts.length - 1; i >= 0; i--) {
const n = normalAt(pts, i);
const w = pts[i][2] * swell;
brush.vertex(pts[i][0] - n[0] * w + random(-3.5, 3.5), pts[i][1] - n[1] * w + random(-3.5, 3.5));
}
brush.endShape(true);
}
// the dark heart of a lane, walked along its centreline so the dust has real weight
function darkLane(pts, swell, op, step) {
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fillTexture(0.5, 0.12);
for (let i = 0; i < pts.length - 1; i++) {
for (let k = 0; k < step; k++) {
const f = (k + random(0.1, 0.9)) / step;
const x = lerp(pts[i][0], pts[i + 1][0], f);
const y = lerp(pts[i][1], pts[i + 1][1], f);
const w = lerp(pts[i][2], pts[i + 1][2], f) * swell;
const n = normalAt(pts, i);
const o = randomGaussian(0, w * 0.22);
brush.fill(SHADOW, constrain(op + random(-24, 22), 40, 255));
brush.circle(x + n[0] * o, y + n[1] * o, w * (0.92 + random(-0.12, 0.18)), true);
}
}
}
// a soft translucent breath of light — no border, so nothing reads as a bubble
function soft(x, y, r, col, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.3, 0);
brush.fill(col, op);
brush.circle(x, y, r, true);
}
// a star cloud: a knot of unresolved light sitting inside the band
function knot(t, of, r, op) {
const p = bandPt(t, of);
brush.noStroke();
brush.fillBleed(0.46, "out");
brush.fillTexture(0.5, 0);
brush.fill(random() < 0.78 ? LIGHT : HAZE, op + random(-12, 14));
brush.circle(p[0] + random(-6, 6), p[1] + random(-6, 6), r * (1 + random(-0.12, 0.15)), true);
brush.fillBleed(0.3, "out");
brush.fill(LIGHT, op * 0.6);
brush.circle(p[0] + random(-9, 9), p[1] + random(-9, 9), r * (0.5 + random(0, 0.12)), true);
}
// one fibre of the band's light: a short curved pencil stroke lying along the axis
function fibre(x, y, len, ang, col, w) {
const c = Math.cos(ang);
const s = Math.sin(ang);
brush.noFill();
brush.set("cpencil", col, w);
brush.beginShape(0.7);
brush.vertex(x, y);
brush.vertex(x + c * len * 0.5 - s * random(-5, 5), y + s * len * 0.5 + c * random(-5, 5));
brush.vertex(x + c * len, y + s * len);
brush.endShape(false);
}
// a resolved star: a short crossed scratch, weighted by magnitude
function star(x, y, mag, col) {
brush.noFill();
brush.set(mag > 2.5 ? "rotring" : "pen", col, max(0.4, mag * 0.33));
const s = mag * 0.5;
brush.line(x - s, y + random(-0.6, 0.6), x + s, y + random(-0.6, 0.6));
if (mag > 2.1) brush.line(x + random(-0.6, 0.6), y - s * 0.88, x + random(-0.6, 0.6), y + s * 0.88);
}
let RIFT_SAMP = [];
function occluded(x, y) {
for (let i = 0; i < RIFT_SAMP.length; i++) {
const s = RIFT_SAMP[i];
if (dist(x, y, s[0], s[1]) < s[2] * 0.92) return true;
}
return false;
}
// small stars crowd the band and let go of the open sky
function starChance(x, y) {
const p = project(x, y);
const w = halfWidth(p[0]);
return 0.07 + 0.88 * pow(constrain(1 - abs(p[1]) / (w * 2.4), 0, 1), 1.5);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(SKY);
const laneA = laneXY(RIFT_A);
const laneB = laneXY(RIFT_B);
const laneC = laneXY(RIFT_C);
RIFT_SAMP = laneA.concat(laneB, laneC);
// airglow lying along the land, strongest to the right where the sky is otherwise empty
soft(516, 714, 198, OCHRE, 34, 0.82);
soft(704, 720, 156, OCHRE, 24, 0.84);
soft(206, 708, 134, MASS, 22, 0.82);
// the body of the band: five continuous lanes, widest and palest first
ribbon(bandLane(0.0, 0.98, 0.1), 1.0, HAZE, 34, 0.62, 0.3);
ribbon(bandLane(0.4, 0.52, 0.14), 1.0, MASS, 30, 0.58, 0.34);
ribbon(bandLane(-0.36, 0.48, 0.12), 1.0, MASS, 30, 0.58, 0.34);
ribbon(bandLane(0.02, 0.6, 0.1), 1.0, HAZE, 34, 0.56, 0.3);
ribbon(bandLane(-0.02, 0.32, 0.08), 1.0, LIGHT, 46, 0.5, 0.28);
ribbon(bandLane(0.05, 0.18, 0.06), 1.0, LIGHT, 58, 0.44, 0.26);
// mottling: the band's light is uneven, brighter low down where the bulge sits
for (let i = 0; i < 44; i++) {
const t = constrain((i + 0.5) / 44 + random(-0.012, 0.012), 0, 1.04);
const of = randomGaussian(0, 0.44);
if (abs(of) > 1.05) continue;
const p = bandPt(t, of);
if (p[1] > horizonAt(p[0]) + 10) continue;
const r = halfWidth(t) * (0.4 + random(-0.1, 0.2));
const roll = random();
const col = roll < 0.44 ? LIGHT : roll < 0.78 ? HAZE : MASS;
const op = (34 + random(-10, 14)) * (1 - 0.3 * t) * (1 - 0.42 * abs(of));
soft(p[0], p[1], r, col, max(9, op), 0.68 + random(0, 0.14));
}
// nine star clouds, hand-set, the three heaviest low in the bulge
knot(0.24, -0.18, 62, 176);
knot(0.3, 0.11, 49, 162);
knot(0.18, 0.26, 41, 150);
knot(0.37, -0.31, 33, 136);
knot(0.45, 0.17, 28, 126);
knot(0.58, -0.13, 23, 116);
knot(0.7, 0.15, 19, 104);
knot(0.83, -0.11, 15, 94);
knot(0.93, 0.07, 12, 84);
// fibres combed along the band's own direction — the light is woven, not sprayed
const BASE_ANG = Math.atan2(UY, UX);
for (let i = 0; i < 120; i++) {
const t = random(0.01, 1);
const of = randomGaussian(0, 0.46);
if (abs(of) > 1.1) continue;
const p = bandPt(t, of);
if (p[1] > horizonAt(p[0]) - 6) continue;
const len = max(16, randomGaussian(46, 18)) * (1 - 0.3 * t);
fibre(p[0], p[1], len, BASE_ANG + randomGaussian(0, 0.15),
random() < 0.62 ? LIGHT : HAZE, max(0.3, randomGaussian(0.6, 0.22)));
}
// the great rift, cut in twice: a soft shoulder, then the dark heart walked along the lane
ribbon(laneA, 1.36, SHADOW, 96, 0.6, 0.4);
ribbon(laneB, 1.28, SHADOW, 84, 0.58, 0.4);
ribbon(laneC, 1.22, SHADOW, 80, 0.56, 0.4);
darkLane(laneA, 1.0, 190, 2);
darkLane(laneB, 0.94, 168, 2);
darkLane(laneC, 0.9, 158, 2);
// filaments peeling off the rift into the light, each one a different reach
brush.noFill();
brush.set("cpencil", SHADOW, 1.3);
brush.beginShape(0.6);
brush.vertex(196, 566);
brush.vertex(232, 604);
brush.vertex(246, 648);
brush.endShape(false);
brush.set("cpencil", SHADOW, 1.1);
brush.beginShape(0.6);
brush.vertex(300, 470);
brush.vertex(268, 442);
brush.vertex(258, 408);
brush.endShape(false);
brush.set("cpencil", SHADOW, 0.9);
brush.beginShape(0.6);
brush.vertex(432, 352);
brush.vertex(464, 380);
brush.vertex(470, 412);
brush.endShape(false);
brush.set("cpencil", SHADOW, 0.8);
brush.line(556, 258, 528, 232);
brush.set("cpencil", SHADOW, 0.65);
brush.line(646, 176, 668, 196);
brush.set("cpencil", SHADOW, 0.9);
brush.line(142, 636, 118, 660);
// globules: five small opaque knots of dust sitting on the brightest light
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.fillTexture(0.6, 0.2);
brush.fill(SHADOW, 196);
brush.circle(212, 528, 11, true);
brush.fill(SHADOW, 172);
brush.circle(268, 588, 8, true);
brush.fill(SHADOW, 184);
brush.circle(178, 620, 6.5, true);
brush.fill(SHADOW, 150);
brush.circle(352, 452, 5.5, true);
brush.fill(SHADOW, 132);
brush.circle(452, 328, 4.5, true);
// the band's edge, stated in broken contour on the side that faces the open sky
brush.noFill();
brush.set("cpencil", HAZE, 0.85);
brush.beginShape(0.5);
brush.vertex(bandPt(0.12, 1.02)[0], bandPt(0.12, 1.02)[1]);
brush.vertex(bandPt(0.22, 1.06)[0], bandPt(0.22, 1.06)[1]);
brush.vertex(bandPt(0.33, 0.98)[0], bandPt(0.33, 0.98)[1]);
brush.endShape(false);
brush.set("cpencil", HAZE, 0.7);
brush.beginShape(0);
brush.vertex(bandPt(0.46, 1.04)[0], bandPt(0.46, 1.04)[1]);
brush.vertex(bandPt(0.56, 0.96)[0], bandPt(0.56, 0.96)[1]);
brush.endShape(false);
brush.set("cpencil", HAZE, 0.6);
brush.beginShape(0);
brush.vertex(bandPt(0.28, -1.02)[0], bandPt(0.28, -1.02)[1]);
brush.vertex(bandPt(0.4, -0.94)[0], bandPt(0.4, -0.94)[1]);
brush.endShape(false);
brush.set("cpencil", HAZE, 0.55);
brush.line(bandPt(0.74, 0.92)[0], bandPt(0.74, 0.92)[1], bandPt(0.86, 0.88)[0], bandPt(0.86, 0.88)[1]);
// the land: bottom eighth only, laid in solid then deepened toward the lower edge
for (let pass = 0; pass < 3; pass++) {
brush.noStroke();
brush.fillBleed(pass === 0 ? 0.06 : 0.24, "out");
brush.fillTexture(pass === 0 ? 0.3 : 0.55, 0);
brush.fill(SHADOW, pass === 0 ? 246 : 132);
brush.beginShape(0.25);
for (let i = 0; i < RIDGE.length; i++) {
brush.vertex(RIDGE[i][0], RIDGE[i][1] + pass * 15 + random(-2.2, 2.2));
}
brush.vertex(818, 818);
brush.vertex(-18, 818);
brush.endShape(true);
}
// scrub breaking the ridge line, each clump a different size
brush.fillBleed(0.14, "out");
brush.fillTexture(0.45, 0.1);
brush.fill(SHADOW, 244);
brush.circle(128, 711, 9, true);
brush.fill(SHADOW, 240);
brush.circle(316, 715, 6, true);
brush.fill(SHADOW, 246);
brush.circle(498, 711, 12, true);
brush.fill(SHADOW, 238);
brush.circle(602, 701, 7.5, true);
brush.fill(SHADOW, 242);
brush.circle(700, 698, 5, true);
brush.fill(SHADOW, 236);
brush.circle(232, 719, 4.5, true);
// the star field: crowded over the band, letting go of the open sky
for (let i = 0; i < 940; i++) {
const x = random(6, 794);
const y = random(6, 760);
if (y > horizonAt(x) - 5) continue;
if (random() > starChance(x, y)) continue;
if (occluded(x, y) && random() < 0.86) continue;
const mag = max(0.9, randomGaussian(1.7, 0.6));
star(x, y, mag, random() < 0.3 ? HAZE : LIGHT);
}
// fourteen resolved stars set by hand, deliberately spread into the empty skies
star(126, 168, 6.6, LIGHT);
star(214, 96, 4.2, LIGHT);
star(88, 366, 5.1, LIGHT);
star(316, 214, 3.6, HAZE);
star(438, 118, 4.7, LIGHT);
star(596, 384, 5.8, LIGHT);
star(688, 512, 4.4, LIGHT);
star(742, 640, 3.8, LIGHT);
star(544, 620, 5.3, LIGHT);
star(414, 668, 3.4, HAZE);
star(660, 268, 3.1, HAZE);
star(266, 470, 6.2, LIGHT);
star(196, 618, 4.9, LIGHT);
star(506, 470, 3.9, LIGHT);
// and five warm ones, the only heat in the picture besides the horizon
star(352, 302, 4.6, OCHRE);
star(236, 556, 3.4, OCHRE);
star(628, 148, 2.9, OCHRE);
star(464, 542, 2.4, OCHRE);
star(104, 496, 2.2, OCHRE);
// haloes on the two brightest
brush.noStroke();
brush.fillBleed(0.6, "out");
brush.fill(LIGHT, 40);
brush.circle(126, 168, 13, true);
brush.fill(LIGHT, 34);
brush.circle(266, 470, 12, true);
brush.fill(OCHRE, 30);
brush.circle(352, 302, 10, true);
}