Soot Above the Banding
2026 · architecture
Working brick keeps its own record: the courses stay honest and the smoke writes the tonal scale from the top down, so the darkest part of the building is simply the part that has worked hardest.
a bottle kiln on the left third, brick cone tapering off the top edge, iron hoops sagging round the drum, soot blackening the upper courses, cobbles and stacked brick weighting the lower right of the yard
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: the kiln's bottle mass stands on the left third and runs off the top edge; the firemouth blacks fall
// on the lower-left third-crossing, and the yard is weighted back with cobbles and stacked brick at lower right.
// layering: warm haze, brick washes, course-staining, brick courses, soot up the flue, iron hoops, then the blacks.
const PALETTE = [
"#e7e0d0", // ground — warm yard paper
"#8f5232", // mass — fired brick
"#5f3721", // shadow — brick in shade
"#413a30", // soot — smoke-stained courses
"#211d18", // iron — hoops, firemouths, last blacks
"#c4692b", // accent — rust bleeding off the banding
];
// half-width of the hovel at a given y: a pinched flue above, the belly low, a small skirt at the footing
const PROFILE = [
[-76, 25], [10, 29], [96, 35], [186, 44], [276, 57],
[366, 76], [452, 100], [530, 126], [596, 148], [648, 160], [690, 164], [712, 156], [726, 167],
];
function axisAt(y) {
return 244 + (y - 726) * 0.034;
}
function halfAt(y) {
if (y <= PROFILE[0][0]) return PROFILE[0][1];
for (let i = 0; i < PROFILE.length - 1; i++) {
const a = PROFILE[i];
const b = PROFILE[i + 1];
if (y <= b[0]) return lerp(a[1], b[1], (y - a[0]) / (b[0] - a[0]));
}
return PROFILE[PROFILE.length - 1][1];
}
// the whole hovel as one loose shape, drawn inset so passes stack without registering
function bodyShape(inset, jitter) {
brush.beginShape(0.4);
for (let i = 0; i < PROFILE.length; i++) {
const y = PROFILE[i][0];
brush.vertex(axisAt(y) - halfAt(y) + inset + random(-jitter, jitter), y + random(-jitter, jitter));
}
for (let i = PROFILE.length - 1; i >= 0; i--) {
const y = PROFILE[i][0];
brush.vertex(axisAt(y) + halfAt(y) - inset + random(-jitter, jitter), y + random(-jitter, jitter));
}
brush.endShape(true);
}
// horizontal marks laid across a band of the drum, between two fractions of its half-width.
// everything that stains this building — soot, shade, the far cheek — is this one gesture at different pressures.
function stain(count, y0, y1, t0, t1, name, tone, len, weight) {
brush.noFill();
for (let i = 0; i < count; i++) {
const y = lerp(y0, y1, i / count) + randomGaussian(0, 6);
const h = halfAt(y);
const ax = axisAt(y);
const x = ax + lerp(t0, t1, pow(random(), 0.72)) * h;
brush.set(name, tone, weight * (0.72 + random(0, 0.6)));
const l = max(8, randomGaussian(len, len * 0.4)) * (random() < 0.5 ? -1 : 1);
brush.line(x, y, constrain(x + l, ax - h - 1, ax + h + 1), y + randomGaussian(0, 1.7));
}
}
// the same mark loose in the yard, where there is no contour to hold it
function dust(count, x0, x1, y, spread, len, weight, tone) {
brush.noFill();
for (let i = 0; i < count; i++) {
const t = i / count;
brush.set("2B", random() < 0.7 ? tone : "#5f3721", weight * (1 - t * 0.5) * (0.7 + random(0, 0.55)));
const px = lerp(x0, x1, t) + randomGaussian(0, 11);
const py = y + t * 11 + randomGaussian(0, spread);
brush.line(px, py, px + max(5, randomGaussian(len, len * 0.4)), py + randomGaussian(0, 1.5));
}
}
// one course of brick: broken segments that sag with the drum
function course(y, tone, weight) {
const h = halfAt(y);
const ax = axisAt(y);
brush.set("cpencil", tone, weight * (0.85 + random(0, 0.35)));
const segs = 2 + floor(random(0, 3));
let x = ax - h + random(3, 12);
for (let s = 0; s < segs; s++) {
const span = ((2 * h) / segs) * (0.5 + random(0, 0.44));
const x2 = min(x + span, ax + h - random(2, 10));
if (x2 <= x + 6) break;
const sag = randomGaussian(0, 1.3);
brush.line(x, y + sag, x2, y + sag + randomGaussian(0, 1.6));
x = x2 + random(6, 18);
if (x > ax + h - 8) break;
}
}
// an iron hoop: two sagging arcs with a gap between them, a rivet, sometimes rust weeping below
function hoop(y, weight, rust) {
const h = halfAt(y);
const ax = axisAt(y);
const cut = constrain(randomGaussian(0.14, 0.22), -0.24, 0.5);
brush.noFill();
brush.set("charcoal", "#211d18", weight);
brush.beginShape(0.5);
brush.vertex(ax - h - 3, y - randomGaussian(2, 0.7));
brush.vertex(ax - h * 0.5, y + random(2, 3.8));
brush.vertex(ax + h * cut, y + random(2.4, 4));
brush.endShape(false);
const gap = cut + random(0.07, 0.16);
brush.set("charcoal", "#211d18", weight * (0.72 + random(0, 0.3)));
brush.beginShape(0.5);
brush.vertex(ax + h * gap, y + random(2, 4));
brush.vertex(ax + h * (gap + (1 - gap) * 0.55), y + random(1, 3));
brush.vertex(ax + h + 3, y - randomGaussian(1.3, 0.7));
brush.endShape(false);
brush.set("2B", "#413a30", weight * 0.55);
brush.line(ax - h * 0.86, y - random(4, 6), ax + h * random(0.3, 0.7), y - random(3, 5));
brush.noStroke();
brush.fill("#211d18", 210);
brush.circle(ax - h * random(0.4, 0.68), y + random(0.5, 3), random(2, 3.2), true);
if (rust) {
const rx = ax + h * constrain(randomGaussian(0.24, 0.22), -0.4, 0.7);
brush.fill("#c4692b", 186 + random(-24, 40));
brush.circle(rx, y + random(7, 13), random(5, 8), true);
brush.fill("#c4692b", 132);
brush.circle(rx + randomGaussian(0, 4), y + random(18, 30), random(3.5, 6), true);
}
}
// a soft smudge of grime, and a weep where rain has carried it down over the brick
function smudge(y, spread, alpha, tone) {
const r = max(7, randomGaussian(spread, spread * 0.3));
const x = axisAt(y) + constrain(randomGaussian(0, 0.46), -0.8, 0.8) * (halfAt(y) - r * 0.2);
brush.fill(tone, alpha + random(-14, 20));
brush.circle(x, y + randomGaussian(0, 5), r, true);
}
function weep(t, y, len, weight) {
const x = axisAt(y) + t * halfAt(y) * 0.86;
brush.set("charcoal", "#413a30", weight);
brush.beginShape(0.5);
brush.vertex(x, y);
brush.vertex(x + randomGaussian(0, 3), y + len * 0.45);
brush.vertex(x + randomGaussian(0, 4), y + len);
brush.endShape(false);
}
// arched firemouth: three passes of pigment and a drawn arch, since watercolour alone never goes black
function firemouth(x, y, w, hgt) {
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fillTexture(0.3, 0.8);
brush.hatchStyle("charcoal", "#211d18", 1.4);
brush.hatch(3.4, 82);
brush.fill("#211d18", 190);
brush.beginShape(0.62);
brush.vertex(x - w, y);
brush.vertex(x - w * 0.94, y - hgt * 0.56);
brush.vertex(x - w * 0.4, y - hgt);
brush.vertex(x + w * 0.46, y - hgt * 0.95);
brush.vertex(x + w * 0.94, y - hgt * 0.5);
brush.vertex(x + w, y);
brush.endShape(true);
brush.noHatch();
brush.noFill();
brush.set("charcoal", "#211d18", 1.5);
brush.beginShape(0.6);
brush.vertex(x - w - 2, y);
brush.vertex(x - w * 0.52, y - hgt - 4);
brush.vertex(x + w * 0.5, y - hgt - 1);
brush.vertex(x + w + 2, y);
brush.endShape(false);
}
// a cobble or a spall of brick lying in the yard, with a tick of shadow under it
function cobble(x, y, r, tone, alpha) {
brush.noStroke();
brush.fillBleed(0.22, "out");
brush.fillTexture(0.5, 0.65);
brush.fill(tone, alpha);
brush.circle(x, y, r, true);
brush.noFill();
brush.set("2B", "#211d18", r > 11 ? 1.05 : 0.75);
brush.line(x - r * 0.7, y + r * 0.76, x + r * 0.56, y + r * 0.68);
if (r > 12) {
brush.set("2H", "#211d18", 0.7);
brush.line(x - r * 0.44, y - r * 0.34, x + r * 0.24, y - r * 0.5);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e7e0d0");
// --- the air of the yard: pale bleeding washes, largest first
brush.noStroke();
brush.fillBleed(0.7, "out");
brush.fillTexture(0.3, 0.35);
brush.fill("#413a30", 22);
brush.circle(214, 176, 296, true);
brush.fill("#8f5232", 22);
brush.circle(304, 566, 240, true);
// --- the brick mass: seven washes, each inset differently so the edges stay loose
brush.fillBleed(0.3, "out");
brush.fillTexture(0.62, 0.55);
brush.fill("#8f5232", 152);
bodyShape(0, 5);
brush.fill("#8f5232", 136);
bodyShape(5, 4);
brush.fill("#5f3721", 108);
bodyShape(12, 3.5);
brush.fill("#8f5232", 118);
bodyShape(2, 4.5);
brush.fill("#5f3721", 92);
bodyShape(20, 3);
brush.fill("#8f5232", 94);
bodyShape(8, 4);
brush.fill("#5f3721", 84);
bodyShape(30, 3);
// the shaded left flank heavy, the far cheek only just turning away from the light
brush.fillBleed(0.36, "out");
for (let i = 0; i < 18; i++) {
const lit = i > 10;
const y = lit ? 120 + (i - 11) * 92 + random(-14, 14) : -30 + i * 72 + random(-12, 12);
const h = halfAt(y);
brush.fill("#5f3721", lit ? 54 + random(-12, 22) : 118 + random(-20, 36));
brush.circle(axisAt(y) + h * (lit ? random(0.72, 0.9) : -random(0.5, 0.74)), y,
max(11, randomGaussian(h * (lit ? 0.26 : 0.44), 6)), true);
}
// course-staining: heavy down the shaded side, lighter across the lit cheek
stain(150, -50, 700, -1, -0.42, "2B", "#5f3721", 20, 0.85);
stain(110, 140, 720, 0.1, 0.94, "cpencil", "#5f3721", 17, 0.75);
// --- brick courses, close at the base, opening out as the cone climbs
let cy = 718;
let gap = 20;
while (cy > -72) {
course(cy, random() < 0.66 ? "#5f3721" : "#413a30", cy > 380 ? 1.15 : 0.8);
gap = gap * 1.03 + random(-1.6, 1.8);
cy -= gap;
}
// --- soot: the flue goes nearly black and the staining thins away down the shoulder
stain(260, -80, 56, -1, 1, "charcoal", "#211d18", 26, 1.3);
stain(210, -60, 190, -1, 1, "charcoal", "#413a30", 34, 1.0);
stain(160, 130, 330, -1, 1, "charcoal", "#413a30", 32, 0.85);
stain(110, 300, 480, -1, 1, "charcoal", "#5f3721", 30, 0.75);
stain(70, 440, 620, -1, 1, "cpencil", "#5f3721", 26, 0.7);
weep(-0.62, 96, 74, 1.1);
weep(-0.18, 128, 108, 0.9);
weep(0.34, 78, 62, 1.2);
weep(0.66, 150, 132, 0.85);
weep(-0.44, 210, 96, 0.8);
brush.noStroke();
brush.fillBleed(0.42, "out");
brush.fillTexture(0.6, 0.45);
for (let i = 0; i < 12; i++) smudge(-74 + i * 11 + random(-5, 5), 17, 126, "#413a30");
for (let i = 0; i < 7; i++) smudge(-64 + i * 17, 12, 156, "#211d18");
for (let i = 0; i < 12; i++) smudge(96 + i * 22 + random(-8, 8), 14, 74, "#413a30");
// a patch of dark replacement brickwork where the drum was rebuilt once — the focal note
brush.fillBleed(0.2, "out");
brush.fillTexture(0.55, 0.6);
brush.fill("#5f3721", 172);
brush.circle(262, 524, 38, true);
brush.fill("#413a30", 128);
brush.circle(250, 544, 27, true);
dust(90, 216, 300, 496, 22, 13, 0.95, "#413a30");
brush.set("charcoal", "#211d18", 1.1);
brush.line(218, 486, 288, 490);
brush.line(224, 574, 296, 570);
brush.set("2B", "#211d18", 0.9);
brush.line(215, 494, 212, 540);
// --- iron banding, unevenly spaced, heaviest where the drum is widest
hoop(-46, 1.15, false);
hoop(56, 1.25, true);
hoop(168, 1.4, false);
hoop(292, 1.5, true);
hoop(418, 1.6, false);
hoop(544, 1.7, false);
hoop(664, 1.85, true);
// --- the firemouths, the darkest notes in the picture, low on the left third
firemouth(188, 716, 37, 66);
firemouth(296, 714, 24, 46);
firemouth(122, 710, 17, 31);
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fill("#c4692b", 136);
brush.circle(196, 700, 9, true);
brush.fill("#c4692b", 92);
brush.circle(296, 696, 5.5, true);
// --- the contour: taut on the right, broken and soft on the left, never one rope
brush.noFill();
brush.set("2B", "#211d18", 1.5);
brush.beginShape(0.4);
for (let i = 0; i < 6; i++) {
const y = PROFILE[i][0];
brush.vertex(axisAt(y) + halfAt(y) + random(-1.4, 1.4), y);
}
brush.endShape(false);
brush.set("2B", "#413a30", 1.3);
brush.beginShape(0.4);
for (let i = 6; i < PROFILE.length; i++) {
const y = PROFILE[i][0];
brush.vertex(axisAt(y) + halfAt(y) + random(-1.6, 1.6), y);
}
brush.endShape(false);
brush.set("charcoal", "#211d18", 1.2);
brush.beginShape(0.4);
brush.vertex(axisAt(-74) - halfAt(-74), -74);
brush.vertex(axisAt(120) - halfAt(120) - 1, 120);
brush.vertex(axisAt(300) - halfAt(300), 300);
brush.endShape(false);
brush.set("2B", "#413a30", 1.2);
brush.line(axisAt(450) - halfAt(450) - 1, 450, axisAt(580) - halfAt(580), 580);
brush.line(axisAt(620) - halfAt(620), 620, axisAt(716) - halfAt(716) + 2, 716);
// --- the yard: the footing pressed onto the ground, then a broken ground line east of it
dust(40, 88, 460, 718, 2.4, 14, 1.15, "#211d18");
brush.set("2B", "#413a30", 1.3);
brush.line(78, 722, 232, 724);
brush.set("2B", "#413a30", 1.1);
brush.line(246, 725, 398, 726);
brush.set("2H", "#413a30", 0.95);
brush.line(414, 727, 508, 730);
brush.line(524, 731, 596, 729);
brush.set("2H", "#413a30", 0.7);
brush.line(618, 731, 674, 729);
brush.line(698, 730, 730, 728);
// the kiln's shadow running right, drawn into the dust as well as washed in
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.4, 0.45);
for (let i = 0; i < 14; i++) {
const t = i / 13;
brush.fill("#413a30", 96 - t * 68 + random(-10, 12));
brush.circle(404 + t * 268 + random(-9, 9), 736 + t * 12 + random(-5, 5), max(12, randomGaussian(24 - t * 8, 4)), true);
}
dust(54, 402, 668, 740, 12, 10, 0.9, "#413a30");
dust(34, 398, 634, 731, 5, 16, 0.95, "#413a30");
// --- cobbles and rubble, the counterweight at lower right
cobble(516, 748, 19, "#5f3721", 214);
cobble(514, 751, 16, "#413a30", 176);
cobble(518, 754, 12, "#211d18", 132);
cobble(560, 768, 15, "#413a30", 204);
cobble(562, 770, 11, "#211d18", 158);
cobble(688, 774, 14, "#5f3721", 194);
cobble(690, 776, 10, "#413a30", 162);
for (let i = 0; i < 17; i++) {
const near = random() < 0.62;
const x = constrain(randomGaussian(near ? 534 : 700, near ? 52 : 56), 408, 788);
const y = constrain(randomGaussian(near ? 754 : 772, 18), 728, 792);
const roll = random();
cobble(x, y, max(3.6, randomGaussian(8.5, 3.4)),
roll < 0.4 ? "#5f3721" : roll < 0.78 ? "#413a30" : "#211d18", 196 + random(-40, 50));
}
// a short stack of brick, squared against all that rubble
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.fillTexture(0.45, 0.8);
brush.fill("#5f3721", 196);
brush.rect(588, 700, 58, 16);
brush.fill("#8f5232", 180);
brush.rect(594, 685, 48, 14);
brush.fill("#413a30", 172);
brush.rect(599, 672, 40, 12);
brush.noFill();
brush.set("charcoal", "#211d18", 1.15);
brush.line(586, 717, 648, 718);
brush.set("2B", "#413a30", 0.9);
brush.line(592, 721, 652, 720);
brush.set("2H", "#211d18", 0.85);
brush.line(609, 701, 610, 715);
// last blacks: three spalls in the near dust, and one rust chip far right
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fill("#211d18", 200);
brush.circle(482, 774, 7, true);
brush.fill("#211d18", 170);
brush.circle(672, 786, 5.2, true);
brush.fill("#c4692b", 132);
brush.circle(748, 764, 5.5, true);
}