Running Stitch, Worn Through
2026 · textile
A quilt is a ledger of small decisions; I wanted the hand's hesitation to be as legible as its rhythm, and the hole to be the most honest thing on the cloth.
kantha running stitch crossing layered old cloth in horizontal bands of shifting density, the quilting lines rippling the surface, broken open by one worn patch at the lower left
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: clusters of running stitch cross layered cloth in horizon bands, the mass held upper-middle-left
// and falling away to bare paper at the lower right; one worn patch opens on the lower-left third intersection
// layering: indigo and ochre cloth washes first, then bare weave and seams, then cluster after cluster of stitches,
// and last the frayed hole, the loose warp threads and a handful of madder marks
const PALETTE = [
"#e6ddcb", // ground — bleached old cotton
"#8d9aa6", // wash — faded indigo cloth
"#2f4155", // mass — indigo stitch thread
"#1c2836", // shadow — the darkest ends and knots
"#7a6449", // shadow — brown thread, the layer beneath
"#b4562f", // accent — madder red, rationed
];
const GROUND = PALETTE[0];
const WASH = PALETTE[1];
const INK = PALETTE[2];
const DEEP = PALETTE[3];
const THREAD = PALETTE[4];
const MADDER = PALETTE[5];
// the worn patch — the one place the cloth has given way
const WORN_X = 252;
const WORN_Y = 548;
const WORN_R = 118;
// each row of stitching: y, x from, x to, pitch, stitch length, weight, ripple group, madder share
// rows come in clusters — four to seven close rows read as a band, and the gaps between clusters breathe
const ROWS = [
[116, -14, 612, 19, 11.5, 2.0, 0, 0.0],
[129, -22, 588, 18, 11.0, 2.3, 0, 0.0],
[141, -10, 626, 20, 12.0, 1.8, 0, 0.0],
[154, -18, 566, 17, 10.5, 2.1, 0, 0.03],
[220, -20, 742, 18, 11.5, 2.4, 1, 0.0],
[231, -12, 758, 17, 11.0, 2.0, 1, 0.0],
[243, -24, 720, 19, 12.0, 2.5, 1, 0.0],
[254, -14, 748, 18, 11.0, 2.2, 1, 0.02],
[266, -18, 704, 17, 10.5, 2.6, 1, 0.0],
[277, -10, 730, 20, 12.5, 1.9, 1, 0.0],
[289, -22, 676, 18, 11.0, 2.3, 1, 0.0],
[344, -16, 690, 18, 11.5, 2.2, 2, 0.0],
[356, -10, 664, 19, 12.0, 2.5, 2, 0.0],
[368, -20, 702, 17, 10.5, 2.0, 2, 0.04],
[380, -12, 646, 18, 11.0, 2.3, 2, 0.0],
[432, -18, 572, 20, 12.0, 2.1, 3, 0.0],
[446, -10, 546, 19, 11.5, 2.4, 3, 0.14],
[459, -22, 592, 21, 12.5, 1.9, 3, 0.0],
[508, -14, 486, 18, 11.0, 2.3, 4, 0.0],
[520, -20, 462, 17, 10.5, 2.0, 4, 0.0],
[533, -10, 502, 19, 11.5, 2.5, 4, 0.0],
[545, -18, 470, 18, 11.0, 2.2, 4, 0.0],
[558, -12, 448, 17, 10.5, 2.4, 4, 0.0],
[570, -22, 492, 20, 12.0, 1.9, 4, 0.0],
[634, -16, 356, 21, 12.0, 2.1, 5, 0.0],
[650, -10, 322, 22, 12.5, 1.8, 5, 0.0],
[714, -20, 264, 24, 12.5, 1.9, 6, 0.0],
];
// single sparse rows wandering through the gaps between clusters, all on the left of the sheet
const STRAY_ROWS = [
[196, -14, 524, 27, 13.0, 1.7, 0, 0.0],
[314, -18, 468, 31, 14.0, 1.5, 2, 0.0],
[406, -12, 424, 29, 13.5, 1.6, 3, 0.0],
[488, -16, 384, 26, 12.5, 1.8, 4, 0.06],
[598, -10, 306, 29, 13.0, 1.6, 5, 0.0],
[676, -14, 236, 32, 13.5, 1.5, 6, 0.0],
];
// the quilting pulls the surface into long slow waves, with a finer tremor riding on them
function ripple(x, group, amp) {
const slow = (noise(x * 0.0038, group * 1.61) - 0.5) * 2 * amp;
const fine = (noise(x * 0.016, group * 3.07) - 0.5) * 2 * amp * 0.34;
return slow + fine;
}
function threadColour(madderShare) {
if (random() < madderShare) return MADDER;
const r = random();
if (r < 0.6) return INK;
if (r < 0.78) return DEEP;
if (r < 0.94) return THREAD;
return WASH;
}
// how much of a stitch survives here: 1 out on sound cloth, near nothing in the worn heart
function survival(x, y) {
const d = Math.hypot(x - WORN_X, (y - WORN_Y) * 1.18);
if (d > WORN_R) return 1;
return constrain(map(d, WORN_R * 0.34, WORN_R, 0.02, 1), 0.02, 1);
}
function stitch(x, y, ang, len, col, w) {
const hx = (Math.cos(ang) * len) / 2;
const hy = (Math.sin(ang) * len) / 2;
brush.set(random() < 0.25 ? "HB" : "2B", col, w);
brush.line(x - hx, y - hy, x + hx, y + hy);
}
function stitchRow(r) {
const y0 = r[0];
const x0 = r[1];
const x1 = r[2];
const pitch = r[3];
const len = r[4];
const wt = r[5];
const group = r[6];
const madder = r[7];
const amp = 11 + group * 1.6;
const phase = random(0, pitch);
const tilt = randomGaussian(0, 0.009); // no row is truly level
const count = Math.floor((x1 - x0) / pitch);
for (let i = 0; i < count; i++) {
const x = x0 + phase + i * pitch + randomGaussian(0, pitch * 0.16);
const ya = y0 + ripple(x, group, amp) + (x - x0) * tilt;
const yb = y0 + ripple(x + len, group, amp) + (x + len - x0) * tilt;
const s = survival(x, ya);
if (random() > s) continue;
if (random() < 0.06) continue; // the hand skips a beat
const worn = s < 0.85;
const ang = Math.atan2(yb - ya, len) + randomGaussian(0, 0.06);
const l = Math.max(4.5, randomGaussian(len * 1.32, len * 0.2)) * (worn ? 0.58 : 1);
const col = worn ? (random() < 0.7 ? THREAD : WASH) : threadColour(madder);
stitch(x, ya, ang, l, col, Math.max(0.7, randomGaussian(worn ? wt * 0.66 : wt * 0.82, 0.18)));
}
}
function clothWash(x, y, rad, col, alpha, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.55 + random(-0.12, 0.12), 0.5);
brush.fill(col, alpha);
brush.circle(x, y, rad, true);
}
// a panel of cloth: a torn-edged quadrilateral laid down in several thin passes, never twice the same edge
function clothPatch(x0, y0, x1, y1, col, alpha, passes) {
for (let p = 0; p < passes; p++) {
const pts = [];
for (let i = 0; i <= 5; i++) {
pts.push([lerp(x0, x1, i / 5) + randomGaussian(0, 10), y0 + randomGaussian(0, 12)]);
}
for (let i = 1; i <= 3; i++) {
pts.push([x1 + randomGaussian(0, 14), lerp(y0, y1, i / 3) + randomGaussian(0, 10)]);
}
for (let i = 1; i <= 5; i++) {
pts.push([lerp(x1, x0, i / 5) + randomGaussian(0, 10), y1 + randomGaussian(0, 12)]);
}
for (let i = 1; i < 3; i++) {
pts.push([x0 + randomGaussian(0, 14), lerp(y1, y0, i / 3) + randomGaussian(0, 10)]);
}
brush.noStroke();
brush.fillBleed(0.16 + random(0, 0.1), "out");
brush.fillTexture(0.62 + random(-0.12, 0.1), 0.45);
brush.fill(col, Math.max(28, randomGaussian(alpha, 18)));
brush.polygon(pts);
}
}
function fray(x, y, len, dir) {
brush.noFill();
brush.set("2H", random() < 0.45 ? THREAD : WASH, 0.8 + random(0, 0.5));
brush.beginShape(0.7);
brush.vertex(x, y);
brush.vertex(x + Math.cos(dir) * len * 0.45 + random(-3, 3), y + Math.sin(dir) * len * 0.45 + random(-3, 3));
brush.vertex(x + Math.cos(dir + 0.55) * len * 0.85 + random(-4, 4), y + Math.sin(dir + 0.45) * len * 0.9 + random(-4, 4));
brush.endShape(false);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(GROUND);
// --- the cloth itself: faded indigo soaked heavy through the left and middle, thinning to the right
clothPatch(-40, 92, 452, 318, WASH, 132, 3); // the big upper panel, running off the left edge
clothPatch(300, 128, 706, 302, WASH, 82, 2); // a paler length laid over it to the right
clothPatch(-36, 296, 396, 474, WASH, 126, 3); // the middle panel, the darkest cloth
clothPatch(342, 328, 634, 470, WASH, 70, 2);
clothPatch(430, 414, 606, 500, WASH, 48, 2);
clothPatch(-30, 466, 414, 692, THREAD, 96, 3); // the older brown cloth, lower left
clothPatch(128, 480, 372, 636, THREAD, 80, 2); // and thicker where it has worn through
clothPatch(-24, 640, 296, 748, THREAD, 54, 2);
clothPatch(96, 176, 668, 226, WASH, 44, 2); // one narrow strip crossing the whole width
clothWash(WORN_X - 12, WORN_Y + 6, 100, THREAD, 74, 0.34);
clothWash(WORN_X + 36, WORN_Y - 32, 60, THREAD, 60, 0.32);
// --- seams where one length of cloth is laid over the next
brush.set("cpencil", WASH, 1.6);
brush.beginShape(0.5);
brush.vertex(-12, 186);
brush.vertex(226, 196);
brush.vertex(472, 182);
brush.vertex(694, 194);
brush.endShape(false);
brush.set("cpencil", THREAD, 1.4);
brush.beginShape(0.5);
brush.vertex(-16, 486);
brush.vertex(192, 496);
brush.vertex(404, 480);
brush.vertex(534, 492);
brush.endShape(false);
brush.set("cpencil", THREAD, 1.1);
brush.beginShape(0.55);
brush.vertex(-14, 608);
brush.vertex(148, 618);
brush.vertex(312, 604);
brush.vertex(416, 616);
brush.endShape(false);
// --- the stitching, cluster by cluster
for (let i = 0; i < ROWS.length; i++) stitchRow(ROWS[i]);
for (let i = 0; i < STRAY_ROWS.length; i++) stitchRow(STRAY_ROWS[i]);
// --- the worn patch: the top cloth gone, its stitches broken, threads let loose
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.62, 0.45);
brush.fill(GROUND, 205);
brush.circle(WORN_X - 8, WORN_Y + 4, 60, true);
brush.fill(GROUND, 178);
brush.circle(WORN_X + 32, WORN_Y - 26, 40, true);
brush.fill(GROUND, 150);
brush.circle(WORN_X - 44, WORN_Y + 32, 34, true);
brush.fill(THREAD, 120);
brush.circle(WORN_X - 58, WORN_Y - 28, 26, true);
brush.fill(THREAD, 96);
brush.circle(WORN_X + 46, WORN_Y + 40, 22, true);
brush.noFill();
for (let i = 0; i < 40; i++) {
const a = random(0, Math.PI * 2);
const rad = Math.max(40, randomGaussian(66, 14));
fray(WORN_X + Math.cos(a) * rad, WORN_Y + Math.sin(a) * rad * 0.9, Math.max(9, randomGaussian(19, 6)), a);
}
// the rim of the hole, given only where the eye needs it
brush.set("2B", THREAD, 1.9);
brush.beginShape(0.6);
brush.vertex(152, 516);
brush.vertex(184, 486);
brush.vertex(230, 474);
brush.endShape(false);
brush.set("2B", THREAD, 1.6);
brush.beginShape(0.6);
brush.vertex(324, 526);
brush.vertex(338, 566);
brush.vertex(318, 602);
brush.endShape(false);
brush.set("2B", THREAD, 1.7);
brush.beginShape(0.6);
brush.vertex(276, 628);
brush.vertex(224, 638);
brush.vertex(178, 618);
brush.endShape(false);
// loose warp threads still bridging the hole
brush.set("2H", THREAD, 1.3);
brush.line(176, 524, 268, 534);
brush.set("2H", DEEP, 1.1);
brush.line(192, 562, 292, 552);
brush.set("2H", THREAD, 1.2);
brush.line(210, 592, 304, 580);
brush.set("2H", THREAD, 1.0);
brush.line(230, 500, 314, 508);
// the last stitches before the cloth gave out, broken off around the rim
brush.set("2B", DEEP, 1.8);
brush.line(160, 494, 173, 496);
brush.set("2B", THREAD, 2.0);
brush.line(196, 478, 210, 481);
brush.set("2B", DEEP, 1.6);
brush.line(300, 498, 313, 500);
brush.set("2B", THREAD, 1.9);
brush.line(342, 546, 353, 549);
brush.set("2B", DEEP, 1.7);
brush.line(288, 622, 300, 619);
brush.set("2B", THREAD, 1.8);
brush.line(206, 644, 219, 641);
brush.set("2B", DEEP, 1.5);
brush.line(148, 574, 159, 576);
// --- last: dark thread ends and knots through the sound cloth
brush.noStroke();
brush.fillBleed(0.3, "out");
for (let i = 0; i < 34; i++) {
const bx = constrain(randomGaussian(288, 196), 10, 716);
const by = constrain(randomGaussian(272, 132), 100, 470);
if (survival(bx, by) < 0.7) continue;
brush.fill(DEEP, 168 + random(-34, 50));
brush.circle(bx, by, Math.max(1.3, randomGaussian(2.4, 0.7)), true);
}
brush.noFill();
// four long dark stitches, the strongest marks, pinning the dense band
brush.set("2B", DEEP, 3.1);
brush.line(232, 252, 268, 255);
brush.set("2B", DEEP, 2.8);
brush.line(406, 268, 442, 264);
brush.set("2B", DEEP, 3.0);
brush.line(126, 288, 164, 291);
brush.set("2B", DEEP, 2.7);
brush.line(552, 232, 586, 236);
// madder marks, loose above and beside the worn patch — the only warm thread that carries
brush.set("2B", MADDER, 2.6);
brush.line(300, 462, 326, 465);
brush.set("2B", MADDER, 2.3);
brush.line(206, 428, 230, 431);
brush.set("2B", MADDER, 2.1);
brush.line(362, 494, 384, 496);
brush.set("2B", MADDER, 2.0);
brush.line(154, 380, 176, 377);
}