BrushArena

September 2026 · Research, Design, Development, Evals.

BrushArena is a benchmark for evaluating a model’s ability to paint. Every language model gets the same brief and writes p5.js code that renders into a painting. You see two paintings side by side without knowing who made them, pick the one you prefer, and the votes add up to a leaderboard.

Most evals measure what is easy to grade, like math, exams and code, so craft and subjective domains are harder to measure. We think the answer is a vibrant ecology of evals, many small ones made by people who know the domain well. BrushArena is ours, and everyone should make one.

My contributions

Design

Evals

Research

The team

Surya

Cameron Franz

Engraved sourdough: a white-line wood engraving of a cut sourdough loaf, a slice and a bread knife on a board, painted in code by Gemini 4 Argon
sketch.js · 1,579 lines
// White-Line Wood Engraving: Sourdough Loaf, Cut Slice & Bread Knife on a Trencher Board
// Rendered with p5.js v2 + p5.brush in WEBGL mode

let alveoliMain = [];
let alveoliSlice = [];
let boardKnots = [];

// Cut Face coordinate frame (Main Loaf)
const CF_CL = { x: 215, y: 598 };
const CF_CR = { x: 785, y: 432 };
const CF_MID = { x: (215 + 785) * 0.5, y: (598 + 432) * 0.5 };
const CF_DX = 785 - 215;
const CF_DY = 432 - 598;
const CF_LEN = Math.hypot(CF_DX, CF_DY);
const CF_HALF = CF_LEN * 0.5;
const EU = { x: CF_DX / CF_LEN, y: CF_DY / CF_LEN }; // (0.960, -0.280)
const EV = { x: -0.135, y: -0.991 }; // Upward arch axis

// Slice coordinate frame (Foreground Left)
const SL_L = { x: 155, y: 658 };
const SL_R = { x: 540, y: 608 };
const SL_MID = { x: (155 + 540) * 0.5, y: (658 + 608) * 0.5 };
const SL_DX = 540 - 155;
const SL_DY = 608 - 658;
const SL_LEN = Math.hypot(SL_DX, SL_DY);
const SL_HALF = SL_LEN * 0.5;
const SU = { x: SL_DX / SL_LEN, y: SL_DY / SL_LEN };
const SV = { x: -0.11, y: 0.994 }; // Forward bulge axis on board

// Knife coordinate frame (Foreground/Middle Right)
const KN_PIVOT = { x: 635, y: 745 }; // Bolster center
const KN_ANG = -39.5 * (Math.PI / 180);
const KU = { x: Math.cos(KN_ANG), y: Math.sin(KN_ANG) }; // Along blade toward tip
const KV = { x: -Math.sin(KN_ANG), y: Math.cos(KN_ANG) }; // Downward across blade width

// Breadboard top polygon vertices
const BOARD_TOP = [
  [475, 195],  // Back corner
  [1255, 615], // Right corner (off-frame tight crop)
  [635, 960],  // Front corner
  [-135, 535]  // Left corner (off-frame tight crop)
];
const BOARD_EDGE_H = 88;

async function setup() {
  createCanvas(1200, 1200, WEBGL);
  brush.scaleBrushes(6);
  angleMode(DEGREES);
  randomSeed(7391);
  noiseSeed(7391);

  // Register custom wood-engraving graver / burin brushes (already sized for 1200x1200)
  brush.add("needle", {
    type: "default",
    weight: 1.25,
    scatter: 0.04,
    sharpness: 0.94,
    grain: 0.82,
    opacity: 215,
    spacing: 0.14,
    pressure: [0.3, 1.0, 0.3],
    noise: 0.28
  });

  brush.add("burin", {
    type: "default",
    weight: 2.15,
    scatter: 0.06,
    sharpness: 0.89,
    grain: 0.76,
    opacity: 238,
    spacing: 0.14,
    pressure: [0.35, 1.0, 0.35],
    noise: 0.32
  });

  brush.add("scorper", {
    type: "default",
    weight: 3.35,
    scatter: 0.09,
    sharpness: 0.84,
    grain: 0.68,
    opacity: 248,
    spacing: 0.14,
    pressure: [0.4, 1.0, 0.4],
    noise: 0.24
  });

  initGeometryData();
}

// ============================================================================
// GEOMETRY & ANALYTICAL SURFACE FUNCTIONS
// ============================================================================

function toCutFaceUV(x, y) {
  let dx = x - CF_MID.x;
  let dy = y - CF_MID.y;
  let det = EU.x * EV.y - EU.y * EV.x;
  let uPx = (dx * EV.y - dy * EV.x) / det;
  let vPx = (EU.x * dy - EU.y * dx) / det;
  return { u: uPx / CF_HALF, v: vPx, uPx: uPx };
}

function fromCutFaceUV(u, v) {
  let uPx = u * CF_HALF;
  return {
    x: CF_MID.x + uPx * EU.x + v * EV.x,
    y: CF_MID.y + uPx * EU.y + v * EV.y
  };
}

function cutFaceArchHeight(u) {
  if (Math.abs(u) >= 1.0) return 0;
  let baseArch = 268 * Math.pow(Math.max(0, 1 - u * u), 0.56);
  // Slight organic fullness on left shoulder
  baseArch *= (1.0 - 0.055 * u);
  // Baker's Ear (grigne) crest & notch around u = -0.22
  let earCrest = 28 * Math.exp(-Math.pow((u + 0.20) / 0.13, 2));
  let earNotch = -14 * Math.exp(-Math.pow((u + 0.36) / 0.055, 2));
  let organic = 4.5 * Math.sin(u * 9.0 + 0.8) + 2.5 * Math.cos(u * 15.0);
  return Math.max(0, baseArch + earCrest + earNotch + organic);
}

function cutFaceBaseOffset(u) {
  return 4.0 * Math.sin(u * Math.PI * 1.5 + 0.4) + 2.0 * Math.cos(u * 8.0);
}

function getCutFaceInfo(x, y) {
  let uv = toCutFaceUV(x, y);
  if (Math.abs(uv.u) > 0.995) return { inside: false, u: uv.u, v: uv.v };
  let vMin = cutFaceBaseOffset(uv.u);
  let vMax = cutFaceArchHeight(uv.u);
  if (uv.v < vMin || uv.v > vMax) return { inside: false, u: uv.u, v: uv.v, vMin, vMax };
  let dTop = vMax - uv.v;
  let dBot = uv.v - vMin;
  let crustTopW = 20 + 7 * Math.exp(-Math.pow((uv.u + 0.20) / 0.14, 2));
  let crustBotW = 13;
  let inCrustRim = (dTop < crustTopW) || (dBot < crustBotW) || (Math.abs(uv.u) > 0.935);
  return {
    inside: true,
    inCrustRim: inCrustRim,
    u: uv.u,
    v: uv.v,
    vMin: vMin,
    vMax: vMax,
    dTop: dTop,
    dBot: dBot
  };
}

// Outer 3D Dome profile of the loaf (parameterized by t in [-1.05, 0.90])
function domeOuterPoint(t) {
  // t = -1.05 is bottom-left heel at C_L; t = 0 is top crown; t = 0.90 meets right shoulder of cut face
  let uClamp = constrain(t, -1.0, 0.88);
  let cfPt = fromCutFaceUV(uClamp, cutFaceArchHeight(uClamp));
  // Offset vector pushing the 3D loaf body back-left from the cut face arch
  let profileEnv = Math.sin( map(constrain(t, -1.05, 0.88), -1.05, 0.88, 0, Math.PI) );
  let heelBulge = Math.exp(-Math.pow((t + 0.72) / 0.42, 2));
  let crownBulge = Math.exp(-Math.pow((t + 0.08) / 0.55, 2));
  let shiftX = -158 * Math.pow(profileEnv, 0.75) - 48 * heelBulge - 18 * crownBulge;
  let shiftY = -102 * Math.pow(profileEnv, 0.82) - 52 * crownBulge + 26 * heelBulge;
  return {
    x: cfPt.x + shiftX,
    y: cfPt.y + shiftY
  };
}

function domeSurfacePoint(t, s) {
  // s = 0 is on the cut face arch; s = 1 is on the back-left outer silhouette
  let uClamp = constrain(t, -1.0, 0.88);
  let p0 = fromCutFaceUV(uClamp, cutFaceArchHeight(uClamp));
  let p1 = domeOuterPoint(t);
  // Add 3D spherical arching across s
  let archMid = Math.sin(s * Math.PI);
  let normalX = -0.68;
  let normalY = -0.73;
  let lift = 28 * archMid * Math.sin(map(constrain(t, -1.0, 0.88), -1.0, 0.88, 0.15, Math.PI - 0.1));
  // Score cleft indentation along t in [-0.39, -0.19]
  let inScore = Math.exp(-Math.pow((t + 0.29) / 0.065, 2)) * (1 - 0.35 * s);
  let earLip = 14 * Math.exp(-Math.pow((t + 0.19) / 0.045, 2)) * (1 - 0.4 * s);
  return {
    x: lerp(p0.x, p1.x, s) + normalX * (lift + earLip - 10 * inScore),
    y: lerp(p0.y, p1.y, s) + normalY * (lift + earLip - 10 * inScore)
  };
}

let loafOutlinePoly = [];
function buildLoafPolygon() {
  loafOutlinePoly = [];
  // Base of cut face from C_L to C_R
  for (let i = 0; i <= 30; i++) {
    let u = lerp(-1, 1, i / 30);
    let p = fromCutFaceUV(u, cutFaceBaseOffset(u));
    loafOutlinePoly.push([p.x, p.y]);
  }
  // Right arch of cut face up to t = 0.86
  for (let i = 0; i <= 10; i++) {
    let u = lerp(1.0, 0.86, i / 10);
    let p = fromCutFaceUV(u, cutFaceArchHeight(u));
    loafOutlinePoly.push([p.x, p.y]);
  }
  // Outer back-left dome silhouette from t = 0.86 down to t = -1.0
  for (let i = 0; i <= 50; i++) {
    let t = lerp(0.86, -1.0, i / 50);
    let p = domeSurfacePoint(t, 1.0);
    loafOutlinePoly.push([p.x, p.y]);
  }
}

function inLoafTotal(x, y) {
  return pointInPoly(x, y, loafOutlinePoly);
}

// Foreground Cut Slice Geometry
function toSliceUV(x, y) {
  let dx = x - SL_MID.x;
  let dy = y - SL_MID.y;
  let det = SU.x * SV.y - SU.y * SV.x;
  let uPx = (dx * SV.y - dy * SV.x) / det;
  let vPx = (SU.x * dy - SU.y * dx) / det;
  return { u: uPx / SL_HALF, v: vPx };
}

function fromSliceUV(u, v) {
  let uPx = u * SL_HALF;
  return {
    x: SL_MID.x + uPx * SU.x + v * SV.x,
    y: SL_MID.y + uPx * SU.y + v * SV.y
  };
}

function sliceArchHeight(u) {
  if (Math.abs(u) >= 1.0) return 0;
  return 132 * Math.pow(Math.max(0, 1 - u * u), 0.58) * (1 + 0.06 * u) + 3 * Math.sin(u * 8);
}

function getSliceInfo(x, y) {
  let uv = toSliceUV(x, y);
  if (Math.abs(uv.u) <= 1.0) {
    let vMin = 3 * Math.sin(uv.u * 5);
    let vMax = sliceArchHeight(uv.u);
    if (uv.v >= vMin && uv.v <= vMax) {
      let inRim = (vMax - uv.v < 14) || (uv.v - vMin < 9) || (Math.abs(uv.u) > 0.93);
      return { insideTop: true, insideWall: false, inRim, u: uv.u, v: uv.v, vMax };
    }
  }
  // Check vertical side crust wall (30px vertical drop below front arch)
  let uvUp = toSliceUV(x, y - 15);
  if (Math.abs(uv.u) <= 1.01) {
    let pArch = fromSliceUV(constrain(uv.u, -1, 1), sliceArchHeight(constrain(uv.u, -1, 1)));
    let dyWall = y - pArch.y;
    let wallH = 31 * Math.pow(Math.max(0, 1 - uv.u * uv.u * 0.85), 0.35);
    if (dyWall >= -2 && dyWall <= wallH && Math.abs(x - pArch.x) < 12) {
      return { insideTop: false, insideWall: true, u: uv.u, dyWall, wallH };
    }
  }
  return { insideTop: false, insideWall: false };
}

function inSliceTotal(x, y) {
  let info = getSliceInfo(x, y);
  if (info.insideTop || info.insideWall) return true;
  // Robust check for side crust wall between top arch and shifted bottom arch
  let uv = toSliceUV(x, y);
  if (Math.abs(uv.u) <= 1.0) {
    let vMax = sliceArchHeight(uv.u);
    if (uv.v > vMax && uv.v <= vMax + 31 * Math.pow(Math.max(0, 1 - uv.u * uv.u * 0.85), 0.35)) {
      return true;
    }
  }
  return false;
}

// Bread Knife Geometry
function toKnifeUV(x, y) {
  let dx = x - KN_PIVOT.x;
  let dy = y - KN_PIVOT.y;
  return {
    s: dx * KU.x + dy * KU.y, // + along blade, - along handle
    w: dx * KV.x + dy * KV.y  // across width
  };
}

function fromKnifeUV(s, w) {
  return {
    x: KN_PIVOT.x + s * KU.x + w * KV.x,
    y: KN_PIVOT.y + s * KU.y + w * KV.y
  };
}

function getKnifeBounds(s) {
  if (s >= 0 && s <= 435) {
    // Blade: s from 0 (bolster) to 435 (tip)
    let t = s / 435;
    let wTop = -21 * (1 - 0.18 * t * t);
    // Curved tip drop near t > 0.82
    if (t > 0.82) {
      let tipT = (t - 0.82) / 0.18;
      wTop += 34 * Math.pow(tipT, 2.1);
    }
    // Scalloped serrated bottom edge
    let baseBot = 24 * (1 - 0.22 * t);
    let serration = 0;
    if (s > 18 && s < 415) {
      let toothPhase = ((s - 18) / 21.0) % 1.0;
      serration = -3.8 * Math.sin(toothPhase * Math.PI);
    }
    return { part: "blade", wTop: wTop, wBot: baseBot + serration };
  } else if (s < 0 && s >= -22) {
    // Steel bolster collar
    let t = -s / 22;
    let halfW = lerp(22, 16, t);
    return { part: "bolster", wTop: -halfW, wBot: halfW + 2 };
  } else if (s < -22 && s >= -265) {
    // Turned wooden handle
    let t = (-s - 22) / 243; // 0 at bolster, 1 at pommel
    let belly = 6.5 * Math.sin(t * Math.PI) - 3.5 * Math.sin(t * Math.PI * 2);
    let pommelFlare = 5.5 * Math.exp(-Math.pow((t - 0.90) / 0.11, 2));
    let roundEnd = t > 0.93 ? -18 * Math.pow((t - 0.93) / 0.07, 2) : 0;
    let halfW = 16.5 + belly + pommelFlare + roundEnd;
    return { part: "handle", wTop: -halfW, wBot: halfW + 3 * Math.sin(t * Math.PI) };
  }
  return { part: "none", wTop: 0, wBot: 0 };
}

function inKnifeTotal(x, y) {
  let kw = toKnifeUV(x, y);
  let b = getKnifeBounds(kw.s);
  if (b.part === "none") return false;
  return kw.w >= b.wTop && kw.w <= b.wBot;
}

function pointInPoly(x, y, poly) {
  let inside = false;
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
    let xi = poly[i][0], yi = poly[i][1];
    let xj = poly[j][0], yj = poly[j][1];
    let intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi + 1e-9) + xi);
    if (intersect) inside = !inside;
  }
  return inside;
}

function inBoardTop(x, y) {
  return pointInPoly(x, y, BOARD_TOP);
}

function initGeometryData() {
  buildLoafPolygon();

  // Two realistic wood knots on the breadboard
  boardKnots = [
    { x: 865, y: 665, rx: 46, ry: 22, ang: 28 * (Math.PI / 180) },
    { x: 215, y: 845, rx: 38, ry: 18, ang: 28 * (Math.PI / 180) }
  ];

  // Generate non-overlapping alveoli (crumb holes) in Main Loaf Cut Face
  alveoliMain = [];
  let attempts = 0;
  while (alveoliMain.length < 185 && attempts < 4500) {
    attempts++;
    let u = random(-0.90, 0.90);
    let vMax = cutFaceArchHeight(u);
    let vMin = cutFaceBaseOffset(u);
    if (vMax - vMin < 36) continue;
    let v = random(vMin + 18, vMax - 24);
    let pt = fromCutFaceUV(u, v);

    // Determine hole size tier: large wild holes, medium pockets, tiny pinholes
    let tier = random();
    let rBase;
    if (alveoliMain.length < 26 && tier < 0.45) {
      rBase = random(16, 29);
    } else if (alveoliMain.length < 85 && tier < 0.78) {
      rBase = random(8.5, 15.5);
    } else {
      rBase = random(3.8, 8.0);
    }

    // Radial orientation from bottom-center of dough rise (u=0, v=25)
    let centerPt = fromCutFaceUV(0.0, 20);
    let radAng = Math.atan2(pt.y - centerPt.y, pt.x - centerPt.x);
    let aspect = random(1.18, 1.68);
    let rx = rBase * aspect;
    let ry = rBase;

    // Check overlap against existing holes
    let ok = true;
    for (let h of alveoliMain) {
      let d = Math.hypot(pt.x - h.x, pt.y - h.y);
      let minDist = (Math.max(rx, ry) + Math.max(h.rx, h.ry)) * 0.78 + 3.5;
      if (d < minDist) {
        ok = false;
        break;
      }
    }
    if (ok) {
      alveoliMain.push({
        x: pt.x,
        y: pt.y,
        u: u,
        v: v,
        rx: rx,
        ry: ry,
        ang: radAng + random(-0.22, 0.22),
        seed: random(1000)
      });
    }
  }

  // Generate foreshortened alveoli in Foreground Cut Slice
  alveoliSlice = [];
  attempts = 0;
  while (alveoliSlice.length < 62 && attempts < 2000) {
    attempts++;
    let u = random(-0.88, 0.88);
    let vMax = sliceArchHeight(u);
    if (vMax < 28) continue;
    let v = random(12, vMax - 16);
    let pt = fromSliceUV(u, v);
    let rBase = alveoliSlice.length < 12 ? random(9, 16) : random(3.5, 8.5);
    let rx = rBase * random(1.3, 1.8);
    let ry = rBase * 0.72; // Foreshortened vertically
    let ang = random(-0.35, 0.15);

    let ok = true;
    for (let h of alveoliSlice) {
      let d = Math.hypot(pt.x - h.x, pt.y - h.y);
      if (d < (rx + h.rx) * 0.72 + 3.0) {
        ok = false;
        break;
      }
    }
    if (ok) {
      alveoliSlice.push({
        x: pt.x,
        y: pt.y,
        rx: rx,
        ry: ry,
        ang: ang,
        seed: random(1000)
      });
    }
  }
}

function holeNormalizedDist(x, y, h) {
  let dx = x - h.x;
  let dy = y - h.y;
  let ca = Math.cos(-h.ang);
  let sa = Math.sin(-h.ang);
  let lx = dx * ca - dy * sa;
  let ly = dx * sa + dy * ca;
  // Slight organic wobble on bubble wall
  let theta = Math.atan2(ly, lx);
  let wobble = 1.0 + 0.08 * Math.sin(theta * 3 + h.seed) + 0.05 * Math.cos(theta * 5 - h.seed);
  return Math.hypot(lx / (h.rx * wobble), ly / (h.ry * wobble));
}

// ============================================================================
// SPLINE CARVING HELPER (White-Line Wood Engraving Burin Engine)
// ============================================================================

function carveSegmentedSpline(rawPts, brushName, col, weightScale = 1.0, curvature = 0.5) {
  // rawPts is array of { x, y, w } where w <= 0 means lifted burin (gap)
  let currentSeg = [];
  for (let i = 0; i < rawPts.length; i++) {
    let p = rawPts[i];
    if (p.w > 0.06 && p.x >= 80 && p.x <= 1120 && p.y >= 80 && p.y <= 1120) {
      currentSeg.push([p.x, p.y, constrain(p.w * weightScale, 0.15, 2.6)]);
    } else {
      if (currentSeg.length >= 2) {
        emitBurinSegment(currentSeg, brushName, col, curvature);
      }
      currentSeg = [];
    }
  }
  if (currentSeg.length >= 2) {
    emitBurinSegment(currentSeg, brushName, col, curvature);
  }
}

function emitBurinSegment(seg, brushName, col, curvature) {
  brush.set(brushName, col, 1.0);
  if (seg.length === 2) {
    let avgW = (seg[0][2] + seg[1][2]) * 0.5;
    brush.strokeWeight(avgW);
    brush.line(seg[0][0], seg[0][1], seg[1][0], seg[1][1]);
  } else {
    brush.spline(seg, curvature);
  }
}

// ============================================================================
// MAIN DRAW LOOP
// ============================================================================

function draw() {
  translate(-width / 2, -height / 2);

  // 1. WARM SEPIA PAPER BASE & SOFT PASTEL UNDERTINTS
  background("#efe2cb");
  brush.noStroke();

  // Warm sepia antique paper wash
  brush.fill("#dec6a2", 75);
  brush.fillBleed(0.25, "out");
  brush.fillTexture(0.55, 0.25);
  brush.rect(40, 40, 1120, 1120, "corner");

  // Soft pastel undertints underlying the block zones
  brush.fill("#e8c6ad", 65); // Soft pastel peach-apricot under sourdough crust
  brush.circle(420, 340, 280, 0.2);
  brush.fill("#f5e6c8", 75); // Soft pastel butter-cream under crumb
  brush.circle(520, 460, 240, 0.2);
  brush.fill("#cbd8de", 65); // Soft pastel powder-blue under knife
  brush.circle(780, 620, 220, 0.2);
  brush.noFill();

  // 2. DENSE BLACK WOODBLOCK GROUND
  // Outer block ink impression
  brush.noStroke();
  brush.wash("#161311", 252);
  brush.rect(64, 64, 1072, 1072, "corner");

  // Deepen velvet-black shadow masses across the block
  brush.wash("#0d0b09", 255);
  brush.rect(76, 76, 1048, 1048, "corner");
  brush.noWash();

  // 3. VICTORIAN WOODBLOCK DOUBLE BORDER RULES
  drawWoodblockBorder();

  // 4. UPPER BACKGROUND & LOWER TABLE VIGNETTE ENGRAVING
  drawBackgroundAndTableRules();

  // 5. WOODEN BREADBOARD (Top Grain, Knots, Knife Scratches & Beveled Edge)
  drawBreadboardTop();
  drawBreadboardFrontEdge();

  // 6. MAIN SOURDOUGH LOAF — 3D DOMED CRUST, EAR & SCORE CLEFT
  drawLoafDomedCrust();

  // 7. MAIN SOURDOUGH LOAF — CUT FACE (Crust Rim, Radiating Crumb & Stippled Holes)
  drawMainLoafCutFace();

  // 8. FOREGROUND CUT SLICE (Side Crust Wall, Foreshortened Crumb & Holes)
  drawForegroundSlice();

  // 9. BREAD KNIFE (Scalloped Steel Blade, Bolster, Turned Handle & Rivets)
  drawBreadKnife();

  // 10. SCATTERED BREAD CRUMBS & FLOUR FLECKS
  drawScatteredCrumbsAndFlour();

  noLoop();
}

// ============================================================================
// REGION RENDERING FUNCTIONS
// ============================================================================

function drawWoodblockBorder() {
  // Outer carved white line inside the black block margin
  brush.set("burin", "#cdb593", 0.95);
  brush.line(73, 73, 1127, 73);
  brush.line(1127, 73, 1127, 1127);
  brush.line(1127, 1127, 73, 1127);
  brush.line(73, 1127, 73, 73);

  // Inner finer carved white rule
  brush.set("needle", "#e3cfb2", 0.75);
  brush.line(80, 80, 1120, 80);
  brush.line(1120, 80, 1120, 1120);
  brush.line(1120, 1120, 80, 1120);
  brush.line(80, 1120, 80, 80);

  // Subtle exterior printer's registration hairline on the sepia paper margin
  brush.set("rotring", "#8c755c", 0.45);
  brush.line(54, 54, 1146, 54);
  brush.line(1146, 54, 1146, 1146);
  brush.line(1146, 1146, 54, 1146);
  brush.line(54, 1146, 54, 54);
}

function drawBackgroundAndTableRules() {
  // Upper background horizontal ruled white lines (Victorian book engraving sky/wall tone)
  for (let y = 86; y <= 550; y += 5.5) {
    let pts = [];
    for (let x = 82; x <= 1118; x += 14) {
      if (inBoardTop(x, y) || inLoafTotal(x, y)) {
        pts.push({ x, y, w: 0 });
        continue;
      }
      // Vignette lighting: warm glow behind right shoulder of loaf (760, 260) and upper-left (160, 260)
      let dRight = Math.hypot(x - 810, y - 280);
      let dLeft = Math.hypot(x - 140, y - 340);
      // Dark halo right above the bright flour-dusted crown (360..580, 150) for contrast
      let dCrown = Math.hypot((x - 450) * 0.75, y - 165);
      let crownSuppress = Math.exp(-Math.pow(dCrown / 135, 2));

      let lum = 0.52 * Math.exp(-Math.pow(dRight / 290, 2)) +
                0.36 * Math.exp(-Math.pow(dLeft / 210, 2)) +
                0.12 * (1 - Math.abs(x - 600) / 550);
      lum *= (1 - 0.78 * crownSuppress);

      // Fade out near top corners
      let cornerDist = Math.min(Math.hypot(x - 80, y - 80), Math.hypot(x - 1120, y - 80));
      if (cornerDist < 170) lum *= (cornerDist / 170);

      let w = lum > 0.08 ? map(lum, 0.08, 0.65, 0.18, 0.92, true) : 0;
      pts.push({ x: x, y: y + 0.8 * Math.sin(x * 0.03 + y * 0.1), w: w });
    }
    let col = y < 240 ? "#9a846d" : "#b8a086";
    carveSegmentedSpline(pts, "needle", col, 1.0, 0.3);
  }

  // Lower dark table surface beneath the breadboard's front edge
  for (let y = 625; y <= 1114; y += 6.5) {
    let pts = [];
    for (let x = 82; x <= 1118; x += 16) {
      // Check if above the bottom of the board's front vertical edge
      let edgeBottomY = x <= 635
        ? map(x, -135, 635, 535 + BOARD_EDGE_H, 960 + BOARD_EDGE_H)
        : map(x, 635, 1255, 960 + BOARD_EDGE_H, 615 + BOARD_EDGE_H);

      if (y < edgeBottomY + 8) {
        pts.push({ x, y, w: 0 });
        continue;
      }
      // Deep shadow directly under the board overhang (`edgeBottomY .. edgeBottomY + 32`)
      let dyUnder = y - edgeBottomY;
      let underShadow = 1 - Math.exp(-Math.pow(dyUnder / 34, 2));
      let vig = 1 - Math.pow(Math.hypot((x - 600) / 580, (y - 950) / 280), 1.6);
      let lum = Math.max(0, 0.42 * underShadow * Math.max(0, vig));
      let w = lum > 0.07 ? map(lum, 0.07, 0.45, 0.18, 0.78, true) : 0;
      pts.push({ x, y, w });
    }
    carveSegmentedSpline(pts, "needle", "#8f7a64", 1.0, 0.25);
  }
}

function boardShadowFactor(x, y) {
  // Returns 0 in deep cast shadow, 1 in full raking light on the board
  let s = 1.0;

  // 1. Cast shadow in front-left and along the base of the Main Loaf
  let uvCF = toCutFaceUV(x, y);
  if (uvCF.u > -1.12 && uvCF.u < 1.02 && uvCF.v < 10 && uvCF.v > -42) {
    let dFront = Math.abs(uvCF.v);
    let uFade = 1 - Math.pow(Math.max(0, (Math.abs(uvCF.u) - 0.85) / 0.25), 2);
    s *= lerp(0.05, 1.0, constrain(dFront / 36, 0, 1)) * Math.max(0, uFade);
  }
  // Cast shadow behind/left of loaf heel (light comes from upper-right)
  let dHeel = Math.hypot((x - 145) * 0.9, (y - 525) * 1.1);
  if (dHeel < 125 && x < 245) {
    s *= lerp(0.0, 1.0, constrain((dHeel - 35) / 90, 0, 1));
  }

  // 2. Cast shadow under and to the left-front of the Foreground Slice
  let uvSL = toSliceUV(x, y);
  if (Math.abs(uvSL.u) < 1.12) {
    let vArch = sliceArchHeight(constrain(uvSL.u, -1, 1)) + 30;
    let dv = uvSL.v - vArch;
    if (dv > -5 && dv < 38) {
      let edgeFade = 1 - constrain((Math.abs(uvSL.u) - 0.88) / 0.24, 0, 1);
      s *= lerp(0.0, 1.0, constrain(dv / 34, 0, 1) * edgeFade + (1 - edgeFade));
    }
    // Shadow in the narrow channel between Loaf base and Slice back edge
    if (uvSL.v < 6 && uvSL.v > -45 && uvSL.u > -0.95 && uvSL.u < 0.95) {
      s *= 0.22;
    }
  }

  // 3. Cast shadow beneath the Bread Knife (blade is slightly raised near bolster, touching near tip)
  let kw = toKnifeUV(x, y);
  if (kw.s > -265 && kw.s < 430) {
    let b = getKnifeBounds(kw.s);
    // Shadow sits slightly below-left of the knife (+w and -s)
    let shadowOffset = kw.s > 0 ? map(kw.s, 0, 435, 16, 6) : 14;
    let dw = kw.w - b.wBot;
    if (dw >= -2 && dw <= shadowOffset + 16) {
      let penumbra = Math.abs(dw - shadowOffset * 0.55) / (shadowOffset * 0.65 + 4);
      if (penumbra < 1.0) {
        s *= lerp(0.0, 1.0, Math.pow(penumbra, 1.4));
      }
    }
  }

  return constrain(s, 0, 1);
}

function drawBreadboardTop() {
  // Board grain axis: from back-left toward front-right (approx +28.8 degrees)
  let gAng = 28.8 * (Math.PI / 180);
  let gCos = Math.cos(gAng);
  let gSin = Math.sin(gAng);
  let pCos = -gSin;
  let pSin = gCos;

  let centerX = 560;
  let centerY = 580;

  // Sweep 195 parallel wood-grain tracks across the board width
  for (let idx = -98; idx <= 98; idx++) {
    let vOff = idx * 4.7;
    let pts = [];
    // Choose warm sepia / soft pastel sage-ochre color based on grain band
    let ringWave = Math.sin(idx * 0.24 + 1.2) * 0.5 + 0.5;
    let col = ringWave > 0.62 ? "#e2d0b3" : (ringWave > 0.28 ? "#c7b293" : "#9c856b");
    let brushName = ringWave > 0.68 ? "burin" : "needle";

    for (let uOff = -680; uOff <= 680; uOff += 11) {
      let bx = centerX + uOff * gCos + vOff * pCos;
      let by = centerY + uOff * gSin + vOff * pSin;

      // Deflect grain smoothly around wood knots and add natural cathedral wave
      let deflect = 6.5 * Math.sin(uOff * 0.0065 + idx * 0.08) + 3.0 * Math.cos(uOff * 0.015 - idx * 0.12);
      let knotDarken = 1.0;

      for (let k of boardKnots) {
        let dx = bx - k.x;
        let dy = by - k.y;
        let ku = dx * Math.cos(-k.ang) - dy * Math.sin(-k.ang);
        let kv = dx * Math.sin(-k.ang) + dy * Math.cos(-k.ang);
        let nd = Math.hypot(ku / (k.rx * 1.8), kv / (k.ry * 2.2));
        if (nd < 2.2) {
          let push = (kv >= 0 ? 1 : -1) * k.ry * 1.15 * Math.exp(-nd * nd * 1.1);
          deflect += push;
          if (nd < 0.75) knotDarken = 0.25;
        }
      }

      let x = bx + deflect * pCos;
      let y = by + deflect * pSin;

      // Mask out points outside board or inside loaf, slice, or knife
      if (!inBoardTop(x, y) || inLoafTotal(x, y) || inSliceTotal(x, y) || inKnifeTotal(x, y)) {
        pts.push({ x, y, w: 0 });
        continue;
      }

      // Compute illumination on the board top surface
      // Brighter around the loaf right side (x ~ 820, y ~ 500) and foreground center (x ~ 620, y ~ 820)
      let dLight1 = Math.hypot((x - 840) * 0.85, (y - 520) * 1.1);
      let dLight2 = Math.hypot((x - 580) * 0.9, (y - 780) * 1.0);
      let baseLum = 0.32 + 0.48 * Math.exp(-Math.pow(dLight1 / 360, 2)) +
                    0.35 * Math.exp(-Math.pow(dLight2 / 310, 2));

      // Modulate by wood cathedral growth rings
      let grainMod = 0.72 + 0.36 * Math.sin(idx * 0.45 + Math.sin(uOff * 0.008) * 2.5);
      let shadow = boardShadowFactor(x, y);
      let lum = baseLum * grainMod * shadow * knotDarken;

      // Brighten slightly near the front beveled edge of the board
      let dEdgeL = Math.abs((x - (-135)) * (960 - 535) - (y - 535) * (635 - (-135))) / Math.hypot(770, 425);
      let dEdgeR = Math.abs((x - 635) * (615 - 960) - (y - 960) * (1255 - 635)) / Math.hypot(620, 345);
      if (Math.min(dEdgeL, dEdgeR) < 18) lum += 0.22 * shadow;

      let w = lum > 0.09 ? map(lum, 0.09, 0.95, 0.16, 1.28, true) : 0;
      pts.push({ x, y, w });
    }
    carveSegmentedSpline(pts, brushName, col, 1.0, 0.45);
  }

  // Carve concentric elliptical rings inside the two wood knots
  for (let k of boardKnots) {
    for (let rScale = 0.28; rScale <= 1.05; rScale += 0.22) {
      let kPts = [];
      for (let a = 0; a <= 360; a += 15) {
        let rad = a * (Math.PI / 180);
        let lx = Math.cos(rad) * k.rx * rScale;
        let ly = Math.sin(rad) * k.ry * rScale;
        let x = k.x + lx * Math.cos(k.ang) - ly * Math.sin(k.ang);
        let y = k.y + lx * Math.sin(k.ang) + ly * Math.cos(k.ang);
        let w = (inBoardTop(x, y) && !inKnifeTotal(x, y)) ? (0.35 + 0.35 * Math.sin(rad * 2)) : 0;
        kPts.push({ x, y, w });
      }
      carveSegmentedSpline(kPts, "needle", "#b89f80", 1.0, 0.5);
    }
  }

  // Carve old knife score scratches across the board's working zone
  randomSeed(9102);
  for (let i = 0; i < 42; i++) {
    let sx = random(280, 960);
    let sy = random(420, 860);
    let ang = random(-52, -18) * (Math.PI / 180);
    let len = random(35, 130);
    let sPts = [];
    for (let t = -0.5; t <= 0.5; t += 0.125) {
      let x = sx + t * len * Math.cos(ang);
      let y = sy + t * len * Math.sin(ang) + 2 * Math.sin(t * 6);
      if (!inBoardTop(x, y) || inLoafTotal(x, y) || inSliceTotal(x, y) || inKnifeTotal(x, y)) {
        sPts.push({ x, y, w: 0 });
      } else {
        let sh = boardShadowFactor(x, y);
        let env = Math.sin((t + 0.5) * Math.PI);
        sPts.push({ x, y, w: sh > 0.25 ? 0.55 * env * sh : 0 });
      }
    }
    carveSegmentedSpline(sPts, "needle", "#eadbc2", 1.0, 0.3);
  }
}

function drawBreadboardFrontEdge() {
  // Left front edge: (-135, 535) to (635, 960), darker shadow face
  for (let i = 0; i <= 140; i++) {
    let t = i / 140;
    let tx = lerp(-135, 635, t);
    let ty = lerp(535, 960, t);
    if (tx < 82 || tx > 1118) continue;

    let pts = [];
    // Vertical/slightly angled end-grain chisel marks
    let dCorner = Math.abs(tx - 635);
    let lumBase = 0.24 + 0.35 * Math.exp(-dCorner / 160) + 0.12 * Math.sin(i * 0.7);
    for (let s = 0; s <= 1.0; s += 0.2) {
      let x = tx + s * 4 * Math.sin(i * 0.3);
      let y = ty + s * BOARD_EDGE_H;
      let vertEnv = (1 - 0.65 * s) * Math.sin(s * Math.PI * 0.85 + 0.2);
      let w = lumBase * vertEnv > 0.08 ? map(lumBase * vertEnv, 0.08, 0.6, 0.18, 0.82, true) : 0;
      pts.push({ x, y, w });
    }
    carveSegmentedSpline(pts, "needle", "#a89074", 1.0, 0.3);
  }

  // Right front edge: (635, 960) to (1255, 615), illuminated raking face
  for (let i = 0; i <= 125; i++) {
    let t = i / 125;
    let tx = lerp(635, 1255, t);
    let ty = lerp(960, 615, t);
    if (tx < 82 || tx > 1118) continue;

    let pts = [];
    let lumBase = 0.55 + 0.28 * Math.sin(t * Math.PI) + 0.16 * Math.sin(i * 0.85);
    for (let s = 0; s <= 1.0; s += 0.2) {
      let x = tx + s * 3 * Math.cos(i * 0.4);
      let y = ty + s * BOARD_EDGE_H;
      let vertEnv = (1 - 0.45 * s) * (0.4 + 0.6 * Math.sin(s * Math.PI));
      let w = map(lumBase * vertEnv, 0.1, 0.9, 0.28, 1.35, true);
      pts.push({ x, y, w });
    }
    let col = (i % 3 === 0) ? "#ebdcc3" : "#ccb696";
    carveSegmentedSpline(pts, "burin", col, 1.0, 0.3);
  }

  // Crisp bright white-line highlight along the front bevel ridge of the board
  let bevelL = [];
  for (let x = 84; x <= 635; x += 12) {
    let y = map(x, -135, 635, 535, 960);
    let sh = boardShadowFactor(x, y - 4);
    let w = (0.35 + 0.65 * Math.pow((x - 84) / 551, 1.5)) * (0.4 + 0.6 * sh);
    bevelL.push({ x, y, w });
  }
  carveSegmentedSpline(bevelL, "burin", "#dfd0b6", 1.0, 0.3);

  let bevelR = [];
  for (let x = 635; x <= 1116; x += 12) {
    let y = map(x, 635, 1255, 960, 615);
    let w = 1.15 - 0.35 * ((x - 635) / 500);
    bevelR.push({ x, y, w });
  }
  carveSegmentedSpline(bevelR, "scorper", "#f5e8d0", 0.95, 0.3);
}

// ============================================================================
// SOURDOUGH LOAF — 3D DOMED CRUST & BAKER'S EAR (GRIGNE)
// ============================================================================

function crustIllumination(t, s) {
  // t in [-1.0, 0.88] around the arch, s in [0, 1] from cut face to back silhouette
  // Upper-right crown (t in [-0.18, 0.65]) is heavily flour-dusted and brightly lit
  let crownLight = Math.exp(-Math.pow((t - 0.16) / 0.48, 2)) * (1 - 0.22 * s * s);
  // Secondary highlight along the raised Baker's Ear ridge (t ~ -0.17)
  let earHighlight = 0.65 * Math.exp(-Math.pow((t + 0.17) / 0.055, 2)) * (1 - 0.3 * s);
  // Deep shadow inside the score cleft right under the overhanging ear (t in [-0.38, -0.20])
  let scoreRecess = Math.exp(-Math.pow((t + 0.26) / 0.055, 2)) * (1 - 0.25 * s);
  // Lower-left flank (t < -0.38) curves away from light into toasted sepia shadow
  let flankLight = 0.42 * Math.exp(-Math.pow((t + 0.56) / 0.32, 2)) * (1 - 0.48 * s);
  // Banneton proofing basket flour rings along the lower flank
  let bannetonRings = (t < -0.40) ? 0.18 * Math.pow(Math.max(0, Math.sin((t + 1.0) * 34 + s * 4)), 2) : 0;

  let lum = crownLight + earHighlight + flankLight + bannetonRings;
  lum *= (1 - 0.82 * scoreRecess);

  // Darken at the very bottom heel where loaf meets board
  if (t < -0.84) {
    lum *= constrain(map(t, -1.0, -0.84, 0.08, 1.0), 0.05, 1.0);
  }
  // Rim light along the upper back silhouette (t in [-0.15, 0.45], s > 0.82)
  if (t > -0.20 && t < 0.50 && s > 0.78) {
    lum += 0.28 * ((s - 0.78) / 0.22);
  }
  return constrain(lum, 0, 1.15);
}

function drawLoafDomedCrust() {
  // 1. Meridian Contour Lines (constant t, wrapping from cut face s=0 back over dome to s=1)
  for (let i = 0; i <= 148; i++) {
    let t = lerp(-0.98, 0.86, i / 148);
    // Skip main meridian lines right inside the score cleft so we can carve perpendicular dough filaments there
    let inCleftZone = (t > -0.37 && t < -0.20);

    let pts = [];
    for (let j = 0; j <= 24; j++) {
      let s = j / 24;
      if (inCleftZone && s < 0.78) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let p = domeSurfacePoint(t, s);
      // Ensure we don't draw inside the cut face interior
      let cf = getCutFaceInfo(p.x, p.y);
      if (cf.inside && !cf.inCrustRim && s > 0.04) {
        pts.push({ x: p.x, y: p.y, w: 0 });
        continue;
      }

      let lum = crustIllumination(t, s);
      // Organic flour variation via noise
      let n = noise(t * 6.0 + 10, s * 6.0 + 10);
      if (t > -0.18 && t < 0.62) {
        lum *= (0.78 + 0.42 * n); // Flour patches
      }

      let w = lum > 0.07 ? map(lum, 0.07, 1.05, 0.18, 1.75, true) : 0;
      pts.push({ x: p.x, y: p.y, w: w });
    }

    // Select soft pastel / warm sepia crust color according to position on dome
    let col, bName;
    if (t > -0.19 && t < 0.55) {
      col = (i % 2 === 0) ? "#f6ead6" : "#e5c5aa"; // Flour cream & soft pastel peach-apricot
      bName = (i % 3 === 0) ? "scorper" : "burin";
    } else if (t >= 0.55) {
      col = "#d9b493";
      bName = "burin";
    } else {
      col = (t > -0.65) ? "#c79e7c" : "#9c7a5c"; // Toasted sepia-ochre flank
      bName = "needle";
    }
    carveSegmentedSpline(pts, bName, col, 1.0, 0.5);
  }

  // 2. Arching Latitude Contour Lines (constant s, sweeping along the dome from heel to right shoulder)
  // This cross-engraving builds the dense chalky flour on the crown and curved crust grain
  for (let j = 1; j <= 26; j++) {
    let s = j / 27;
    let pts = [];
    for (let i = 0; i <= 90; i++) {
      let t = lerp(-0.95, 0.85, i / 90);
      if (t > -0.37 && t < -0.20 && s < 0.78) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let p = domeSurfacePoint(t, s);
      let lum = crustIllumination(t, s);
      // Emphasize cross-lines in the bright flour zones and banneton ridges
      let w = lum > 0.26 ? map(lum, 0.26, 1.08, 0.15, 1.35, true) : 0;
      pts.push({ x: p.x, y: p.y, w: w });
    }
    let col = (j % 2 === 0) ? "#f9f1e2" : "#e2bfa3";
    carveSegmentedSpline(pts, "burin", col, 0.9, 0.5);
  }

  // 3. The Baker's Score / Grigne Cleft (Stretched dough filaments & sharp overhanging Ear Lip)
  // Overhanging jagged Ear Lip along t = -0.185
  let earLipPts = [];
  for (let j = 0; j <= 32; j++) {
    let s = (j / 32) * 0.82;
    let jag = 0.012 * Math.sin(j * 2.3) + 0.008 * Math.cos(j * 4.1);
    let p = domeSurfacePoint(-0.185 + jag, s);
    let w = (1.65 - 0.9 * s) * (0.75 + 0.35 * Math.sin(j * 1.7));
    earLipPts.push({ x: p.x, y: p.y, w: w });
  }
  carveSegmentedSpline(earLipPts, "scorper", "#fbf3e6", 1.0, 0.4);

  // Stretched dough ribs bridging perpendicularly across the score cleft (t from -0.38 to -0.19)
  for (let j = 0; j <= 52; j++) {
    let s = (j / 52) * 0.80;
    let ribPts = [];
    for (let k = 0; k <= 10; k++) {
      let uFrac = k / 10; // 0 = lower edge of score (-0.38), 1 = deep under ear lip (-0.19)
      let t = lerp(-0.385, -0.195, uFrac);
      let p = domeSurfacePoint(t, s + 0.025 * Math.sin(uFrac * Math.PI));
      // Deeply shadowed under the lip (uFrac > 0.72), catching glossy light near lower edge (uFrac in [0.1, 0.55])
      let ribLum = Math.sin(uFrac * Math.PI * 0.88) * (1 - 0.65 * Math.pow(uFrac, 1.8)) * (1 - 0.55 * s);
      let w = ribLum > 0.1 ? map(ribLum, 0.1, 0.65, 0.2, 1.05, true) : 0;
      ribPts.push({ x: p.x, y: p.y, w: w });
    }
    let col = (j % 2 === 0) ? "#e6c7ab" : "#c49c7b";
    carveSegmentedSpline(ribPts, "needle", col, 1.0, 0.4);
  }

  // 4. Baked Crust Blisters (Tiny dark rings with bright carved upper-right crescents)
  randomSeed(3318);
  for (let b = 0; b < 85; b++) {
    let t = random(-0.88, 0.75);
    let s = random(0.06, 0.92);
    if (t > -0.37 && t < -0.20 && s < 0.78) continue;
    let center = domeSurfacePoint(t, s);
    let r = random(2.5, 6.5);
    let lum = crustIllumination(t, s);
    if (lum < 0.12) continue;

    // Carve tiny crescent highlight on upper-right of blister
    let cPts = [];
    for (let a = -75; a <= 65; a += 35) {
      let rad = a * (Math.PI / 180);
      cPts.push({
        x: center.x + Math.cos(rad) * r,
        y: center.y + Math.sin(rad) * r * 0.8,
        w: 0.45 * lum * Math.cos(rad * 0.8)
      });
    }
    carveSegmentedSpline(cPts, "needle", "#f7ecda", 1.0, 0.5);
  }
}

// ============================================================================
// SOURDOUGH LOAF — CUT FACE (CRUST RIM, RADIATING CRUMB & STIPPLED HOLES)
// ============================================================================

function crumbMatrixLineWeight(x, y, holeList) {
  // Returns modulated burin weight for a point in the crumb matrix, breaking cleanly inside holes
  let minD = 999;
  for (let h of holeList) {
    let nd = holeNormalizedDist(x, y, h);
    if (nd < 0.98) return 0; // Inside alveolus cavity -> stop white matrix line!
    if (nd < minD) minD = nd;
  }
  // Ridges right around the edge of a bubble (nd in [1.0, 1.35]) catch extra bright light
  let ridgeBoost = minD < 1.38 ? 1.25 : 1.0;
  return ridgeBoost;
}

function drawMainLoafCutFace() {
  // 1. Cut Crust Rim (Short dense radial burin cuts around the outer 20px arch & bottom crust)
  for (let i = 0; i <= 185; i++) {
    let u = lerp(-0.99, 0.99, i / 185);
    let vMax = cutFaceArchHeight(u);
    let crustW = 20 + 7 * Math.exp(-Math.pow((u + 0.20) / 0.14, 2));
    let pOuter = fromCutFaceUV(u, vMax);
    let pInner = fromCutFaceUV(u, vMax - crustW);

    let rimPts = [];
    for (let k = 0; k <= 5; k++) {
      let f = k / 5; // 0 = outer edge, 1 = inner transition to crumb
      let x = lerp(pOuter.x, pInner.x, f);
      let y = lerp(pOuter.y, pInner.y, f);
      // Darker toasted outer crust, brightening sharply as it meets the pale crumb at f -> 1
      let lum = (0.25 + 0.75 * Math.pow(f, 1.3)) * (0.55 + 0.45 * Math.exp(-Math.pow((u - 0.15) / 0.75, 2)));
      rimPts.push({ x, y, w: lum * 1.15 });
    }
    let col = (i % 2 === 0) ? "#e8caa9" : "#c79f79";
    carveSegmentedSpline(rimPts, "needle", col, 1.0, 0.3);
  }

  // Bottom crust rim ticks along the base from C_L to C_R
  for (let i = 0; i <= 115; i++) {
    let u = lerp(-0.96, 0.96, i / 115);
    let vMin = cutFaceBaseOffset(u);
    let pBot = fromCutFaceUV(u, vMin);
    let pTop = fromCutFaceUV(u, vMin + 13);
    brush.set("needle", "#b59270", 0.55);
    brush.line(pBot.x, pBot.y, pTop.x, pTop.y);
  }

  // 2. Family 1 of Crumb Matrix: Fine, closely spaced RADIATING white lines fanning from bottom-center
  let radOrigin = fromCutFaceUV(-0.02, -18);
  for (let i = 0; i <= 190; i++) {
    let angDeg = lerp(-168, -12, i / 190);
    let ang = angDeg * (Math.PI / 180);
    let dirX = Math.cos(ang);
    let dirY = Math.sin(ang);
    let perpX = -dirY;
    let perpY = dirX;

    let pts = [];
    for (let r = 22; r <= 375; r += 6.5) {
      let bx = radOrigin.x + r * dirX;
      let by = radOrigin.y + r * dirY;

      // Gently deflect the ray around nearby alveoli (like stretched gluten strands)
      let pushX = 0, pushY = 0;
      for (let h of alveoliMain) {
        let dx = bx - h.x;
        let dy = by - h.y;
        let d = Math.hypot(dx, dy);
        let radMean = (h.rx + h.ry) * 0.5;
        if (d > radMean * 0.85 && d < radMean * 2.1) {
          let factor = (radMean * 0.42) * Math.exp(-Math.pow((d - radMean) / (radMean * 0.65), 2));
          pushX += (dx / d) * factor;
          pushY += (dy / d) * factor;
        }
      }

      let x = bx + pushX + 2.2 * Math.sin(r * 0.045 + i * 0.3) * perpX;
      let y = by + pushY + 2.2 * Math.sin(r * 0.045 + i * 0.3) * perpY;

      let cf = getCutFaceInfo(x, y);
      if (!cf.inside || cf.inCrustRim) {
        pts.push({ x, y, w: 0 });
        continue;
      }

      let holeMod = crumbMatrixLineWeight(x, y, alveoliMain);
      if (holeMod <= 0) {
        pts.push({ x, y, w: 0 });
        continue;
      }

      // Crumb lighting: luminous across upper/right and center, slightly softer toward bottom-left
      let crumbLum = 0.62 + 0.35 * ((cf.u + 1.0) * 0.5) + 0.22 * (cf.v / 260);
      let w = constrain(crumbLum * holeMod * 1.12, 0.35, 1.85);
      pts.push({ x, y, w });
    }

    let col = (i % 3 === 0) ? "#faf2e1" : ((i % 3 === 1) ? "#efe0be" : "#dec7a2");
    let bName = (i % 2 === 0) ? "burin" : "scorper";
    carveSegmentedSpline(pts, bName, col, 0.92, 0.45);
  }

  // 3. Family 2 of Crumb Matrix: Arching contour lines following the rise of the loaf
  for (let j = 1; j <= 88; j++) {
    let vFrac = j / 89;
    let pts = [];
    for (let i = 0; i <= 72; i++) {
      let u = lerp(-0.95, 0.95, i / 72);
      let vMin = cutFaceBaseOffset(u) + 12;
      let vMax = cutFaceArchHeight(u) - 18;
      if (vMax <= vMin) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let v = lerp(vMin, vMax, vFrac) + 3.0 * Math.sin(u * 10 + j * 0.5);
      let p = fromCutFaceUV(u, v);

      // Deflect slightly around holes
      let pushX = 0, pushY = 0;
      for (let h of alveoliMain) {
        let dx = p.x - h.x;
        let dy = p.y - h.y;
        let d = Math.hypot(dx, dy);
        let radMean = (h.rx + h.ry) * 0.5;
        if (d > radMean * 0.85 && d < radMean * 1.9) {
          let factor = (radMean * 0.36) * Math.exp(-Math.pow((d - radMean) / (radMean * 0.55), 2));
          pushX += (dx / d) * factor;
          pushY += (dy / d) * factor;
        }
      }

      let x = p.x + pushX;
      let y = p.y + pushY;
      let cf = getCutFaceInfo(x, y);
      if (!cf.inside || cf.inCrustRim) {
        pts.push({ x, y, w: 0 });
        continue;
      }

      let holeMod = crumbMatrixLineWeight(x, y, alveoliMain);
      if (holeMod <= 0) {
        pts.push({ x, y, w: 0 });
        continue;
      }

      let crumbLum = 0.55 + 0.35 * ((u + 1.0) * 0.5) + 0.25 * vFrac;
      let w = constrain(crumbLum * holeMod * 0.95, 0.3, 1.55);
      pts.push({ x, y, w });
    }
    let col = (j % 2 === 0) ? "#f5e8cc" : "#e5d2af";
    carveSegmentedSpline(pts, "burin", col, 0.88, 0.45);
  }

  // 4. Alveoli Rim Ridges & Tiny Stippled Flecks Inside Every Crumb Hole
  drawAlveoliDetails(alveoliMain, false);
}

function drawAlveoliDetails(holeList, isForeshortened) {
  randomSeed(5512);
  for (let h of holeList) {
    // A. Carve a crisp white gluten rim around the alveolus perimeter
    // Light is from upper-right (-40 deg), so the lower-left inner wall/rim catches direct light
    // and the upper-right outer edge catches a sharp cut-plane ridge!
    let rimPts = [];
    for (let a = 0; a <= 360; a += 18) {
      let theta = a * (Math.PI / 180);
      let wobble = 1.0 + 0.08 * Math.sin(theta * 3 + h.seed) + 0.05 * Math.cos(theta * 5 - h.seed);
      let lx = Math.cos(theta) * (h.rx * wobble + 1.6);
      let ly = Math.sin(theta) * (h.ry * wobble + 1.6);
      let x = h.x + lx * Math.cos(h.ang) - ly * Math.sin(h.ang);
      let y = h.y + lx * Math.sin(h.ang) + ly * Math.cos(h.ang);

      // World angle of this point on the rim
      let worldAng = Math.atan2(y - h.y, x - h.x);
      // Lower-left rim (worldAng around +135 deg / 2.35 rad) and upper-right edge catch bright highlights
      let rimLight = 0.55 + 0.45 * Math.cos(worldAng - 2.35);
      rimPts.push({ x, y, w: constrain(rimLight * 1.15, 0.28, 1.35) });
    }
    carveSegmentedSpline(rimPts, "burin", "#fbf3e3", 0.9, 0.5);

    // B. Tiny Stippled Flecks & Concave Inner Contour Threads Inside the Crumb Hole
    let area = h.rx * h.ry;
    let numStipples = constrain(Math.floor(area * 0.42), 5, 75);

    for (let s = 0; s < numStipples; s++) {
      // Distribute stipples inside the ellipse
      let rFrac = Math.sqrt(random(0.05, 0.88));
      let theta = random(Math.PI * 2);
      let lx = Math.cos(theta) * h.rx * rFrac;
      let ly = Math.sin(theta) * h.ry * rFrac;
      let sx = h.x + lx * Math.cos(h.ang) - ly * Math.sin(h.ang);
      let sy = h.y + lx * Math.sin(h.ang) + ly * Math.cos(h.ang);

      // Inside a concave hole lit from upper-right (-0.7, -0.7):
      // The upper-right inside of the hole is in deep cast shadow from the rim.
      // The lower-left inside of the hole faces the light!
      let normX = (sx - h.x) / h.rx;
      let normY = (sy - h.y) / h.ry;
      let cavityLight = (-normX * 0.68 + normY * 0.73) * 0.5 + 0.42;

      // Leave the deepest upper-right crescent mostly pure black with only rare tiny flecks
      if (cavityLight < 0.28 && random() < 0.78) continue;

      let fleckLen = random(1.4, 3.8) * (isForeshortened ? 0.8 : 1.0);
      // Orient fleck tangent to the concave bubble wall
      let tangAng = Math.atan2(sy - h.y, sx - h.x) + Math.PI * 0.5 + random(-0.25, 0.25);
      let dx = Math.cos(tangAng) * fleckLen * 0.5;
      let dy = Math.sin(tangAng) * fleckLen * 0.5;

      let col = cavityLight > 0.62 ? "#ebd7b7" : (cavityLight > 0.40 ? "#c7ae8c" : "#947c61");
      let w = map(cavityLight, 0.15, 0.9, 0.32, 0.88, true);
      brush.set("needle", col, w);
      brush.line(sx - dx, sy - dy, sx + dx, sy + dy);
    }
  }
}

// ============================================================================
// FOREGROUND CUT SLICE ON THE BOARD
// ============================================================================

function drawForegroundSlice() {
  // 1. Vertical Side Crust Wall of the Slice (30px thick front arch resting on the board)
  for (let i = 0; i <= 120; i++) {
    let u = lerp(-0.98, 0.98, i / 120);
    let vArch = sliceArchHeight(u);
    let pTop = fromSliceUV(u, vArch);
    let wallH = 31 * Math.pow(Math.max(0, 1 - u * u * 0.85), 0.35);
    if (wallH < 4) continue;

    // Illumination on the curved vertical crust wall: darker on left (u < 0), brighter on right (u > 0.1)
    let wallLum = 0.22 + 0.58 * Math.pow((u + 1.0) * 0.5, 1.4) + 0.12 * Math.sin(i * 0.8);
    let pts = [];
    for (let k = 0; k <= 4; k++) {
      let f = k / 4;
      let x = pTop.x + 2 * Math.sin(f * Math.PI);
      let y = pTop.y + f * wallH;
      let vertEnv = Math.sin(f * Math.PI * 0.85 + 0.15) * (1 - 0.35 * f);
      let w = wallLum * vertEnv > 0.08 ? map(wallLum * vertEnv, 0.08, 0.75, 0.18, 0.98, true) : 0;
      pts.push({ x, y, w });
    }
    let col = u > 0.1 ? "#d8b493" : "#a38062";
    carveSegmentedSpline(pts, "needle", col, 1.0, 0.3);
  }

  // Top crust rim border of the slice
  let rimArchPts = [];
  for (let i = 0; i <= 70; i++) {
    let u = lerp(-0.98, 0.98, i / 70);
    let p = fromSliceUV(u, sliceArchHeight(u) - 4);
    let w = 0.55 + 0.55 * ((u + 1) * 0.5);
    rimArchPts.push({ x: p.x, y: p.y, w });
  }
  carveSegmentedSpline(rimArchPts, "burin", "#e8cba8", 0.95, 0.4);

  // 2. Foreshortened Crumb Matrix on the Slice Top
  // Family A: Lines roughly parallel to back cut edge
  for (let j = 1; j <= 38; j++) {
    let vFrac = j / 39;
    let pts = [];
    for (let i = 0; i <= 54; i++) {
      let u = lerp(-0.94, 0.94, i / 54);
      let vMax = sliceArchHeight(u) - 10;
      if (vMax <= 8) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let v = lerp(7, vMax, vFrac) + 2.0 * Math.sin(u * 9 + j);
      let p = fromSliceUV(u, v);
      let holeMod = crumbMatrixLineWeight(p.x, p.y, alveoliSlice);
      if (holeMod <= 0) {
        pts.push({ x: p.x, y: p.y, w: 0 });
        continue;
      }
      let lum = 0.58 + 0.32 * ((u + 1) * 0.5) - 0.12 * (1 - vFrac);
      pts.push({ x: p.x, y: p.y, w: lum * holeMod * 1.05 });
    }
    let col = (j % 2 === 0) ? "#f4e6ca" : "#dfd0b0";
    carveSegmentedSpline(pts, "burin", col, 0.9, 0.4);
  }

  // Family B: Radial lines fanning across the slice
  let slOrigin = fromSliceUV(0, -10);
  for (let i = 0; i <= 75; i++) {
    let ang = lerp(12, 168, i / 75) * (Math.PI / 180);
    let pts = [];
    for (let r = 14; r <= 220; r += 6) {
      let uPx = Math.cos(ang) * r * 1.15;
      let vPx = Math.sin(ang) * r * 0.72;
      let u = uPx / SL_HALF;
      let v = vPx;
      if (Math.abs(u) > 0.93 || v < 7 || v > sliceArchHeight(u) - 10) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let p = fromSliceUV(u, v);
      let holeMod = crumbMatrixLineWeight(p.x, p.y, alveoliSlice);
      if (holeMod <= 0) {
        pts.push({ x: p.x, y: p.y, w: 0 });
        continue;
      }
      pts.push({ x: p.x, y: p.y, w: 0.78 * holeMod });
    }
    carveSegmentedSpline(pts, "needle", "#f7edd6", 0.95, 0.4);
  }

  // 3. Stippled flecks & rims inside the Slice's alveoli
  drawAlveoliDetails(alveoliSlice, true);
}

// ============================================================================
// BREAD KNIFE (STEEL BLADE, SCALLOPED SERRATIONS, BOLSTER & WOODEN HANDLE)
// ============================================================================

function drawBreadKnife() {
  // 1. Steel Blade Longitudinal Engraving (s from 0 to 435)
  // Soft pastel powder-blue & silver-ivory lines modulated by metallic reflection bands
  for (let i = 0; i <= 15; i++) {
    let wFrac = i / 15; // 0 = spine (top), 1 = near serrated bevel (bottom)
    let pts = [];
    for (let s = 2; s <= 432; s += 7) {
      let b = getKnifeBounds(s);
      let w = lerp(b.wTop + 1.8, b.wBot - 4.5, wFrac);
      if (w >= b.wBot - 2) {
        pts.push({ x: 0, y: 0, w: 0 });
        continue;
      }
      let p = fromKnifeUV(s, w);

      // Diagonal specular reflection bands across the flat of the steel blade
      let bandCoord = s * 0.028 - w * 0.055;
      let spec1 = Math.exp(-Math.pow(s - 155 - w * 1.2, 2) / 3800);
      let spec2 = Math.exp(-Math.pow(s - 315 - w * 1.2, 2) / 2600);
      let spec3 = Math.exp(-Math.pow(s - 55 - w * 1.2, 2) / 1800);
      let darkBand = Math.exp(-Math.pow(s - 235 - w * 1.2, 2) / 1900);

      let metalLum = 0.35 + 0.65 * spec1 + 0.58 * spec2 + 0.42 * spec3 + 0.14 * Math.sin(bandCoord * 3.0);
      metalLum *= (1 - 0.68 * darkBand);
      // Extra bright along the top spine edge (wFrac == 0)
      if (i === 0) metalLum = Math.max(metalLum, 0.85);

      let lineW = metalLum > 0.08 ? map(metalLum, 0.08, 1.05, 0.18, 1.45, true) : 0;
      pts.push({ x: p.x, y: p.y, w: lineW });
    }
    let col = (i === 0 || i === 5 || i === 11) ? "#f4f5f6" : ((i % 2 === 0) ? "#d2dee4" : "#adbcc4");
    let bName = (i === 0) ? "scorper" : "burin";
    carveSegmentedSpline(pts, bName, col, 0.92, 0.25);
  }

  // Diagonal cross-hatched sheen strokes in the two main specular reflection bands on the blade
  for (let s = 25; s <= 405; s += 6.5) {
    let spec1 = Math.exp(-Math.pow(s - 155, 2) / 3400);
    let spec2 = Math.exp(-Math.pow(s - 315, 2) / 2400);
    let spec = Math.max(spec1, spec2);
    if (spec < 0.22) continue;

    let b = getKnifeBounds(s);
    let p1 = fromKnifeUV(s - 8, b.wTop + 2.5);
    let p2 = fromKnifeUV(s + 8, b.wBot - 6.0);
    brush.set("needle", "#eef2f5", spec * 0.85);
    brush.line(p1.x, p1.y, p2.x, p2.y);
  }

  // 2. Scalloped Serrated Cutting Edge (Distinct engraved flutes & razor-sharp tooth glints)
  for (let s = 18; s < 412; s += 21.0) {
    let bLeft = getKnifeBounds(s);
    let bMid = getKnifeBounds(s + 10.5);
    let bRight = getKnifeBounds(s + 21.0);

    // Bright scalloped bevel arc hugging each flute
    let scallopPts = [];
    for (let k = 0; k <= 6; k++) {
      let f = k / 6;
      let ss = s + f * 21.0;
      let bb = getKnifeBounds(ss);
      let fluteDepth = 5.5 * Math.sin(f * Math.PI);
      let p = fromKnifeUV(ss, bb.wBot - fluteDepth);
      // Right side of each scallop catches sharp light
      let toothLum = 0.35 + 0.75 * Math.pow(f, 1.2);
      scallopPts.push({ x: p.x, y: p.y, w: toothLum * 1.05 });
    }
    carveSegmentedSpline(scallopPts, "needle", "#f6f7f8", 1.0, 0.45);

    // Vertical hollow-ground flute lines inside each scallop
    for (let m = 1; m <= 3; m++) {
      let ss = s + m * 5.2;
      let bb = getKnifeBounds(ss);
      let pTop = fromKnifeUV(ss, bb.wBot - 5.2);
      let pBot = fromKnifeUV(ss, bb.wBot - 0.5);
      brush.set("needle", "#c8d5dc", 0.55);
      brush.line(pTop.x, pTop.y, pBot.x, pBot.y);
    }
  }

  // 3. Sculpted Steel Bolster Collar (s from -22 to 0)
  for (let s = -22; s <= 0; s += 2.8) {
    let b = getKnifeBounds(s);
    let bolsterPts = [];
    for (let k = 0; k <= 8; k++) {
      let f = k / 8; // 0 = top ridge, 1 = bottom shadow
      let w = lerp(b.wTop, b.wBot, f);
      let p = fromKnifeUV(s + 2.5 * Math.sin(f * Math.PI), w);
      let cylLum = Math.sin((1 - f * 0.82) * Math.PI) * (0.55 + 0.45 * Math.cos((s + 11) * 0.25));
      bolsterPts.push({ x: p.x, y: p.y, w: cylLum * 1.15 });
    }
    carveSegmentedSpline(bolsterPts, "needle", "#e2eaee", 1.0, 0.45);
  }

  // 4. Turned Wooden Handle (s from -265 to -22) & Three Brass Rivets
  let rivets = [-62, -135, -208];
  let rivetRadius = 6.2;

  // Longitudinal wood-grain & cylindrical highlight lines along the handle
  for (let i = 0; i <= 14; i++) {
    let f = i / 14; // 0 = upper lit ridge, 1 = lower shadowed belly
    let pts = [];
    for (let s = -263; s <= -23; s += 6) {
      let b = getKnifeBounds(s);
      let w = lerp(b.wTop + 1.5, b.wBot - 1.5, f);

      // Check if inside one of the 3 circular brass rivets
      let inRivet = false;
      for (let rs of rivets) {
        if (Math.hypot(s - rs, w - 1.0) < rivetRadius + 1.8) {
          inRivet = true;
          break;
        }
      }
      let p = fromKnifeUV(s, w);
      if (inRivet) {
        pts.push({ x: p.x, y: p.y, w: 0 });
        continue;
      }

      // Cylindrical lighting across handle width (brightest near f = 0.22)
      let cylLight = Math.exp(-Math.pow((f - 0.22) / 0.36, 2));
      let grainWave = 0.78 + 0.28 * Math.sin(s * 0.08 + i * 0.9);
      let lum = cylLight * grainWave;
      let lineW = lum > 0.08 ? map(lum, 0.08, 0.95, 0.16, 1.18, true) : 0;
      pts.push({ x: p.x, y: p.y, w: lineW });
    }
    let col = f < 0.38 ? "#e6ccb0" : (f < 0.68 ? "#c29b8b" : "#8f6d61"); // Soft dusty rosewood pastel
    carveSegmentedSpline(pts, "needle", col, 1.0, 0.4);
  }

  // Cross-contour turned rings along the wooden handle
  for (let s = -258; s <= -26; s += 7.5) {
    let b = getKnifeBounds(s);
    let ringPts = [];
    for (let k = 0; k <= 8; k++) {
      let f = k / 8;
      let w = lerp(b.wTop + 1.5, b.wBot - 1.5, f);
      let inRivet = false;
      for (let rs of rivets) {
        if (Math.hypot(s - rs, w - 1.0) < rivetRadius + 1.8) inRivet = true;
      }
      let p = fromKnifeUV(s + 3.5 * Math.sin(f * Math.PI), w);
      if (inRivet) {
        ringPts.push({ x: p.x, y: p.y, w: 0 });
        continue;
      }
      let cylLight = Math.exp(-Math.pow((f - 0.24) / 0.34, 2));
      ringPts.push({ x: p.x, y: p.y, w: cylLight > 0.22 ? cylLight * 0.68 : 0 });
    }
    carveSegmentedSpline(ringPts, "needle", "#d6b49d", 1.0, 0.45);
  }

  // Carve the Three Brass Rivets
  for (let rs of rivets) {
    let center = fromKnifeUV(rs, 1.0);
    let rPts = [];
    for (let a = 0; a <= 360; a += 24) {
      let rad = a * (Math.PI / 180);
      let x = center.x + Math.cos(rad) * rivetRadius;
      let y = center.y + Math.sin(rad) * rivetRadius;
      let glint = 0.45 + 0.55 * Math.cos(rad + 0.7);
      rPts.push({ x, y, w: glint * 0.95 });
    }
    carveSegmentedSpline(rPts, "needle", "#f5e3be", 1.0, 0.5);

    // Bright specular slash across center of brass rivet
    brush.set("burin", "#fbf1dc", 0.85);
    brush.line(center.x - 2.5, center.y - 2.0, center.x + 2.5, center.y + 2.0);
  }
}

// ============================================================================
// SCATTERED BREAD CRUMBS & FLOUR DUSTING
// ============================================================================

function drawScatteredCrumbsAndFlour() {
  randomSeed(8841);

  // Cluster zones: in front of the cut face, around the slice, near the knife blade, and on the blade flat
  let crumbClusters = [
    { cx: 585, cy: 615, rx: 140, ry: 75, count: 55 }, // Between loaf, slice & knife
    { cx: 340, cy: 835, rx: 160, ry: 55, count: 42 }, // In front of cut slice
    { cx: 790, cy: 525, rx: 110, ry: 65, count: 38 }, // Near right corner of loaf & knife tip
    { cx: 760, cy: 655, rx: 120, ry: 70, count: 32 }  // Along knife blade side
  ];

  for (let cl of crumbClusters) {
    for (let i = 0; i < cl.count; i++) {
      let ang = random(Math.PI * 2);
      let dist = Math.pow(random(), 0.7) * 1.0;
      let x = cl.cx + Math.cos(ang) * cl.rx * dist;
      let y = cl.cy + Math.sin(ang) * cl.ry * dist;

      if (!inBoardTop(x, y) || inLoafTotal(x, y) || inSliceTotal(x, y)) continue;

      let sizeTier = random();
      if (sizeTier < 0.26 && !inKnifeTotal(x, y)) {
        // Larger porous sourdough crumb chunk (5px - 11px) with dark base shadow & carved white cluster
        let r = random(3.2, 6.8);
        // Tiny dark shadow under left-bottom of crumb
        brush.noStroke();
        brush.wash("#0b0908", 245);
        brush.circle(x - 1.8, y + 2.2, r * 1.1, 0.25);
        brush.noWash();

        // Carve 4-7 tiny irregular white stipple strokes forming the illuminated crumb top
        let subCount = Math.floor(r * 1.3);
        for (let k = 0; k < subCount; k++) {
          let ox = random(-r * 0.65, r * 0.75);
          let oy = random(-r * 0.75, r * 0.55);
          let col = (ox - oy > 0) ? "#faf1df" : "#dcc49e";
          let w = (ox - oy > 0) ? random(0.65, 1.15) : random(0.35, 0.65);
          brush.set("needle", col, w);
          brush.line(x + ox - 1.2, y + oy - 0.8, x + ox + 1.4, y + oy + 0.8);
        }
      } else {
        // Fine crumb fleck or flour stipple (works both on the board and clinging to the knife blade!)
        let flLen = random(1.2, 3.0);
        let fang = random(Math.PI * 2);
        let col = random() < 0.55 ? "#f6ebd4" : "#d6be9b";
        brush.set("needle", col, random(0.4, 0.85));
        brush.line(
          x - Math.cos(fang) * flLen * 0.5,
          y - Math.sin(fang) * flLen * 0.5,
          x + Math.cos(fang) * flLen * 0.5,
          y + Math.sin(fang) * flLen * 0.5
        );
      }
    }
  }

  // Extra fine flour stipples along the top crown of the loaf and on the steel knife flat
  for (let i = 0; i < 65; i++) {
    let s = random(35, 390);
    let b = getKnifeBounds(s);
    let w = random(b.wTop + 4, b.wBot - 6);
    let p = fromKnifeUV(s, w);
    brush.set("needle", "#f7efe0", random(0.35, 0.75));
    brush.line(p.x - 1, p.y - 0.5, p.x + 1.2, p.y + 0.5);
  }
}

Images made in code. “Engraved sourdough” by Gemini 4 Argon, next to the p5.brush sketch that produced it.

Which model is better for me?

As AI models become smarter it becomes harder to tell them apart on the things you use them for. Most benchmarks can’t help much when the work is subjective. I paint. I have worked visually for a long time, and I know a good painting when I see one, and I can feel something is off. So instead of assessing models with their ability to solve math proofs, I thought I’d ask them to make something I can judge: a painting.

This isn’t a new idea. When researchers at Microsoft first tested GPT-4, one of their headline examples was asking it to draw a unicorn in TikZ, a language for diagrams, and watching the drawings improve as training went on, in the paper they called Sparks of AGI. Simon Willison has asked every new model to draw a pelican riding a bicycle as an SVG since 2024. A drawing is easy to judge, and it shows how a model handles colour, composition and structure all at once in a single visual artefact.

Cam and I had already done a version of this. For my thesis we trained a model to paint with code, and the reward signal that worked was built from paintings we had rated by hand, one at a time. BrushArena builds on that idea: each model gets the same brief and thousands of people decide which painting is better. Their votes build a global leaderboard with your own picks next to it, so you can see your preferences as they compare to a global leaderboard.

Three unicorns drawn by GPT-4 in TikZ about a month apart, each more detailed than the last (Figure 1.3 of Sparks of AGI)

Drawing has been a way to read a model for a while. BrushArena makes it a benchmark.

Unicorns by GPT-4 in TikZ, Figure 1.3 of “Sparks of Artificial General Intelligence” (Microsoft Research, 2023) · pelican by Claude 3.5 Sonnet for Simon Willison’s pelican-on-a-bicycle test (2024) · hibiscus by our own model during training.

So I asked the models something I can judge: can you make a painting of some chillis on a patterned background? They write code that renders into paintings, and I can evaluate them.

Painting A: scattered red chillis and small blue flowers on a pale grid

Same brief, two models, both write code to make paintings.

How it works

Every model gets the same brief, the same settings and the same tools, through the same API. It writes a complete p5.brush sketch, and the sketch is rendered in a sandbox into a 1200 × 1200 painting. Paintings are shown to voters in blind pairs, and a vote is one question: which do you prefer?

465 briefs one per round Brief chillis on a pattern 28 models each writes p5.brush JS Render sandbox → 1200px Blind pair A vs B, no names You vote then see who painted Leaderboard everyone’s votes My Taste yours, next to theirs votes decide which pairs come next

One round of BrushArena.

The BrushArena vote screen on desktop: the brief “stereoscope card”, two paintings side by side, and the buttons I prefer A and I prefer B, with Tie, Skip and I recognise a work below

Blind voting on models’ paintings on BrushArena.

The Playground: “What should we paint?” input bar open over a painted landscape, with this week’s rounds above

Playground for testing out models and seeing their paintings

The BrushArena leaderboard: rank, model, score with its range, and votes
Preference vs cost chart on BrushArena

Leaderboard & Preference vs Cost on BrushArena

My Taste: global preference bars next to Surya's own preference bars for the top models

Your own votes next to everyone’s

The leaderboard

BrushArena has a leaderboard. On November 2nd 2026 we have 7,209 votes in, and the top of the board is close: six models sit inside GPT-6 Astra’s 95% interval.

What it taught me is that most of these models are good enough for whatever we throw at them. The question that matters is which one we feel an affinity for.

1300140015001600170018006 models overlapAstra’s intervalrating → (dot = rating, line = 95% interval)Gemini 4 ArgonGPT-6 AstraGemini 3.8 FlashGPT-6.1 SolClaude Opus 5.5Kimi K3.1 ProClaude Fable 5.1GPT-6 SolClaude Sonnet 5.5Claude Opus 5GPT-5.6 SolGemini 3.1 Pro PreviewClaude Haiku 5.5Grok 4.6GLM 5.3DeepSeek V4.1 FlashGLM 5.3 FlashMuse Spark 1.3Kimi K3Qwen3.8 Max (0902)Claude Sonnet 5Qwen3.8 FlashQwen3.8 27BHy4 previewDeepSeek V4 Pro 0813InklingLaguna S 2.1Mistral Medium 3.5

The live board on 2 Nov 2026: each model’s rating with its 95% interval. The shaded band is GPT-6 Astra’s interval.

What we can learn

After seeing every model paint, I can tell different models apart quite well now. Three of them, in particular, have a distinct style and way of working.

GPT-6 Astra

The one I use for drawing people: a cyclist, a crowd, a gardener with a hose. Clean, almost vector lines.

Three paintings by GPT-6 Astra: a cyclist racing downhill on top; a crowded night market and a gardener watering roses below

Top: a cyclist racing downhill. Below: a crowded night market, and a gardener watering roses.

Claude Opus 5.5

Quiet. Warm beige paper, a few confident, primitive marks. Sepia tones. When it misses, it misses by being too simple. I asked for a wool sweater and got a chair.

Three paintings by Claude Opus 5.5: a peacock on top; an apple and a chair (asked for a wool sweater) below

Top: a peacock showing its tail. Below: an apple, and what it painted when I asked for “a wool sweater”: a chair.

Gemini 4 Argon

The loud one: a stained-glass cat, a jewelled sewing machine, a synthwave horse. I assumed it wrote short code. It’s the opposite: close to four times as much as Astra. It keeps adding, and sometimes it goes muddy.

Three paintings by Gemini 4 Argon: a stained-glass cat on top; a jewelled sewing machine and a synthwave horse below

Top: a cat with a ball of yarn, in stained glass. Below: a jewelled sewing machine mid-stitch, and a galloping horse in synthwave colours.

02505007501000lines of code per painting (median) →Gemini 4 Argon998Gemini 3.8 Flash384Grok 4.6308GPT-5.6 Sol292Claude Opus 5291Qwen3.8 Flash288Muse Spark 1.3267GPT-6 Astra260Qwen3.8 27B248GPT-6.1 Sol245Claude Fable 5.1244Qwen3.8 Max (0902)238DeepSeek V4.1 Flash233GPT-6 Sol214GLM 5.3207DeepSeek V4 Pro 0813201Claude Sonnet 5.5191Kimi K3180Claude Opus 5.5179GLM 5.3 Flash171Claude Sonnet 5165Hy4 preview148Gemini 3.1 Pro Preview136Claude Haiku 5.5133Mistral Medium 3.583Inkling36≈ 3.8× Astra

Median lines of code per painting, every model on the same brief.

Developing an intuition for a model’s ability

The most valuable thing BrushArena taught us was what each model is good at. Each one has its own skill set and its own spikes in capability, and once we could see them we knew which one to reach for when making something: Astra for people, Opus for something muted, Argon for maximalism.

That matters most in creative work, where one tool doesn’t fit every need. And I only learned this by making an eval in a domain I know and observing closely.

Astra's crowded night market

A toolkit developed from evaluating models.

An ecology of evals

Researchers at AI labs are often observing Twitter and other social media and will pay attention to the evals or benchmarks that garner attention. They will also buy data and RL environments for those domains. In the past that meant coding tasks and workflows for professional work, so that is where models improve fastest. Lately they are focused on animation and creativity, and we start to see models get better at these tasks since people are paying attention to them now. But subjective domains are harder to measure and need benchmarks and evals too and often the people best positioned for a particular line of work are those who use it every day for that line of work.

A row of small, different plants growing from the same soil

Lots of small evals, each made by the people who know the space well.

You don’t need to work at a lab, or be especially technical, to do that. Pick a domain you care about and understand deeply. Make it legible. Get people to care.

A painted heron on a wooden pier

When people care, the labs care and they will train their models to be better at those domains. As Paige Bailey put it, ship an eval people care about and you decide what every lab hill climbs next.

A domain you know painting, music, cooking… An eval made legible, e.g. blind pairs People care they vote, share, argue Labs climb it it shows up in the next launch chart models get better at your thing

Steering from outside the labs. Paige Bailey on owning the goalposts.

Every model launch card tells us how good the model is on someone else’s benchmark. Often that metric isn’t meaningful to the work I do. BrushArena is my eval, for the thing I care about, and it has taught me more about different AI models than any model card. Which is why I’d love to see you make one for yourself and others who are in a similar line of work.

Uncle Sam pointing at the viewer, painted in code

I want you to make an eval.

Go vote at brusharena.art. Then go make your own eval.

Sixteen paintings from BrushArena, each by a different model, in a four by four grid

A few paintings from the arena.

A huge thank you to Cameron Franz, who built BrushArena with me; to Alejandro for p5.brush; to the Prime Intellect RL residency; to new interfaces for having me talk about it; and to everyone who voted.

You can email me. I’m active on Twitter, occasionally on LinkedIn, and surfing the internet on Are.na.

©2019-2026 SURYA NARREDDI.

You can email me. I’m active on Twitter, occasionally on LinkedIn, and surfing the internet on Are.na.

©2019-2026 SURYA NARREDDI.