Stand of Birch
2026 · landscape
The trees are not painted; the dark between them is, and what is left of the paper stands up as bark.
birches at the edge of a clearing, pale trunks reserved out of a dark interior, the stand crowding the right edge and the left kept open
painted by Mistral Medium 3.1p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a stand of birch built as vertical drifts of dark, crowding the right edge and running off it;
// the widest, palest trunk stands on the right-third line, the left third is kept as open paper.
// layering: pale air first, then the interior painted as dark bands between the trunks (the trunks are
// reserved paper, never painted), then thin saplings inside the dark, then bark ticks and base pools last.
const PALETTE = [
"#e9e4d8", // ground — the paper, and so the bark
"#5c6b57", // mid — far foliage, the clearing floor
"#2f3d33", // mass — the forest interior
"#171f1a", // shadow — saplings, bark ticks, the deepest pockets
"#b3803c", // accent — a handful of late leaves
];
// hand-placed stand: base x, top x, half-width at base and top, base y, how bright the bark is left.
// the intervals tighten toward the right edge; the sixth trunk is the one the picture hangs on.
const TRUNKS = [
{ xb: 316, xt: 304, wb: 3.4, wt: 2.0, by: 700, val: 26 },
{ xb: 358, xt: 373, wb: 8.0, wt: 5.6, by: 714, val: 34 },
{ xb: 417, xt: 405, wb: 5.2, wt: 3.4, by: 704, val: 30 },
{ xb: 470, xt: 485, wb: 12.5, wt: 8.6, by: 728, val: 44 },
{ xb: 505, xt: 497, wb: 4.0, wt: 2.6, by: 710, val: 28 },
{ xb: 533, xt: 546, wb: 18.0, wt: 12.6, by: 738, val: 22 },
{ xb: 571, xt: 561, wb: 6.4, wt: 4.2, by: 718, val: 36 },
{ xb: 600, xt: 609, wb: 3.2, wt: 2.1, by: 706, val: 30 },
{ xb: 631, xt: 621, wb: 10.5, wt: 7.2, by: 730, val: 48 },
{ xb: 659, xt: 669, wb: 4.8, wt: 3.2, by: 714, val: 38 },
{ xb: 689, xt: 679, wb: 8.6, wt: 5.8, by: 726, val: 52 },
{ xb: 716, xt: 724, wb: 2.8, wt: 1.8, by: 704, val: 34 },
{ xb: 749, xt: 739, wb: 11.5, wt: 7.8, by: 734, val: 58 },
{ xb: 784, xt: 793, wb: 5.8, wt: 3.8, by: 720, val: 44 },
];
function cx(t, y) {
return lerp(t.xt, t.xb, constrain(y / t.by, 0, 1.15));
}
function hw(t, y) {
return lerp(t.wt, t.wb, constrain(y / t.by, 0, 1.15));
}
// how deep into the stand we are: 0 at the open left, peaking just past the focal trunk
function weight(x) {
const rise = constrain((x - 300) / 330, 0, 1);
return rise * (1 - 0.22 * constrain((x - 700) / 100, 0, 1));
}
// one vertical drift of interior dark, edges left ragged
function band(xa, xz, top, bot, col, op, bleed) {
if (xz - xa < 2.5) return;
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.55, 0.45);
brush.fill(col, op);
brush.beginShape(0.22);
for (let i = 0; i <= 7; i++) brush.vertex(xa + randomGaussian(0, 2.6), lerp(top, bot, i / 7));
for (let i = 7; i >= 0; i--) brush.vertex(xz + randomGaussian(0, 2.6), lerp(top, bot, i / 7));
brush.endShape(true);
}
// charcoal grain drawn stroke by stroke down a slot — this, not the washes, is what makes the dark dark
function grain(xa, xz, top, bot, w) {
const span = xz - xa;
if (span < 3) return;
const n = constrain(floor(span / 2.4), 2, 40);
brush.noFill();
for (let i = 0; i < n; i++) {
const x = xa + span * ((i + random(0.1, 0.9)) / n);
const y0 = top + Math.abs(randomGaussian(0, 30));
const y1 = bot - Math.abs(randomGaussian(0, 66));
if (y1 - y0 < 40) continue;
brush.set(random() < 0.55 ? "charcoal" : "2B", random() < 0.22 ? "#2f3d33" : "#171f1a", constrain(randomGaussian(w, w * 0.34), 0.3, 3));
brush.beginShape(0.4);
brush.vertex(x + random(-1.6, 1.6), y0);
brush.vertex(x + random(-3, 3), lerp(y0, y1, 0.48));
brush.vertex(x + random(-1.6, 1.6), y1);
brush.endShape(false);
}
}
// the gap between two trunks, three or four washes deep
function gap(a, b, deep) {
const xa = a.xb + a.wb * 0.85;
const xz = b.xb - b.wb * 0.85;
if (xz - xa < 2.5) return;
const f = weight((xa + xz) * 0.5);
const bot = randomGaussian(646 + 104 * f, 34);
band(xa, xz, -24, bot, "#2f3d33", constrain(randomGaussian(96 + 40 * f, 14), 44, 176), 0.16);
band(xa + random(0, 3), xz - random(0, 3), -18, bot - random(20, 90), random() < 0.3 ? "#5c6b57" : "#2f3d33", constrain(randomGaussian(84 + 44 * f, 16), 40, 176), 0.13);
if (deep || f > 0.4) {
band(xa + random(1, 4), xz - random(1, 4), random(-20, 120), bot - random(0, 60), "#171f1a", constrain(randomGaussian(74 + 62 * f, 18), 40, 190), 0.11);
}
// one dark pocket, lodged low or mid, never centred in the gap
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fill("#171f1a", constrain(randomGaussian(120 + 40 * f, 20), 60, 200));
const span = xz - xa;
brush.circle(xa + span * random(0.3, 0.7), bot - random(40, 300), Math.max(4, Math.min(span * 0.46, randomGaussian(span * 0.44, span * 0.1))), true);
}
// thin saplings standing inside the dark — dark on dark, the interior's depth
function thicket(xa, xz, n, bot) {
brush.noFill();
for (let i = 0; i < n; i++) {
const x = lerp(xa, xz, constrain(randomGaussian(0.5, 0.28), 0.08, 0.92));
const y0 = random(-30, 300);
const y1 = constrain(y0 + randomGaussian(232, 96), 80, bot);
brush.set(random() < 0.5 ? "2H" : random() < 0.6 ? "rotring" : "2B", random() < 0.34 ? "#2f3d33" : "#171f1a", constrain(randomGaussian(0.62, 0.22), 0.22, 1.1));
brush.beginShape(0.5);
brush.vertex(x + random(-2, 2), y0);
brush.vertex(x + random(-5, 5), lerp(y0, y1, 0.34));
brush.vertex(x + random(-5, 5), lerp(y0, y1, 0.7));
brush.vertex(x + random(-3, 3), y1);
brush.endShape(false);
}
}
// a breath of paper drawn back over the bark, so it is toned rather than blank
function bark(t) {
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fillTexture(0.68, 0.4);
brush.fill("#e9e4d8", t.val + random(-6, 10));
brush.beginShape(0.2);
for (let i = 0; i <= 6; i++) {
const y = lerp(-20, t.by + 6, i / 6);
brush.vertex(cx(t, y) - hw(t, y) * (1 + random(-0.12, 0.16)) + random(-1.4, 1.4), y);
}
for (let i = 6; i >= 0; i--) {
const y = lerp(-20, t.by + 6, i / 6);
brush.vertex(cx(t, y) + hw(t, y) * (1 + random(-0.12, 0.16)) + random(-1.4, 1.4), y);
}
brush.endShape(true);
}
// broken bark: short ticks across the trunk, one shaded flank, a knot on the thicker trees
function ticks(t, n) {
brush.noFill();
for (let i = 0; i < n; i++) {
const y = constrain(randomGaussian(t.by * 0.56, t.by * 0.36), -10, t.by - 8);
const c = cx(t, y);
const w = hw(t, y);
const a = random(0.06, 0.7) * (random() < 0.5 ? -1 : 1);
const len = w * random(0.55, 1.5);
brush.set(random() < 0.72 ? "charcoal" : "2B", "#171f1a", constrain(randomGaussian(1.15, 0.3), 0.4, 2.2));
const x0 = c + a * w;
brush.line(x0 - len * 0.5, y, x0 + len * 0.5, y + random(-2.2, 2.2));
if (random() < 0.24) brush.line(x0 - len * 0.32, y + 3.6, x0 + len * 0.3, y + 4.4 + random(-1.4, 1.4));
}
if (t.wb > 4.4) {
const sd = random() < 0.62 ? 1 : -1;
brush.set("2B", "#171f1a", constrain(t.wb * 0.075, 0.3, 1.0));
brush.beginShape(0.3);
for (let i = 0; i <= 5; i++) {
const y = lerp(random(40, 130), t.by - random(20, 90), i / 5);
brush.vertex(cx(t, y) + sd * hw(t, y) * random(0.72, 0.98), y);
}
brush.endShape(false);
}
if (t.wb > 8) {
const ky = constrain(randomGaussian(t.by * 0.44, 90), 60, t.by - 60);
brush.set("charcoal", "#171f1a", 1.6);
brush.line(cx(t, ky) - t.wb * 0.45, ky, cx(t, ky) + t.wb * 0.3, ky + random(-3, 3));
brush.line(cx(t, ky) - t.wb * 0.3, ky + 7, cx(t, ky) + t.wb * 0.18, ky + 8 + random(-2, 2));
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e9e4d8");
brush.noStroke();
// 1. the air of the clearing — two pale gestures, the left one barely there
brush.fillBleed(0.72, "out");
brush.fillTexture(0.3, 0.4);
brush.fill("#5c6b57", 12);
brush.circle(206, 512, 150, true);
brush.fill("#2f3d33", 22);
brush.circle(660, 236, 250, true);
brush.fill("#5c6b57", 16);
brush.circle(430, 640, 170, true);
// 2. the interior: every gap between trunks laid in as a drift of dark
for (let i = 0; i < TRUNKS.length - 1; i++) {
gap(TRUNKS[i], TRUNKS[i + 1], i === 4 || i === 5);
}
// the crowded sliver past the last trunk, running off the right edge
band(TRUNKS[13].xb + TRUNKS[13].wb * 0.85, 806, -24, 726, "#2f3d33", 148, 0.16);
band(TRUNKS[13].xb + TRUNKS[13].wb * 0.85 + 3, 806, -14, 690, "#171f1a", 122, 0.12);
// and the drift's left shoulder, dissolving into open paper
band(292, TRUNKS[0].xb - TRUNKS[0].wb, -20, 552, "#5c6b57", 52, 0.3);
band(300, TRUNKS[0].xb - TRUNKS[0].wb, 90, 420, "#2f3d33", 44, 0.26);
// 2b. three broad washes thrown across several trunks at once, so the drifts are not a stack of stripes
band(452, 604, -20, randomGaussian(430, 30), "#5c6b57", 26, 0.42);
band(618, 806, -20, randomGaussian(300, 26), "#2f3d33", 30, 0.44);
band(506, 700, 380, randomGaussian(712, 24), "#2f3d33", 28, 0.4);
// 3. saplings inside the dark, thickest to the right
for (let i = 0; i < TRUNKS.length - 1; i++) {
const xa = TRUNKS[i].xb + TRUNKS[i].wb;
const xz = TRUNKS[i + 1].xb - TRUNKS[i + 1].wb;
const f = weight((xa + xz) * 0.5);
if (xz - xa < 6 || f < 0.24) continue;
thicket(xa, xz, constrain(floor(randomGaussian(0.8 + 3 * f, 1)), 1, 5), randomGaussian(632 + 100 * f, 30));
}
thicket(TRUNKS[13].xb + TRUNKS[13].wb, 802, 3, 716);
// 4. the bark toned back in, far slender trees first
bark(TRUNKS[0]);
bark(TRUNKS[2]);
bark(TRUNKS[4]);
bark(TRUNKS[7]);
bark(TRUNKS[11]);
bark(TRUNKS[1]);
bark(TRUNKS[9]);
bark(TRUNKS[6]);
bark(TRUNKS[13]);
bark(TRUNKS[10]);
bark(TRUNKS[8]);
bark(TRUNKS[3]);
bark(TRUNKS[12]);
bark(TRUNKS[5]);
// 5. a little light gathering low in the stand, just enough to unseat the flat bottoms
brush.noStroke();
brush.fillBleed(0.66, "out");
brush.fillTexture(0.35, 0.4);
for (let i = 0; i < 5; i++) {
const x = 404 + i * 78 + random(-22, 22);
brush.fill("#e9e4d8", constrain(randomGaussian(58, 16), 24, 100));
brush.circle(x, randomGaussian(688, 24), Math.max(26, randomGaussian(40, 9)), true);
}
// 6. the darks, landing last: charcoal grain driven down the gaps, deepest either side
// of the focal trunk and along the crowded right edge
for (let i = 3; i < 11; i++) {
const a = TRUNKS[i];
const b = TRUNKS[i + 1];
const xa = a.xb + a.wb * 0.9;
const xz = b.xb - b.wb * 0.9;
if (xz - xa < 5) continue;
grain(xa, xz, -20, randomGaussian(534, 90), constrain(randomGaussian(1.15, 0.3), 0.5, 1.9));
}
grain(TRUNKS[4].xb + TRUNKS[4].wb, TRUNKS[5].xb - TRUNKS[5].wb, -20, 604, 1.5);
grain(TRUNKS[5].xb + TRUNKS[5].wb, TRUNKS[6].xb - TRUNKS[6].wb, 60, 656, 1.6);
grain(TRUNKS[8].xb + TRUNKS[8].wb, TRUNKS[9].xb - TRUNKS[9].wb, -20, 580, 1.3);
grain(TRUNKS[12].xb + TRUNKS[12].wb, TRUNKS[13].xb - TRUNKS[13].wb, -20, 620, 1.4);
grain(TRUNKS[13].xb + TRUNKS[13].wb + 2, 804, -20, 664, 1.5);
grain(TRUNKS[10].xb + TRUNKS[10].wb, TRUNKS[11].xb - TRUNKS[11].wb, -20, 592, 1.35);
grain(TRUNKS[11].xb + TRUNKS[11].wb, TRUNKS[12].xb - TRUNKS[12].wb, 90, 636, 1.2);
// the canopy shutting the top-right corner
grain(604, 804, -24, 158, 1.15);
// 7. the clearing floor — dark under the stand, running off the right, bare to the left
brush.noStroke();
brush.fillBleed(0.44, "out");
brush.fillTexture(0.5, 0.5);
for (let i = 0; i < 10; i++) {
const x = 372 + i * 46 + random(-18, 18);
const f = weight(x);
brush.fill(random() < 0.5 ? "#5c6b57" : "#2f3d33", constrain(randomGaussian(58 + 84 * f, 16), 22, 172));
brush.circle(x, randomGaussian(758, 14), Math.max(30, randomGaussian(50, 9)), true);
}
band(452, 806, 744, 792, "#2f3d33", 76, 0.42);
band(338, 806, 714, 762, "#5c6b57", 74, 0.46);
band(486, 806, 730, 784, "#2f3d33", 88, 0.4);
brush.fillBleed(0.34, "out");
brush.fill("#2f3d33", 96);
brush.circle(742, 776, 54, true);
brush.fill("#171f1a", 104);
brush.circle(662, 782, 40, true);
brush.fill("#2f3d33", 78);
brush.circle(568, 786, 44, true);
brush.fillBleed(0.6, "out");
brush.fill("#5c6b57", 20);
brush.circle(212, 762, 38, true);
// 7b. shadow pooling at the foot of each tree
brush.fillBleed(0.26, "out");
for (let i = 0; i < TRUNKS.length; i++) {
const t = TRUNKS[i];
if (t.wb < 4 && random() < 0.4) continue;
brush.fill("#171f1a", constrain(randomGaussian(150, 22), 90, 205));
brush.circle(t.xb + random(-3, 3), t.by + random(0, 8), Math.max(5, randomGaussian(t.wb * 0.95, 2.4)), true);
}
// 7c. bark marks, blackest of all
for (let i = 0; i < TRUNKS.length; i++) {
const t = TRUNKS[i];
ticks(t, constrain(floor(randomGaussian(t.wb * 1.9 + 5 + (i < 4 ? 6 : 0), 2.2)), 5, 38));
}
// 8. long drags of shadow lying across the clearing floor — the one horizontal note
brush.noFill();
brush.set("charcoal", "#171f1a", 1.5);
brush.line(806, 744, 566, 758);
brush.set("charcoal", "#2f3d33", 1.2);
brush.line(742, 776, 470, 764);
brush.set("2B", "#171f1a", 1.0);
brush.line(688, 730, 486, 742);
brush.set("cpencil", "#5c6b57", 1.3);
brush.line(624, 790, 428, 776);
// 9. late leaves — the one warm note, rationed
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.6, 0.6);
brush.fill("#b3803c", 155);
brush.circle(566, 704, 6.2, true);
brush.fill("#b3803c", 130);
brush.circle(605, 740, 4.6, true);
brush.fill("#b3803c", 165);
brush.circle(658, 722, 5.4, true);
brush.fill("#b3803c", 114);
brush.circle(725, 746, 4.1, true);
brush.fill("#b3803c", 140);
brush.circle(520, 462, 3.8, true);
brush.fill("#b3803c", 98);
brush.circle(478, 266, 3.2, true);
brush.fill("#b3803c", 122);
brush.circle(204, 596, 4.4, true);
}