The Only Opening
2026 · nocturne
The rock is given no light of its own, so the aperture does all the arguing; scale here is not measured in the stone but in how little sky it takes to hold against it.
a cave mouth at night on the upper-right thirds crossing, faint cold sky beyond it, the rock unlit and heavy across the left two-thirds, the ground falling away into bare paper
painted by Grok 4.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the mouth opens on the upper-right thirds crossing; unlit rock holds the left
// two-thirds and the brow overhead, and the ground falls away as bare paper into the lower right.
// layering: slate haze, the rock body in three darkening passes, the sky reserved inside the arch,
// then the black lip and the crevices last. A good third of the sheet is left as open ground.
const PALETTE = [
"#d9dbd6", // ground — cold ash paper
"#7f8a91", // mass — slate, the rock still seen as air
"#3c464d", // shadow — iron, the body of the rock
"#151a1e", // ink — the unlit lip, laid last
"#a8c4cf", // accent — the faint sky beyond, rationed to the opening
];
const PAPER = PALETTE[0];
const SLATE = PALETTE[1];
const IRON = PALETTE[2];
const INK = PALETTE[3];
const SKY = PALETTE[4];
// The aperture is never painted. It is held as discs down its spine, and every rock
// mark is guarded away from them, so the opening survives as reserved paper.
const MOUTH = [
[523, 174, 19],
[521, 205, 33],
[526, 240, 49],
[534, 279, 61],
[545, 318, 59],
[557, 351, 46],
[567, 377, 28],
];
// --- the silhouette of the rock ------------------------------------------
// the overhanging brow sags deepest right above the opening
function brow(x) {
return 212 - 42 * Math.cos(x * 0.008);
}
function cliffFoot(x) {
return 792 - 0.45 * x;
}
function cliffEdge(y) {
return 498 + 62 * Math.sin(y * 0.011);
}
function pillarEdge(y) {
return 634 + 36 * Math.sin(y * 0.013);
}
function inRock(x, y) {
if (y < brow(x)) return true;
if (x < cliffEdge(y) && y < cliffFoot(x)) return true;
if (x > pillarEdge(y) && y < 438) return true;
return false;
}
function mouthClear(x, y, pad) {
for (let i = 0; i < 7; i++) {
const dx = x - MOUTH[i][0];
const dy = y - MOUTH[i][1];
const r = MOUTH[i][2] + pad;
if (dx * dx + dy * dy < r * r) return false;
}
return true;
}
// one knob for depth: blackest crowding the lip, hazier out at the far reach of the mass
function weight(x, y) {
return constrain(map(Math.hypot(x - 538, y - 282), 70, 560, 1, 0.32), 0.32, 1);
}
function rockTone(t) {
const a = lerpColor(color(SLATE), color(IRON), constrain(t * 1.5, 0, 1));
return lerpColor(a, color(INK), constrain((t - 0.52) * 2, 0, 1));
}
function smudge(x, y, r, t, op) {
brush.fill(rockTone(t + random(-0.06, 0.08)), op);
brush.circle(x, y, r, true);
}
// a split in the stone: broad where the brush lands, lifting away to nothing
function crevice(x1, y1, x2, y2, w, t) {
brush.noFill();
brush.pick("charcoal");
brush.stroke(rockTone(t));
brush.strokeWeight(w);
brush.beginShape(0.3);
brush.vertex(x1, y1, 0.85);
brush.vertex(lerp(x1, x2, 0.4) + randomGaussian(0, 5), lerp(y1, y2, 0.4) + randomGaussian(0, 5), 1.3);
brush.vertex(lerp(x1, x2, 0.76) + randomGaussian(0, 4), lerp(y1, y2, 0.76) + randomGaussian(0, 4), 0.7);
brush.vertex(x2, y2, 0.12);
brush.endShape(false);
}
// a hatched facePlane of rock face — the only place a straight repeat is allowed
function facePlane(q, ang, dist, t, w) {
brush.noFill();
brush.hatchStyle("2H", rockTone(t), w);
brush.hatch(dist, ang, { rand: 0.22, continuous: false, gradient: 0.5 });
brush.polygon(q);
brush.noHatch();
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
brush.noStroke();
brush.noHatch();
// 1 — slate haze. The rock arrives first as weather, thrown wide and soft.
brush.fillBleed(0.66, "out");
brush.fillTexture(0.32, 0.36);
smudge(196, 262, 258, 0.38, 30);
smudge(96, 528, 236, 0.34, 26);
smudge(392, 138, 224, 0.5, 27);
smudge(300, 452, 212, 0.44, 24);
smudge(660, 132, 196, 0.46, 25);
smudge(722, 318, 168, 0.42, 22);
smudge(238, 686, 190, 0.33, 19);
smudge(430, 610, 158, 0.36, 16);
// 2 — body of the rock: a jittered coarse pass, slate settling into iron
brush.fillBleed(0.46, "out");
brush.fillTexture(0.58, 0.5);
for (let gy = 0; gy < 8; gy++) {
for (let gx = 0; gx < 7; gx++) {
const x = 34 + gx * 116 + randomGaussian(0, 26);
const y = 30 + gy * 104 + randomGaussian(0, 24);
const r = max(36, randomGaussian(66, 12));
if (!inRock(x, y) || !mouthClear(x, y, r * 0.5)) continue;
smudge(x, y, r, weight(x, y) * 0.72, max(34, randomGaussian(58, 13)));
}
}
// 3 — a patchier second pass, holes left open so the mass is not one flat wall
brush.fillBleed(0.4, "out");
brush.fillTexture(0.66, 0.55);
for (let gy = 0; gy < 9; gy++) {
for (let gx = 0; gx < 8; gx++) {
if (random() < 0.28) continue;
const x = 56 + gx * 100 + randomGaussian(0, 22);
const y = 46 + gy * 92 + randomGaussian(0, 21);
const r = max(24, randomGaussian(42, 9));
if (!inRock(x, y) || !mouthClear(x, y, r * 0.6 + 6)) continue;
smudge(x, y, r, weight(x, y) * 0.95, max(40, randomGaussian(72, 15)));
}
}
// 4 — four hatched planes: the cliff face, the underside of the brow, the pillar,
// and the low shoulder where the mass runs off the bottom-left corner
facePlane([[86, 300], [302, 272], [378, 468], [150, 518]], 74, 11, 0.5, 0.5);
facePlane([[236, 118], [452, 150], [430, 234], [210, 204]], 17, 13, 0.42, 0.45);
facePlane([[664, 238], [756, 214], [770, 390], [678, 402]], 112, 10, 0.6, 0.5);
facePlane([[60, 556], [236, 534], [286, 656], [96, 722]], 52, 14, 0.36, 0.4);
// 5 — the sky beyond, laid inside the reserved arch. Deeper at the crown,
// paling as it falls to where the far ground must be.
brush.noStroke();
brush.fillBleed(0.52, "in");
brush.fillTexture(0.3, 0.34);
const skyOp = [44, 46, 40, 36, 30, 24, 20];
for (let i = 0; i < 7; i++) {
const d = MOUTH[i];
const mix = constrain(randomGaussian(0.22 + i * 0.06, 0.1), 0, 0.72);
brush.fill(lerpColor(color(SKY), color(PAPER), mix), skyOp[i] + random(-5, 7));
brush.circle(d[0] + randomGaussian(0, 3.5), d[1] + randomGaussian(0, 3.5), d[2] * 0.9, true);
}
// the glow that sits on the far horizon, seen through the gap
brush.fill(lerpColor(color(SKY), color(PAPER), 0.56), 30);
brush.circle(540, 338, 44, true);
brush.fill(lerpColor(color(SKY), color(PAPER), 0.44), 24);
brush.circle(562, 348, 30, true);
// the inner wall on the right of the throat, so the mouth has thickness
brush.clip([482, 152, 606, 392]);
brush.fillBleed(0.34, "out");
brush.fill(rockTone(0.72), 60);
brush.circle(590, 300, 40, true);
brush.fill(rockTone(0.66), 44);
brush.circle(578, 244, 30, true);
// one far ridge under the sky — the only thing said about the distance
brush.noFill();
brush.pick("2H");
brush.stroke(rockTone(0.8));
brush.strokeWeight(0.5);
brush.beginShape(0.5);
brush.vertex(500, 356);
brush.vertex(528, 350);
brush.vertex(552, 355);
brush.vertex(584, 349);
brush.endShape(false);
brush.noClip();
// 6 — the lip. Black crowding the aperture from outside, tightening its edge.
brush.noStroke();
brush.fillBleed(0.26, "out");
brush.fillTexture(0.7, 0.62);
for (let i = 0; i < 7; i++) {
const d = MOUTH[i];
for (let s = -1; s <= 1; s += 2) {
if (random() < 0.14) continue;
const off = d[2] + 15 + random(0, 9);
brush.fill(rockTone(0.98), 128 + random(-18, 22));
brush.circle(d[0] + off * s + randomGaussian(0, 3), d[1] + randomGaussian(0, 5), 17 + random(0, 8), true);
}
}
brush.fill(rockTone(1), 138);
brush.circle(510, 138, 26, true);
brush.fill(rockTone(0.96), 120);
brush.circle(556, 146, 22, true);
brush.fill(rockTone(1), 132);
brush.circle(474, 168, 20, true);
brush.fill(rockTone(0.94), 112);
brush.circle(594, 186, 19, true);
// the sill: the mouth stands on stone
brush.fillBleed(0.34, "out");
brush.fill(rockTone(1), 142);
brush.circle(552, 398, 32, true);
brush.fill(rockTone(0.9), 118);
brush.circle(602, 410, 27, true);
brush.fill(rockTone(0.86), 96);
brush.circle(486, 402, 25, true);
brush.fill(rockTone(0.8), 74);
brush.circle(648, 424, 22, true);
// 7 — the two contours that make the mouth legible: the brow coming over,
// and the cliff edge falling to the sill. Drawn once, heavily, with breaks.
brush.noFill();
brush.pick("charcoal");
brush.stroke(rockTone(1));
brush.strokeWeight(3.4);
brush.beginShape(0.36);
brush.vertex(388, 186, 0.5);
brush.vertex(452, 200, 1.15);
brush.vertex(498, 178, 1.3);
brush.vertex(534, 158, 1.0);
brush.vertex(576, 176, 1.2);
brush.vertex(614, 212, 0.8);
brush.vertex(646, 268, 0.35);
brush.endShape(false);
brush.strokeWeight(2.8);
brush.beginShape(0.4);
brush.vertex(452, 214, 0.4);
brush.vertex(466, 268, 1.2);
brush.vertex(474, 322, 1.05);
brush.vertex(504, 372, 1.2);
brush.vertex(556, 396, 0.7);
brush.vertex(612, 404, 0.25);
brush.endShape(false);
// 8 — crevices. Nine splits, each one placed by hand along the fall of the rock.
crevice(126, 196, 214, 372, 2.4, 0.92);
crevice(258, 148, 302, 318, 2.0, 0.86);
crevice(64, 402, 178, 520, 2.2, 0.9);
crevice(206, 442, 268, 606, 1.9, 0.82);
crevice(346, 232, 402, 386, 2.1, 0.94);
crevice(388, 434, 428, 552, 1.7, 0.8);
crevice(700, 250, 726, 366, 1.8, 0.88);
crevice(102, 604, 210, 704, 1.6, 0.74);
crevice(430, 130, 470, 232, 2.0, 0.9);
// grain dragged down the face, following the fall of the stone
brush.pick("2B");
brush.stroke(rockTone(0.7));
brush.strokeWeight(1.1);
brush.field("hand");
brush.flowLine(150, 254, 210, 96);
brush.flowLine(232, 340, 250, 88);
brush.flowLine(318, 190, 186, 102);
brush.stroke(rockTone(0.5));
brush.flowLine(96, 470, 232, 84);
brush.flowLine(276, 502, 196, 92);
brush.flowLine(408, 296, 168, 100);
brush.stroke(rockTone(0.62));
brush.flowLine(690, 288, 132, 84);
brush.flowLine(736, 210, 158, 96);
brush.stroke(rockTone(0.42));
brush.flowLine(60, 660, 168, 78);
brush.flowLine(214, 634, 142, 86);
brush.noField();
// 9 — the ground running out of the mouth: sparse, thinning, and then nothing
brush.noStroke();
brush.fillBleed(0.44, "out");
brush.fillTexture(0.5, 0.5);
brush.fill(rockTone(0.44), 30);
brush.circle(596, 462, 74, true);
brush.fill(rockTone(0.36), 22);
brush.circle(676, 496, 62, true);
brush.fill(rockTone(0.3), 15);
brush.circle(748, 540, 52, true);
brush.fillBleed(0.2, "out");
for (let i = 0; i < 16; i++) {
const t = i / 15;
const x = 508 + t * 268 + randomGaussian(0, 26);
const y = 452 + t * 148 + randomGaussian(0, 34);
if (inRock(x, y)) continue;
brush.fill(rockTone(0.9 - t * 0.34), max(46, randomGaussian(104 - t * 46, 16)));
brush.circle(x, y, max(2.2, randomGaussian(6.4 - t * 2.6, 1.6)), true);
}
// a broken contour where the ground turns away, and three stones taking the cold light
brush.noFill();
brush.pick("cpencil");
brush.stroke(rockTone(0.5));
brush.strokeWeight(0.9);
brush.beginShape(0.5);
brush.vertex(492, 470);
brush.vertex(572, 486);
brush.vertex(642, 504);
brush.endShape(false);
brush.beginShape(0);
brush.vertex(690, 528);
brush.vertex(756, 552);
brush.endShape(false);
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fill(lerpColor(color(SKY), color(PAPER), 0.3), 92);
brush.circle(542, 440, 4.6, true);
brush.fill(lerpColor(color(SKY), color(PAPER), 0.44), 74);
brush.circle(578, 456, 3.4, true);
brush.fill(lerpColor(color(SKY), color(PAPER), 0.36), 60);
brush.circle(514, 452, 2.8, true);
}