Every Patch A Different Age
2026 · textile
Nothing here was designed; it accumulated. The oldest cloth is the palest, the newest repair the darkest, and the stitches refuse to respect any edge they cross.
boro cloth climbing a diagonal out of the lower-left corner, patch sewn over patch, each indigo faded to its own depth, sashiko running stitches carrying straight across the old seams onto bare hemp
painted by Grok 4.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: mended patches climb a diagonal out of the lower-left corner; the largest, palest,
// most-washed scrap sits upper-right of centre, and the darkest new repairs land on it last.
// layering: hemp ground, indigo bleed under the pile, patches back-to-front, old seams, then
// sashiko running stitches crossing every seam. Upper-left and lower-right corners stay bare cloth.
const PALETTE = [
"#e3dbc9", // ground — undyed hemp, the cloth this all began as
"#1d2b40", // mass — deepest indigo, the newest repair
"#3a5470", // mid — indigo a few washes old
"#6c869a", // worn — indigo rubbed thin at the folds
"#a9b2b0", // pale — the oldest scrap, washed to grey
"#9c5439", // accent — one madder patch and a handful of rust stitches
];
const HEMP = PALETTE[0];
const INK = PALETTE[1];
const INDIGO = PALETTE[2];
const WORN = PALETTE[3];
const PALE = PALETTE[4];
const MADDER = PALETTE[5];
// A patch is never square: eight points, each corner and mid-edge pulled off true.
function patchOutline(cx, cy, w, h, angDeg, wob) {
const a = (angDeg * Math.PI) / 180;
const ca = Math.cos(a);
const sa = Math.sin(a);
const corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
const pts = [];
for (let i = 0; i < 4; i++) {
const p = corners[i];
const q = corners[(i + 1) % 4];
for (let k = 0; k < 2; k++) {
const t = k * 0.5;
const lx = (p[0] + (q[0] - p[0]) * t) * w * 0.5 + randomGaussian(0, wob);
const ly = (p[1] + (q[1] - p[1]) * t) * h * 0.5 + randomGaussian(0, wob);
pts.push([cx + lx * ca - ly * sa, cy + lx * sa + ly * ca]);
}
}
return pts;
}
function traceShape(pts, curv, dx, dy) {
brush.beginShape(curv);
for (const p of pts) brush.vertex(p[0] + dx, p[1] + dy, 1);
brush.endShape(true);
}
// One patch. Dye halo first, then two soft tone passes, then the cloth itself built
// from crossed threads — warp still holding its indigo, weft rubbed a shade paler.
// o.warp is thread spacing: the tighter and heavier, the less this scrap has faded.
function layPatch(o) {
const pts = patchOutline(o.x, o.y, o.w, o.h, o.ang, o.wob);
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.3, 0.25);
brush.fill(o.col, 30 + random(0, 14));
traceShape(pts, 0.2, randomGaussian(0, 5), randomGaussian(0, 5));
brush.fillBleed(0.12, "out");
brush.fillTexture(0.7, 0.85);
brush.fill(o.col, o.alpha + random(-8, 8));
traceShape(pts, 0.1, 0, 0);
brush.fill(o.alt, o.alpha * 0.68 + random(-6, 14));
traceShape(pts, 0.12, randomGaussian(0, 2.6), randomGaussian(0, 2.6));
brush.noFill();
brush.hatchStyle("cpencil", o.col, o.wt);
brush.hatch(o.warp, o.ang + 86 + random(-3, 3), { rand: 0.14, continuous: true, gradient: 0.3 });
traceShape(pts, 0.1, 0, 0);
brush.hatchStyle("cpencil", o.alt, o.wt * 0.92);
brush.hatch(o.warp * 1.15, o.ang + 3 + random(-3, 3), { rand: 0.2, continuous: true, gradient: 0.4 });
traceShape(pts, 0.1, 0, 0);
brush.noHatch();
// a cut edge, drawn along two sides only — cloth is hemmed, not outlined
brush.set("2H", o.col, 0.3 + o.wt * 0.22);
brush.beginShape(0.1);
for (let i = 0; i < 5; i++) brush.vertex(pts[i][0], pts[i][1], 1);
brush.endShape(false);
return pts;
}
// The seam of an older patch, still visible under the newer cloth sewn over it.
function seam(x0, y0, x1, y1, col, wt) {
brush.noFill();
brush.set("2B", col, wt);
brush.beginShape(0.4);
brush.vertex(x0, y0);
brush.vertex(lerp(x0, x1, 0.34) + randomGaussian(0, 4), lerp(y0, y1, 0.34) + randomGaussian(0, 4));
brush.vertex(lerp(x0, x1, 0.7) + randomGaussian(0, 4), lerp(y0, y1, 0.7) + randomGaussian(0, 4));
brush.vertex(x1, y1);
brush.endShape(false);
}
// A row of running stitch. It does not stop at the patch edge — that is the whole point.
function stitchRow(x0, y0, x1, y1, step, col, alt, wt, tip) {
const len = Math.hypot(x1 - x0, y1 - y0);
const ux = (x1 - x0) / len;
const uy = (y1 - y0) / len;
const n = Math.min(60, Math.floor(len / step));
brush.noFill();
brush.set(tip, col, wt);
for (let i = 0; i < n; i++) {
const t = i * step + randomGaussian(0, step * 0.09);
const d = step * (0.5 + random(0, 0.22));
const off = randomGaussian(0, 1.7);
const ax = x0 + ux * t - uy * off;
const ay = y0 + uy * t + ux * off;
if (random() < 0.06) continue; // a dropped stitch
brush.stroke(random() < 0.24 ? alt : col);
brush.strokeWeight(wt * (0.82 + random(0, 0.4)));
brush.line(ax, ay, ax + ux * d + randomGaussian(0, 1), ay + uy * d + randomGaussian(0, 1));
}
}
// Loose warp threads escaping a cut edge.
function fray(x, y, dir, len) {
brush.noFill();
brush.set("2H", random() < 0.62 ? INDIGO : WORN, 0.9);
brush.field("hand");
brush.flowLine(x, y, len, dir + randomGaussian(0, 7));
brush.noField();
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(HEMP);
// Indigo that has bled out of the pile into the bare hemp — only along the diagonal.
brush.noStroke();
brush.fillBleed(0.72, "out");
brush.fillTexture(0.3, 0.3);
brush.fill(WORN, 16);
brush.circle(150, 690, 208, true);
brush.fill(WORN, 13);
brush.circle(352, 498, 196, true);
brush.fill(PALE, 15);
brush.circle(568, 306, 236, true);
brush.fill(INDIGO, 11);
brush.circle(690, 190, 148, true);
// --- the pile, back to front -------------------------------------------------
// the oldest, palest, largest scrap: upper-right of centre, the piece everything else is sewn onto
const pPale = layPatch({ x: 540, y: 314, w: 286, h: 248, ang: -7, col: PALE, alt: WORN, alpha: 72, wob: 5.5, warp: 8, wt: 0.75 });
// a broad mid-indigo field carrying the middle of the diagonal
const pMid = layPatch({ x: 384, y: 442, w: 208, h: 182, ang: -5, col: INDIGO, alt: WORN, alpha: 100, wob: 5, warp: 5.4, wt: 1.15 });
// two worn scraps stepping down to the left
layPatch({ x: 246, y: 476, w: 146, h: 134, ang: 15, col: WORN, alt: PALE, alpha: 80, wob: 4.5, warp: 7, wt: 0.85 });
layPatch({ x: 206, y: 636, w: 196, h: 164, ang: 7, col: INDIGO, alt: INK, alpha: 105, wob: 5, warp: 5, wt: 1.2 });
// the corner scrap, running off two edges — deepest indigo, most recently dyed
layPatch({ x: 84, y: 748, w: 216, h: 178, ang: -9, col: INK, alt: INDIGO, alpha: 120, wob: 5.5, warp: 4.2, wt: 1.4 });
// an offshoot breaking the diagonal downward so it is a pile, not a stripe
layPatch({ x: 480, y: 556, w: 132, h: 122, ang: 19, col: WORN, alt: INDIGO, alpha: 88, wob: 4, warp: 6, wt: 0.9 });
// a hard new patch over the middle seam
layPatch({ x: 306, y: 552, w: 158, h: 142, ang: -13, col: INK, alt: INDIGO, alpha: 112, wob: 5, warp: 4.6, wt: 1.3 });
// top-right scrap, half off the canvas
layPatch({ x: 706, y: 172, w: 158, h: 148, ang: 11, col: INDIGO, alt: INK, alpha: 105, wob: 5, warp: 5, wt: 1.15 });
// a bridging scrap, so the diagonal never breaks
layPatch({ x: 452, y: 388, w: 132, h: 120, ang: -19, col: INDIGO, alt: WORN, alpha: 92, wob: 4.5, warp: 6, wt: 1 });
// one madder scrap, the only warm thing in the cloth
layPatch({ x: 612, y: 392, w: 88, h: 80, ang: -21, col: MADDER, alt: WORN, alpha: 102, wob: 3.5, warp: 6, wt: 0.7 });
// the darkest, newest scrap, sitting on the pale patch's lower-left shoulder
layPatch({ x: 442, y: 300, w: 104, h: 96, ang: 24, col: INK, alt: INDIGO, alpha: 128, wob: 4, warp: 4, wt: 1.5 });
// --- old seams, showing through -----------------------------------------------
seam(pPale[0][0], pPale[0][1], pPale[2][0], pPale[2][1], INDIGO, 0.95);
seam(pPale[4][0], pPale[4][1], pPale[6][0], pPale[6][1], INDIGO, 0.8);
seam(pMid[0][0], pMid[0][1], pMid[3][0], pMid[3][1], INK, 1.05);
seam(210, 720, 322, 620, INK, 0.9);
seam(268, 546, 402, 470, INK, 1.15);
seam(430, 352, 522, 250, INK, 0.85);
seam(636, 236, 736, 138, INK, 0.85);
seam(120, 660, 44, 780, WORN, 1);
// --- sashiko: dark thread, and it never stops at a seam ------------------------
stitchRow(6, 770, 244, 720, 17, INK, INDIGO, 1.75, "pen");
stitchRow(30, 722, 268, 674, 16, INK, WORN, 1.6, "pen");
stitchRow(48, 680, 296, 630, 17, INK, INDIGO, 1.8, "pen");
stitchRow(74, 642, 328, 590, 16, INDIGO, INK, 1.65, "pen");
stitchRow(94, 606, 372, 552, 17, INK, INDIGO, 1.85, "pen");
stitchRow(140, 570, 424, 516, 16, INK, WORN, 1.6, "pen");
stitchRow(176, 528, 470, 474, 17, INK, INDIGO, 1.75, "pen");
stitchRow(198, 488, 508, 432, 16, INDIGO, INK, 1.6, "pen");
stitchRow(238, 450, 546, 392, 17, INK, INDIGO, 1.7, "pen");
stitchRow(316, 410, 626, 350, 16, INK, INDIGO, 1.8, "pen");
stitchRow(340, 370, 664, 310, 17, INDIGO, INK, 1.6, "pen");
stitchRow(368, 330, 692, 270, 16, INK, INDIGO, 1.85, "pen");
stitchRow(398, 290, 716, 232, 17, INDIGO, INK, 1.65, "pen");
stitchRow(438, 250, 742, 192, 16, INK, WORN, 1.75, "pen");
stitchRow(486, 212, 762, 156, 17, INK, INDIGO, 1.7, "pen");
stitchRow(560, 176, 780, 130, 16, INDIGO, INK, 1.55, "pen");
// one row begun much later, in the same direction but out of step
stitchRow(470, 426, 688, 386, 16, INK, WORN, 1.65, "pen");
// the cross-family: fewer, at a steeper angle, tacking the layers down
stitchRow(252, 704, 298, 486, 19, INK, INDIGO, 1.7, "pen");
stitchRow(150, 762, 184, 618, 18, INK, WORN, 1.55, "pen");
stitchRow(424, 502, 468, 306, 19, INK, INDIGO, 1.65, "pen");
stitchRow(566, 424, 604, 196, 20, INK, INDIGO, 1.6, "pen");
stitchRow(668, 348, 700, 168, 19, INDIGO, INK, 1.5, "pen");
stitchRow(624, 400, 652, 240, 18, INK, INDIGO, 1.55, "pen");
// a few rust stitches, where someone ran out of indigo thread
stitchRow(492, 372, 596, 352, 15, MADDER, INK, 1.7, "pen");
stitchRow(268, 618, 348, 600, 15, MADDER, INDIGO, 1.6, "pen");
// --- last darks: worn-through holes, and threads coming loose -------------------
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.7, 0.6);
brush.fill(INK, 205);
brush.circle(292, 566, 11, true);
brush.fill(INK, 186);
brush.circle(178, 700, 8.5, true);
brush.fill(INK, 190);
brush.circle(455, 314, 7.5, true);
brush.fill(INK, 196);
brush.circle(76, 762, 10, true);
brush.fill(INDIGO, 168);
brush.circle(612, 262, 6.5, true);
brush.fill(INK, 176);
brush.circle(350, 472, 9, true);
brush.fill(INK, 158);
brush.circle(498, 432, 7, true);
brush.fill(INK, 170);
brush.circle(666, 238, 8, true);
fray(-4, 690, 174, 40);
fray(6, 812, 96, 34);
fray(316, 712, 58, 38);
fray(340, 704, 44, 28);
fray(214, 424, 296, 38);
fray(180, 440, 250, 32);
fray(500, 620, 84, 42);
fray(524, 606, 58, 34);
fray(690, 262, 108, 40);
fray(704, 246, 86, 30);
fray(414, 208, 252, 40);
fray(398, 226, 236, 32);
fray(668, 300, 128, 34);
fray(650, 468, 122, 30);
fray(388, 348, 214, 34);
fray(58, 668, 316, 38);
}