Where The Teeth Touch
2026 · study
Wear is a kind of record: each tooth keeps the count of everything it has pushed against, and one of them has stopped counting.
two gears meshing in sepia ink, teeth worn unevenly and one crown broken clean off, shadowed flanks hatched
painted by Mistral Medium 3.1p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a worn gear pair meshes right of centre, the great wheel running off the right and
// bottom edges; light comes from the upper left, so every shadowed flank is hatched in sepia while
// the left third is left as bare paper.
// layering: pale washes, hatched shadow, ink profile, then the darkest roots and the chipped tooth.
const PALETTE = [
"#e6dcc3", // ground: aged paper
"#ad8455", // mass: sepia wash
"#6d4a2a", // shadow: hatching
"#2e2318", // ink: iron-gall line and root darks
"#4d6470", // accent: cold light on fresh-broken metal
];
const PAPER = "#e6dcc3";
const WASH = "#ad8455";
const SEPIA = "#6d4a2a";
const INK = "#2e2318";
const STEEL = "#4d6470";
// the great wheel's tooth that has lost its crown
const CHIP = 2;
function makeGear(cx, cy, n, rRoot, rTip, phase) {
const wear = [];
for (let i = 0; i < n; i++) {
const w = 1.2 + 8.6 * noise(i * 0.43 + cx * 0.013) + randomGaussian(0, 1.2);
wear.push(constrain(w, 0, 12));
}
return { cx, cy, n, rRoot, rTip, phase, wear, step: 360 / n };
}
// a point on the gear, never quite where the compass would have put it
function gp(g, aDeg, r) {
const a = aDeg + randomGaussian(0, 0.3);
const rr = r + randomGaussian(0, 0.9);
return [g.cx + cos(a) * rr, g.cy + sin(a) * rr];
}
// squat trapezoid: wide root land, flat crown, the crown lowered by its own wear
function tooth(g, i) {
const a = g.phase + i * g.step;
const s = g.step;
const w = g.wear[i];
const rt = g.rTip - w;
const lead = 0.27 + w * 0.006;
const trail = 0.75 - w * 0.004;
return [
gp(g, a + s * 0.02, g.rRoot),
gp(g, a + s * 0.13, g.rRoot + 2.6),
gp(g, a + s * lead, rt - random(0, 1.6)),
gp(g, a + s * (lead + trail) * 0.5, rt + randomGaussian(0, 0.9)),
gp(g, a + s * trail, rt - w * 0.22),
gp(g, a + s * 0.88, g.rRoot + 2.0),
gp(g, a + s * 0.98, g.rRoot),
];
}
// the same tooth sheared off on a diagonal: the crown is gone, the break is jagged
function chipTooth(g, i) {
const a = g.phase + i * g.step;
const s = g.step;
const h = g.rTip - g.rRoot;
return [
gp(g, a + s * 0.02, g.rRoot),
gp(g, a + s * 0.14, g.rRoot + 2.4),
gp(g, a + s * 0.29, g.rRoot + h * 0.22),
gp(g, a + s * 0.38, g.rRoot + h * 0.55),
gp(g, a + s * 0.47, g.rRoot + h * 0.33),
gp(g, a + s * 0.58, g.rRoot + h * 0.86),
gp(g, a + s * 0.72, g.rTip - 5),
gp(g, a + s * 0.88, g.rRoot + 2.0),
gp(g, a + s * 0.98, g.rRoot),
];
}
function shadowed(g, i) {
const a = g.phase + (i + 0.5) * g.step;
return cos(a) + sin(a) > randomGaussian(-0.12, 0.14);
}
function handArc(cx, cy, r, a0, a1, steps, wob) {
brush.beginShape(0.6);
for (let i = 0; i <= steps; i++) {
const a = lerp(a0, a1, i / steps) + randomGaussian(0, 0.7);
const rr = r * (1 + randomGaussian(0, wob));
brush.vertex(cx + cos(a) * rr, cy + sin(a) * rr);
}
brush.endShape(a1 - a0 > 358);
}
function bodyWash(g) {
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.5, 0.5);
brush.fill(WASH, 74 + random(-8, 8));
brush.circle(g.cx, g.cy, g.rRoot * 0.99, true);
brush.fill(WASH, 58);
brush.circle(g.cx + g.rRoot * 0.18, g.cy + g.rRoot * 0.16, g.rRoot * 0.7, true);
brush.fill(SEPIA, 46);
brush.circle(g.cx + g.rRoot * 0.33, g.cy + g.rRoot * 0.28, g.rRoot * 0.44, true);
}
// the crescent of the disc that turns away from the light, hatched like an engraving
function crescent(g, a0, a1, hAng, dist, alpha, inner) {
const pts = [];
for (let i = 0; i <= 20; i++) {
const a = lerp(a0, a1, i / 20);
const r = g.rRoot * (0.985 + randomGaussian(0, 0.008));
pts.push([g.cx + cos(a) * r, g.cy + sin(a) * r]);
}
for (let i = 20; i >= 0; i--) {
const a = lerp(a0, a1, i / 20);
const r = g.rRoot * (inner + 0.16 * noise(i * 0.28 + 4));
pts.push([g.cx + cos(a) * r, g.cy + sin(a) * r]);
}
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fillTexture(0.45, 0.5);
brush.hatchStyle("HB", SEPIA, 1.1);
brush.hatch(dist, hAng, { rand: 0.15 });
brush.fill(SEPIA, alpha);
brush.polygon(pts);
brush.noHatch();
}
function flankShade(g, i, pts, dist) {
const a = g.phase + i * g.step;
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fillTexture(0.4, 0.6);
brush.hatchStyle("HB", SEPIA, 1.0);
brush.hatch(dist + random(-0.3, 0.4), 52 + randomGaussian(0, 4), { rand: 0.2 });
brush.fill(SEPIA, 128 + random(-18, 22));
brush.polygon([
pts[3],
pts[4],
pts[5],
gp(g, a + g.step * 0.5, g.rRoot * 0.86),
]);
brush.noHatch();
}
function inkProfile(pts, w, name) {
brush.noFill();
brush.noHatch();
brush.set(name, INK, w);
brush.beginShape(0.05);
for (let k = 0; k < pts.length; k++) brush.vertex(pts[k][0], pts[k][1]);
brush.endShape(false);
}
// re-inking: the contour is gone over again where the light dies
function reInk(pts) {
brush.noFill();
brush.set("2B", INK, 2.3 + random(0, 0.6));
brush.beginShape(0.05);
brush.vertex(pts[3][0], pts[3][1]);
brush.vertex(pts[4][0], pts[4][1]);
brush.vertex(pts[5][0], pts[5][1]);
brush.endShape(false);
}
function spoke(g, a, halfIn, halfOut) {
brush.noFill();
brush.noHatch();
brush.set("cpencil", INK, 1.8 + random(0, 0.5));
for (let s = -1; s <= 1; s += 2) {
brush.beginShape(0.35);
for (let k = 0; k <= 3; k++) {
const t = k / 3;
const r = lerp(g.rRoot * 0.29, g.rRoot * 0.95, t);
const ha = lerp(halfIn, halfOut, t) * s;
brush.vertex(g.cx + cos(a + ha) * r + random(-1.6, 1.6), g.cy + sin(a + ha) * r + random(-1.6, 1.6));
}
brush.endShape(false);
}
// the trailing side of the spoke takes a slab of hatching
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.hatchStyle("HB", SEPIA, 0.95);
brush.hatch(3.4, 52 + randomGaussian(0, 3), { rand: 0.2 });
brush.fill(SEPIA, 112);
brush.polygon([
[g.cx + cos(a + halfIn) * g.rRoot * 0.31, g.cy + sin(a + halfIn) * g.rRoot * 0.31],
[g.cx + cos(a + halfOut) * g.rRoot * 0.93, g.cy + sin(a + halfOut) * g.rRoot * 0.93],
[g.cx + cos(a + halfOut * 0.2) * g.rRoot * 0.93, g.cy + sin(a + halfOut * 0.2) * g.rRoot * 0.93],
[g.cx + cos(a - halfIn * 0.1) * g.rRoot * 0.31, g.cy + sin(a - halfIn * 0.1) * g.rRoot * 0.31],
]);
brush.noHatch();
}
function turnArrow(cx, cy, r, a0, a1, sense) {
brush.noFill();
brush.noHatch();
brush.set("rotring", SEPIA, 1.3);
handArc(cx, cy, r, a0, a1, 9, 0.012);
const tx = cx + cos(a1) * r;
const ty = cy + sin(a1) * r;
const tang = a1 + 90 * sense;
brush.line(tx, ty, tx - cos(tang + 25) * 15, ty - sin(tang + 25) * 15);
brush.line(tx, ty, tx - cos(tang - 25) * 13, ty - sin(tang - 25) * 13);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
const big = makeGear(652, 512, 22, 208, 236, 7.1);
const pin = makeGear(424, 259, 11, 96, 124, 15.3);
// a handful of teeth taken down further than the rest, by hand
big.wear[3] = 11.4;
big.wear[4] = 9.2;
big.wear[5] = 3.1;
big.wear[12] = 10.9;
big.wear[13] = 5.2;
big.wear[19] = 11.2;
pin.wear[2] = 10.6;
pin.wear[6] = 8.4;
pin.wear[9] = 11.4;
// washes: the air the mechanism sits in, all of it right of the left third
brush.noStroke();
brush.fillBleed(0.62, "out");
brush.fillTexture(0.42, 0.4);
brush.fill(WASH, 24);
brush.circle(648, 498, 238, true);
brush.fill(WASH, 17);
brush.circle(436, 268, 156, true);
brush.fill(SEPIA, 30);
brush.circle(752, 690, 172, true);
brush.fill(SEPIA, 22);
brush.circle(596, 716, 142, true);
brush.fill(WASH, 16);
brush.circle(768, 206, 118, true);
brush.fill(SEPIA, 14);
brush.circle(510, 404, 98, true);
brush.fill(SEPIA, 12);
brush.circle(452, 620, 84, true);
// construction: the line of centres, faint, carried a little past the pinion
brush.noFill();
brush.set("2H", SEPIA, 0.8);
brush.line(650, 510, 428, 262);
brush.set("2H", SEPIA, 0.6);
brush.line(404, 236, 344, 168);
brush.set("2H", SEPIA, 0.9);
brush.line(630, 512, 674, 512);
brush.line(652, 490, 652, 534);
brush.line(404, 259, 444, 259);
brush.line(424, 240, 424, 278);
bodyWash(big);
bodyWash(pin);
crescent(big, -48, 140, 52, 5.2, 78, 0.4);
crescent(big, 2, 106, 52, 3.4, 92, 0.62);
crescent(pin, -40, 134, 49, 4.0, 82, 0.36);
crescent(pin, 6, 100, 49, 2.8, 96, 0.58);
// teeth: shaded flanks first, then the pen profile over the hatching
for (let i = 0; i < big.n; i++) {
const pts = i === CHIP ? chipTooth(big, i) : tooth(big, i);
const dark = shadowed(big, i);
if (dark) flankShade(big, i, pts, 3.0);
inkProfile(pts, i === CHIP ? 2.2 : 1.5 + random(0, 0.5), "pen");
if (dark) reInk(pts);
}
for (let i = 0; i < pin.n; i++) {
const pts = tooth(pin, i);
const dark = shadowed(pin, i);
if (dark) flankShade(pin, i, pts, 2.7);
inkProfile(pts, 1.4 + random(0, 0.5), "pen");
if (dark) reInk(pts);
}
// root circles, broken the way a hand breaks them
brush.noFill();
brush.set("pen", INK, 1.3);
handArc(big.cx, big.cy, big.rRoot, 6, 122, 12, 0.006);
brush.set("pen", INK, 1.0);
handArc(big.cx, big.cy, big.rRoot, 150, 246, 10, 0.007);
brush.set("2B", INK, 1.9);
handArc(big.cx, big.cy, big.rRoot, 264, 352, 10, 0.006);
brush.set("pen", INK, 1.15);
handArc(pin.cx, pin.cy, pin.rRoot, 20, 148, 11, 0.008);
brush.set("2B", INK, 1.6);
handArc(pin.cx, pin.cy, pin.rRoot, 178, 336, 12, 0.007);
// three spokes, deliberately unevenly spaced
spoke(big, 27, 7.6, 4.4);
spoke(big, 133, 8.8, 5.1);
spoke(big, 251, 6.7, 4.0);
// hub, boss and squared bore of the great wheel
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.3, "out");
brush.fill(WASH, 128);
brush.circle(big.cx + 3, big.cy + 3, 58, true);
brush.fill(SEPIA, 96);
brush.circle(big.cx + 12, big.cy + 11, 40, true);
brush.noFill();
brush.set("pen", INK, 1.8);
handArc(big.cx, big.cy, 56, 0, 360, 22, 0.02);
brush.set("pen", INK, 1.2);
handArc(big.cx, big.cy, 39, 0, 360, 18, 0.03);
brush.noStroke();
brush.fillBleed(0.12, "out");
brush.fill(INK, 196);
brush.polygon([[636, 496], [670, 490], [676, 524], [642, 530]]);
brush.noFill();
brush.set("pen", INK, 1.7);
brush.beginShape(0.05);
brush.vertex(635, 495);
brush.vertex(670, 489);
brush.vertex(677, 525);
brush.vertex(641, 531);
brush.endShape(true);
// pinion hub: solid, smaller bore
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fill(WASH, 120);
brush.circle(pin.cx + 2, pin.cy + 3, 33, true);
brush.noFill();
brush.set("pen", INK, 1.6);
handArc(pin.cx, pin.cy, 31, 0, 360, 18, 0.025);
brush.noStroke();
brush.fill(INK, 186);
brush.polygon([[414, 247], [436, 244], [439, 267], [417, 270]]);
brush.noFill();
brush.set("pen", INK, 1.5);
brush.beginShape(0.05);
brush.vertex(413, 246);
brush.vertex(436, 243);
brush.vertex(440, 268);
brush.vertex(416, 271);
brush.endShape(true);
// the darkest ink, saved for the pockets where the teeth actually touch
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fill(INK, 190);
brush.circle(477, 366, 7.4, true);
brush.fill(INK, 168);
brush.circle(521, 324, 6.0, true);
brush.fill(INK, 200);
brush.circle(499, 345, 5.2, true);
brush.fill(INK, 150);
brush.circle(458, 387, 4.4, true);
brush.noFill();
brush.set("2B", INK, 2.6);
brush.line(462, 381, 488, 355);
brush.set("2B", INK, 2.2);
brush.line(506, 338, 531, 313);
brush.set("2B", INK, 1.6);
brush.line(451, 393, 470, 374);
brush.line(536, 305, 550, 291);
// the chip: cold light on the fracture, and two flakes gone loose above it
brush.noStroke();
brush.fillBleed(0.24, "out");
brush.fillTexture(0.5, 0.6);
brush.fill(STEEL, 118);
brush.polygon([[598, 288], [614, 268], [626, 284], [608, 300]]);
brush.noFill();
brush.set("rotring", STEEL, 1.4);
brush.line(597, 290, 615, 267);
brush.line(609, 301, 627, 285);
brush.set("2B", INK, 2.4);
brush.line(596, 292, 616, 266);
brush.noStroke();
brush.fill(INK, 140);
brush.circle(654, 224, 3.4, true);
brush.fill(STEEL, 124);
brush.circle(676, 202, 2.6, true);
// two short annotations of the sense of rotation
turnArrow(big.cx, big.cy, 142, 148, 206, 1);
turnArrow(pin.cx, pin.cy, 62, 252, 190, -1);
}