Chopped Rock, Loose Below
2026 · landscape
Rock is not an outline filled in; it is an accumulation of blows. The slope below is the same rock, only sorted by how far it fell.
a cliff face built from short axe-cut strokes in the upper left, its scree fan running down the steep diagonal to boulders in the lower right, dust where the stone meets the wall
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a cliff wedges into the upper-left, its foot spilling a scree fan down a steep diagonal to the lower-right
// layering: grey rock body and slope bed first, then axe-cut chops plane by plane, then gravel and boulders, the blackest cuts last
// the paper stays open: the whole upper-right void and the lower-left mist, better than a third of the sheet
const PALETTE = [
"#e7dfcb", // ground — aged silk
"#a39b89", // mass — lit stone
"#63665e", // shadow — the grey body of the rock
"#2a2b27", // ink — the chopped darks
"#8f5b33", // accent — iron stain on stone, rationed
];
const PAPER = PALETTE[0];
const STONE = PALETTE[1];
const GREY = PALETTE[2];
const INK = PALETTE[3];
const IRON = PALETTE[4];
// one ink tone: t = 0 is the blackest chop, t = 1 the palest grey. A rare mark rusts.
function tone(t) {
const base = lerpColor(color(INK), color(GREY), constrain(t, 0, 1));
if (random() < 0.04) return lerpColor(base, color(IRON), 0.55);
return lerpColor(base, color(PAPER), constrain(randomGaussian(0.05, 0.05), 0, 0.2));
}
// the axe-cut: broad where the brush lands, tapering off as it lifts
function chop(x, y, ang, len, t, w) {
brush.stroke(tone(t));
brush.strokeWeight(w);
brush.beginShape(0.08);
brush.vertex(x, y, 1.35);
brush.vertex(x + Math.cos(ang) * len * 0.48 + randomGaussian(0, 1.4),
y + Math.sin(ang) * len * 0.48 + randomGaussian(0, 1.4), 0.72);
brush.vertex(x + Math.cos(ang) * len, y + Math.sin(ang) * len, 0.1);
brush.endShape(false);
}
// a point inside a quad [tl, tr, br, bl], v running down the plane
function at(q, u, v) {
const ax = lerp(q[0][0], q[1][0], u);
const ay = lerp(q[0][1], q[1][1], u);
const bx = lerp(q[3][0], q[2][0], u);
const by = lerp(q[3][1], q[2][1], u);
return [lerp(ax, bx, v), lerp(ay, by, v)];
}
// one rock plane: short broad chops crowding toward its lower edge, darkening as they fall
function facet(q, n, ang, spread, tTop, tFoot, len) {
for (let i = 0; i < n; i++) {
const u = constrain(randomGaussian(0.5, 0.32), 0.02, 0.98);
const v = constrain(randomGaussian(0.58, 0.3), 0.02, 0.99);
const p = at(q, u, v);
const l = Math.max(11, randomGaussian(len * (0.62 + 0.7 * v), len * 0.28));
chop(p[0], p[1], ang + randomGaussian(0, spread), l,
lerp(tTop, tFoot, v), Math.max(1.1, randomGaussian(2.5, 0.6)));
}
}
// a slab of rock body, its right edge cut hard against the empty air
function slab(pts, col, alpha, curve) {
brush.noStroke();
brush.fillBleed(0.26, "out");
brush.fillTexture(0.5, 0.42);
brush.fill(col, alpha);
brush.beginShape(curve);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(true);
brush.noFill();
}
// an angular stone, five or six hard facets, never twice the same
function stone(x, y, r, faces) {
const lit = random() < 0.32;
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fillTexture(0.6, 0.5);
brush.fill(lit ? STONE : GREY, lit ? Math.max(150, randomGaussian(196, 22))
: Math.max(120, randomGaussian(168, 26)));
brush.beginShape(0);
for (let i = 0; i < faces; i++) {
const a = (i / faces) * Math.PI * 2 + randomGaussian(0, 0.3);
const rr = r * constrain(randomGaussian(1, 0.24), 0.5, 1.5);
brush.vertex(x + Math.cos(a) * rr, y + Math.sin(a) * rr);
}
brush.endShape(true);
brush.noFill();
}
// the shaded underside of a stone and the dark it throws downslope
function underside(x, y, r, t, chops) {
for (let i = 0; i < chops; i++) {
const a = randomGaussian(0.9, 0.5);
const px = x + Math.cos(a - 0.8) * r * random(0.2, 0.95);
const py = y + Math.sin(a - 0.8) * r * random(0.1, 0.9);
chop(px, py, a, Math.max(8, randomGaussian(r * 1.1, r * 0.25)), t,
Math.max(0.9, randomGaussian(2, 0.5)));
}
}
// where the fan lies: spine downslope, half-width swelling toward the base
const FAN = [[320, 418], [398, 478], [478, 542], [560, 612], [648, 688], [750, 782]];
function fanPoint(s, lateral) {
const f = constrain(s, 0, 1) * (FAN.length - 1);
const i = Math.min(FAN.length - 2, Math.floor(f));
const k = f - i;
const x = lerp(FAN[i][0], FAN[i + 1][0], k);
const y = lerp(FAN[i][1], FAN[i + 1][1], k);
const dx = FAN[i + 1][0] - FAN[i][0];
const dy = FAN[i + 1][1] - FAN[i][1];
const m = Math.hypot(dx, dy) || 1;
const half = 46 + 190 * pow(constrain(s, 0, 1), 0.72);
return [x - (dy / m) * lateral * half, y + (dx / m) * lateral * half];
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// ---- the rock's body: three grey slabs, one lit shoulder, one black cleft
slab([[-40, -40], [188, -40], [236, 48], [250, 152], [216, 162], [-40, 152]], GREY, 128, 0.08);
slab([[-40, 152], [216, 162], [252, 150], [294, 268], [242, 322], [-40, 316]], GREY, 118, 0.08);
slab([[-40, 316], [242, 322], [296, 266], [348, 440], [302, 406], [198, 456], [-40, 436]], GREY, 108, 0.1);
slab([[-40, 20], [96, -40], [140, 96], [66, 236], [-40, 208]], STONE, 96, 0.2);
slab([[140, 100], [212, 112], [246, 304], [156, 318]], INK, 72, 0.1);
slab([[246, 298], [292, 260], [346, 436], [298, 400]], INK, 64, 0.06);
// ---- the slope's bed, laid under everything that fell on it
brush.noStroke();
brush.fillBleed(0.46, "out");
brush.fillTexture(0.4, 0.42);
brush.fill(GREY, 62);
brush.beginShape(0.24);
for (let i = 0; i <= 5; i++) brush.vertex(...fanPoint(i / 5, -0.92));
for (let i = 5; i >= 0; i--) brush.vertex(...fanPoint(i / 5, 0.98));
brush.endShape(true);
brush.fill(GREY, 52);
brush.beginShape(0.24);
for (let i = 0; i <= 5; i++) brush.vertex(...fanPoint(i / 5, -0.36));
for (let i = 5; i >= 0; i--) brush.vertex(...fanPoint(i / 5, 0.54));
brush.endShape(true);
brush.fill(GREY, 12);
brush.circle(648, 178, 148, true);
brush.noFill();
// ---- the cliff, nine planes. The left shoulder takes the light; the mid column is a cleft.
brush.pick("2B");
const A_top = [[-40, -40], [118, -40], [128, 156], [-40, 148]];
const B_top = [[118, -40], [190, -40], [214, 158], [128, 156]];
const C_top = [[190, -40], [238, 50], [250, 150], [214, 158]];
const A_mid = [[-40, 148], [128, 156], [140, 328], [-40, 314]];
const B_mid = [[128, 156], [214, 158], [242, 320], [140, 328]];
const C_mid = [[214, 158], [250, 150], [292, 266], [242, 320]];
const A_low = [[-40, 314], [140, 328], [176, 452], [-40, 434]];
const B_low = [[140, 328], [242, 320], [300, 404], [198, 456]];
const C_low = [[242, 320], [292, 266], [346, 438], [300, 404]];
facet(A_top, 22, 1.9, 0.18, 0.86, 0.5, 30);
facet(B_top, 24, 1.76, 0.15, 0.6, 0.26, 27);
facet(C_top, 14, 1.5, 0.2, 0.5, 0.2, 22);
facet(A_mid, 22, 1.97, 0.2, 0.7, 0.38, 29);
facet(B_mid, 38, 1.68, 0.13, 0.24, 0.03, 25);
facet(C_mid, 20, 1.28, 0.24, 0.4, 0.14, 23);
facet(A_low, 22, 2.04, 0.22, 0.62, 0.34, 27);
facet(B_low, 30, 1.6, 0.16, 0.2, 0.02, 24);
facet(C_low, 28, 1.4, 0.18, 0.18, 0.01, 26);
// the cleft's grain, hatched rather than chopped, then closed off again
brush.hatchStyle("2B", INK, 1.1);
brush.hatch(12, 104, { rand: 0.26, continuous: false, gradient: 0.45 });
brush.noStroke();
brush.noFill();
brush.polygon([[152, 190], [206, 184], [232, 300], [166, 310]]);
brush.noHatch();
// the silhouette against the void: broken, never one contour
brush.pick("charcoal");
chop(198, -20, 1.12, 66, 0.04, 3);
chop(236, 44, 1.7, 56, 0.08, 2.6);
chop(230, 92, 1.34, 40, 0.18, 1.8);
chop(249, 152, 1.26, 58, 0.06, 2.8);
chop(274, 210, 1.02, 44, 0.12, 2.2);
chop(290, 266, 1.44, 68, 0.03, 3.2);
chop(306, 336, 1.16, 52, 0.08, 2.6);
chop(332, 382, 1.36, 58, 0.02, 3.4);
// a few long dry strokes down the wall — the fall of the rock, felt not drawn
brush.pick("cpencil");
chop(58, -18, 1.66, 196, 0.4, 1.6);
chop(148, 22, 1.74, 224, 0.3, 1.8);
chop(110, 208, 1.62, 168, 0.36, 1.5);
chop(196, 176, 1.7, 188, 0.16, 1.7);
chop(26, 294, 1.56, 156, 0.46, 1.3);
chop(84, 396, 1.71, 118, 0.42, 1.3);
// ---- mist gathering at the cliff foot, laid back over the rock
brush.noStroke();
brush.fillBleed(0.68, "out");
brush.fillTexture(0.26, 0.32);
brush.fill(PAPER, 128);
brush.circle(96, 458, 118, true);
brush.fill(PAPER, 106);
brush.circle(226, 478, 92, true);
brush.noFill();
// ---- dust: the finest waste, held high where the fan meets the wall
brush.pick("2H");
for (let i = 0; i < 250; i++) {
const s = constrain(randomGaussian(0.15, 0.14), 0, 0.5);
const lat = constrain(randomGaussian(0, 0.46), -1.1, 1.1);
const p = fanPoint(s, lat);
chop(p[0], p[1], randomGaussian(0.76, 0.32),
Math.max(3, randomGaussian(5 + 15 * s, 2.4)),
constrain(randomGaussian(0.44 - 0.3 * s, 0.16), 0, 1),
Math.max(0.4, randomGaussian(0.9, 0.25)));
}
// rills where the dust has run, following the hand of the slope
brush.stroke(tone(0.66));
brush.strokeWeight(0.9);
brush.field("hand");
brush.flowLine(344, 440, 104, 44);
brush.flowLine(376, 466, 126, 38);
brush.flowLine(406, 444, 88, 52);
brush.flowLine(436, 502, 138, 40);
brush.flowLine(470, 474, 96, 46);
brush.noField();
// ---- gravel: the middle grade, chips with a dark lip downslope
brush.pick("2B");
for (let i = 0; i < 150; i++) {
const s = constrain(randomGaussian(0.5, 0.2), 0.12, 0.98);
const lat = constrain(randomGaussian(0, 0.5), -1.12, 1.12);
const p = fanPoint(s, lat);
const l = Math.max(9, randomGaussian(11 + 20 * s, 4));
chop(p[0], p[1], randomGaussian(1, 0.42), l,
constrain(randomGaussian(0.34 - 0.26 * s, 0.18), 0, 1),
Math.max(0.9, randomGaussian(2, 0.5)));
if (random() < 0.3) {
chop(p[0] + randomGaussian(0, 3.5), p[1] + randomGaussian(5, 2.6),
randomGaussian(0.26, 0.4), l * 0.62,
constrain(randomGaussian(0.12, 0.1), 0, 0.5), 2.2);
}
}
// ---- cobbles: stone enough to own a lit face, thickening toward the base
for (let i = 0; i < 34; i++) {
const s = constrain(randomGaussian(0.72, 0.17), 0.32, 1.04);
const lat = constrain(randomGaussian(0, 0.52), -1.06, 1.06);
const p = fanPoint(s, lat);
const r = Math.max(6, randomGaussian(7 + 16 * s, 3));
stone(p[0], p[1], r, random() < 0.4 ? 6 : 5);
underside(p[0], p[1], r, constrain(randomGaussian(0.16, 0.12), 0, 0.55), r > 13 ? 3 : 2);
}
// ---- the boulders, hand-set, heaviest where the slope runs out
brush.pick("charcoal");
stone(546, 558, 46, 6);
underside(546, 558, 46, 0.02, 7);
chop(506, 592, 0.3, 62, 0.0, 3.4);
chop(560, 600, 0.14, 48, 0.04, 2.8);
stone(644, 634, 37, 5);
underside(644, 634, 37, 0.04, 6);
chop(616, 664, 0.2, 52, 0.02, 3);
stone(470, 612, 28, 6);
underside(470, 612, 28, 0.08, 4);
chop(452, 636, 0.4, 36, 0.03, 2.4);
stone(730, 708, 42, 5);
underside(730, 708, 42, 0.06, 6);
chop(696, 740, 0.16, 58, 0.02, 3.2);
stone(602, 744, 33, 6);
underside(602, 744, 33, 0.1, 5);
chop(576, 772, 0.24, 40, 0.05, 2.6);
stone(796, 632, 30, 5);
underside(796, 632, 30, 0.12, 4);
stone(494, 500, 19, 5);
underside(494, 500, 19, 0.16, 3);
stone(664, 798, 34, 6);
underside(664, 798, 34, 0.14, 4);
// ---- three strays that have rolled clear of the fan, into the open
stone(340, 590, 13, 5);
underside(340, 590, 13, 0.2, 2);
stone(252, 684, 10, 5);
underside(252, 684, 10, 0.26, 2);
chop(424, 732, 0.34, 24, 0.16, 1.6);
// ---- last darks: the shadow the wall casts into its own scree
chop(312, 428, 0.64, 66, 0.0, 3.6);
chop(340, 448, 0.48, 54, 0.01, 3);
chop(294, 414, 0.88, 46, 0.03, 2.6);
chop(368, 482, 0.56, 48, 0.05, 2.4);
chop(528, 542, 0.7, 36, 0.0, 2.8);
chop(660, 662, 0.42, 40, 0.02, 2.6);
chop(744, 762, 0.28, 48, 0.04, 3);
}