First Light, Stepping Down
2026 · landscape
A hillside of water is a hillside of held sky; I painted the intervals between the steps and let the paper do the mist.
flooded rice terraces stepping down a hillside at dawn, each step holding a different sliver of reflected sky
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a diagonal cascade of flooded terrace slivers climbing out of the bottom-right corner toward
// the upper-left, the brightest step held as near-bare paper on the thirds crossing at (268, 262).
// layering: dawn air first, then the earth between the steps, then water far-to-near, then wall ink and reeds.
// the valley at top-right and the slope at bottom-left stay unpainted — the mist here is paper, not pigment.
const PALETTE = [
"#e9e3d5", // ground — warm paper, and the mist itself
"#93a0a8", // mass — pale reflected sky, the high steps
"#5e6b71", // mass — slate water where no sky lands
"#2e373a", // shadow — mud-wall ink, laid last
"#79806f", // mass — grey-green embankment
"#d3a077", // accent — first light, rationed to three marks
];
// one row per step: centre, half-length, thickness, rise (degrees, upslope end higher), sag, water tone.
// the intervals are deliberately uneven and the tones deliberately jump — a hillside is not a ladder,
// and no two steps hold the same sliver of sky.
const TERRACES = [
{ x: 158, y: 166, len: 94, th: 12, tilt: 19, bow: 14, tone: 0.9 }, // highest sliver, half lost in mist
{ x: 236, y: 208, len: 114, th: 14, tilt: 17, bow: 19, tone: 0.42 },
{ x: 268, y: 262, len: 138, th: 20, tilt: 15, bow: 22, tone: 1.0 }, // the bright step — reserved paper
{ x: 304, y: 312, len: 148, th: 16, tilt: 16, bow: 17, tone: 0.22 },
{ x: 342, y: 356, len: 184, th: 24, tilt: 13, bow: 28, tone: 0.84 },
{ x: 360, y: 400, len: 156, th: 14, tilt: 18, bow: 21, tone: 0.34 },
{ x: 408, y: 442, len: 252, th: 28, tilt: 12, bow: 33, tone: 0.7 },
{ x: 456, y: 494, len: 200, th: 19, tilt: 15, bow: 24, tone: 0.24 },
{ x: 498, y: 538, len: 270, th: 33, tilt: 11, bow: 37, tone: 0.82 },
{ x: 546, y: 600, len: 182, th: 22, tilt: 14, bow: 27, tone: 0.18 },
{ x: 602, y: 650, len: 272, th: 38, tilt: 10, bow: 40, tone: 0.4 },
{ x: 656, y: 716, len: 242, th: 29, tilt: 13, bow: 30, tone: 0.12 },
{ x: 720, y: 778, len: 274, th: 46, tilt: 9, bow: 34, tone: 0.2 },
];
// a point on a step's rim. u runs -1 (upslope end) to 1; side -1 is the far lip, 1 the near lip.
// the half-thickness never falls below a hair, so no sliver ever folds back through itself.
function rim(t, u, side, inset) {
const a = (t.tilt * Math.PI) / 180;
const half = Math.max(2.6, t.th * 0.5 * (1 - 0.42 * u * u) - inset);
return [
t.x + u * t.len * Math.cos(a),
t.y + u * t.len * Math.sin(a) + t.bow * (1 - u * u) + side * half,
];
}
// both rims, wobbled — no two passes over one step land on the same line
function rims(t, inset) {
const far = [];
const near = [];
for (let i = 0; i <= 9; i++) {
const u = -1 + i / 4.5;
const f = rim(t, u, -1, inset);
const n = rim(t, u, 1, inset);
far.push([f[0] + randomGaussian(0, 1.7), f[1] + randomGaussian(0, 1.2)]);
near.push([n[0] + randomGaussian(0, 1.7), n[1] + randomGaussian(0, 1.2)]);
}
return { far, near };
}
function ribbon(t, col, op, bleed, inset, tex) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(tex + random(0, 0.16), 0.44);
brush.fill(col, op);
const r = rims(t, inset);
brush.beginShape(0.3);
for (let i = 0; i < r.far.length; i++) brush.vertex(r.far[i][0], r.far[i][1]);
for (let i = r.near.length - 1; i >= 0; i--) brush.vertex(r.near[i][0], r.near[i][1]);
brush.endShape(true);
}
// the earth held between two steps: a band as thick as the gap, built from the pair's average line
function embankment(i, col, op) {
const a = TERRACES[i];
const b = TERRACES[i + 1];
const top = a.y + a.th * 0.5;
const bottom = b.y - b.th * 0.5;
ribbon(
{
x: (a.x + b.x) / 2 + random(-10, 10),
y: (top + bottom) / 2,
len: min(a.len, b.len) * (1.0 + random(0, 0.1)),
th: max(14, bottom - top + 8),
tilt: (a.tilt + b.tilt) / 2,
bow: (a.bow + b.bow) / 2,
},
col,
op,
0.3,
0,
0.6,
);
}
// water: some steps are almost all sky and stay paper, some hold only slate
function paintStep(t) {
if (t.tone > 0.78) {
ribbon(t, "#93a0a8", 52 + random(0, 26), 0.55, 0, 0.3);
if (random() < 0.5) ribbon(t, "#5e6b71", 34 + random(0, 18), 0.34, t.th * 0.24, 0.45);
} else if (t.tone > 0.42) {
ribbon(t, "#93a0a8", 132 + random(-16, 16), 0.5, 0, 0.36);
ribbon(t, "#5e6b71", 92 + random(-14, 20), 0.34, t.th * 0.2, 0.5);
} else {
ribbon(t, "#5e6b71", 222 + random(-16, 16), 0.42, 0, 0.4);
ribbon(t, "#2e373a", 138 + random(0, 46), 0.26, t.th * 0.26, 0.55);
}
}
// broken ink along a rim — the wall is felt in pieces, never outlined all the way round
function inkEdge(pts, col, weight, curv) {
brush.noFill();
brush.set(random() < 0.55 ? "charcoal" : "2B", col, weight);
let i = 0;
while (i < pts.length - 2) {
const end = min(pts.length - 1, i + 2 + floor(random(0, 4)));
brush.beginShape(curv);
for (let k = i; k <= end; k++) {
brush.vertex(pts[k][0] + randomGaussian(0, 1.5), pts[k][1] + randomGaussian(0, 1.4));
}
brush.endShape(false);
i = end + (random() < 0.4 ? 1 : 2);
}
}
function wall(t, weight, from, to) {
const r = rims(t, 0);
const cut = [];
for (let i = from; i <= to; i++) cut.push(r.near[i]);
inkEdge(cut, random() < 0.8 ? "#2e373a" : "#5e6b71", weight, 0.45);
}
// a bund cutting across two steps — the only thing that crosses the sweep
function bund(i, u, drift, weight) {
const a = rim(TERRACES[i], u, 1, 0);
const b = rim(TERRACES[i + 1], u + drift, -1, 0);
brush.noFill();
brush.set(random() < 0.5 ? "charcoal" : "2B", "#2e373a", weight);
const over = 9 + random(0, 9);
brush.beginShape(0.5);
brush.vertex(a[0] + randomGaussian(0, 2), a[1] - over);
brush.vertex((a[0] + b[0]) / 2 + randomGaussian(0, 5), (a[1] + b[1]) / 2 + randomGaussian(0, 3));
brush.vertex(b[0] + randomGaussian(0, 2), b[1] + over * 0.7);
brush.endShape(false);
}
// reeds standing off a wall: short, leaning, never the same lean twice
function reeds(x, y, n, len) {
brush.set("2B", "#2e373a", 0.8 + random(0, 0.4));
brush.noFill();
for (let i = 0; i < n; i++) {
const bx = x + randomGaussian(0, 11);
const by = y + randomGaussian(0, 3);
const l = len * (0.6 + random(0, 0.7));
const lean = randomGaussian(0, 0.22);
brush.beginShape(0.5);
brush.vertex(bx, by);
brush.vertex(bx + lean * l * 0.6, by - l * 0.6);
brush.vertex(bx + lean * l * 1.6, by - l);
brush.endShape(false);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e9e3d5");
// ---- dawn air: five large washes, every one thinner than it looks
brush.noStroke();
brush.fillBleed(0.66, "out");
brush.fillTexture(0.3, 0.4);
brush.fill("#5e6b71", 44);
brush.circle(104, 130, 226, true);
brush.fill("#5e6b71", 34);
brush.circle(30, 322, 204, true);
brush.fill("#93a0a8", 30);
brush.circle(346, 200, 190, true);
brush.fill("#5e6b71", 40);
brush.circle(716, 668, 252, true);
brush.fill("#5e6b71", 26);
brush.circle(420, 706, 172, true);
// the light itself: a warm breath sitting over the bright step
brush.fillBleed(0.5, "out");
brush.fill("#d3a077", 132);
brush.circle(236, 232, 96, true);
brush.fill("#d3a077", 88);
brush.circle(320, 262, 74, true);
// ---- the hillside: earth between the near steps, air between the far ones
embankment(3, "#79806f", 96);
embankment(5, "#79806f", 118);
embankment(6, "#79806f", 104);
embankment(7, "#79806f", 136);
embankment(8, "#79806f", 148);
embankment(9, "#79806f", 192);
embankment(10, "#79806f", 168);
embankment(11, "#79806f", 206);
// one dark shoulder immediately under the bright step, so the paper there can glow
embankment(2, "#5e6b71", 128);
// ---- water, far step to near step
for (let i = 0; i < TERRACES.length; i++) paintStep(TERRACES[i]);
// the bright step, lifted twice and left mostly as paper; two more steps catch the same light
ribbon(TERRACES[2], "#e9e3d5", 238, 0.2, -4, 0.24);
ribbon(TERRACES[2], "#e9e3d5", 190, 0.34, 1, 0.3);
ribbon(TERRACES[2], "#d3a077", 104, 0.3, 6, 0.4);
ribbon(TERRACES[4], "#d3a077", 58, 0.36, 7, 0.45);
ribbon(TERRACES[0], "#e9e3d5", 132, 0.4, -2, 0.32);
// ---- walls, darkest and last, the weight falling away up the slope
wall(TERRACES[12], 3.6, 0, 9);
wall(TERRACES[11], 3.0, 2, 9);
wall(TERRACES[10], 3.2, 0, 7);
wall(TERRACES[9], 2.2, 3, 9);
wall(TERRACES[8], 2.7, 0, 9);
wall(TERRACES[7], 1.5, 4, 9);
wall(TERRACES[6], 2.2, 0, 6);
wall(TERRACES[5], 1.3, 5, 9);
wall(TERRACES[4], 1.6, 0, 5);
wall(TERRACES[3], 2.4, 0, 9);
wall(TERRACES[2], 1.5, 1, 7);
wall(TERRACES[1], 1.0, 0, 4);
// ---- bunds, hand-placed where the eye wants a rung across the cascade
bund(10, -0.52, 0.08, 2.2);
bund(8, 0.24, -0.12, 1.9);
bund(6, -0.31, 0.14, 1.7);
bund(9, 0.62, -0.05, 2.0);
bund(11, 0.05, 0.06, 2.4);
bund(4, -0.66, 0.1, 1.3);
bund(2, 0.44, -0.2, 1.5);
// ---- a tree mass at the left edge, the one thing standing up out of the water
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.fillTexture(0.7, 0.7);
brush.fill("#2e373a", 248);
brush.circle(206, 386, 16, true);
brush.fill("#2e373a", 232);
brush.circle(188, 374, 12, true);
brush.fill("#2e373a", 216);
brush.circle(221, 398, 10, true);
brush.fill("#79806f", 198);
brush.circle(232, 380, 7, true);
brush.noFill();
brush.set("charcoal", "#2e373a", 2.2);
brush.line(208, 400, 213, 428);
brush.set("2B", "#2e373a", 1.3);
brush.line(213, 428, 210, 442);
// a second, smaller bush further down the slope
brush.noStroke();
brush.fill("#2e373a", 224);
brush.circle(286, 486, 10, true);
brush.fill("#2e373a", 190);
brush.circle(273, 478, 7, true);
brush.noFill();
brush.set("2B", "#2e373a", 1.2);
brush.line(287, 496, 290, 512);
// ---- reeds: a stand at the near corner, one stray high on the slope
reeds(664, 730, 8, 40);
reeds(748, 692, 5, 31);
reeds(548, 752, 6, 34);
reeds(430, 620, 4, 22);
reeds(392, 470, 3, 18);
reeds(248, 286, 2, 12);
// ---- three last dark marks: the shadow the water sits against
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fill("#2e373a", 208);
brush.circle(624, 668, 11, true);
brush.fill("#2e373a", 176);
brush.circle(304, 288, 5, true);
brush.fill("#2e373a", 160);
brush.circle(458, 516, 7, true);
}