The Arete at Four O'Clock
2026 · landscape
A shadow on sand is not sand made darker. It is a shape with a border you could cut out with scissors. So each dune here is two facets and one edge, the blackest of them set where the paper runs out at the lower left, the rest stepping away until there is nothing left to say.
wind-carved dune noses stepping up and away to the right, each one a flat lit facet of sand and a flat dark facet of shade meeting along a cut crest, the near slip face almost black in the lower left, ripples raking down the sunlit flank, the far crests thinning until they give out into bare paper
painted by Kimi K3p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: four dune noses. Each crest is an arc with one summit; the sun is low on the right, so
// every dune is a flat dark facet left of its summit and flat lit sand right of it, the two meeting along
// an arete cut down toward the viewer. The summits step up and to the right — (268,470), (430,368),
// (610,288), (830,232) — so the mass is anchored in the lower-left third and the ridge lines thin and
// give out into bare paper at the upper right. Each dune is a silhouette closed off the bottom of the
// paper, so the nearer one always crops the one behind and nothing is left floating.
// layering: two bleeding washes; then far to near — silhouette, ripples, grain, shadow facet; the crest
// lips, inked crests, the scoured black on the near slip face and the ember struck last.
const PALETTE = [
"#f2e9d8", // paper
"#e8cd9a", // sand
"#c4913f", // ochre
"#3a3d5c", // shade
"#161520", // ink
"#b5502a", // ember
];
const K1 = [[-90, 742], [0, 672], [92, 596], [175, 528], [232, 486], [268, 470], [318, 490], [382, 532], [452, 590], [530, 660], [614, 740], [700, 826], [790, 900]];
const V1 = [[268, 470], [238, 556], [212, 646], [186, 744], [158, 850], [136, 900]];
const K2 = [[-90, 570], [20, 512], [130, 458], [242, 414], [340, 384], [430, 368], [500, 384], [578, 418], [660, 462], [746, 516], [834, 578], [890, 622]];
// a shadow facet is cropped by the dune in front, so its foot only ever tracks just under that crest —
// carried further down it would show through the nearer sand
const F2 = [[-90, 570], [20, 512], [130, 458], [242, 414], [340, 384], [430, 368], [400, 424], [368, 478], [334, 530], [240, 568], [120, 646], [0, 702], [-150, 754]];
const F3 = [[-40, 452], [78, 406], [170, 382], [238, 356], [322, 344], [432, 314], [532, 298], [610, 288], [566, 342], [524, 392], [486, 436], [440, 454], [300, 472], [160, 512], [20, 562], [-150, 602]];
const K3 = [[-40, 452], [78, 406], [170, 382], [238, 356], [322, 344], [432, 314], [532, 298], [610, 288], [690, 302], [770, 326], [850, 358], [890, 378]];
const K4 = [[340, 312], [430, 292], [520, 276], [605, 258], [686, 244], [760, 234], [830, 232], [890, 240]];
const V4 = [[830, 232], [792, 262], [744, 292], [686, 320]];
// ripples: the same motif two scales down, lying parallel to the flank they sit on. Three only, at
// three different widths, and none of them crossing the whole flank.
const P1 = [[352, 528], [426, 566], [500, 612], [570, 662], [636, 716]];
const P2 = [[330, 676], [418, 722], [506, 780], [596, 846], [680, 916]];
const P4 = [[506, 448], [592, 482], [682, 522], [770, 568], [858, 620]];
const P6 = [[648, 338], [722, 364], [796, 398]];
const P7 = [[600, 900], [700, 838], [812, 790], [890, 764]];
const P8 = [[746, 448], [820, 478], [890, 512]];
// the reach of the near flank, one flat step warmer than the sand above it
const OCH = [[178, 920], [242, 822], [336, 744], [472, 678], [640, 626], [800, 594], [890, 580], [890, 930], [200, 930]];
function duneAt(pts, t) {
const s = constrain(t, 0, 1) * (pts.length - 1);
const i = min(floor(s), pts.length - 2);
const f = s - i;
return [lerp(pts[i][0], pts[i + 1][0], f), lerp(pts[i][1], pts[i + 1][1], f)];
}
function duneDir(pts, t) {
const s = constrain(t, 0, 1) * (pts.length - 1);
const i = min(floor(s), pts.length - 2);
const dx = pts[i + 1][0] - pts[i][0];
const dy = pts[i + 1][1] - pts[i][1];
const m = max(0.001, Math.sqrt(dx * dx + dy * dy));
return [dx / m, dy / m];
}
function duneJit(pts, amt) {
const out = [];
for (let i = 0; i < pts.length; i++) out.push([pts[i][0] + randomGaussian(0, amt), pts[i][1] + randomGaussian(0, amt)]);
return out;
}
// a flat tone with a cut edge: laid on in passes, then scoured with tight hatch only where it must go
// truly dark, so the shadow is pigment sitting on the paper and never a tint washed over it
function duneMass(poly, col, op, passes, gap, ang, hName, hCol, hW) {
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.03, "out");
brush.fillTexture(0.16, 0.02);
for (let p = 0; p < passes; p++) {
brush.fill(col, constrain(randomGaussian(op, 7), 14, 255));
brush.polygon(duneJit(poly, 2.4));
}
if (gap > 0) {
brush.noFill();
brush.hatchStyle(hName, hCol, hW);
brush.hatch(gap, ang, { rand: 0.18, continuous: false });
brush.polygon(duneJit(poly, 1.4));
brush.noHatch();
}
}
// everything below a crest, run off the bottom of the paper: the dune's whole near face
function duneBody(crest) {
const out = duneJit(crest, 2);
out.push([crest[crest.length - 1][0] + 60, 920]);
out.push([crest[0][0] - 60, 920]);
return out;
}
// the shadow facet: crest from its left horn to the summit, down the arete, then back along the foot
function duneFacet(crest, upto, arete, footY) {
const out = [];
for (let i = 0; i <= upto; i++) out.push([crest[i][0] + randomGaussian(0, 2.2), crest[i][1] + randomGaussian(0, 2)]);
for (let i = 1; i < arete.length; i++) out.push([arete[i][0] + randomGaussian(0, 3), arete[i][1] + randomGaussian(0, 3)]);
out.push([crest[0][0] - 60, footY]);
return out;
}
// a flat band hanging below a line, hard along the top, closing at both ends: a ripple's shadow at
// small width, the ochre reach of a flank at large width
function duneBelow(pts, w, col, op, passes, lipOp) {
const n = 11;
const top = [];
const bot = [];
for (let k = 0; k <= n; k++) {
const t = k / n;
const p = duneAt(pts, t);
const d = duneDir(pts, t);
// flat for most of the run, closing only in the last stretch, so a ripple is a band and not a lens
const taper = 1 - 0.92 * pow(max(0, (abs(t - 0.5) - 0.38) / 0.14), 1.7);
const h = max(1.6, w * constrain(taper, 0.08, 1.2) * constrain(randomGaussian(1, 0.13), 0.62, 1.38));
top.push([p[0] + randomGaussian(0, 2), p[1] + randomGaussian(0, 1.4)]);
bot.push([p[0] - d[1] * h + randomGaussian(0, 2), p[1] + d[0] * h + randomGaussian(0, 2)]);
}
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.04, "out");
brush.fillTexture(0.18, 0.02);
for (let q = 0; q < passes; q++) {
brush.fill(col, constrain(randomGaussian(op, 6), 14, 255));
brush.beginShape(0.12);
for (let k = 0; k <= n; k++) brush.vertex(top[k][0] + randomGaussian(0, 1.4), top[k][1] + randomGaussian(0, 1.4));
for (let k = n; k >= 0; k--) brush.vertex(bot[k][0] + randomGaussian(0, 1.6), bot[k][1] + randomGaussian(0, 1.6));
brush.endShape(true);
}
if (lipOp > 0) {
brush.fill("#f2e9d8", lipOp);
brush.beginShape(0.12);
for (let k = 0; k <= n; k++) brush.vertex(top[k][0], top[k][1]);
for (let k = n; k >= 0; k--) {
const d = duneDir(pts, k / n);
brush.vertex(top[k][0] + d[1] * 5.2, top[k][1] - d[0] * 5.2);
}
brush.endShape(true);
}
}
// sand grain: short flecks lying down the slope, scattered below a line, lit flanks only
function duneGrain(pts, t0, t1, spread, count, weight) {
brush.noFill();
brush.noHatch();
for (let k = 0; k < count; k++) {
const t = constrain(randomGaussian((t0 + t1) / 2, (t1 - t0) * 0.36), t0, t1);
const p = duneAt(pts, t);
const d = duneDir(pts, t);
const off = constrain(abs(randomGaussian(0, spread * 0.44)) + 3, 3, spread);
const len = constrain(randomGaussian(9, 4), 2, 22);
const r = random();
const c = r < 0.74 ? "#c4913f" : "#161520";
brush.set(r < 0.6 ? "spray" : r < 0.9 ? "2H" : "HB", c, constrain(randomGaussian(weight, 0.14), 0.16, 1.1));
brush.line(
p[0] - d[1] * off - d[0] * len * 0.5, p[1] + d[0] * off - d[1] * len * 0.5,
p[0] - d[1] * off + d[0] * len * 0.5, p[1] + d[0] * off + d[1] * len * 0.5
);
}
}
// the sliver of light the crest carries before the shadow begins
function duneLip(pts, t0, t1, h, col, op) {
const n = 10;
const near = [];
const far = [];
for (let k = 0; k <= n; k++) {
const t = lerp(t0, t1, k / n);
const p = duneAt(pts, t);
const d = duneDir(pts, t);
const w = max(1.4, h * constrain(randomGaussian(1, 0.22), 0.4, 1.7));
near.push([p[0], p[1] + randomGaussian(0, 0.9)]);
far.push([p[0] + d[1] * w, p[1] - d[0] * w]);
}
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.05, "out");
brush.fillTexture(0.18, 0.02);
brush.fill(col, op);
brush.beginShape(0.12);
for (let k = 0; k <= n; k++) brush.vertex(near[k][0], near[k][1]);
for (let k = n; k >= 0; k--) brush.vertex(far[k][0], far[k][1]);
brush.endShape(true);
}
// the crest, broken, so it never closes into a contour drawn round a shape
function duneInk(pts, t0, t1, col, w, name, gap) {
brush.noFill();
brush.noHatch();
const segs = 8;
for (let k = 0; k < segs; k++) {
if (random() < gap) continue;
const ta = lerp(t0, t1, k / segs);
const tb = lerp(t0, t1, (k + 0.95) / segs);
const a = duneAt(pts, ta);
const b = duneAt(pts, tb);
const m = duneAt(pts, (ta + tb) / 2);
brush.set(name, col, constrain(randomGaussian(w, w * 0.16), 0.18, 4));
brush.beginShape(0.14);
brush.vertex(a[0] + randomGaussian(0, 1), a[1] + randomGaussian(0, 1));
brush.vertex(m[0] + randomGaussian(0, 2.4), m[1] + randomGaussian(0, 2.4));
brush.vertex(b[0] + randomGaussian(0, 1), b[1] + randomGaussian(0, 1));
brush.endShape(false);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#f2e9d8");
// 1 — washes: cool air where the crests give out, warm glare across the near flank
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.7, "out");
brush.fillTexture(0.2, 0.05);
brush.fill("#3a3d5c", 13);
brush.circle(720, 200, 230, false);
brush.fill("#e8cd9a", 24);
brush.circle(560, 620, 250, true);
// 2 — dune four: the last legible crest before the paper takes over
duneMass(duneBody(K4), "#e8cd9a", 84, 2, 0, 0, "2H", "#c4913f", 0.4);
duneMass(duneFacet(K4, 6, V4, 330), "#3a3d5c", 108, 3, 0, 0, "2H", "#3a3d5c", 0.4);
duneGrain(K4, 0.8, 1.0, 26, 34, 0.4);
// 3 — dune three: the palest of the shadow facets, so three grey steps read up the picture
duneMass(duneBody(K3), "#e8cd9a", 130, 3, 0, 0, "2H", "#c4913f", 0.4);
duneBelow(P6, 8, "#3a3d5c", 128, 2, 30);
duneGrain(K3, 0.68, 0.98, 40, 80, 0.5);
duneMass(F3, "#3a3d5c", 164, 4, 0, 0, "2H", "#3a3d5c", 0.5);
// 4 — dune two
duneMass(duneBody(K2), "#e8cd9a", 210, 5, 0, 0, "2H", "#c4913f", 0.4);
duneBelow(P4, 18, "#3a3d5c", 178, 3, 92);
duneBelow(P8, 7, "#3a3d5c", 146, 2, 40);
duneGrain(K2, 0.5, 0.96, 70, 170, 0.65);
duneMass(F2, "#3a3d5c", 232, 5, 0, 0, "2H", "#3a3d5c", 0.7);
duneMass(duneFacet(K2, 4, [[340, 384], [308, 444], [276, 504], [244, 558]], 572), "#3a3d5c", 178, 3, 0, 0, "HB", "#161520", 0.6);
duneMass(duneFacet(K2, 3, [[242, 414], [216, 470], [188, 524], [158, 574]], 590), "#161520", 132, 3, 0, 0, "HB", "#161520", 0.6);
// 5 — dune one: the near mass. its lit facet is the widest passage of light in the picture, its slip
// face the one black, and the two meet at the summit on the left third.
duneMass(duneBody(K1), "#e8cd9a", 246, 8, 0, 0, "2H", "#c4913f", 0.4);
duneMass(OCH, "#c4913f", 76, 3, 0, 0, "2H", "#c4913f", 0.4);
duneBelow(P1, 24, "#3a3d5c", 194, 3, 122);
duneBelow(P2, 38, "#3a3d5c", 206, 3, 132);
duneBelow(P7, 20, "#3a3d5c", 170, 3, 86);
duneGrain(K1, 0.44, 0.9, 74, 240, 0.8);
duneGrain(P1, 0.08, 0.92, 44, 130, 0.7);
duneGrain(P2, 0.4, 0.98, 40, 90, 0.6);
duneGrain(P7, 0.1, 0.9, 30, 60, 0.5);
// 6 — three broad scours dragged down the near flank: the large gestures
brush.noFill();
brush.set("spray", "#c4913f", 5.2);
brush.line(360, 528, 486, 610);
brush.set("spray", "#c4913f", 3.4);
brush.line(318, 650, 424, 728);
brush.set("spray", "#c4913f", 2.4);
brush.line(566, 668, 700, 756);
// 7 — the near slip face: three stacked darks, the last scoured tight so it holds the picture down
duneMass(duneFacet(K1, 5, V1, 780), "#3a3d5c", 240, 6, 0, 0, "2H", "#3a3d5c", 0.85);
duneMass(duneFacet(K1, 5, [[268, 470], [240, 552], [216, 638], [192, 736], [168, 850], [150, 920]], 920), "#161520", 200, 6, 3.0, 52, "2B", "#161520", 1.0);
duneMass(duneFacet(K1, 5, [[268, 470], [248, 542], [230, 616], [210, 706], [190, 810], [176, 920]], 920), "#161520", 218, 4, 2.4, 24, "2B", "#161520", 1.1);
// reflected warmth pooling in the corner, so the black is pigment and not a hole in the paper
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.46, "out");
brush.fillTexture(0.3, 0.06);
brush.fill("#c4913f", 22);
brush.circle(30, 856, 180, false);
brush.fill("#3a3d5c", 28);
brush.circle(170, 890, 140, false);
// 8 — lips and inks: the crest catches light, then takes the pen
duneLip(K1, 0.22, 0.42, 7, "#f2e9d8", 142);
duneLip(K1, 0.42, 0.62, 6, "#e8cd9a", 158);
duneLip(K2, 0.3, 0.5, 5.5, "#f2e9d8", 128);
duneLip(K3, 0.46, 0.68, 3.6, "#e8cd9a", 104);
duneInk(K1, 0.04, 0.44, "#161520", 2.6, "rotring", 0.06);
duneInk(K1, 0.42, 0.68, "#161520", 1.6, "2B", 0.12);
duneInk(K1, 0.64, 0.98, "#3a3d5c", 0.9, "2B", 0.24);
duneInk(K2, 0.06, 0.48, "#161520", 1.8, "rotring", 0.1);
duneInk(K2, 0.46, 0.86, "#3a3d5c", 0.9, "2H", 0.22);
duneInk(K3, 0.06, 0.42, "#161520", 1.35, "2B", 0.12);
duneInk(K3, 0.4, 0.74, "#3a3d5c", 0.9, "2H", 0.2);
duneInk(K4, 0.1, 0.84, "#3a3d5c", 0.55, "2H", 0.3);
// 9 — last darks: tooth dragged down the slip face where the black is deepest
brush.noFill();
// the drag runs down the arete, not straight down the paper, so it stays inside the black
for (let k = 0; k < 96; k++) {
const t = constrain(randomGaussian(0.24, 0.12), 0.02, 0.38);
const p = duneAt(K1, t);
const d = duneDir(K1, t);
const off = abs(randomGaussian(0, 108)) + 10;
const len = constrain(randomGaussian(50, 20), 12, 100);
const bx = p[0] - 0.36 * off;
const by = p[1] + 0.93 * off;
brush.set(random() < 0.5 ? "2B" : "HB", "#161520", constrain(randomGaussian(0.46, 0.15), 0.16, 0.9));
brush.line(bx - d[0] * len * 0.45, by - d[1] * len * 0.45, bx + d[0] * len * 0.55, by + d[1] * len * 0.55);
}
brush.set("2B", "#161520", 1.7);
brush.line(92, 646, 224, 748);
brush.set("2B", "#161520", 1.2);
brush.line(-10, 742, 118, 848);
// 10 — a few flecks out on the bare paper at the dissolve, so the emptiness still reads as sand
brush.noFill();
brush.set("spray", "#c4913f", 0.6);
brush.line(748, 196, 786, 192);
brush.line(604, 224, 636, 220);
brush.set("2H", "#3a3d5c", 0.4);
brush.line(828, 206, 866, 202);
brush.line(686, 210, 714, 206);
// 11 — the ember, rationed: the sun striking the near summit on the left third
brush.noFill();
const e0 = duneAt(K1, 0.44);
const e1 = duneAt(K1, 0.52);
brush.set("pen", "#b5502a", 1.3);
brush.beginShape(0.1);
brush.vertex(e0[0], e0[1] - 3);
brush.vertex((e0[0] + e1[0]) / 2, (e0[1] + e1[1]) / 2 - 4);
brush.vertex(e1[0], e1[1] - 3);
brush.endShape(false);
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.fill("#b5502a", 226);
brush.circle(e0[0] + 6, e0[1] - 5, 5.4, true);
brush.fill("#b5502a", 138);
brush.circle(e1[0] + 14, e1[1] - 4, 3.2, true);
const e2 = duneAt(K2, 0.5);
brush.fill("#b5502a", 120);
brush.circle(e2[0] + 8, e2[1] - 3, 2.8, true);
}