Out of Register
2026 · textile
The blur is not a flaw in the weave; it is the only proof that the pattern was tied by hand before anyone knew what it would become.
an indigo ikat cloth where the resist has bled, the vertical repeat crowding the left selvedge and one column slipping out of alignment
painted by Grok 4.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: a warp-ikat repeat marching down the cloth, motifs large and crowded against the left
// selvedge and shrinking to bare cotton on the right; the fourth column has slipped out of register and
// prints twice. layering: dye-bath washes and warp threads first, then the repeat back-to-front (palest
// right column first), and last the madder hooks and near-black ink ticks that hold the slipped column.
const PALETTE = [
"#e9e2d3", // ground — undyed cotton
"#f3ece0", // resist — the core the dye never reached
"#57628f", // wash — indigo run thin
"#33406e", // mass — indigo body
"#1a2140", // shadow — near-black indigo ink
"#a53f2b", // accent — madder, rationed
];
// eight points around a waisted lozenge, every one nudged off true
function motifOutline(cx, cy, w, h) {
const k = [
[0, -0.5], [0.34, -0.24], [0.5, -0.02], [0.33, 0.23],
[0, 0.5], [-0.33, 0.24], [-0.5, 0.01], [-0.34, -0.23],
];
const pts = [];
for (let i = 0; i < 8; i++) {
pts.push([
cx + k[i][0] * w + randomGaussian(0, w * 0.045),
cy + k[i][1] * h + randomGaussian(0, h * 0.03),
]);
}
return pts;
}
function dye(base, amt) {
const towards = random() < 0.5 ? "#1a2140" : "#57628f";
return lerpColor(color(base), color(towards), random(0, amt));
}
function bodyColour(depth) {
const r = random();
if (depth) return r < 0.34 ? "#33406e" : r < 0.88 ? "#1a2140" : "#57628f";
return r < 0.62 ? "#33406e" : r < 0.9 ? "#57628f" : "#1a2140";
}
function shape(pts, curve) {
brush.beginShape(curve);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1], 1);
brush.endShape(true);
}
// the whole point of ikat: dye creeps along the warp, so every boundary grows teeth
function feather(pts, cy, col, len) {
brush.noFill();
for (let i = 0; i < 8; i++) {
const a = pts[i];
const b = pts[(i + 1) % 8];
for (let k = 0; k < 3; k++) {
const t = 0.18 + k * 0.31 + random(-0.09, 0.09);
const x = a[0] + (b[0] - a[0]) * t;
const y = a[1] + (b[1] - a[1]) * t;
const dir = y < cy ? -1 : 1;
const L = constrain(randomGaussian(len, len * 0.45), len * 0.25, len * 2.2);
brush.set("2B", col, 0.55 + random(0, 0.7));
brush.line(x + random(-2, 2), y - dir * L * 0.12, x + random(-4, 4), y + dir * L);
}
}
}
// a hand-drawn, deliberately broken edge — never a closed outline
function brokenEdge(pts, col) {
brush.noFill();
brush.set("2B", col, 0.9 + random(0, 0.6));
for (let i = 0; i < 8; i++) {
if (random() < 0.32) continue;
const a = pts[i];
const b = pts[(i + 1) % 8];
brush.line(a[0] + random(-2.5, 2.5), a[1] + random(-2, 2), b[0] + random(-2.5, 2.5), b[1] + random(-2, 2));
}
}
function motif(cx, cy, w, h, alpha, depth, slipped) {
const pts = motifOutline(cx, cy, w, h);
const base = bodyColour(depth);
brush.noStroke();
// a slipped motif prints twice: the failed register lands well down-left of where it was tied
if (slipped) {
brush.fillBleed(0.5, "out");
brush.fillTexture(0.34, 0.4);
const gpts = motifOutline(cx - 30, cy + 34, w * 1.02, h * 0.94);
brush.fill(dye("#57628f", 0.3), alpha * 0.72);
shape(gpts, 0.28);
feather(gpts, cy + 34, dye("#33406e", 0.3), h * 0.2);
}
// body, two passes so the pigment pools unevenly
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fillTexture(0.4, 0.45);
brush.fill(dye(base, 0.22), alpha);
shape(pts, 0.24);
brush.fillBleed(0.34, "out");
brush.fill(dye(base, 0.3), alpha * 0.7);
shape(motifOutline(cx + random(-5, 5), cy + random(-6, 6), w * 0.86, h * 0.9), 0.3);
// on the crowded left the dye sits twice as long: a deep pool crowding one flank
if (depth) {
brush.fillBleed(0.36, "out");
brush.fillTexture(0.2, 0.5);
brush.fill("#1a2140", constrain(alpha * 0.86, 110, 225));
const s = random() < 0.5 ? -1 : 1;
shape(motifOutline(cx + s * w * 0.2, cy + random(-h * 0.12, h * 0.12), w * 0.54, h * 0.7), 0.34);
}
// combed dark: on the left the warp itself shows through the dye, dense and never quite vertical
if (depth && random() < 0.85) {
brush.noFill();
brush.hatchStyle("2B", dye("#1a2140", 0.16), 0.55 + random(0, 0.35));
brush.hatch(slipped ? 5 + random(0, 2) : 6.5 + random(0, 3), random(-4, 4), { rand: 0.38, continuous: false });
shape(motifOutline(cx + random(-5, 5), cy + random(-6, 6), w * 0.8, h * 0.86), 0.3);
brush.noHatch();
}
// reserved heart, off-centre so the resist reads as tied by hand
brush.fillBleed(0.2, "out");
brush.fillTexture(0.4, 0.6);
brush.fill("#f3ece0", constrain(alpha * 1.15, 90, 235));
shape([
[cx + randomGaussian(0, 4), cy - h * 0.2 + randomGaussian(0, 5)],
[cx + w * 0.17 + randomGaussian(0, 4), cy - h * 0.02 + randomGaussian(0, 4)],
[cx + randomGaussian(0, 4), cy + h * 0.21 + randomGaussian(0, 5)],
[cx - w * 0.16 + randomGaussian(0, 4), cy + randomGaussian(0, 4)],
], 0.42);
feather(pts, cy, dye(depth ? "#1a2140" : "#33406e", 0.3), h * 0.26);
brokenEdge(pts, dye(depth ? "#1a2140" : "#33406e", 0.25));
// two long warp bleeds dropping out of the motif
brush.noFill();
brush.set("2H", "#57628f", 0.8);
brush.line(cx + random(-w * 0.3, w * 0.3), cy + h * 0.44, cx + random(-w * 0.34, w * 0.34), cy + h * 0.44 + randomGaussian(h * 0.55, h * 0.18));
brush.line(cx + random(-w * 0.3, w * 0.3), cy - h * 0.44, cx + random(-w * 0.34, w * 0.34), cy - h * 0.44 - randomGaussian(h * 0.45, h * 0.16));
}
// the small mark that sits between motifs in the repeat
function filler(cx, cy, s, alpha) {
brush.noFill();
brush.set("2B", dye("#33406e", 0.35), 0.85);
brush.line(cx - s, cy - s * 0.7, cx + s * 0.9, cy + s * 0.8);
brush.line(cx + s * 0.85, cy - s * 0.8, cx - s * 0.95, cy + s * 0.75);
brush.line(cx + random(-2, 2), cy - s * 1.6, cx + random(-2, 2), cy + s * 1.6);
if (random() < 0.28) {
brush.noStroke();
brush.fillBleed(0.45, "out");
brush.fill("#a53f2b", constrain(alpha * 0.85, 90, 190));
brush.circle(cx + random(-2, 2), cy + random(-2, 2), s * 0.5, true);
}
}
function column(x, y0, step, w, h, n, alpha, drift, depth, slipped) {
for (let i = 0; i < n; i++) {
const cy = y0 + i * step + randomGaussian(0, step * 0.09);
const cx = x + drift * i + randomGaussian(0, 6);
motif(
cx, cy,
w * (1 + randomGaussian(0, 0.08)),
h * (1 + randomGaussian(0, 0.07)),
constrain(alpha + randomGaussian(0, 16), 40, 242),
depth, slipped,
);
if (i < n - 1 && random() < 0.8) {
filler(cx + drift * 0.5 + random(-8, 8), cy + step * 0.5 + random(-8, 8), w * 0.1, alpha);
}
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#e9e2d3");
// the bath: broad indigo soaks weighting the left, thinning to nothing before the right edge
brush.noStroke();
brush.fillBleed(0.9, "out");
brush.fillTexture(0.3, 0.3);
brush.fill("#33406e", 34);
brush.circle(52, 268, 226, true);
brush.fill("#57628f", 30);
brush.circle(126, 596, 244, true);
brush.fill("#33406e", 24);
brush.circle(230, 400, 206, true);
brush.fill("#57628f", 17);
brush.circle(352, 690, 176, true);
brush.fill("#57628f", 11);
brush.circle(496, 214, 162, true);
// warp threads, combed dense at the selvedge and giving out before the right edge
brush.noFill();
for (let i = 0; i < 150; i++) {
const x = constrain(randomGaussian(150, 165), -12, 660);
const keep = 1 - x / 760;
if (random() > keep * keep + 0.05) continue;
const y = random(-40, 300);
const len = randomGaussian(430, 150);
brush.set(random() < 0.5 ? "2H" : "cpencil", random() < 0.25 ? "#33406e" : "#57628f", 0.5 + random(0, 0.5));
brush.line(x, y, x + randomGaussian(0, 7), y + Math.max(120, len));
}
// the selvedge itself, hatched like held threads
brush.noFill();
brush.hatchStyle("cpencil", "#33406e", 0.6);
brush.hatch(8, 91, { rand: 0.18, continuous: false });
brush.rect(-8, 84, 30, 640);
brush.noHatch();
// the repeat, back to front — the far right ghosts first, the left mass last
// every column keeps the same phase but its own interval, so the repeat slowly loses agreement
column(712, 58, 190, 52, 80, 4, 104, -2, false, false);
column(568, 52, 180, 66, 100, 4, 146, 2, false, false);
column(430, 48, 172, 78, 116, 5, 162, -3, false, false);
column(196, 44, 166, 88, 132, 5, 212, -2, true, false);
column(98, 40, 160, 94, 140, 5, 228, 3, true, false);
column(2, 46, 170, 92, 136, 5, 204, 2, true, false);
// one motif twice the size of the rest, run in from the selvedge — the largest gesture
motif(58, 430, 158, 228, 232, true, false);
// the column out of register: half a repeat low, leaning right as it falls, printing twice
column(300, 132, 158, 90, 136, 5, 234, 11, true, true);
// ink last: a handful of ticks along the slipped edges, none quite parallel
brush.noFill();
brush.set("rotring", "#1a2140", 0.8);
brush.line(316, 528, 313, 566);
brush.line(360, 570, 356, 610);
brush.line(300, 634, 304, 676);
brush.line(366, 646, 362, 684);
brush.line(286, 578, 282, 614);
brush.line(60, 176, 63, 218);
brush.line(130, 306, 126, 346);
brush.line(22, 430, 25, 474);
brush.line(172, 534, 168, 574);
brush.line(92, 658, 96, 698);
brush.line(210, 710, 206, 748);
// the darkest pool in the picture, pressed against the slipped heart
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.2, 0.5);
brush.fill("#1a2140", 182);
shape([[318, 618], [336, 646], [316, 682], [300, 648]], 0.34);
// one madder heart burning through the slipped motif — the only warm mark that carries
brush.fillBleed(0.2, "out");
brush.fillTexture(0.3, 0.6);
brush.fill("#a53f2b", 226);
shape([[338, 618], [353, 642], [337, 668], [323, 643]], 0.38);
brush.fillBleed(0.85, "out");
brush.fill("#a53f2b", 58);
brush.circle(356, 676, 7, true);
// two madder dots far off in the sparse right, so the field does not split in half
brush.fillBleed(0.4, "out");
brush.fill("#a53f2b", 124);
brush.circle(582, 292, 5.5, true);
brush.fill("#a53f2b", 82);
brush.circle(716, 512, 4.5, true);
// the bleed that gives the technique away: long soft smears off the bottom edge
brush.fillBleed(0.95, "out");
brush.fillTexture(0.22, 0.25);
brush.fill("#33406e", 30);
brush.circle(176, 792, 162, true);
brush.fill("#57628f", 18);
brush.circle(306, 812, 134, true);
}