Ring at Maximum
2026 · celestial
Nothing is missing from the sun. Something has merely been laid over it, and the light has to go around rather than out.
an annular eclipse held at maximum, the ring of sun unbroken but uneven in brightness, its corona raking away to the lower left
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the annular ring hangs on the upper-right thirds crossing, sunk in a smoky aureole that
// drags away to the lower left where the corona streams off the bottom edge. the deep untouched dark is
// left standing in the lower-right and the upper-left corners — on a night sheet, that is the bare paper.
// layering: dark ground, warm aureole washes, radial pastel grain, corona wisps and bloom, the ring in
// radial strokes with whole sectors nearly extinguished, the moon dropped over it, its rim relit, darks last.
const PALETTE = [
"#251f2b", // ground — smoke-dark sky
"#453b50", // mass — lifted aubergine haze
"#0d0b11", // shadow — the moon, a hole punched in the sky
"#f7eedb", // light — the ring
"#d99a41", // accent — the one hot passage, rationed
"#9a91a6", // haze — corona grey
];
const CX = 534; // sun centre: upper-right thirds crossing
const CY = 266;
const R = 132; // solar limb
const MOX = 10; // the moon sits low and right of true centre, so the ring
const MOY = -8; // runs thick at the lower left and thins away above
const RM = 107;
// how much the sky lifts at (x, y): brightest against the limb, dragged out along the
// corona's axis to the lower left, dying to bare ground at the far corners
function glow(x, y) {
const dx = x - CX;
const dy = y - CY;
const u = (-dx + dy) * 0.7071; // + toward the lower left
const v = (dx + dy) * 0.7071;
const uu = u > 0 ? u * 0.56 : u * 1.8;
const t = Math.hypot(uu / 430, v / 246);
const grain = noise(x * 0.004, y * 0.004) * 0.52 - 0.2;
return constrain(1.14 - t + grain, 0, 1);
}
// the width of the ring along a bearing: thinnest where the moon has drifted
function ringWidth(a) {
return R - RM - (MOX * Math.cos(a) + MOY * Math.sin(a));
}
// brightness around the limb: two noise samples taken off a circle, gathered by a broad
// swell so the ring dims in places without ever going out
function limbLight(a) {
const n = noise(3.2 + Math.cos(a) * 0.9, 6.7 + Math.sin(a) * 0.9);
const m = noise(11.4 + Math.cos(a) * 2.3, 2.3 + Math.sin(a) * 2.3);
const swell = noise(21.5 + Math.cos(a) * 0.55, 17.2 + Math.sin(a) * 0.55);
return constrain(0.3 + (n * 0.52 + m * 0.34 - 0.06) * (swell * 1.5 + 0.3), 0.2, 1);
}
// the corona does not stand off the limb evenly: it gathers into the lower-left wedge
function wedge(x, y) {
const c = Math.cos(Math.atan2(y - CY, x - CX) - 2.34);
return 0.2 + 0.8 * Math.pow(Math.max(0, c), 1.4);
}
// the moon's limb: a drawn near-circle, so its edge curves instead of faceting
function disc(x, y, r, op, jitter) {
brush.fill(PALETTE[2], op);
brush.beginShape(0.5);
for (let i = 0; i < 46; i++) {
const a = (i / 46) * Math.PI * 2;
const rr = r + randomGaussian(0, jitter);
brush.vertex(x + Math.cos(a) * rr, y + Math.sin(a) * rr);
}
brush.endShape(true);
}
function haze(x, y, r, t, op) {
brush.fill(lerpColor(color(PALETTE[0]), color(PALETTE[1]), constrain(t, 0, 1)), op);
brush.circle(x, y, r, true);
}
// a corona streamer: a feathered, tapering path leaving the limb at bearing a
function wisp(a, len, bend, spread, pale, weight) {
const pts = [];
let ang = a;
let x = CX + Math.cos(a) * (R + 3);
let y = CY + Math.sin(a) * (R + 3);
const steps = 8;
for (let i = 0; i <= steps; i++) {
pts.push([x, y]);
ang += bend / steps + randomGaussian(0, 0.022);
const step = (len / steps) * (0.72 + i * 0.08);
x += Math.cos(ang) * step;
y += Math.sin(ang) * step;
}
for (let k = -2; k <= 2; k++) {
const fade = pale * (k === 0 ? 1 : k === 1 || k === -1 ? 0.7 : 0.42);
const c = lerpColor(color(PALETTE[0]), color(PALETTE[3]), constrain(fade, 0, 1));
const off = k * spread * (0.85 + random(0, 0.4));
brush.noFill();
brush.set(k === 0 ? "cpencil" : "2H", c, weight * (k === 0 ? 1 : 0.58));
brush.beginShape(0.6);
for (let i = 0; i <= steps; i++) {
const s = 0.35 + i * 0.14; // the streamer opens as it leaves the limb
brush.vertex(
pts[i][0] - Math.sin(a) * off * s + random(-1.6, 1.6),
pts[i][1] + Math.cos(a) * off * s + random(-1.6, 1.6),
);
}
brush.endShape(false);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PALETTE[0]);
brush.noStroke();
// 1. the aureole — the sky the covered sun still manages to light, pooled and uneven
brush.fillBleed(0.72, "out");
brush.fillTexture(0.34, 0.38);
haze(CX - 30, CY + 30, 322, 0.9, 104);
haze(CX - 152, CY + 146, 284, 0.76, 84);
haze(CX + 16, CY - 24, 226, 1.0, 118);
haze(CX - 278, CY + 274, 232, 0.56, 60);
haze(CX - 404, CY + 396, 188, 0.38, 42);
haze(CX + 96, CY + 74, 158, 0.5, 34);
brush.fill(PALETTE[4], 22);
brush.circle(CX - 12, CY + 8, 232, true);
brush.fill(PALETTE[4], 14);
brush.circle(CX - 142, CY + 134, 202, true);
// smoke scattered where the aureole is strongest
brush.fillBleed(0.56, "out");
brush.fillTexture(0.55, 0.45);
let laid = 0;
for (let i = 0; i < 480 && laid < 38; i++) {
const x = randomGaussian(384, 232);
const y = randomGaussian(330, 224);
const g = glow(x, y);
if (g < 0.26 || random() > g * 0.9) continue;
if (dist(x, y, CX, CY) < R + 18) continue;
haze(x, y, Math.max(32, randomGaussian(76, 18)), g * 1.1, Math.max(20, randomGaussian(30 + g * 40, 12)));
laid++;
}
// 2. pastel grain running radially out of the eclipse, gathered into the corona's wedge
for (let i = 0; i < 900; i++) {
const x = randomGaussian(396, 236);
const y = randomGaussian(348, 228);
if (x < -30 || x > 830 || y < -30 || y > 830) continue;
const d = dist(x, y, CX, CY);
if (d < R + 12) continue;
const g = glow(x, y) * wedge(x, y);
if (random() > g * 1.1) continue;
const dir = Math.atan2(y - CY, x - CX) + randomGaussian(0, 0.19);
const long = random() < 0.11;
const len = long ? Math.max(40, randomGaussian(96, 30)) : Math.max(7, randomGaussian(19, 8));
const c = lerpColor(color(PALETTE[1]), color(PALETTE[5]), constrain(randomGaussian(g * 0.44, 0.14), 0, 0.62));
brush.set(random() < 0.6 ? "cpencil" : "charcoal", c, 0.45 + g * 0.8);
brush.line(x, y, x + Math.cos(dir) * len, y + Math.sin(dir) * len);
}
// two long drags following the corona out of the picture, kept faint
brush.field("curved");
brush.set("cpencil", lerpColor(color(PALETTE[1]), color(PALETTE[5]), 0.24), 1.2);
brush.flowLine(386, 400, 224, 218);
brush.set("cpencil", lerpColor(color(PALETTE[1]), color(PALETTE[5]), 0.13), 1.0);
brush.flowLine(300, 508, 186, 204);
brush.noField();
// 3. corona — a few long streamers to the lower left, a short fringe elsewhere
wisp(2.3, 372, 0.13, 13, 0.66, 1.5);
wisp(2.55, 300, -0.17, 11, 0.56, 1.3);
wisp(2.09, 262, 0.23, 10, 0.6, 1.25);
wisp(2.78, 214, -0.1, 8, 0.44, 1.1);
wisp(1.88, 192, 0.27, 8, 0.5, 1.05);
wisp(2.41, 164, 0.03, 7, 0.72, 1.0);
wisp(1.66, 116, 0.3, 6, 0.36, 0.9);
wisp(3.16, 104, -0.26, 6, 0.3, 0.85);
wisp(1.32, 74, 0.18, 5, 0.28, 0.75);
wisp(0.52, 56, -0.15, 4, 0.22, 0.7);
wisp(5.66, 52, 0.16, 4, 0.2, 0.7);
wisp(4.6, 44, -0.12, 4, 0.19, 0.65);
wisp(3.84, 40, 0.14, 4, 0.21, 0.65);
// pale bloom carried along the streamers so the corona has body, not only line
brush.noStroke();
brush.fillBleed(0.66, "out");
brush.fillTexture(0.28, 0.32);
brush.fill(PALETTE[3], 26);
brush.circle(CX - 138, CY + 130, 162, true);
brush.fill(PALETTE[3], 17);
brush.circle(CX - 252, CY + 238, 136, true);
brush.fill(PALETTE[3], 11);
brush.circle(CX - 348, CY + 330, 112, true);
brush.fill(PALETTE[5], 34);
brush.circle(CX - 78, CY + 68, 122, true);
brush.fill(PALETTE[5], 22);
brush.circle(CX - 192, CY + 174, 106, true);
brush.fill(PALETTE[5], 15);
brush.circle(CX - 60, CY + 168, 92, true);
// 4. the ring: radial strokes, whole sectors falling nearly to the value of the sky
for (let i = 0; i < 400; i++) {
const a = (i / 400) * Math.PI * 2 + randomGaussian(0, 0.007);
const lit = limbLight(a);
if (lit < 0.34 && random() < 0.34) continue; // the faint side thins towards a thread
const w = ringWidth(a) * (0.8 + lit * 0.5);
const inner = R - w + randomGaussian(0, 1.3);
// tufts: the fringe clumps and then falls quiet, rather than combing evenly round
const ragged = noise(41 + Math.cos(a) * 5.6, 8 + Math.sin(a) * 5.6);
const tuft = Math.pow(constrain(ragged * 1.5 - 0.28, 0, 1), 1.7) * lit;
const outer = R + randomGaussian(0.5, 1.7) + lit * (1.0 + tuft * 17);
const hot = lit > 0.66 && random() < 0.2;
let c = lerpColor(color(PALETTE[1]), color(PALETTE[3]), constrain(0.3 + lit * 0.85, 0, 1));
if (hot) c = lerpColor(c, color(PALETTE[4]), constrain(randomGaussian(0.36, 0.16), 0.05, 0.7));
brush.set(random() < 0.66 ? "cpencil" : "2B", c, 0.6 + lit * 1.8);
brush.line(CX + Math.cos(a) * inner, CY + Math.sin(a) * inner, CX + Math.cos(a) * outer, CY + Math.sin(a) * outer);
}
// the warm passage low on the ring, laid in loosely and left broken
for (let i = 0; i < 150; i++) {
const a = 1.82 + (i / 150) * 0.92 + randomGaussian(0, 0.01);
if (random() < 0.28) continue;
const inner = R - ringWidth(a) * (0.94 + random(0, 0.22));
const outer = R + randomGaussian(3.0, 2.6);
const c = lerpColor(color(PALETTE[3]), color(PALETTE[4]), constrain(randomGaussian(0.42, 0.18), 0, 0.86));
brush.set(random() < 0.5 ? "cpencil" : "2H", c, 1.0 + random(0, 1.0));
brush.line(CX + Math.cos(a) * inner, CY + Math.sin(a) * inner, CX + Math.cos(a) * outer, CY + Math.sin(a) * outer);
}
// and the cold brightening opposite it, upper right
for (let i = 0; i < 90; i++) {
const a = 5.34 + (i / 90) * 0.56 + randomGaussian(0, 0.009);
const inner = R - ringWidth(a) * 0.86;
const outer = R + randomGaussian(2.4, 2.6);
const c = lerpColor(color(PALETTE[1]), color(PALETTE[3]), constrain(randomGaussian(0.86, 0.14), 0.3, 1));
brush.set("2B", c, 0.8 + random(0, 0.7));
brush.line(CX + Math.cos(a) * inner, CY + Math.sin(a) * inner, CX + Math.cos(a) * outer, CY + Math.sin(a) * outer);
}
// 5. the moon: dropped over the inside of the ring, blacker than any sky
brush.noStroke();
brush.fillBleed(0.12, "in");
brush.fillTexture(0.22, 0.9);
disc(CX + MOX, CY + MOY, RM * 0.995, 248, 0.8);
disc(CX + MOX - 4, CY + MOY + 3, RM * 0.94, 226, 1.4);
brush.fillTexture(0.34, 0.8);
disc(CX + MOX + 5, CY + MOY - 4, RM * 0.84, 198, 2.2);
// 6. the inner rim relit hard against the disc — the sliver the moon does not quite cover
for (let i = 0; i < 380; i++) {
const a = (i / 380) * Math.PI * 2 + randomGaussian(0, 0.008);
const lit = limbLight(a);
if (random() > 0.16 + lit * 0.8) continue;
const inner = R - ringWidth(a) * (0.9 + random(0, 0.14));
const c = lerpColor(color(PALETTE[5]), color(PALETTE[3]), constrain(0.42 + lit * 0.8, 0, 1));
brush.set("2H", c, 0.45 + lit * 1.0);
const out = inner + 1.8 + lit * 3.0;
brush.line(CX + Math.cos(a) * inner, CY + Math.sin(a) * inner, CX + Math.cos(a) * out, CY + Math.sin(a) * out);
}
// three brightenings on the thick lower-left arc, each a few strokes gone over twice
const chips = [2.02, 2.28, 2.61];
const chipSpan = [0.075, 0.055, 0.045];
for (let k = 0; k < 3; k++) {
for (let i = 0; i < 26; i++) {
const a = chips[k] + (i / 26) * chipSpan[k] + randomGaussian(0, 0.006);
const inner = R - ringWidth(a) * (0.98 + random(0, 0.1));
const c = lerpColor(color(PALETTE[3]), color(PALETTE[4]), constrain(randomGaussian(0.16, 0.1), 0, 0.4));
brush.set(random() < 0.6 ? "2H" : "cpencil", c, 0.8 + random(0, 0.8));
const out = inner + 5.5 + random(0, 4);
brush.line(CX + Math.cos(a) * inner, CY + Math.sin(a) * inner, CX + Math.cos(a) * out, CY + Math.sin(a) * out);
}
}
// 7. darks last: sky biting into the outer limb where the corona runs faint
for (let i = 0; i < 110; i++) {
const a = randomGaussian(5.1, 1.1);
if (limbLight(a) > 0.46) continue;
const r0 = R + 2 + random(0, 4);
const c = lerpColor(color(PALETTE[0]), color(PALETTE[2]), constrain(randomGaussian(0.44, 0.18), 0, 0.9));
brush.set("cpencil", c, 0.6 + random(0, 0.7));
const len = randomGaussian(11, 5);
brush.line(CX + Math.cos(a) * r0, CY + Math.sin(a) * r0, CX + Math.cos(a) * (r0 + len), CY + Math.sin(a) * (r0 + len));
}
// three specks of far light, well clear of the ring — all the sky gets by way of stars
brush.noStroke();
brush.fill(PALETTE[3], 138);
brush.circle(180, 226, 2.2, true);
brush.fill(PALETTE[3], 98);
brush.circle(258, 122, 1.7, true);
brush.fill(PALETTE[5], 120);
brush.circle(132, 392, 2.0, true);
}