Where The Crust Breaks
2026 · landscape
Distance is a temperature here, not a detail. The cracks stop being cracks long before the ridge does — so the picture puts everything it owns at your feet, where the plates are wide enough to stand on and dark enough to hold the paper down.
a cracked salt flat coming at the viewer under an almost empty sky — the polygon crust unreadable at the pale ridge, merging forward into wide black-seamed plates with water still standing in them
painted by DeepSeek V4p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the horizon sits at y=566 — the upper two thirds is warm empty air, the ridge a change of
// temperature rather than a shape. Below it the crust comes at the viewer: an unreadable network at the horizon,
// cracks merging forward until the bottom edge is five or six wide plates with black seams. One long crack climbs
// from the bottom-left corner toward the light on the right third; the darkest, wettest plates gather at the left
// foot, and the lower right keeps a pale sheen and most of its paper.
// layering: air washes -> ridge -> the light and its ember -> the flat's body -> plate fills far to near -> the
// horizon glaze that eats the far detail -> the wet plates, glazed -> grain -> seams -> bloom -> the black cracks.
const PALETTE = [
"#f0e8db", // ground — bare salt, the paper itself
"#e9cda3", // haze — the warm air standing on the flat
"#b09a80", // crust — damp plate, the mid-tone of the near ground
"#8f8d94", // distance — the cool grey the ridge is made of
"#38332d", // ink — the black seams, struck last
"#b4552c", // ember — one warm break at the horizon, rationed
];
const HORIZON = 566;
const NEAR = 842; // the nearest crack row, just off the bottom edge
const VX = 470; // the column the flat runs out to, on the right third
const ROWS = [568, 571, 576, 583, 593, 607, 626, 653, 692, 748, 842];
let CRUST = null;
const saltDepth = (y) => constrain((y - HORIZON) / (NEAR - HORIZON), 0, 1);
const plateWidth = (d) => 9 + 152 * Math.pow(constrain(d, 0, 1), 1.15);
const saltSide = (x) => constrain(map(x, 430, 830, 1, 0.34), 0.34, 1);
// cracks laid at the horizon and carried forward row by row, merging wherever two have come closer than a
// plate is wide — so the survivors run long and the rest die as T-junctions.
function saltLattice() {
const rows = [], first = [];
let x = -160;
for (let i = 0; i < 220; i++) {
first.push({ x: x, up: -1, live: true });
x += Math.max(5, randomGaussian(11.5, 3));
if (x > 980) break;
}
rows.push(first);
for (let k = 1; k < ROWS.length; k++) {
const d = saltDepth(ROWS[k]), prev = rows[k - 1], target = plateWidth(d);
const grow = (1 + 0.52 * d) / (1 + 0.52 * saltDepth(ROWS[k - 1]));
const out = [];
let last = -1e6;
for (let i = 0; i < prev.length; i++) {
const nx = VX + (prev[i].x - VX) * grow + randomGaussian(0, 1.1 + 3.6 * d);
if (nx - last < target * (0.62 + abs(randomGaussian(0, 0.34)))) continue;
out.push({ x: nx, up: i, live: true });
prev[i].live = false; // this crack carries on; the ones skipped end here
last = nx;
}
rows.push(out);
}
return rows;
}
function saltBlot(col, alpha, x, y, r) {
brush.fill(col, alpha); brush.circle(x, y, r, true);
}
function saltBand(col, alpha, x, y, w, h) {
brush.fill(col, alpha); brush.rect(x, y, w, h);
}
// A plate with water still in it. One wash cannot get dark here, so the tone is glazed: the same plate laid
// three to six times, re-jittered each pass. Then its seam — undrawn, a dark patch is a smudge, not a plate.
function dampPlate(pts, layers, col, alpha, edge, hatchDist) {
brush.noStroke(); brush.noHatch();
brush.fillBleed(0.1, "out"); brush.fillTexture(0.16, 0.42);
let poly = pts;
for (let k = 0; k < layers; k++) {
poly = [];
for (let i = 0; i < pts.length; i++) poly.push([pts[i][0] + randomGaussian(0, 3.6), pts[i][1] + randomGaussian(0, 2.6)]);
brush.fill(col, alpha * (0.82 + 0.3 * random()));
brush.polygon(poly);
}
if (hatchDist) {
brush.hatchStyle("cpencil", "#38332d", Math.max(0.6, randomGaussian(1.0, 0.2)));
brush.hatch(hatchDist, randomGaussian(0, 60), { rand: 0.3 });
brush.fill(col, alpha * 0.4); brush.polygon(poly); brush.noHatch();
}
if (edge) {
brush.noFill();
brush.set("2B", "#38332d", Math.max(0.6, randomGaussian(edge, 0.25)));
brush.beginShape(0);
for (let i = 0; i < poly.length; i++) brush.vertex(poly[i][0], poly[i][1]);
brush.endShape(true);
}
}
function saltSlab(col, alpha, pts) {
brush.noStroke(); brush.fill(col, alpha);
const poly = [];
for (let i = 0; i < pts.length; i++) poly.push([pts[i][0] + randomGaussian(0, 3), pts[i][1] + randomGaussian(0, 2)]);
brush.polygon(poly);
}
// a bank of distance: no detail, only a temperature and a ragged top. Straight-sided on purpose — a closed
// spline this wide swings its own belly out into the picture.
function saltBank(col, alpha, base, pts) {
brush.noStroke(); brush.fill(col, alpha);
const poly = [];
for (let i = 0; i < pts.length; i++) poly.push([pts[i][0], pts[i][1] + randomGaussian(0, 1.1)]);
poly.push([pts[pts.length - 1][0], base]);
poly.push([pts[0][0], base]);
brush.polygon(poly);
}
function saltSeam(name, col, w, curve, x0, y0, x1, y1, bend) {
brush.noFill(); brush.set(name, col, Math.max(0.3, w));
brush.beginShape(curve); brush.vertex(x0, y0);
brush.vertex((x0 + x1) / 2 + randomGaussian(0, bend), (y0 + y1) / 2 + randomGaussian(0, bend * 0.7));
brush.vertex(x1, y1); brush.endShape(false);
}
function saltCrack(name, w, curve, pts) {
brush.noFill(); brush.set(name, "#38332d", w);
brush.beginShape(curve);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(false);
}
function saltNick(name, w, x1, y1, x2, y2) {
brush.noFill(); brush.set(name, "#38332d", w); brush.line(x1, y1, x2, y2);
}
function seamInk(d) {
if (d < 0.09) return { b: "2H", c: random() < 0.6 ? "#b09a80" : "#8f8d94" };
if (d < 0.22) return { b: "2H", c: random() < 0.5 ? "#8f8d94" : "#b09a80" };
if (d < 0.42) return { b: "HB", c: random() < 0.55 ? "#8f8d94" : "#38332d" };
if (d < 0.66) return { b: random() < 0.5 ? "HB" : "2B", c: random() < 0.7 ? "#38332d" : "#8f8d94" };
return { b: random() < 0.45 ? "charcoal" : "2B", c: "#38332d" };
}
function plateFill(fl, fr, nl, nr, yf, yn, d, x) {
const wet = saltSide(x), r = random();
let col = "#b09a80", alpha = 60 + 120 * d;
// the bleached plates belong to the bright right side, the dark wet ones to the left foot
if (r < 0.08 + 0.18 * wet * d) { col = "#38332d"; alpha = 26 + 110 * d; }
else if (r < 0.26 + 0.16 * wet) { col = "#8f8d94"; alpha = 56 + 130 * d; }
else if (r > 0.88) { col = "#f0e8db"; alpha = 100 + 100 * (1 - wet); }
else if (r > 0.8) { col = "#e9cda3"; alpha = 60 + 110 * d; }
const j = () => randomGaussian(0, 1.4 + 2.6 * d);
brush.noStroke();
if (d > 0.3 && random() < 0.3) {
brush.hatchStyle("cpencil", random() < 0.3 ? "#38332d" : "#8f8d94", 0.7);
brush.hatch(9 + random(0, 7), randomGaussian(14, 26), { rand: 0.35 });
} else brush.noHatch();
brush.fillBleed(0.1 + 0.14 * d, "out"); brush.fillTexture(0.18, 0.45);
brush.fill(col, alpha * (0.7 + 0.45 * wet));
brush.polygon([[fl + j(), yf + j() * 0.3], [fr + j(), yf + j() * 0.3],
[nr + j(), yn + j() * 0.5], [nl + j(), yn + j() * 0.5]]);
brush.noHatch();
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES); brush.scaleBrushes(1.5); noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#f0e8db");
CRUST = saltLattice();
// ---- the air: three large warm gestures, thickening only where it meets the ground ----
brush.noStroke(); brush.fillTexture(0.3, 0.3); brush.fillBleed(0.6, "out");
saltBlot("#e9cda3", 26, 596, 250, 320);
saltBlot("#8f8d94", 14, 168, 176, 268);
brush.fillBleed(0.44, "out");
saltBand("#e9cda3", 22, -60, 344, 920, 130);
saltBand("#e9cda3", 38, -60, 442, 920, 84);
saltBand("#e9cda3", 62, -60, 500, 920, 44);
saltBand("#e9cda3", 84, 236, 512, 560, 52);
// two low breaths of warm air, soft-edged — the only marks in the top two thirds
brush.fillBleed(0.7, "out");
saltBlot("#e9cda3", 40, 660, 512, 168);
saltBlot("#e9cda3", 28, 348, 486, 132);
// ---- the ridge: a thin cool streak, no shape in it, and it gives up where the light stands ----
brush.noStroke(); brush.fillBleed(0.2, "out"); brush.fillTexture(0.4, 0.5);
saltBank("#8f8d94", 145, 570, [[-40, 558], [46, 552], [148, 548], [242, 556]]);
saltBank("#8f8d94", 96, 570, [[266, 557], [340, 551], [420, 555], [458, 560]]);
saltBank("#8f8d94", 112, 570, [[648, 557], [704, 551], [788, 548], [842, 554]]);
saltBank("#8f8d94", 88, 566, [[-40, 550], [88, 539], [212, 548], [300, 545]]);
saltBank("#38332d", 58, 566, [[100, 556], [164, 548], [230, 555]]);
saltBank("#38332d", 34, 568, [[710, 559], [774, 552], [842, 556]]);
brush.noFill();
brush.set("2H", "#8f8d94", 1.1); brush.line(-30, 567, 268, 566);
brush.set("2H", "#8f8d94", 0.8); brush.line(644, 568, 842, 566);
brush.set("spray", "#8f8d94", 0.5); brush.line(-20, 563, 226, 561);
// ---- the light on the horizon, with one ember break on the right third ----
brush.noStroke(); brush.fillBleed(0.5, "out");
saltBlot("#e9cda3", 96, 528, 544, 62);
saltBlot("#e9cda3", 122, 556, 558, 46);
saltBlot("#e9cda3", 92, 606, 564, 30);
brush.fillBleed(0.24, "out");
saltBlot("#b4552c", 108, 534, 566, 9);
saltBlot("#b4552c", 140, 552, 567, 7);
saltBlot("#b4552c", 86, 578, 568, 6);
brush.noFill(); brush.set("2B", "#b4552c", 0.9); brush.line(520, 568, 566, 567);
brush.noStroke(); brush.fillBleed(0.26, "out");
saltBand("#f0e8db", 200, -40, 570, 920, 7);
// ---- the flat's body: three big washes, each one heavier and cooler than the last ----
brush.fillBleed(0.4, "out"); brush.fillTexture(0.4, 0.4);
saltBand("#e9cda3", 74, -60, 578, 920, 54);
saltBand("#b09a80", 84, -60, 616, 920, 92);
saltBand("#b09a80", 132, -60, 678, 920, 176);
// and a middle wash to carry the eye from the dissolved distance down into the weight
brush.fillBleed(0.4, "out"); brush.fillTexture(0.2, 0.45);
saltBand("#b09a80", 96, -60, 636, 920, 70);
// the near shelf, cut as slabs: it comes forward in three unequal steps, never on one ruled line
brush.fillBleed(0.12, "out"); brush.fillTexture(0.14, 0.4);
saltSlab("#b09a80", 190, [[-60, 708], [150, 686], [332, 702], [332, 842], [-60, 842]]);
saltSlab("#b09a80", 178, [[302, 696], [540, 678], [702, 692], [702, 842], [302, 842]]);
saltSlab("#b09a80", 128, [[664, 690], [912, 704], [912, 842], [664, 842]]);
saltSlab("#e9cda3", 150, [[452, 720], [654, 706], [688, 774], [478, 790]]);
brush.fillBleed(0.4, "out"); brush.fillTexture(0.2, 0.45);
saltBand("#f0e8db", 190, 508, 618, 404, 100); // the wet sheen keeping the right open
saltSlab("#f0e8db", 185, [[590, 748], [900, 726], [912, 842], [618, 842]]);
// ---- the plates, far to near ----
for (let k = 1; k < ROWS.length; k++) {
const yf = ROWS[k - 1], yn = ROWS[k], d = saltDepth(yn);
const near = CRUST[k], far = CRUST[k - 1];
const keep = constrain(0.06 + 1.2 * Math.pow(d, 0.9), 0, 1);
for (let j = 0; j < near.length - 1; j++) {
const nl = near[j].x, nr = near[j + 1].x, xm = (nl + nr) / 2;
if (xm < -80 || xm > 880) continue;
if (random() > keep * (0.45 + 0.55 * saltSide(xm))) continue;
plateFill(far[near[j].up].x, far[near[j + 1].up].x, nl, nr, yf, yn, d, xm);
}
}
// the horizon glaze: whatever detail the far crust still has, the light takes it back
brush.noStroke(); brush.noHatch();
brush.fillBleed(0.5, "out"); brush.fillTexture(0.35, 0.35);
saltBand("#f0e8db", 150, -60, 569, 920, 20);
saltBand("#e9cda3", 66, -60, 574, 920, 30);
saltBand("#f0e8db", 86, 300, 578, 420, 26);
// the ridge restated over the glaze — everything else is allowed to eat it, this much is not
brush.fillBleed(0.16, "out");
saltBank("#8f8d94", 120, 569, [[-40, 557], [58, 551], [162, 548], [248, 556]]);
saltBank("#8f8d94", 96, 569, [[664, 557], [726, 551], [800, 549], [842, 554]]);
brush.noFill();
brush.set("2H", "#8f8d94", 1.2);
brush.line(-30, 567, 252, 566);
brush.set("2H", "#8f8d94", 0.9);
brush.line(660, 567, 842, 565);
// ---- the water still lying in the crust at the viewer's feet: laid over the plates, not under them ----
brush.noStroke(); brush.noHatch();
brush.fillBleed(0.1, "out"); brush.fillTexture(0.14, 0.4);
dampPlate([[-60, 656], [140, 646], [176, 692], [-60, 702]], 2, "#8f8d94", 110, 0.7, 0);
dampPlate([[188, 648], [368, 640], [396, 684], [212, 692]], 2, "#b09a80", 130, 0.8, 26);
dampPlate([[52, 692], [246, 678], [280, 726], [78, 740]], 3, "#38332d", 66, 1.0, 0);
dampPlate([[-60, 772], [126, 748], [174, 842], [-60, 842]], 4, "#38332d", 96, 1.6, 0);
dampPlate([[144, 742], [350, 720], [404, 842], [192, 842]], 6, "#38332d", 120, 2.4, 9); // the knot
dampPlate([[152, 756], [332, 736], [384, 842], [200, 842]], 3, "#38332d", 84, 0, 0);
dampPlate([[196, 806], [380, 790], [420, 842], [214, 842]], 5, "#38332d", 104, 2.0, 0);
dampPlate([[356, 732], [518, 722], [554, 806], [384, 820]], 3, "#8f8d94", 190, 1.7, 14);
dampPlate([[428, 798], [576, 788], [608, 842], [450, 842]], 2, "#38332d", 70, 1.3, 0);
dampPlate([[-60, 700], [96, 690], [126, 748], [-60, 762]], 2, "#38332d", 62, 1.1, 0);
dampPlate([[604, 800], [744, 792], [768, 842], [624, 842]], 2, "#8f8d94", 130, 0.9, 0);
dampPlate([[74, 738], [258, 722], [292, 776], [102, 794]], 4, "#38332d", 88, 1.4, 0);
dampPlate([[268, 716], [430, 704], [458, 752], [292, 766]], 3, "#8f8d94", 175, 1.2, 18);
dampPlate([[-60, 726], [64, 716], [88, 768], [-60, 780]], 3, "#38332d", 74, 1.1, 0);
// grit gathering in the wet plates: short, and pointing every way
brush.noFill();
for (let i = 0; i < 130; i++) {
const y = constrain(randomGaussian(788, 44), 706, NEAR);
const x = constrain(randomGaussian(230, 155), -60, 600);
const a = random(0, 6.283), len = 3 + abs(randomGaussian(6, 4)); // radians: Math.cos ignores angleMode
brush.set("spray", random() < 0.6 ? "#38332d" : "#8f8d94", Math.max(0.4, randomGaussian(0.85, 0.24)));
brush.line(x, y, x + Math.cos(a) * len, y + Math.sin(a) * len);
}
// ---- grain: grit in the near crust, gone before the middle distance ----
brush.noFill();
for (let i = 0; i < 260; i++) {
const y = HORIZON + 12 + pow(random(), 0.42) * (NEAR - HORIZON - 12);
const d = saltDepth(y), x = constrain(randomGaussian(290, 250), -60, 866);
if (x > 520 && random() < 0.5) continue; // the right foot keeps its paper
const r = random();
const col = r < 0.4 ? "#b09a80" : r < 0.72 ? "#8f8d94" : r < 0.82 ? "#e9cda3" : "#38332d";
const len = (7 + 42 * d) * (0.5 + abs(randomGaussian(0, 0.6)));
brush.set("spray", col, Math.max(0.4, randomGaussian(0.6 + 1.5 * d, 0.3)));
brush.line(x, y, x + len, y + randomGaussian(0, 3 * d));
}
// ---- the seams, far to near: thickening, darkening, and finally black ----
for (let k = 1; k < ROWS.length; k++) {
const yf = ROWS[k - 1], yn = ROWS[k], d = saltDepth(yn);
const near = CRUST[k], far = CRUST[k - 1];
const bend = 0.6 + 5.4 * d;
const keep = constrain(0.1 + 1.25 * Math.pow(d, 0.75), 0, 1);
// cracks running away from the viewer
for (let j = 0; j < near.length; j++) {
const nx = near[j].x;
if (nx < -90 || nx > 890) continue;
if (random() > keep * saltSide(nx)) continue;
const ink = seamInk(d), w = randomGaussian(0.4 + 3.6 * Math.pow(d, 1.2), 0.3);
saltSeam(ink.b, ink.c, w, 0.5, far[near[j].up].x, yf, nx, yn, bend);
}
// the near edge of each plate
for (let j = 0; j < near.length - 1; j++) {
const nl = near[j].x, nr = near[j + 1].x, xm = (nl + nr) / 2;
if (xm < -80 || xm > 880) continue;
if (random() > keep * 0.86 * saltSide(xm)) continue;
const ink = seamInk(d), w = randomGaussian(0.4 + 3.2 * Math.pow(d, 1.2), 0.3);
const wob = randomGaussian(0, 1 + 3 * d);
saltSeam(ink.b, ink.c, w, 0.45, nl, yn + wob, nr, yn - wob, bend);
}
// cracks that merged and died: a stub forward into the plate, then nothing
if (d > 0.16) {
for (let i = 0; i < far.length; i++) {
if (!far[i].live) continue;
if (random() > 0.42) continue;
const ink = seamInk(d * 0.7), f = 0.2 + abs(randomGaussian(0, 0.34));
saltNick(ink.b, Math.max(0.35, randomGaussian(0.5 + 1.6 * d, 0.25)),
far[i].x, yf, far[i].x + randomGaussian(0, 5 * d), yf + (yn - yf) * Math.min(f, 0.85));
}
}
}
// ---- salt bloom: pale crust standing up along the near seams ----
brush.noStroke(); brush.fillBleed(0.3, "out"); brush.fillTexture(0.72, 0.6);
saltSlab("#f0e8db", 200, [[206, 748], [268, 742], [276, 772], [212, 778]]);
saltSlab("#f0e8db", 180, [[364, 786], [430, 780], [438, 810], [372, 816]]);
saltBlot("#f0e8db", 165, 170, 706, 13);
saltBlot("#f0e8db", 140, 556, 728, 11);
saltBlot("#f0e8db", 150, 88, 664, 10);
// ---- the blacks, last: the spine climbs from the bottom-left corner to the light, thinning as it goes ----
saltCrack("charcoal", 2.6, 0.25, [[-20, 842], [48, 830], [164, 800], [246, 764]]);
saltCrack("2B", 2.0, 0.2, [[246, 764], [332, 730], [398, 700]]);
saltCrack("2B", 1.2, 0.2, [[398, 700], [446, 668], [478, 644]]);
saltCrack("2B", 0.7, 0.2, [[478, 644], [506, 618], [522, 602]]);
saltCrack("2B", 2.2, 0.2, [[128, 818], [170, 766], [156, 722], [184, 684]]);
saltCrack("2B", 1.6, 0.2, [[164, 800], [214, 838], [230, 842]]);
saltCrack("2B", 1.5, 0.2, [[332, 730], [356, 776], [344, 818]]);
saltCrack("2B", 1.4, 0.25, [[478, 842], [502, 776], [482, 726], [506, 690]]);
saltCrack("2B", 1.0, 0.2, [[652, 842], [678, 784], [664, 742]]);
saltNick("charcoal", 1.8, 246, 764, 328, 786);
saltNick("2B", 1.5, 170, 766, 96, 752);
saltNick("2B", 1.1, 502, 776, 574, 796);
saltNick("2B", 0.9, 76, 730, 128, 720);
saltNick("2B", 0.8, 752, 830, 782, 776);
saltNick("2B", 1.0, 398, 700, 442, 712);
saltCrack("2B", 1.6, 0.15, [[-30, 776], [126, 760], [268, 748], [366, 730]]);
saltCrack("2B", 1.3, 0.15, [[-20, 812], [148, 796], [304, 790], [412, 776]]);
// pigment pooling where the cracks meet, a chip of crust lifted, and one ember touch out on the flat
brush.noStroke(); brush.noHatch();
brush.fillBleed(0.3, "out"); brush.fillTexture(0.2, 0.5);
saltBlot("#38332d", 170, 248, 762, 11);
saltBlot("#38332d", 150, 170, 768, 8);
saltBlot("#38332d", 130, 336, 786, 7);
saltBlot("#38332d", 120, 108, 806, 6);
saltBlot("#38332d", 110, 500, 778, 5);
saltBlot("#f0e8db", 220, 292, 744, 7);
saltBlot("#b4552c", 64, 588, 632, 5);
}