Condyle and Socket
2026 · study
A joint is two surfaces agreeing to touch. Draw them apart by a hair and the whole argument becomes visible: the cartilage still standing at the edges, and the small bare place in the middle where sixteen years of walking finally got through.
the condyle of a large femur seated in its socket, drawn with the calm of an engineer: the joint space open at the horns and worn through to bare bone where the two carried the load, one shadow going off into empty paper
painted by Kimi K2.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the condyle of a large femur holds the upper-right thirds crossing, its shaft running
// off the top-right corner as one silhouette; the socket is seated hard against it from below-left,
// and the joint space between the two — open a little at the horns — closes to nothing where the load
// passed, bare bone there and the only colour in the sheet. One shadow trails lower-left into empty
// paper. layering: air, shadow, bone laid opaque, the dome engraved, ink contour, pits last.
const PALETTE = [
"#ece3d0", // ground: warm bench paper
"#c9a97a", // bone: the lit mass
"#8a6f4c", // shade: the turn of the form and the cast shadow
"#3d3427", // umber: the outer face of the socket, the cup floor
"#1c1810", // ink: sepia-black engraving, contour and pits
"#a8512b", // accent: bone laid bare where the cartilage wore through
];
const C_PAPER = "#ece3d0";
const C_BONE = "#c9a97a";
const C_WARM = "#8a6f4c";
const C_DEEP = "#3d3427";
const C_INK = "#1c1810";
const C_RUST = "#a8512b";
const D2R = Math.PI / 180;
const CX = 570; // condyle centre — the upper-right thirds crossing
const CY = 252;
const CR = 126;
const DARK_A = 138; // the angle at which the light leaves the condyle
const LIT_A = -42; // and the angle it comes from
const WORN_A = 152; // the angle at which the two bones bore on each other
const WALL = 52; // thickness of the socket's cut wall
const S0 = 92; // the socket's horns, each one a cut end
const S1 = 200;
// the shaft, leaving the condyle's upper-right shoulder and running off the corner
const SH_A = [636, 178];
const SH_B = [890, -46];
const SH_NX = 0.6620;
const SH_NY = 0.7495;
// the condyle is an ellipsoid, not a ball: the wobble is the same on every pass
function jtRad(a) {
const r = a * D2R;
return CR * (1 + 0.062 * Math.cos(2 * r - 0.7) + 0.036 * Math.sin(3 * r + 1.1) - 0.03 * Math.cos(r));
}
function jtPt(a, k) {
const rr = jtRad(a) * k;
return [CX + Math.cos(a * D2R) * rr, CY + Math.sin(a * D2R) * rr];
}
// n+1 points along either bone: pass a radius factor for the condyle, a wall fraction for the socket
function jtArc(a0, a1, n, k, sock) {
const out = [];
for (let i = 0; i <= n; i++) {
const a = a0 + (a1 - a0) * (i / n);
out.push(sock ? jtSockPt(a, k) : jtPt(a, k));
}
return out;
}
// the joint space: a little open at the horns, worn to nothing where the load passed through
function jtGap(a) {
const d = Math.min(1, Math.abs(a - WORN_A) / 52);
return 3.4 + 15 * Math.pow(d, 1.6);
}
// f measures across the socket wall: 0 at the bearing lip, 1 at the outer surface
function jtSockPt(a, f) {
const rr = jtRad(a) + jtGap(a) + WALL * (1 + 0.16 * Math.cos(2 * a * D2R + 0.4)) * f;
return [CX + Math.cos(a * D2R) * rr, CY + Math.sin(a * D2R) * rr];
}
function jtSockBand(a0, a1, n, f0, f1) {
return jtArc(a0, a1, n, f1, 1).concat(jtArc(a0, a1, n, f0, 1).reverse());
}
// the crescent of joint space still standing between condyle and socket
function jtGlaze(a0, a1, n) {
return jtArc(a0, a1, n, 1.0).concat(jtArc(a0, a1, n, -0.02, 1).reverse());
}
function jtShape(pts, curv, jit, close) {
brush.beginShape(curv);
for (let i = 0; i < pts.length; i++) {
brush.vertex(pts[i][0] + randomGaussian(0, jit), pts[i][1] + randomGaussian(0, jit));
}
brush.endShape(close);
}
// a watercolour fill is translucent whatever its alpha, so real tone is built by repeating the pass
function jtWedgeN(pts, col, alpha, bleed, jit, passes) {
brush.noStroke();
brush.noHatch();
brush.fillTexture(0.3, 0.4);
for (let i = 0; i < passes; i++) {
brush.fillBleed(bleed * (1 + 0.3 * i), "out");
brush.fill(col, alpha);
jtShape(pts, 0, jit * (1 + 0.5 * i), true);
}
}
// a crescent whose inner edge closes onto the outer at both ends: shading has no straight edges
function jtCrescent(a0, a1, n, kIn, kOut) {
const outer = [];
const inner = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const a = a0 + (a1 - a0) * t;
const s = Math.sin(Math.PI * t);
outer.push(jtPt(a, kOut));
inner.push(jtPt(a, kOut - (kOut - kIn) * s));
}
return outer.concat(inner.reverse());
}
// the shaft as a tapering lens along its axis; the far end leaves the paper
function jtShaft(w0, w1, n, jit) {
const dx = SH_B[0] - SH_A[0];
const dy = SH_B[1] - SH_A[1];
const lit = [];
const dark = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const w = (w0 + (w1 - w0) * t) * (1 + randomGaussian(0, 0.03));
const x = SH_A[0] + dx * t;
const y = SH_A[1] + dy * t;
dark.push([x + SH_NX * w + randomGaussian(0, jit), y + SH_NY * w + randomGaussian(0, jit)]);
lit.push([x - SH_NX * w + randomGaussian(0, jit), y - SH_NY * w + randomGaussian(0, jit)]);
}
return { lit: lit, dark: dark };
}
// one closed silhouette for the whole bone: head rim, out along the shaft, back down its underside
function jtHull(sh) {
return jtArc(2, 268, 32, 1.0).concat(sh.lit, sh.dark.slice().reverse());
}
// a tapering lens along an axis — the shape a low light throws across paper
function jtLens(x0, y0, x1, y1, w0, w1, n) {
const dx = x1 - x0;
const dy = y1 - y0;
const m = Math.hypot(dx, dy) || 1;
const nx = -dy / m;
const ny = dx / m;
const side = [];
const back = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const w = (w0 + (w1 - w0) * t * t) * (1 + randomGaussian(0, 0.06));
const x = x0 + dx * t;
const y = y0 + dy * t;
side.push([x + nx * w, y + ny * w]);
back.push([x - nx * w * 0.74, y - ny * w * 0.74]);
}
return side.concat(back.reverse());
}
function jtTurn(a) {
return Math.max(0, 1 - Math.abs(a - DARK_A) / 82);
}
// the dome engraved: latitudes dense and black at the rim, thinning and shortening inward, each one
// stopped short at a different place so the family reads as drawn and not as nested rings
function jtDome(n, mid, kOut, kIn, wide, narrow, w0, w1, dark) {
brush.noFill();
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
const k = kOut - (kOut - kIn) * Math.pow(t, 0.9);
const half = wide + (narrow - wide) * t;
const a0 = mid - half * random(0.58, 1.0) + randomGaussian(0, 4);
const a1 = mid + half * random(0.58, 1.0) + randomGaussian(0, 4);
const seg = Math.max(4, Math.round((a1 - a0) / 15));
const wt = Math.max(0.34, w0 + (w1 - w0) * t + randomGaussian(0, 0.12));
let bn = "2H";
let col = C_WARM;
if (dark) {
bn = t < 0.26 ? "rotring" : t < 0.64 ? "pen" : "2H";
col = t < 0.6 ? C_INK : t < 0.84 ? C_DEEP : C_WARM;
}
brush.set(bn, col, wt);
jtShape(jtArc(a0, a1, seg, k + randomGaussian(0, 0.005)), 0.3, 0.8, false);
}
}
// crossed once on the meridian, which is how black gets made here
function jtMeridians(n, kIn, kOut, spread, w) {
brush.noFill();
for (let i = 0; i < n; i++) {
const a = randomGaussian(DARK_A, spread);
const p = jtPt(a, kOut + randomGaussian(0, 0.012));
const q = jtPt(a, kIn + randomGaussian(0, 0.035));
brush.set(random() < 0.45 ? "rotring" : "pen", C_INK, Math.max(0.4, w + randomGaussian(0, 0.16)));
brush.line(p[0], p[1], q[0], q[1]);
}
}
// the shaft's flanks ruled with the run of the bone, heavy below, barely there above
function jtShaftRule(sh, n, side, w0, w1, dmax) {
brush.noFill();
const edge = side > 0 ? sh.dark : sh.lit;
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
const d = 2.5 + dmax * Math.pow(t, 1.4);
const upto = Math.max(3, Math.min(10, Math.round(9 - 5 * t + randomGaussian(0, 1.3))));
const pts = [];
for (let j = 0; j < upto; j++) {
pts.push([
edge[j][0] - side * SH_NX * d + randomGaussian(0, 0.9),
edge[j][1] - side * SH_NY * d + randomGaussian(0, 0.9),
]);
}
brush.set(t < 0.4 ? "rotring" : t < 0.7 ? "pen" : "2H", t < 0.66 ? C_INK : C_DEEP,
Math.max(0.34, w0 + (w1 - w0) * t + randomGaussian(0, 0.11)));
jtShape(pts, 0.1, 0.7, false);
}
}
// where the socket's bone was cut: the wall's thickness closed off and shaded across
function jtCutEnd(a, dx, dy) {
const p = jtSockPt(a, -0.04);
const q = jtSockPt(a, 1.04);
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.12, "out");
brush.fill(C_DEEP, 98);
brush.polygon([p, q, [q[0] + dx * 7, q[1] + dy * 7], [p[0] + dx * 6, p[1] + dy * 6]]);
brush.noFill();
for (let i = 0; i < 7; i++) {
const x = p[0] + (q[0] - p[0]) * (i / 6);
const y = p[1] + (q[1] - p[1]) * (i / 6);
brush.set("pen", C_INK, Math.max(0.4, randomGaussian(0.85, 0.16)));
brush.line(x, y, x + dx * random(3.5, 7.5), y + dy * random(3.5, 7.5));
}
brush.set("2B", C_INK, 2.0);
brush.line(p[0], p[1], q[0], q[1]);
}
// small dark marks scattered where the bearing surface failed
function jtSpeck(n, col, spread, k, kv, r, base, onSock) {
for (let i = 0; i < n; i++) {
const a = randomGaussian(WORN_A, spread);
const q = onSock ? jtSockPt(a, randomGaussian(k, kv))
: jtPt(a, Math.min(1.006, k + randomGaussian(0, kv)));
brush.fill(col, base + random(-28, 44));
brush.circle(q[0], q[1], r * (1 + randomGaussian(0, 0.24)), true);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(C_PAPER);
// 1 — two breaths of air: warm behind the bone, cool where the shadow will lie
brush.noStroke();
brush.fillTexture(0.4, 0.4);
brush.fillBleed(0.75, "out");
brush.fill(C_BONE, 34);
brush.circle(668, 212, 240, true);
brush.fillBleed(0.82, "out");
brush.fill(C_WARM, 18);
brush.circle(316, 452, 158, true);
// 2 — the cast shadow: darkest against the socket, drawn out to the lower left and let go
brush.noStroke();
brush.fillTexture(0.5, 0.36);
brush.fillBleed(0.72, "out");
brush.fill(C_WARM, 58);
jtShape(jtLens(424, 384, 176, 618, 66, 15, 9), 0.45, 9.0, true);
// and then pooled, not laid: a shadow has no outline, so its body is built from overlaps
for (let i = 0; i < 11; i++) {
const t = i / 10;
const x = 408 - 228 * t + randomGaussian(0, 13);
const y = 380 + 224 * t + randomGaussian(0, 13);
brush.fillBleed(0.44 + 0.26 * t, "out");
brush.fill(t < 0.26 ? C_DEEP : C_WARM, Math.max(26, 118 - 84 * t + random(-14, 18)));
brush.circle(x, y, (54 - 34 * t) * (1 + randomGaussian(0, 0.15)), true);
}
brush.noFill();
const drift = [[C_WARM, 8.2, 402, 384, 240, 552], [C_DEEP, 4.6, 398, 372, 320, 448],
[C_WARM, 3.0, 268, 526, 176, 622], [C_BONE, 2.4, 206, 588, 150, 640]];
for (let i = 0; i < drift.length; i++) {
const d = drift[i];
brush.set("spray", d[0], d[1]);
brush.line(d[2], d[3], d[4], d[5]);
}
// 3 — the bone: one silhouette, three opaque passes, so it parts company with the paper
const sh = jtShaft(72, 34, 9, 1.5);
brush.noStroke();
brush.fillTexture(0.5, 0.5);
const passes = [[248, 0.08, 1.8, 0, 0], [226, 0.14, 3.0, 74, 2.8], [154, 0.28, 4.6, 78, 4.2]];
for (let i = 0; i < 3; i++) {
const q = passes[i];
brush.fillBleed(q[1], "out");
brush.fill(C_BONE, q[0]);
jtShape(jtHull(i === 0 ? sh : jtShaft(q[3], 34 + i * 2, 9, q[4])), 0.2, q[2], true);
}
// 4 — the turn of the dome: pigment pooled first, then the latitudes ruled over it
jtWedgeN(jtCrescent(14, 258, 24, 0.08, 1.015), C_WARM, 118, 0.3, 3.4, 2);
jtWedgeN(jtCrescent(28, 244, 22, 0.20, 1.015), C_WARM, 150, 0.22, 2.6, 3);
jtWedgeN(jtCrescent(64, 214, 18, 0.42, 1.012), C_WARM, 158, 0.16, 2.0, 3);
jtWedgeN(jtCrescent(96, 184, 14, 0.70, 1.008), C_DEEP, 156, 0.12, 1.8, 2);
// a broad sparse family carries the mid-tone across the shaded flank
jtDome(17, DARK_A, 0.93, 0.34, 78, 46, 0.8, 0.45, true);
// and a tight one packs the rind at the rim until it is black
jtDome(26, DARK_A, 1.0, 0.74, 68, 30, 1.7, 0.72, true);
jtMeridians(26, 0.82, 1.0, 22, 1.0);
jtMeridians(12, 0.60, 0.90, 30, 0.7);
// the lit half touched only at its rim, so the crown stays the brightest thing in the sheet
jtDome(4, LIT_A, 0.985, 0.82, 52, 32, 0.6, 0.42, false);
// the lower-right quadrant turns too, and needs a few lines to say so
jtDome(9, 62, 0.99, 0.56, 42, 22, 0.85, 0.45, true);
// reflected light along the extreme rim, so the dome does not read as a hole
jtWedgeN(jtCrescent(104, 190, 14, 0.982, 1.028), C_BONE, 232, 0.12, 1.1, 1);
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fill(C_PAPER, 92);
brush.circle(616, 186, 38, true);
// the shaft: ruled hard along its underside, whispered along the lit edge
jtShaftRule(sh, 11, 1, 1.45, 0.45, 30);
jtShaftRule(sh, 5, -1, 0.55, 0.4, 15);
// 5 — the socket: a cut wall whose whole outer face is turned away from the light
brush.noStroke();
brush.noHatch();
brush.fillTexture(0.6, 0.45);
brush.fillBleed(0.08, "out");
brush.fill(C_BONE, 246);
jtShape(jtSockBand(S0, S1, 26, 0, 1), 0, 1.8, true);
jtWedgeN(jtSockBand(S0, S1, 24, 0.26, 1.02), C_WARM, 196, 0.12, 2.0, 3);
jtWedgeN(jtSockBand(S0 + 3, S1 - 3, 22, 0.56, 1.01), C_DEEP, 182, 0.1, 1.8, 3);
jtWedgeN(jtSockBand(S0, S1, 24, -0.04, 0.22), C_BONE, 216, 0.14, 1.4, 2);
// the wall ruled across its cut thickness, weighted by how far the light has gone
for (let i = 0; i <= 26; i++) {
const a = S0 + 2 + ((S1 - S0 - 4) * i) / 26;
const w = jtTurn(a);
const p = jtSockPt(a, 0.2);
const q = jtSockPt(a, 0.95);
const jx = randomGaussian(0, 1.5);
const jy = randomGaussian(0, 1.5);
brush.set(w > 0.5 ? "rotring" : "pen", C_INK,
Math.max(0.36, 0.42 + 1.05 * w + randomGaussian(0, 0.13)));
brush.line(p[0] + jx, p[1] + jy, q[0] + jx, q[1] + jy);
}
// and crossed along the wall, so its outer face is the darkest bone in the picture
brush.noFill();
for (let i = 0; i < 8; i++) {
const f = 0.46 + (i / 7) * 0.52;
brush.set(i % 3 === 0 ? "rotring" : "pen", C_INK, Math.max(0.4, 1.2 - 0.5 * (i / 7) + randomGaussian(0, 0.1)));
jtShape(jtArc(104 + random(-8, 8), 188 + random(-8, 8), 14, f + randomGaussian(0, 0.012), 1), 0.25, 0.9, false);
}
// the cup floor, seen past each horn where the condyle does not fill it
jtWedgeN(jtSockBand(S0, S0 + 16, 6, -0.06, 0.42), C_INK, 188, 0.12, 1.6, 1);
jtWedgeN(jtSockBand(S1 - 14, S1, 6, -0.06, 0.38), C_INK, 166, 0.12, 1.6, 1);
// 6 — the contact dark, where the socket's outer face all but touches the bench
jtWedgeN(jtSockBand(112, 192, 14, 1.0, 1.58), C_WARM, 116, 0.32, 3.4, 2);
jtWedgeN(jtSockBand(120, 184, 14, 0.99, 1.36), C_DEEP, 148, 0.24, 2.6, 2);
brush.noFill();
for (let i = 0; i < 6; i++) {
const a = randomGaussian(150, 18);
const p = jtSockPt(a, 1.01);
const q = jtSockPt(a, 1.26 + randomGaussian(0, 0.08));
brush.set(random() < 0.5 ? "charcoal" : "2B", C_DEEP, Math.max(1.2, randomGaussian(2.5, 0.5)));
brush.line(p[0], p[1], q[0] + randomGaussian(0, 3), q[1] + randomGaussian(0, 3));
}
// the two cut ends of the socket's bone, closed off and shaded across the wall
jtCutEnd(S0, 0.42, 0.91);
jtCutEnd(S1, -0.86, -0.51);
// 7 — the joint space: cartilage still standing at each horn, gone entirely at the middle
jtWedgeN(jtGlaze(S0 + 3, 132, 9), C_PAPER, 246, 0.06, 0.8, 3);
jtWedgeN(jtGlaze(172, S1 - 3, 8), C_PAPER, 244, 0.06, 0.8, 3);
brush.noFill();
brush.set("2H", C_WARM, 0.5);
jtShape(jtArc(S0 + 8, 124, 9, 1.05), 0.3, 1.0, false);
jtShape(jtArc(178, S1 - 6, 7, 1.05), 0.3, 1.0, false);
// the one note of colour: bone laid bare on the condyle where the glaze wore through
jtWedgeN(jtCrescent(134, 172, 9, 0.91, 1.006), C_RUST, 210, 0.12, 1.4, 3);
brush.noStroke();
brush.fillTexture(0.45, 0.55);
brush.fillBleed(0.16, "out");
jtSpeck(10, C_RUST, 11, 0.955, 0.026, 7.0, 214, false);
jtSpeck(6, C_RUST, 10, 0.06, 0.04, 5.0, 200, true);
// 8 — the ink contour, weighted by the light: iron on the bearing arc, a whisper above
brush.noFill();
const rim = [["2H", 0.9, 244, 268, 6, 1.2], ["HB", 1.5, 188, 248, 10, 1.2],
["charcoal", 3.1, 88, 192, 16, 1.0], ["2B", 1.9, 34, 92, 10, 1.2], ["2H", 1.0, 4, 36, 7, 1.3]];
for (let i = 0; i < rim.length; i++) {
const c = rim[i];
brush.set(c[0], C_INK, c[1]);
jtShape(jtArc(c[2], c[3], c[4], 1.0), 0.3, c[5], false);
}
brush.set("2H", C_INK, 0.85);
jtShape(sh.lit, 0.15, 1.3, false);
brush.set("2B", C_INK, 2.1);
jtShape(sh.dark, 0.15, 1.1, false);
const cup = [["HB", 1.4, S0, S1, 24, 0.0, 1.0], ["2B", 2.2, 100, 190, 18, 1.0, 1.1],
["2B", 1.4, S0, 102, 5, 1.0, 1.2], ["2B", 1.4, 188, S1, 5, 1.0, 1.2]];
for (let i = 0; i < cup.length; i++) {
const c = cup[i];
brush.set(c[0], C_INK, c[1]);
jtShape(jtArc(c[2], c[3], c[4], c[5], 1), 0.2, c[6], false);
}
// 9 — the intercondylar groove, running out of the shaft and dying at the bearing arc
brush.set("HB", C_INK, 1.3);
jtShape([[676, 152], [606, 208], [548, 272], [506, 336]], 0.45, 1.1, false);
brush.set("2H", C_DEEP, 0.8);
jtShape([[698, 176], [636, 228], [584, 290]], 0.45, 1.0, false);
// 10 — muscle scars on the shaft, struck across the grain of its ruling
brush.set("2B", C_INK, 1.4);
brush.line(704, 138, 676, 172);
brush.line(734, 158, 706, 190);
brush.set("2H", C_INK, 0.9);
brush.line(760, 118, 738, 144);
// 12 — last and darkest: the pits where the bearing surface gave out
brush.noStroke();
brush.fillBleed(0.08, "out");
jtSpeck(26, C_INK, 13, 0.968, 0.028, 2.7, 200, false);
jtSpeck(11, C_INK, 11, 0.06, 0.03, 2.5, 186, true);
brush.fillBleed(0.12, "out");
brush.fill(C_INK, 228);
brush.circle(430, 300, 7.0, true);
brush.fill(C_INK, 192);
brush.circle(418, 318, 5.0, true);
}