Sienna Finds Its Edge
2026 · color field
Nothing here was drawn: the pigment was allowed up as far as it wanted, and where it lost the strength to keep climbing it broke into grains.
burnt sienna soaking up from the bottom edge into bare warm paper, cresting left of centre and breaking into grains where it thins
painted by Kimi K3p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: burnt sienna rises from the bottom edge, cresting left of centre and sinking away to the right; the top stays bare warm paper
// layering: a warm breath above the wet front, then the body of the stain in blob passes, granulation crowding the thinning edge
// last: the pooled dark along the bottom, dry drags through it, and the crowded pigment line where the water stopped
const PALETTE = [
"#f1e6d4", // ground — bare warm paper
"#cf9463", // wash — sienna thinned to almost nothing at the front
"#a9552b", // mass — burnt sienna, the body of the stain
"#78321a", // pool — where the pigment settled along the bottom edge
"#7d8478", // grain — cool mineral separation, the accent
];
// The wet front. Hand-set control points: it climbs hard to a crest left of
// centre, slumps, lifts once more, then gives up towards the right edge.
const RIDGE = [
[-40, 612], [78, 548], [170, 468], [250, 392], [316, 428],
[392, 474], [468, 448], [546, 522], [632, 576], [716, 594], [840, 626],
];
function ridgeY(x) {
let i = 0;
for (let k = 0; k < RIDGE.length - 2; k++) if (RIDGE[k + 1][0] < x) i = k + 1;
const a = RIDGE[i];
const b = RIDGE[i + 1];
const t = constrain((x - a[0]) / (b[0] - a[0]), 0, 1);
const s = t * t * (3 - 2 * t); // ease, so the front curves instead of kinking
const base = a[1] + (b[1] - a[1]) * s;
const broad = (noise(x * 0.0085, 4.2) - 0.5) * 44;
const fine = (noise(x * 0.043, 11.6) - 0.5) * 18;
return base + broad + fine;
}
// signed depth into the stain — negative is dry paper above the front
function depth(x, y) {
const grain = (noise(x * 0.009 + 30.0, y * 0.009 + 2.4) - 0.5) * 42;
return y - ridgeY(x) + grain;
}
// how hard the pigment is granulating here: strongest where the stain has
// climbed highest and gone thin, almost nothing where it lies low and wet
function thinness(x) {
return constrain(map(ridgeY(x), 620, 380, 0.18, 1.0), 0.18, 1.0);
}
function stainColor(d) {
const r = random();
if (d > 215) return r < 0.6 ? "#78321a" : r < 0.9 ? "#a9552b" : "#cf9463";
if (d > 78) return r < 0.66 ? "#a9552b" : r < 0.93 ? "#78321a" : "#cf9463";
return r < 0.74 ? "#cf9463" : "#a9552b";
}
// a wicking tongue: pigment carried up a fibre further than the rest of the front
function tongue(x, top, w, col, op) {
const base = ridgeY(x) + 20;
const mid = (base + top) * 0.5;
brush.noStroke();
brush.fillBleed(0.78, "out");
brush.fillTexture(0.3, 0.4);
brush.fill(col, op);
brush.beginShape(0.75);
brush.vertex(x - w * 0.5, base);
brush.vertex(x - w * 0.32 + randomGaussian(0, 5), mid + 8);
brush.vertex(x + randomGaussian(0, 6), top);
brush.vertex(x + w * 0.3 + randomGaussian(0, 5), mid - 6);
brush.vertex(x + w * 0.5, base);
brush.endShape(true);
// grains shaken off the tip, where the water gave out
brush.fillBleed(0.2, "out");
brush.fillTexture(0.72, 0.6);
for (let i = 0; i < 7; i++) {
if (random() < 0.28) continue;
brush.fill(random() < 0.78 ? "#a9552b" : "#7d8478", Math.max(60, randomGaussian(128, 30)));
brush.circle(
x + randomGaussian(0, w * 0.36),
top + Math.abs(randomGaussian(16, 18)),
Math.max(1.4, randomGaussian(3.2, 1.2)),
true,
);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#f1e6d4");
brush.noStroke();
// ---- 1. the breath: warmth hanging in the paper just above the wet edge,
// weighted to the left where the stain has climbed
brush.fillBleed(0.72, "out");
brush.fillTexture(0.3, 0.38);
for (let i = 0; i < 36; i++) {
const x = randomGaussian(290, 215);
if (x < -30 || x > 830) continue;
const y = ridgeY(x) - Math.abs(randomGaussian(52, 44));
if (y < 150) continue;
brush.fill("#cf9463", Math.max(9, randomGaussian(25, 9)));
brush.circle(x, y, Math.max(44, randomGaussian(78, 15)), true);
}
// ---- 2. the body: broad pale soak over everything the water reached
brush.fillBleed(0.52, "out");
brush.fillTexture(0.44, 0.46);
for (let gy = 0; gy < 6; gy++) {
for (let gx = 0; gx < 9; gx++) {
const x = 34 + gx * 94 + randomGaussian(0, 21);
const y = 400 + gy * 80 + randomGaussian(0, 21);
const d = depth(x, y);
if (d < -14) continue;
brush.fill(stainColor(d * 0.5), Math.max(30, randomGaussian(74, 18)));
brush.circle(x, y, Math.max(46, randomGaussian(70, 11)), true);
}
}
// ---- 3. the mass, where the sienna has real body
brush.fillBleed(0.42, "out");
brush.fillTexture(0.58, 0.52);
for (let gy = 0; gy < 5; gy++) {
for (let gx = 0; gx < 7; gx++) {
const x = 62 + gx * 118 + randomGaussian(0, 24);
const y = 452 + gy * 86 + randomGaussian(0, 22);
const d = depth(x, y);
if (d < 62) continue;
brush.fill(stainColor(d), Math.max(62, randomGaussian(112, 20)));
brush.circle(x, y, Math.max(38, randomGaussian(58, 10)), true);
}
}
// ---- 4. the settled dark, low and left of centre
brush.fillBleed(0.32, "out");
brush.fillTexture(0.64, 0.58);
for (let gy = 0; gy < 4; gy++) {
for (let gx = 0; gx < 6; gx++) {
const x = 70 + gx * 132 + randomGaussian(0, 22);
const y = 596 + gy * 68 + randomGaussian(0, 20);
if (depth(x, y) < 178) continue;
if (random() < 0.2 + 0.4 * (x / 800)) continue; // thins out to the right
brush.fill(random() < 0.58 ? "#78321a" : "#a9552b", Math.max(70, randomGaussian(126, 22)));
brush.circle(x, y, Math.max(30, randomGaussian(48, 9)), true);
}
}
// ---- 5. six pools placed by hand — the weight of the picture sits here,
// just under the crest, and runs off the bottom edge
brush.fillBleed(0.26, "out");
brush.fill("#78321a", 152);
brush.circle(258, 552, 74, true);
brush.fill("#78321a", 134);
brush.circle(196, 668, 86, true);
brush.fill("#78321a", 120);
brush.circle(336, 742, 78, true);
brush.fill("#a9552b", 128);
brush.circle(96, 596, 62, true);
brush.fill("#78321a", 104);
brush.circle(474, 716, 58, true);
brush.fill("#a9552b", 96);
brush.circle(618, 780, 52, true);
// ---- 6. granulation: the whole point. Pigment breaks into grains along the
// front, crowding thickest where the stain has thinned climbing
brush.fillBleed(0.16, "out");
brush.fillTexture(0.74, 0.62);
for (let i = 0; i < 58; i++) {
const x = -24 + i * 15 + randomGaussian(0, 5);
if (x < -20 || x > 824) continue;
const fy = ridgeY(x);
const th = thinness(x);
for (let j = 0; j < 4; j++) {
if (random() > th * 0.82) continue;
const y = fy + randomGaussian(16, 26);
const cool = random() < 0.1;
brush.fill(
cool ? "#7d8478" : random() < 0.6 ? "#a9552b" : "#78321a",
Math.max(52, randomGaussian(146 * th + 40, 30)),
);
brush.circle(x + randomGaussian(0, 7), y, Math.max(1.3, randomGaussian(4.4 * th + 1.2, 1.6)), true);
}
}
// grains thrown clear of the front, drying alone on the paper
for (let i = 0; i < 30; i++) {
const x = randomGaussian(268, 190);
if (x < 10 || x > 790) continue;
const y = ridgeY(x) - Math.abs(randomGaussian(30, 34));
if (y < 120) continue;
brush.fill(random() < 0.82 ? "#a9552b" : "#7d8478", Math.max(50, randomGaussian(110, 34)));
brush.circle(x, y, Math.max(1.2, randomGaussian(2.6, 1.0)), true);
}
// ---- 7. four tongues wicking above the front, tallest at the crest
tongue(244, 296, 92, "#cf9463", 54);
tongue(150, 402, 66, "#cf9463", 44);
tongue(398, 396, 74, "#a9552b", 36);
tongue(556, 452, 58, "#cf9463", 34);
// ---- 8. dry drags through the wet body, horizontal, the way a soak settles
brush.noFill();
brush.set("charcoal", "#78321a", 1.1);
brush.field("waves");
for (let i = 0; i < 62; i++) {
const x = randomGaussian(300, 200);
const y = randomGaussian(660, 105);
if (x < 6 || x > 794 || y < 6 || y > 794) continue;
if (depth(x, y) < 96) continue;
brush.flowLine(x, y, Math.max(20, randomGaussian(64, 22)), randomGaussian(2, 9));
}
brush.noField();
// ---- 9. the crowded line: where the water stopped, pigment stacked against
// its own edge. Three passes, only along the climbing left half
brush.set("cpencil", "#78321a", 1.3);
brush.beginShape(0.55);
brush.vertex(64, 556 + randomGaussian(0, 4));
brush.vertex(158, 476 + randomGaussian(0, 4));
brush.vertex(238, 404 + randomGaussian(0, 4));
brush.vertex(304, 438 + randomGaussian(0, 4));
brush.endShape(false);
brush.set("2B", "#78321a", 0.9);
brush.beginShape(0.55);
brush.vertex(186, 462);
brush.vertex(252, 400);
brush.vertex(322, 446);
brush.vertex(388, 486);
brush.endShape(false);
brush.set("2H", "#a9552b", 0.85);
brush.beginShape(0.6);
brush.vertex(432, 462);
brush.vertex(506, 486);
brush.vertex(572, 538);
brush.endShape(false);
// ---- 10. two last darks pressed into the pool, and one cool grain adrift
// high in the empty paper to hold the top corner down
brush.noStroke();
brush.fillBleed(0.18, "out");
brush.fillTexture(0.7, 0.6);
brush.fill("#78321a", 176);
brush.circle(228, 604, 26, true);
brush.fill("#78321a", 160);
brush.circle(288, 700, 20, true);
brush.fill("#7d8478", 128);
brush.circle(612, 214, 3.4, true);
brush.fill("#a9552b", 96);
brush.circle(668, 262, 2.4, true);
}