{
 "genart": "1.2",
 "id": "saint-blessing",
 "title": "A Saint Blessing",
 "created": "2026-09-13T00:00:00.000Z",
 "modified": "2026-09-14T04:12:05.490Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1200,
  "height": 1200,
  "pixelDensity": 2
 },
 "parameters": [],
 "colors": [],
 "state": {
  "seed": 7,
  "params": {},
  "colorPalette": []
 },
 "algorithm": "// Stained glass: the material. The builder prepends this file to each sheet's\n// algorithm, so every sheet in the series is made of the same glass. Series-\n// local on purpose (plan section 7): it moves to a shared package only when a\n// second series needs it, and that move gets an ADR.\n//\n// The model is TRANSMISSION, not deposit. Nothing here is laid on paper:\n//   - Each piece of pot-metal glass has a colour at unit thickness. Its\n//     transmittance at relative density d is colour^d (Beer-Lambert), so a\n//     thicker patch is darker AND more saturated, never merely greyer. Density\n//     wanders inside a piece (a cloud), and thick glass is lumpy, so warped\n//     ridged noise lays soft wavy bands of thickness over it (ref 08).\n//   - Corrosion is a STIPPLE, not a blot: drifts of 1-4px dark dots, densest\n//     toward the grozed edge of the piece (refs 02, 05, 08). Larger pits too.\n//   - Grisaille is vitreous paint fired onto the glass. It only blocks light,\n//     and at full opacity still passes a little brown light (ref C): a trace\n//     line with a loaded start, a thin matt wash with brush drag, and light\n//     scraped back OUT of the paint with a stick (refs C, E, 02).\n//   - Lead is a separate opaque layer over the joints. Its width wanders slowly\n//     (Erin's pick, probe v2 tile 5), and joints thicken smoothly. A piece laid\n//     later sits over earlier leads.\n//   - Light spill: bright transmitted light blurs over the dark lead beside it\n//     (Erin's pick, probe v2 tile 7).\n//\n// Marks are placed in logical px and laid in device px, so the material is\n// the same physical size at any pixelDensity.\nfunction glassSheet(ctx, W, H, seed) {\n  var PW = ctx.canvas.width, PH = ctx.canvas.height;\n  var D = PW / W;\n  var N = PW * PH;\n  var TR = new Float32Array(N), TG = new Float32Array(N), TB = new Float32Array(N);\n  var glass = new Float32Array(N);   // 1 where glass fills the pixel\n  var paint = new Float32Array(N);   // grisaille opacity, 0 clear .. 1 opaque\n  var lead = new Float32Array(N);    // lead coverage\n  var ridge = new Float32Array(N);   // lead profile, 0 at the flange .. 1 on the crown\n  var marks = 0, pieces = 0;\n\n  function rngFrom(a) {\n    return function () {\n      a |= 0; a = a + 0x6D2B79F5 | 0;\n      var t = Math.imul(a ^ a >>> 15, 1 | a);\n      t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;\n      return ((t ^ t >>> 14) >>> 0) / 4294967296;\n    };\n  }\n  function hash2(ix, iy, salt) {\n    var h = Math.imul(ix | 0, 374761393) ^ Math.imul(iy | 0, 668265263) ^ Math.imul(salt | 0, 1597334677);\n    h = Math.imul(h ^ h >>> 13, 1274126177);\n    return ((h ^ h >>> 16) >>> 0) / 4294967296;\n  }\n  /** Smooth value noise, 0..1. */\n  function noise(x, y, salt) {\n    var ix = Math.floor(x), iy = Math.floor(y);\n    var fx = x - ix, fy = y - iy;\n    fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy);\n    var a = hash2(ix, iy, salt), b = hash2(ix + 1, iy, salt);\n    var c = hash2(ix, iy + 1, salt), d = hash2(ix + 1, iy + 1, salt);\n    return (a + (b - a) * fx) * (1 - fy) + (c + (d - c) * fx) * fy;\n  }\n  function fbm(x, y, salt, oct) {\n    var s = 0, a = 0.5, f = 1, n = 0;\n    for (var i = 0; i < oct; i++) { s += a * noise(x * f, y * f, salt + i * 31); n += a; a *= 0.5; f *= 2.03; }\n    return s / n;\n  }\n  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }\n  function rgb01(hex) {\n    var n = parseInt(hex.slice(1), 16);\n    return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];\n  }\n  function inPoly(pts, x, y) {\n    var inside = false;\n    for (var i = 0, j = pts.length - 1; i < pts.length; j = i++) {\n      var a = pts[i], b = pts[j];\n      if ((a.y > y) !== (b.y > y) && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;\n    }\n    return inside;\n  }\n  function inClip(c, x, y) { return !c || (x >= c.x && x < c.x + c.w && y >= c.y && y < c.y + c.h); }\n\n  /** Rasterise a polygon (optionally clipped to a rect) to an antialiased mask in device px. */\n  function polyMask(pts, clip) {\n    var minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;\n    pts.forEach(function (p) {\n      if (p.x < minx) minx = p.x; if (p.x > maxx) maxx = p.x;\n      if (p.y < miny) miny = p.y; if (p.y > maxy) maxy = p.y;\n    });\n    if (clip) {\n      minx = Math.max(minx, clip.x); miny = Math.max(miny, clip.y);\n      maxx = Math.min(maxx, clip.x + clip.w); maxy = Math.min(maxy, clip.y + clip.h);\n    }\n    var x0 = Math.max(0, Math.floor(minx * D) - 1), y0 = Math.max(0, Math.floor(miny * D) - 1);\n    var x1 = Math.min(PW, Math.ceil(maxx * D) + 1), y1 = Math.min(PH, Math.ceil(maxy * D) + 1);\n    var w = x1 - x0, h = y1 - y0;\n    if (w <= 0 || h <= 0) return null;\n    var c = typeof document !== \"undefined\" ? document.createElement(\"canvas\") : new OffscreenCanvas(w, h);\n    c.width = w; c.height = h;\n    var cx = c.getContext(\"2d\");\n    if (clip) { cx.beginPath(); cx.rect(clip.x * D - x0, clip.y * D - y0, clip.w * D, clip.h * D); cx.clip(); }\n    cx.beginPath();\n    pts.forEach(function (p, i) { if (i) cx.lineTo(p.x * D - x0, p.y * D - y0); else cx.moveTo(p.x * D - x0, p.y * D - y0); });\n    cx.closePath();\n    cx.fillStyle = \"#fff\";\n    cx.fill();\n    return { x0: x0, y0: y0, w: w, h: h, a: cx.getImageData(0, 0, w, h).data };\n  }\n  /** Distance from each mask pixel to the piece's edge, in device px (two-pass chamfer). */\n  function edgeDistance(m) {\n    var w = m.w, h = m.h, dd = new Float32Array(w * h), S2 = 1.4142, q, v, i, j;\n    for (q = 0; q < w * h; q++) dd[q] = m.a[q * 4 + 3] > 127 ? 1e6 : 0;\n    for (j = 0; j < h; j++) {\n      for (i = 0; i < w; i++) {\n        q = j * w + i; if (dd[q] === 0) continue; v = dd[q];\n        if (i > 0) v = Math.min(v, dd[q - 1] + 1);\n        if (j > 0) {\n          v = Math.min(v, dd[q - w] + 1);\n          if (i > 0) v = Math.min(v, dd[q - w - 1] + S2);\n          if (i < w - 1) v = Math.min(v, dd[q - w + 1] + S2);\n        }\n        dd[q] = v;\n      }\n    }\n    for (j = h - 1; j >= 0; j--) {\n      for (i = w - 1; i >= 0; i--) {\n        q = j * w + i; if (dd[q] === 0) continue; v = dd[q];\n        if (i < w - 1) v = Math.min(v, dd[q + 1] + 1);\n        if (j < h - 1) {\n          v = Math.min(v, dd[q + w] + 1);\n          if (i < w - 1) v = Math.min(v, dd[q + w + 1] + S2);\n          if (i > 0) v = Math.min(v, dd[q + w - 1] + S2);\n        }\n        dd[q] = v;\n      }\n    }\n    return dd;\n  }\n\n  // --- Glass -----------------------------------------------------------------\n  /**\n   * Lay one piece of pot-metal glass.\n   * o.color      transmittance at unit density (hex)\n   * o.mottle     0..1+, cloudy density and lumpy thickness inside the piece\n   * o.scale      cloud size in logical px (default 60)\n   * o.streak     0..1+, density streaks along o.angle (flashed ruby)\n   * o.seeds      bubbles per 400 px^2\n   * o.pits       corrosion pits per 400 px^2, clustered, edge-weighted\n   * o.weather    0..1, corrosion stipple drifts, edge-weighted\n   * o.clip       optional rect\n   */\n  function piece(pts, o) {\n    o = o || {};\n    var m = polyMask(pts, o.clip);\n    if (!m) return;\n    var col = rgb01(o.color || \"#dfe3d2\");\n    var lr = Math.log(Math.max(col[0], 0.004)), lg = Math.log(Math.max(col[1], 0.004)), lb = Math.log(Math.max(col[2], 0.004));\n    var salt = o.salt != null ? o.salt : (pieces++ * 7919 + seed * 13 + 1);\n    var mot = o.mottle == null ? 0.5 : o.mottle, ms = o.scale || 60;\n    var st = o.streak || 0, ca = Math.cos(o.angle || 0), sa = Math.sin(o.angle || 0);\n    var wea = o.weather || 0;\n    // o.edge: denser toward the grozed edge, so a piece glows in its middle like\n    // a jewel set in dark (ref 14 corner, ref 08 green). o.grain: a fine light\n    // and dark sandpaper grain in the glass itself (ref 08).\n    var edge = o.edge || 0, edgeW = o.edgeW || 7, grain = o.grain || 0;\n    // o.cloud / o.relief weight the two parts of the mottle separately (default\n    // 1 each): a panel wants less airbrush cloud and more lumpy thickness.\n    var cloudW = o.cloud == null ? 1 : o.cloud, reliefW = o.relief == null ? 1 : o.relief;\n    var dist = (edge || o.pits || o.seeds || wea) ? edgeDistance(m) : null;\n    var count = 0;\n    for (var j = 0; j < m.h; j++) {\n      for (var i = 0; i < m.w; i++) {\n        var a = m.a[(j * m.w + i) * 4 + 3] / 255;\n        if (a === 0) continue;\n        count++;\n        var px = m.x0 + i, py = m.y0 + j, k = py * PW + px;\n        var x = (px + 0.5) / D, y = (py + 0.5) / D;\n        var d = 1;\n        if (mot > 0) {\n          var cl = fbm(x / ms, y / ms, salt, 4) - 0.5;\n          cl = cl * (1.6 - Math.abs(cl) * 1.2);\n          // Lumpy thickness: warped ridged noise, soft wavy bands (ref 08).\n          var wx = x + 22 * (noise(x / 80, y / 80, salt + 8) - 0.5), wy = y + 22 * (noise(x / 80, y / 80, salt + 9) - 0.5);\n          var rel = 1 - Math.abs(2 * fbm(wx / 34, wy / 34, salt + 10, 2) - 1);\n          d += mot * (cl * 1.3 * cloudW + 0.55 * reliefW * (rel - 0.6));\n        }\n        if (st > 0) {\n          var u = x * ca + y * sa, v = -x * sa + y * ca;\n          d += st * ((fbm(u / (ms * 1.2), v / (ms * 0.14), salt + 2, 3) - 0.5) * 1.6 + 0.5 * (noise(u / 40, v / 1.2, salt + 7) - 0.5));\n        }\n        if (edge > 0) d += edge * 1.2 * Math.exp(-dist[j * m.w + i] / D / edgeW);\n        // Grain at two sizes (one device px, and ~1.25 logical px) so it survives\n        // the downscale to display instead of averaging away.\n        if (grain > 0) d += grain * ((hash2(px, py, salt + 14) + hash2(Math.floor(x * 0.8), Math.floor(y * 0.8), salt + 15)) * 0.5 - 0.5) * 1.1;\n        // Thin glass still colours the light: never let a patch go near white.\n        if (d < 0.45) d = 0.45;\n        TR[k] = TR[k] * (1 - a) + Math.exp(lr * d) * a;\n        TG[k] = TG[k] * (1 - a) + Math.exp(lg * d) * a;\n        TB[k] = TB[k] * (1 - a) + Math.exp(lb * d) * a;\n        glass[k] = glass[k] * (1 - a) + a;\n        paint[k] *= 1 - a; lead[k] *= 1 - a; ridge[k] *= 1 - a;\n      }\n    }\n    if (!o.pits && !o.seeds && !wea) return;\n    var area = count / (D * D);\n    var rng = rngFrom(salt * 977 + 3);\n    function edgeAt(px, py) {\n      var i = px - m.x0, j = py - m.y0;\n      if (i < 0 || j < 0 || i >= m.w || j >= m.h) return -1;\n      return m.a[(j * m.w + i) * 4 + 3] > 0 ? dist[j * m.w + i] / D : -1;\n    }\n    /** Scatter n spots; accept(sx, sy, edgeDist) filters, fn(k, dx, dy, r) marks each pixel. */\n    function spots(n, rmin, rmax, accept, fn) {\n      var bx = m.x0 / D, by = m.y0 / D, bw = m.w / D, bh = m.h / D;\n      for (var t = 0, placed = 0; t < n * 6 && placed < n; t++) {\n        var sx = bx + rng() * bw, sy = by + rng() * bh;\n        var ed = edgeAt(Math.floor(sx * D), Math.floor(sy * D));\n        if (ed < 0 || !accept(sx, sy, ed)) continue;\n        placed++;\n        var r = rmin + (rmax - rmin) * rng() * rng(), R = r * 1.8;\n        for (var qy = Math.floor((sy - R) * D); qy <= Math.ceil((sy + R) * D); qy++) {\n          for (var qx = Math.floor((sx - R) * D); qx <= Math.ceil((sx + R) * D); qx++) {\n            if (edgeAt(qx, qy) < 0) continue;\n            fn(qy * PW + qx, (qx + 0.5) / D - sx, (qy + 0.5) / D - sy, r);\n          }\n        }\n      }\n    }\n    if (wea) {\n      // Fine, dense, clumped: a grain in drifts, not a spatter of round dots.\n      spots(Math.round(wea * area / 8), 0.22, 1.0, function (sx, sy, ed) {\n        var e = Math.exp(-ed / 10), c = fbm(sx / 22, sy / 22, salt + 11, 3);\n        return rng() < clamp01((c - 0.53 + 0.22 * wea) * 7) * (0.35 + 0.65 * e) + 0.08 * e;\n      }, function (k, dx, dy, r) {\n        var ax = (k % PW + 0.5) / D, ay = (Math.floor(k / PW) + 0.5) / D;\n        var rr = r * (0.6 + 0.8 * noise(ax * 1.4, ay * 1.4, salt + 13));\n        var cov = clamp01((rr - Math.sqrt(dx * dx + dy * dy)) * D + 0.5);\n        if (cov <= 0) return;\n        // A partial brown crust: the glass still shows through most dots.\n        TR[k] = TR[k] * (1 - 0.6 * cov) + 0.05 * cov;\n        TG[k] = TG[k] * (1 - 0.64 * cov) + 0.035 * cov;\n        TB[k] = TB[k] * (1 - 0.7 * cov) + 0.02 * cov;\n      });\n    }\n    if (o.pits) {\n      spots(Math.round(o.pits * area / 900), 0.5, 3, function (sx, sy, ed) {\n        var e = Math.exp(-ed / 8);\n        return rng() < 0.25 + 0.75 * e && (fbm(sx / 22, sy / 22, salt + 6, 2) > 0.42 || rng() < e);\n      }, function (k, dx, dy, r) {\n        // A ragged crater, never a perfect disc.\n        var ax = (k % PW + 0.5) / D, ay = (Math.floor(k / PW) + 0.5) / D;\n        var rr = r * (0.65 + 0.7 * noise(ax * 0.9, ay * 0.9, salt + 12));\n        var dd = Math.sqrt(dx * dx + dy * dy);\n        var cov = clamp01((rr - dd) * D + 0.5);\n        if (cov <= 0) return;\n        var mul = 1 - 0.62 * cov * (0.7 + 0.3 * clamp01(1 - dd / rr));\n        TR[k] *= mul; TG[k] *= mul * 0.97; TB[k] *= mul * 0.94;\n      });\n    }\n    if (o.seeds) {\n      spots(Math.round(o.seeds * area / 400), 0.4, 2.2, function () { return true; }, function (k, dx, dy, r) {\n        var u = (dx * ca + dy * sa) / 1.6, v = -dx * sa + dy * ca;\n        var dd = Math.sqrt(u * u + v * v);\n        var core = clamp01((r * 0.7 - dd) * D + 0.5);\n        if (core > 0) { TR[k] += (1 - TR[k]) * 0.35 * core; TG[k] += (1 - TG[k]) * 0.35 * core; TB[k] += (1 - TB[k]) * 0.35 * core; }\n        var e = (dd - r) * D / 0.9, rim = Math.exp(-e * e);\n        var mul = 1 - 0.45 * rim;\n        TR[k] *= mul; TG[k] *= mul; TB[k] *= mul;\n      });\n    }\n  }\n\n  // --- Grisaille ---------------------------------------------------------------\n  /**\n   * A painted (or, with o.erase, scraped) line along pts.\n   * o.w max width, o.a opacity, o.taperIn/o.taperOut fractions of the length\n   * (0 = blunt), o.load extra paint where the brush lands (0..1), o.rough edge\n   * raggedness, o.chip fine chipping (scrapes), o.clip.\n   */\n  function trace(pts, o) {\n    o = o || {};\n    if (pts.length < 2) return;\n    var w = o.w || 1.5, A = o.a == null ? 0.95 : o.a;\n    var tin = o.taperIn == null ? 0.3 : o.taperIn, tout = o.taperOut == null ? 0.45 : o.taperOut;\n    var rough = o.rough == null ? 0.35 : o.rough, chip = o.chip || 0, load = o.load || 0;\n    var salt = o.salt != null ? o.salt : (marks++ * 131 + seed * 7 + 500);\n    var cum = [0];\n    for (var i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));\n    var L = cum[cum.length - 1] || 1;\n    for (i = 0; i < pts.length - 1; i++) {\n      var P = pts[i], Q = pts[i + 1], dx = Q.x - P.x, dy = Q.y - P.y, len = Math.hypot(dx, dy), l2 = len * len || 1e-6;\n      var ex = w * (1 + load) + 1.5;\n      var X0 = Math.max(0, Math.floor((Math.min(P.x, Q.x) - ex) * D)), X1 = Math.min(PW - 1, Math.ceil((Math.max(P.x, Q.x) + ex) * D));\n      var Y0 = Math.max(0, Math.floor((Math.min(P.y, Q.y) - ex) * D)), Y1 = Math.min(PH - 1, Math.ceil((Math.max(P.y, Q.y) + ex) * D));\n      for (var py = Y0; py <= Y1; py++) {\n        for (var px = X0; px <= X1; px++) {\n          var x = (px + 0.5) / D, y = (py + 0.5) / D;\n          if (!inClip(o.clip, x, y)) continue;\n          var t = ((x - P.x) * dx + (y - P.y) * dy) / l2;\n          t = t < 0 ? 0 : t > 1 ? 1 : t;\n          var qx = P.x + dx * t - x, qy = P.y + dy * t - y, d = Math.sqrt(qx * qx + qy * qy);\n          var s = cum[i] + t * len, u = s / L;\n          var pr = 1;\n          if (tin > 0) pr = Math.min(pr, u / tin);\n          if (tout > 0) pr = Math.min(pr, (1 - u) / tout);\n          pr = pr < 0 ? 0 : Math.pow(pr, 0.6);\n          // Pressure wanders along the stroke, and a loaded brush lands heavy.\n          var press = (0.7 + 0.6 * noise(s / 18, i * 0.01, salt + 3)) * (1 + load * Math.exp(-s / (w * 1.5)));\n          var hw = Math.max(0.22, w * 0.5 * press * (0.12 + 0.88 * pr)) * (1 + rough * (noise(x * 0.5, y * 0.5, salt) - 0.5));\n          if (chip) hw += chip * (noise(x * 2.2, y * 2.2, salt + 2) - 0.5);\n          var cov = clamp01((hw - d) * D + 0.5);\n          // A faint fringe where the paint spread into the glass.\n          var fr = clamp01((hw + 0.9 - d) / 0.9) * 0.18;\n          if (fr > cov) cov = fr;\n          if (cov <= 0) continue;\n          var k = py * PW + px;\n          var v = A * cov * (0.78 + 0.22 * noise(x / 1.6, y / 1.6, salt + 1));\n          if (o.erase) paint[k] *= 1 - v;\n          else if (v > paint[k]) paint[k] = v;\n        }\n      }\n    }\n  }\n  /** A crack: a hairline, dark where the break catches no light. */\n  function crack(pts, o) {\n    o = o || {};\n    trace(pts, { w: o.w || 0.7, a: o.a || 0.8, taperIn: 0.04, taperOut: 0.04, rough: 0.2, clip: o.clip });\n  }\n  function streakAt(x, y, o, salt) {\n    var ca = Math.cos(o.angle || 0), sa = Math.sin(o.angle || 0);\n    var u = x * ca + y * sa, v = -x * sa + y * ca;\n    return fbm(u / (o.len || 42), v / (o.width || 1.8), salt, 3);\n  }\n  /** A matt wash over rect r, weighted by mask(x, y) (boolean or 0..1), streaked by the brush along o.angle. */\n  function matt(mask, r, o) {\n    o = o || {};\n    var A = o.a == null ? 0.5 : o.a, drag = o.drag == null ? 0.5 : o.drag;\n    var salt = o.salt != null ? o.salt : (marks++ * 131 + seed * 7 + 700);\n    var X0 = Math.max(0, Math.floor(r.x * D)), X1 = Math.min(PW - 1, Math.ceil((r.x + r.w) * D));\n    var Y0 = Math.max(0, Math.floor(r.y * D)), Y1 = Math.min(PH - 1, Math.ceil((r.y + r.h) * D));\n    for (var py = Y0; py <= Y1; py++) {\n      for (var px = X0; px <= X1; px++) {\n        var x = (px + 0.5) / D, y = (py + 0.5) / D;\n        var mw = +mask(x, y);\n        if (!(mw > 0)) continue;\n        var v = clamp01(mw * A * (1 + drag * (streakAt(x, y, o, salt) - 0.5) * 2));\n        var k = py * PW + px;\n        if (o.lift) paint[k] *= 1 - v;\n        else paint[k] = 1 - (1 - paint[k]) * (1 - v);\n      }\n    }\n  }\n  /** Scrape light back out of the paint along pts: a stick, rough edged. */\n  function scrape(pts, o) {\n    o = o || {};\n    trace(pts, { w: o.w || 2.4, a: o.a == null ? 0.96 : o.a, taperIn: o.taperIn == null ? 0.06 : o.taperIn,\n      taperOut: o.taperOut == null ? 0.12 : o.taperOut, rough: o.rough == null ? 0.7 : o.rough,\n      chip: o.chip == null ? 0.5 : o.chip, erase: true, clip: o.clip });\n  }\n  /** Hatch inside mask(x, y) over rect r: straight trace lines o.spacing apart at o.angle. */\n  function hatch(mask, r, o) {\n    o = o || {};\n    var s = o.spacing || 3, ca = Math.cos(o.angle || 0), sa = Math.sin(o.angle || 0);\n    var corners = [[r.x, r.y], [r.x + r.w, r.y], [r.x, r.y + r.h], [r.x + r.w, r.y + r.h]];\n    var nmin = Infinity, nmax = -Infinity, dmin = Infinity, dmax = -Infinity;\n    corners.forEach(function (c) {\n      var n = -c[0] * sa + c[1] * ca, dd = c[0] * ca + c[1] * sa;\n      nmin = Math.min(nmin, n); nmax = Math.max(nmax, n); dmin = Math.min(dmin, dd); dmax = Math.max(dmax, dd);\n    });\n    for (var n = nmin + s * 0.5; n < nmax; n += s) {\n      var run = null;\n      for (var dd = dmin; dd <= dmax + 1; dd += 0.75) {\n        var x = dd * ca - n * sa, y = dd * sa + n * ca;\n        var ok = dd <= dmax && mask(x, y);\n        if (ok) { if (!run) run = [{ x: x, y: y }]; run[1] = { x: x, y: y }; }\n        else if (run) {\n          if (run[1] && Math.hypot(run[1].x - run[0].x, run[1].y - run[0].y) > 1.5)\n            trace(run, { w: o.w || 0.6, a: o.a == null ? 0.6 : o.a, taperIn: 0.08, taperOut: 0.08, rough: 0.25 });\n          run = null;\n        }\n      }\n    }\n  }\n\n  // --- Lead ------------------------------------------------------------------------\n  /**\n   * Lead came along pts. o.w width, o.wobble 0..1 width wander (positional, so\n   * two pieces sharing an edge agree), o.rough edge raggedness, o.closed, o.clip.\n   */\n  function leadLine(pts, o) {\n    o = o || {};\n    if (o.closed) pts = pts.concat([pts[0]]);\n    var w = o.w || 6, wob = o.wobble || 0, rough = o.rough || 0;\n    for (var i = 0; i < pts.length - 1; i++) {\n      var P = pts[i], Q = pts[i + 1], dx = Q.x - P.x, dy = Q.y - P.y, l2 = dx * dx + dy * dy || 1e-6;\n      var ex = w * (0.5 + wob) + 2;\n      var X0 = Math.max(0, Math.floor((Math.min(P.x, Q.x) - ex) * D)), X1 = Math.min(PW - 1, Math.ceil((Math.max(P.x, Q.x) + ex) * D));\n      var Y0 = Math.max(0, Math.floor((Math.min(P.y, Q.y) - ex) * D)), Y1 = Math.min(PH - 1, Math.ceil((Math.max(P.y, Q.y) + ex) * D));\n      for (var py = Y0; py <= Y1; py++) {\n        for (var px = X0; px <= X1; px++) {\n          var x = (px + 0.5) / D, y = (py + 0.5) / D;\n          if (!inClip(o.clip, x, y)) continue;\n          var t = ((x - P.x) * dx + (y - P.y) * dy) / l2;\n          t = t < 0 ? 0 : t > 1 ? 1 : t;\n          var qx = P.x + dx * t - x, qy = P.y + dy * t - y, d = Math.sqrt(qx * qx + qy * qy);\n          // Slow wander, gentle edge: lead is soft metal, not torn paper.\n          var hw = w * 0.5 * (1 + wob * (noise(x / 28, y / 28, seed + 900) - 0.5) * 2);\n          if (rough) hw += rough * (noise(x * 0.25, y * 0.25, seed + 901) - 0.5) * 0.7;\n          var cov = clamp01((hw - d) * D + 0.5);\n          if (cov <= 0) continue;\n          var k = py * PW + px;\n          if (cov > lead[k]) lead[k] = cov;\n          var rg = 1 - (d / hw) * (d / hw);\n          if (rg > ridge[k]) ridge[k] = rg;\n        }\n      }\n    }\n  }\n  /** A solder joint: the leads thicken smoothly where they meet. */\n  function solder(cx, cy, o) {\n    o = o || {};\n    var r = o.r || 4, lump = o.lump == null ? 0.1 : o.lump, R = r * (1 + lump) + 1;\n    for (var py = Math.max(0, Math.floor((cy - R) * D)); py <= Math.min(PH - 1, Math.ceil((cy + R) * D)); py++) {\n      for (var px = Math.max(0, Math.floor((cx - R) * D)); px <= Math.min(PW - 1, Math.ceil((cx + R) * D)); px++) {\n        var x = (px + 0.5) / D, y = (py + 0.5) / D;\n        if (!inClip(o.clip, x, y)) continue;\n        var rr = r * (1 + lump * (noise(x / 5, y / 5, seed + 902) - 0.5) * 2);\n        var d = Math.hypot(x - cx, y - cy);\n        var cov = clamp01((rr - d) * D + 0.5);\n        if (cov <= 0) continue;\n        var k = py * PW + px;\n        if (cov > lead[k]) lead[k] = cov;\n        var rg = 1 - (d / rr) * (d / rr);\n        if (rg > ridge[k]) ridge[k] = rg;\n      }\n    }\n  }\n\n  // --- The window, lit from behind --------------------------------------------------\n  function boxBlur(a, r) {\n    var b = new Float32Array(N), inv = 1 / (2 * r + 1);\n    for (var it = 0; it < 3; it++) {\n      for (var y = 0; y < PH; y++) {\n        var row = y * PW, acc = 0;\n        for (var x = -r; x <= r; x++) acc += a[row + (x < 0 ? 0 : x >= PW ? PW - 1 : x)];\n        for (x = 0; x < PW; x++) {\n          b[row + x] = acc * inv;\n          var xi = x + r + 1, xo = x - r;\n          acc += a[row + (xi >= PW ? PW - 1 : xi)] - a[row + (xo < 0 ? 0 : xo)];\n        }\n      }\n      for (x = 0; x < PW; x++) {\n        acc = 0;\n        for (y = -r; y <= r; y++) acc += b[(y < 0 ? 0 : y >= PH ? PH - 1 : y) * PW + x];\n        for (y = 0; y < PH; y++) {\n          a[y * PW + x] = acc * inv;\n          var yi = y + r + 1, yo = y - r;\n          acc += b[(yi >= PH ? PH - 1 : yi) * PW + x] - b[(yo < 0 ? 0 : yo) * PW + x];\n        }\n      }\n    }\n    return a;\n  }\n  /**\n   * Light the window. o.light rgb 0..1, o.wall hex (the stone around it),\n   * o.lead hex, o.paint hex (grisaille tint), o.halo number or fn(x, y) for\n   * the light spill strength, o.haloR spill radius in logical px.\n   */\n  function finish(o) {\n    o = o || {};\n    var Lt = o.light || [1.0, 0.97, 0.9];\n    var wall = rgb01(o.wall || \"#12110f\"), lc = rgb01(o.lead || \"#2a2926\"), tint = rgb01(o.paint || \"#2e211c\");\n    var GR = new Float32Array(N), GG = new Float32Array(N), GB = new Float32Array(N);\n    for (var k = 0; k < N; k++) {\n      if (glass[k] <= 0) continue;\n      // Fired paint at full opacity still passes a little brown light (ref C):\n      // mix toward the tint rather than compounding toward black.\n      var p = paint[k];\n      GR[k] = Lt[0] * TR[k] * (1 - p * (1 - tint[0])) * glass[k];\n      GG[k] = Lt[1] * TG[k] * (1 - p * (1 - tint[1])) * glass[k];\n      GB[k] = Lt[2] * TB[k] * (1 - p * (1 - tint[2])) * glass[k];\n    }\n    // o.haloThreshold: only light brighter than this spills (default 0.25). A\n    // panel with dark leads wants it higher, so pearls glow and blue does not fog.\n    var halo = o.halo, BR = null, BG = null, BB = null, ht = o.haloThreshold == null ? 0.25 : o.haloThreshold;\n    // o.leadSheen: highlight on the lead's crown (default 0.12). o.haloOnLead:\n    // how much of the spill lands on the lead (default 0.85); low keeps leads\n    // crisp and near-black with no grey fringe.\n    var sheen = o.leadSheen == null ? 0.12 : o.leadSheen, hol = o.haloOnLead == null ? 0.85 : o.haloOnLead;\n    if (halo) {\n      var hr = Math.max(1, Math.round((o.haloR || 5) * D));\n      BR = boxBlur(GR.slice(), hr); BG = boxBlur(GG.slice(), hr); BB = boxBlur(GB.slice(), hr);\n    }\n    var img = ctx.createImageData(PW, PH), out = img.data;\n    for (var py = 0, o4 = 0; py < PH; py++) {\n      var y = (py + 0.5) / D;\n      for (var px = 0; px < PW; px++, o4 += 4) {\n        k = py * PW + px;\n        var x = (px + 0.5) / D;\n        var g = glass[k], wn = (0.8 + 0.4 * fbm(x / 9, y / 9, seed + 950, 2)) * (1 - g);\n        var r = GR[k] + wall[0] * wn, gg = GG[k] + wall[1] * wn, bb = GB[k] + wall[2] * wn;\n        var c = lead[k];\n        if (c > 0) {\n          var rg = ridge[k], sh = 0.88 + 0.24 * noise(x / 6, y / 6, seed + 951), hi = sheen * rg * rg;\n          r = r * (1 - c) + (lc[0] * sh + hi) * c;\n          gg = gg * (1 - c) + (lc[1] * sh + hi) * c;\n          bb = bb * (1 - c) + (lc[2] * sh + hi) * c;\n        }\n        if (halo) {\n          var kk = typeof halo === \"function\" ? halo(x, y) : halo;\n          if (kk > 0) {\n            // Only light brighter than a threshold spills, and mostly over the\n            // lead beside it; below that the glass would just look fogged.\n            var f = kk * (0.15 + hol * c) * 1.3;\n            r += f * Math.max(0, BR[k] - ht); gg += f * Math.max(0, BG[k] - ht); bb += f * Math.max(0, BB[k] - ht);\n          }\n        }\n        out[o4] = 255 * clamp01(r); out[o4 + 1] = 255 * clamp01(gg); out[o4 + 2] = 255 * clamp01(bb); out[o4 + 3] = 255;\n      }\n    }\n    ctx.putImageData(img, 0, 0);\n  }\n  /** Text over the finished sheet (probe labels only; never in a work). */\n  function label(text, x, y, o) {\n    o = o || {};\n    ctx.save();\n    ctx.setTransform(D, 0, 0, D, 0, 0);\n    ctx.font = (o.size || 12) + \"px Helvetica, Arial, sans-serif\";\n    ctx.fillStyle = o.color || \"#a9a49a\";\n    ctx.fillText(text, x, y);\n    ctx.restore();\n  }\n\n  return { D: D, rngFrom: rngFrom, noise: noise, fbm: fbm, inPoly: inPoly, piece: piece, trace: trace, crack: crack,\n    matt: matt, scrape: scrape, hatch: hatch, lead: leadLine, solder: solder, finish: finish, label: label };\n}\n\n// Stained glass: the cutting geometry. How a glazier's cartoon becomes pieces:\n// convex splits, a field broken into offcuts, ring segments, and leaf shapes\n// swept along an axis. Prepended after glass.js by build-sheet.cjs. Pure\n// geometry, no drawing. Series-local, like the material.\nfunction glassGeom() {\n  function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }\n  function side(a, b, p) { return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x); }\n  function area(p) {\n    var s = 0;\n    for (var i = 0; i < p.length; i++) { var j = (i + 1) % p.length; s += p[i].x * p[j].y - p[j].x * p[i].y; }\n    return Math.abs(s) / 2;\n  }\n  function centroid(p) {\n    var x = 0, y = 0;\n    p.forEach(function (v) { x += v.x; y += v.y; });\n    return { x: x / p.length, y: y / p.length };\n  }\n  /** Split a convex polygon by the infinite line through a and b. */\n  function split(poly, a, b) {\n    var L = [], R = [];\n    for (var i = 0; i < poly.length; i++) {\n      var p = poly[i], q = poly[(i + 1) % poly.length];\n      var sp = side(a, b, p), sq = side(a, b, q);\n      if (sp >= 0) L.push(p);\n      if (sp <= 0) R.push(p);\n      if ((sp > 0 && sq < 0) || (sp < 0 && sq > 0)) {\n        var t = sp / (sp - sq), m = { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t };\n        L.push(m); R.push(m);\n      }\n    }\n    return [L, R].filter(function (q) { return q.length >= 3 && area(q) > 4; });\n  }\n  function splitAll(ps, a, b) {\n    var out = [];\n    ps.forEach(function (p) { split(p, a, b).forEach(function (q) { out.push(q); }); });\n    return out;\n  }\n  /**\n   * Break a convex polygon into offcuts: split the largest piece n times near\n   * its centre. opts.across cuts across each piece's long axis (so offcuts stay\n   * chunky, never slivers); opts.eligible(p) limits which pieces may be split.\n   */\n  function irregular(poly, n, rng, opts) {\n    opts = opts || {};\n    var ps = [poly];\n    for (var i = 0; i < n; i++) {\n      var bi = -1, ba = 0;\n      ps.forEach(function (p, j) {\n        if (opts.eligible && !opts.eligible(p)) return;\n        var A = area(p); if (A > ba) { ba = A; bi = j; }\n      });\n      if (bi < 0) break;\n      var c = centroid(ps[bi]), ang;\n      if (opts.across) {\n        var sxx = 0, syy = 0, sxy = 0;\n        ps[bi].forEach(function (v) { var dx = v.x - c.x, dy = v.y - c.y; sxx += dx * dx; syy += dy * dy; sxy += dx * dy; });\n        ang = 0.5 * Math.atan2(2 * sxy, sxx - syy) + Math.PI / 2 + (rng() - 0.5) * 0.7;\n      } else ang = rng() * Math.PI;\n      var a = { x: c.x + (rng() - 0.5) * 10, y: c.y + (rng() - 0.5) * 10 };\n      var s = split(ps[bi], a, { x: a.x + Math.cos(ang), y: a.y + Math.sin(ang) });\n      ps.splice.apply(ps, [bi, 1].concat(s));\n    }\n    return ps;\n  }\n  function circle(cx, cy, r, n) {\n    var pts = [];\n    for (var i = 0; i < n; i++) { var a = i / n * Math.PI * 2; pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r }); }\n    return pts;\n  }\n  function arc(O, r, a0, a1, n) {\n    var pts = [];\n    for (var i = 0; i <= n; i++) { var a = a0 + (a1 - a0) * i / n; pts.push({ x: O.x + Math.cos(a) * r, y: O.y + Math.sin(a) * r }); }\n    return pts;\n  }\n  /** One piece of a ring between radii r0 < r1 and angles a0..a1. */\n  function ringSegment(O, r0, r1, a0, a1, n) {\n    return arc(O, r1, a0, a1, n).concat(arc(O, r0, a1, a0, n));\n  }\n  /** Quadratic Bezier a -> c through control b, n segments. */\n  function curve(a, b, c, n) {\n    var pts = [];\n    for (var i = 0; i <= n; i++) {\n      var s = i / n, m = 1 - s;\n      pts.push({ x: m * m * a.x + 2 * m * s * b.x + s * s * c.x, y: m * m * a.y + 2 * m * s * b.y + s * s * c.y });\n    }\n    return pts;\n  }\n  function lengths(axis) {\n    var cum = [0];\n    for (var i = 1; i < axis.length; i++) cum.push(cum[i - 1] + dist(axis[i], axis[i - 1]));\n    return cum;\n  }\n  /** Point and unit normal at fraction s (0..1) of the axis length. */\n  function sample(axis, s) {\n    var cum = lengths(axis), L = cum[cum.length - 1] || 1, target = Math.max(0, Math.min(1, s)) * L;\n    var i = 1;\n    while (i < axis.length - 1 && cum[i] < target) i++;\n    var a = axis[i - 1], b = axis[i], seg = cum[i] - cum[i - 1] || 1, t = (target - cum[i - 1]) / seg;\n    var dx = b.x - a.x, dy = b.y - a.y, l = Math.hypot(dx, dy) || 1;\n    return { x: a.x + dx * t, y: a.y + dy * t, nx: -dy / l, ny: dx / l };\n  }\n  /** The part of an axis between fractions s0 and s1, as points. */\n  function slice(axis, s0, s1, n) {\n    var pts = [];\n    for (var i = 0; i <= n; i++) { var p = sample(axis, s0 + (s1 - s0) * i / n); pts.push({ x: p.x, y: p.y }); }\n    return pts;\n  }\n  /** A leaf or strap: the polygon swept along an axis with half-width wFn(s). */\n  function ribbon(axis, wFn) {\n    var cum = lengths(axis), L = cum[cum.length - 1] || 1, left = [], right = [];\n    for (var i = 0; i < axis.length; i++) {\n      var a = axis[Math.max(0, i - 1)], b = axis[Math.min(axis.length - 1, i + 1)];\n      var dx = b.x - a.x, dy = b.y - a.y, l = Math.hypot(dx, dy) || 1, nx = -dy / l, ny = dx / l;\n      var w = wFn(cum[i] / L);\n      left.push({ x: axis[i].x + nx * w, y: axis[i].y + ny * w });\n      right.push({ x: axis[i].x - nx * w, y: axis[i].y - ny * w });\n    }\n    return left.concat(right.reverse());\n  }\n  /** Points every `spacing` along a path, keeping `margin` clear of both ends. */\n  function every(pts, spacing, margin) {\n    var cum = lengths(pts), L = cum[cum.length - 1], out = [];\n    for (var s = margin; s <= L - margin; s += spacing) {\n      var p = sample(pts, s / L);\n      out.push({ x: p.x, y: p.y });\n    }\n    return out;\n  }\n  function ss(e0, e1, v) { var q = Math.max(0, Math.min(1, (v - e0) / (e1 - e0))); return q * q * (3 - 2 * q); }\n\n  return { dist: dist, area: area, centroid: centroid, split: split, splitAll: splitAll, irregular: irregular,\n    circle: circle, arc: arc, ringSegment: ringSegment, curve: curve, sample: sample, slice: slice,\n    ribbon: ribbon, every: every, ss: ss };\n}\n\n// Stained glass: the glazier's working kit, shared by the series' sheets.\n// Prepended after glass.js and geom.js. Painting helpers built on the material\n// (matt at the edge, soft shading bands, pearls, rosettes) and outline helpers\n// for hand-authored pieces (smooth closed outlines, curves through points).\n// Series-local, like the material.\nfunction glassKit(G, K, rng) {\n  function inside(p) { return function (x, y) { return G.inPoly(p, x, y); }; }\n  function bboxOf(p) {\n    var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n    p.forEach(function (v) { x0 = Math.min(x0, v.x); y0 = Math.min(y0, v.y); x1 = Math.max(x1, v.x); y1 = Math.max(y1, v.y); });\n    return { x: x0 - 2, y: y0 - 2, w: x1 - x0 + 4, h: y1 - y0 + 4 };\n  }\n  function edgeDist(p, x, y) {\n    var best = Infinity;\n    for (var i = 0, j = p.length - 1; i < p.length; j = i++) {\n      var a = p[j], b = p[i], dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy || 1e-6;\n      var t = Math.max(0, Math.min(1, ((x - a.x) * dx + (y - a.y) * dy) / l2));\n      var ex = a.x + dx * t - x, ey = a.y + dy * t - y, d = ex * ex + ey * ey;\n      if (d < best) best = d;\n    }\n    return Math.sqrt(best);\n  }\n  /** Paint laid heaviest along the leads and fading inward over `width` px. */\n  function edgeMatt(p, a, width) {\n    G.matt(function (x, y) { return G.inPoly(p, x, y) ? K.ss(width, 0, edgeDist(p, x, y)) : 0; }, bboxOf(p), { a: a, drag: 0.1, angle: rng() * 3 });\n  }\n  /** A four-petal rosette scraped out of a matt. */\n  function rosette(c, rad) {\n    for (var pt = 0; pt < 4; pt++) {\n      var petal = [];\n      for (var q = 0; q <= 10; q++) {\n        var s = q / 10, pr = rad * Math.sin(Math.PI * s), pa = pt * Math.PI / 2 + 0.3 + (s - 0.5) * 1.3;\n        petal.push({ x: c.x + Math.cos(pa) * pr, y: c.y + Math.sin(pa) * pr });\n      }\n      G.scrape(petal, { w: Math.max(2, rad * 0.42), taperIn: 0, taperOut: 0, rough: 0.5, chip: 0.3 });\n    }\n    G.scrape([{ x: c.x - 0.4, y: c.y }, { x: c.x + 0.4, y: c.y }], { w: Math.max(2.6, rad * 0.45), taperIn: 0, taperOut: 0 });\n  }\n  /** Pearls: round lights scraped out of a matt, every `spacing` along a path (ref 08). */\n  function pearls(path, spacing, size) {\n    K.every(path, spacing, spacing * 0.55).forEach(function (p) {\n      G.trace([p, { x: p.x + 0.3, y: p.y + 0.2 }], { w: size * (0.94 + rng() * 0.12), a: 0.98, taperIn: 0, taperOut: 0, rough: 0.05, erase: true });\n    });\n  }\n  function samplesOf(axis, n) {\n    var out = [];\n    for (var q = 0; q <= n; q++) { var p = K.sample(axis, q / n); p.s = q / n; out.push(p); }\n    return out;\n  }\n  function nearest(samples, x, y) {\n    var best = samples[0], bd = Infinity;\n    for (var q = 0; q < samples.length; q++) {\n      var p = samples[q], d = (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y);\n      if (d < bd) { bd = d; best = p; }\n    }\n    return { p: best, off: (x - best.x) * best.nx + (y - best.y) * best.ny };\n  }\n  /** Soft matt shading inside a swept piece: weight(u, s), u = signed offset / half-width, s along the axis. */\n  function shade(poly, samples, wFn, weight, a) {\n    G.matt(function (x, y) {\n      if (!G.inPoly(poly, x, y)) return 0;\n      var q = nearest(samples, x, y), w = Math.max(1, wFn(q.p.s));\n      return Math.max(0, Math.min(1, weight(q.off / w, q.p.s)));\n    }, bboxOf(poly), { a: a, drag: 0.08, angle: rng() * 3 });\n  }\n  function tapered(wmax, pw) { return function (s) { return wmax * Math.pow(Math.sin(Math.PI * s), pw) + 0.6; }; }\n  function scallop(fn, n, depth) { return function (s) { return fn(s) * (1 - depth + depth * Math.abs(Math.sin(s * Math.PI * n))) + 0.6; }; }\n  function cr(p0, p1, p2, p3, t) {\n    var t2 = t * t, t3 = t2 * t;\n    return {\n      x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),\n      y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),\n    };\n  }\n  /** A smooth closed outline through hand-placed points (Catmull-Rom), n samples per span. */\n  function smoothClosed(pts, n) {\n    var out = [], m = pts.length;\n    for (var i = 0; i < m; i++) {\n      for (var k = 0; k < n; k++) out.push(cr(pts[(i - 1 + m) % m], pts[i], pts[(i + 1) % m], pts[(i + 2) % m], k / n));\n    }\n    return out;\n  }\n  /** A smooth open curve through points (Catmull-Rom, ends repeated), n samples per span. */\n  function through(pts, n) {\n    var out = [], m = pts.length;\n    for (var i = 0; i < m - 1; i++) {\n      for (var k = 0; k < n; k++) out.push(cr(pts[Math.max(0, i - 1)], pts[i], pts[i + 1], pts[Math.min(m - 1, i + 2)], k / n));\n    }\n    out.push(pts[m - 1]);\n    return out;\n  }\n  /** Pull every point of an outline inside radius r of O (so a piece never passes under the wall). */\n  function clampToDisc(pts, O, r) {\n    return pts.map(function (p) {\n      var d = Math.hypot(p.x - O.x, p.y - O.y);\n      return d <= r ? p : { x: O.x + (p.x - O.x) * r / d, y: O.y + (p.y - O.y) * r / d };\n    });\n  }\n  /** An ellipse outline, rotated by rot. */\n  function ellipse(cx, cy, rx, ry, rot, n) {\n    var pts = [], c = Math.cos(rot || 0), s = Math.sin(rot || 0);\n    for (var i = 0; i < n; i++) {\n      var a = i / n * Math.PI * 2, ex = Math.cos(a) * rx, ey = Math.sin(a) * ry;\n      pts.push({ x: cx + ex * c - ey * s, y: cy + ex * s + ey * c });\n    }\n    return pts;\n  }\n\n  /** Distance from (x, y) to a polyline. */\n  function nearPath(pts, x, y) {\n    var best = Infinity;\n    for (var i = 0; i < pts.length - 1; i++) {\n      var a = pts[i], b = pts[i + 1], dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy || 1e-6;\n      var t = Math.max(0, Math.min(1, ((x - a.x) * dx + (y - a.y) * dy) / l2));\n      var ex = a.x + dx * t - x, ey = a.y + dy * t - y, d = ex * ex + ey * ey;\n      if (d < best) best = d;\n    }\n    return Math.sqrt(best);\n  }\n  /** The runs of a polyline lying at least `inset` inside a piece, so paint stops short of the lead. */\n  function clipRuns(poly, pts, inset) {\n    var runs = [], cur = [];\n    pts.forEach(function (p) {\n      var ok = G.inPoly(poly, p.x, p.y) && G.inPoly(poly, p.x + inset, p.y) && G.inPoly(poly, p.x - inset, p.y) &&\n        G.inPoly(poly, p.x, p.y + inset) && G.inPoly(poly, p.x, p.y - inset);\n      if (ok) cur.push(p); else { if (cur.length > 2) runs.push(cur); cur = []; }\n    });\n    if (cur.length > 2) runs.push(cur);\n    return runs;\n  }\n  /** Trace a line inside a piece only (default 9px in), one stroke per inside run. */\n  function traceIn(poly, pts, o, inset) { clipRuns(poly, pts, inset == null ? 9 : inset).forEach(function (r) { G.trace(r, o); }); }\n  /** Soft matt shadow either side of each fold line, inside a piece. */\n  function foldShadow(poly, lines, a, spread) {\n    G.matt(function (x, y) {\n      if (!G.inPoly(poly, x, y)) return 0;\n      var w = 0;\n      for (var i = 0; i < lines.length; i++) {\n        var d = nearPath(lines[i], x, y);\n        if (d < spread * 3) w = Math.max(w, Math.exp(-(d * d) / (spread * spread)));\n      }\n      return w;\n    }, bboxOf(poly), { a: a, drag: 0.1, angle: 1.57 });\n  }\n  /** A fold from a to b, bowed `bow` px to its left, ending in a hook of size |hook| turned to the side of hook's sign. */\n  function hooked(a, b, bow, hook) {\n    var dx = b.x - a.x, dy = b.y - a.y, l = Math.hypot(dx, dy) || 1, nx = -dy / l, ny = dx / l, tx = dx / l, ty = dy / l;\n    var pts = through([a, { x: a.x + dx * 0.35 + nx * bow, y: a.y + dy * 0.35 + ny * bow },\n      { x: a.x + dx * 0.7 + nx * bow * 0.6, y: a.y + dy * 0.7 + ny * bow * 0.6 }, b], 8);\n    if (!hook) return pts;\n    var s = Math.abs(hook), sg = hook < 0 ? -1 : 1;\n    function at(t, n) { return { x: b.x + tx * s * t + nx * sg * s * n, y: b.y + ty * s * t + ny * sg * s * n }; }\n    return pts.concat(through([b, at(0.45, 0.3), at(0.5, 1), at(0.05, 1.3), at(-0.4, 1.2)], 4).slice(1));\n  }\n\n  return { inside: inside, bboxOf: bboxOf, edgeDist: edgeDist, edgeMatt: edgeMatt, rosette: rosette, pearls: pearls,\n    samplesOf: samplesOf, nearest: nearest, shade: shade, tapered: tapered, scallop: scallop,\n    smoothClosed: smoothClosed, through: through, clampToDisc: clampToDisc, ellipse: ellipse,\n    nearPath: nearPath, clipRuns: clipRuns, traceIn: traceIn, foldShadow: foldShadow, hooked: hooked };\n}\n\n// Stained glass: robed figures for scenes. Prepended after glass.js, geom.js\n// and kit.js. The grammar is the figure medallion's v5, which Erin accepted\n// (\"looks good\"): every part its own piece with the lead on the contour; heads\n// that tilt and turn, the far half of the face narrower, the beard swinging to\n// an off-centre point; hair and beard painted as masses with light scraped\n// through; hooked folds. Figures here are full-length and small, as in a\n// medallion window's compartments (ref 13): the lead keeps its real width while\n// the figure shrinks, so small figures read more leaded, as real ones do.\n// Series-local, like the material. api = { piece(poly, color, o), lead(poly, w) }.\nfunction glassFigures(G, K, T, api) {\n  function P(x, y) { return { x: x, y: y }; }\n  var piece = api.piece, leadOf = api.lead;\n  function scrapeIf(pts, o) { if (pts.length > 1) G.scrape(pts, o); }\n\n  /**\n   * A head with its hair and beard, after figure medallion v5, at any size.\n   * o: c (centre), ry (half-height px), facing (+1 turned to our right), tilt (radians, toward facing),\n   * beard (bool), hair (\"full\" | \"short\" | \"tonsure\"), flesh, rng. Returns the head piece.\n   */\n  function head(o) {\n    var rh = o.rng, fc = o.facing || 1, k = o.ry / 102, ROT = fc * (o.tilt || 0), TURN = 0.3;\n    var cR = Math.cos(ROT), sR = Math.sin(ROT), HRX = 80 * k, HRY = 102 * k, f = 34 * k, cx = o.c.x, cy = o.c.y;\n    /** Stroke widths shrink less than the head, or small faces vanish. */\n    function wk(w) { return w * (0.4 + 0.6 * k); }\n    /** Head frame (lx toward the side the head turns to) to page, tilted. */\n    function Hp(lx, ly) { var X = fc * lx; return P(cx + X * cR - ly * sR, cy + X * sR + ly * cR); }\n    function Hl(x, y) { var dx = x - cx, dy = y - cy; return { x: fc * (dx * cR + dy * sR), y: -dx * sR + dy * cR }; }\n    /** Face coordinates: the features' axis sits TURN toward the far side, the near half wider. */\n    function Fp(u, v) { return Hp((TURN + u * (u < 0 ? 1.06 : 0.74)) * f, (v - 0.06) * f); }\n    function Fu(x, y) { var l = Hl(x, y), a = l.x / f - TURN; return { u: a / (a < 0 ? 1.06 : 0.74), v: l.y / f + 0.06 }; }\n    /** A point d (in v5 pixels) inside the head's ellipse at ellipse angle ang. */\n    function onSkull(ang, d) { return Hp(Math.cos(ang) * (HRX - d * k), Math.sin(ang) * (HRY - d * k)); }\n\n    var POINT = Math.PI / 2 - 0.3, bump = o.beard ? 13 * k : 0;\n    var hd = T.ellipse(0, 0, HRX, HRY, 0, 90).map(function (p) {\n      var a = Math.atan2(p.y / HRY, p.x / HRX), da = a - POINT, m = 1 + bump * Math.exp(-(da * da) / 0.09) / Math.hypot(p.x, p.y);\n      return Hp(p.x * m, p.y * m);\n    });\n    piece(hd, o.flesh, { mottle: 0.35, weather: 0.2, pits: 0.1, edge: 0.2, edgeW: Math.max(3, 7 * k) });\n    T.edgeMatt(hd, 0.45, Math.max(5, 20 * k));\n    // eye sockets, and the far cheek turning away into shadow\n    G.matt(function (x, y) {\n      if (!G.inPoly(hd, x, y)) return 0;\n      var q = Fu(x, y), u = q.u, v = q.v, w = 0, l = Hl(x, y), ed = T.edgeDist(hd, x, y);\n      [-0.82, 0.86].forEach(function (ec) { w += Math.exp(-((u - ec) * (u - ec)) / 0.28 - ((v + 0.35) * (v + 0.35)) / 0.12); });\n      return Math.min(1, w * 0.9 + 0.3 * Math.max(0, l.x / HRX) * Math.exp(-(ed * ed) / (260 * k * k)));\n    }, T.bboxOf(hd), { a: 0.38, drag: 0.1, angle: 1.3 });\n\n    // hair: a painted mass, light strands scraped through it, locks traced over\n    var style = o.hair || \"full\";\n    // \"short\" is shallower and lighter: scene v2's short hair, as deep and dark as \"full\", read as a helmet\n    var reachNear = style === \"short\" ? 1.3 : 1.8, reachFar = style === \"short\" ? 1.05 : 1.35, bald = style === \"tonsure\" ? 0.55 : 0, thin = style === \"short\" ? 0.7 : 1;\n    function crownAngle(x, y) { var l = Hl(x, y); return Math.atan2(l.x / HRX, -l.y / HRY); }\n    function hairMax(th) { return th < 0 ? reachNear : reachFar; }\n    function hairDepth(th) { var s = Math.min(1, Math.abs(th) / hairMax(th)); return (th < 0 ? 32 * (1 - 0.45 * s) : 18 * (1 - 0.6 * s)) * k * thin; }\n    G.matt(function (x, y) {\n      if (!G.inPoly(hd, x, y)) return 0;\n      var th = crownAngle(x, y), at = Math.abs(th), dh = hairDepth(th), ed = T.edgeDist(hd, x, y);\n      return K.ss(dh, dh - 12 * k, ed) * K.ss(hairMax(th), hairMax(th) - 0.35, at) * (bald ? K.ss(bald - 0.15, bald + 0.1, at) : 1);\n    }, T.bboxOf(hd), { a: style === \"short\" ? 0.38 : 0.5, drag: 0.12, angle: 1.57 });\n    function lockPath(d, th0, th1, ph) {\n      var pts = [], st = th1 > th0 ? 0.06 : -0.06;\n      for (var th = th0; st > 0 ? th <= th1 : th >= th1; th += st) pts.push(onSkull(-Math.PI / 2 + th, d + 3 * Math.sin(th * 7 + ph)));\n      return pts;\n    }\n    var HC = Hp(0, 0), cs = Math.max(0.45, k), ins = Math.max(1.5, 3 * k);\n    [-1, 1].forEach(function (side) {\n      var locks = side < 0 ? [[10, reachNear + 0.15], [16, reachNear], [22, reachNear - 0.35], [28, 0.9]] : [[8, reachFar], [13, reachFar - 0.25], [18, 0.75]];\n      locks.forEach(function (L, j) {\n        var d = L[0] + (rh() - 0.5) * 2, th0 = side * Math.max(bald, 0.08 + j * 0.05), th1 = side * L[1], ph = rh() * 6;\n        if (Math.abs(th1) < Math.abs(th0) + 0.2) return;\n        var pts = lockPath(d, th0, th1, ph), lock = pts;\n        if (pts.length < 4) return;\n        if (j < locks.length - 1) {\n          var e = pts[pts.length - 1], q = pts[pts.length - 3];\n          var tx = e.x - q.x, ty = e.y - q.y, tl = Math.hypot(tx, ty) || 1, nx = HC.x - e.x, ny = HC.y - e.y, nl = Math.hypot(nx, ny) || 1;\n          tx /= tl; ty /= tl; nx /= nl; ny /= nl;\n          lock = pts.concat(T.through([e, P(e.x + (tx * 6 + nx * 2) * cs, e.y + (ty * 6 + ny * 2) * cs), P(e.x + (tx * 6 + nx * 9) * cs, e.y + (ty * 6 + ny * 9) * cs),\n            P(e.x + (tx + nx * 11) * cs, e.y + (ty + ny * 11) * cs), P(e.x + (-tx * 2 + nx * 7) * cs, e.y + (-ty * 2 + ny * 7) * cs)], 4).slice(1));\n        }\n        scrapeIf(lockPath(d + 4.5, th0 + side * 0.12, th1 - side * 0.15, ph), { w: wk(1.6), a: 0.55, rough: 0.5, chip: 0.3, taperIn: 0.3, taperOut: 0.5 });\n        T.traceIn(hd, lock, { w: wk(2.6 + rh() * 0.7), load: 0.45, taperIn: 0.1, taperOut: 0.35, rough: 0.4 }, ins);\n        T.traceIn(hd, lockPath(d - 3.5, th0 + side * 0.05, th1 - side * 0.08, ph), { w: wk(1.1), a: 0.8, taperIn: 0.15, taperOut: 0.5, rough: 0.3 }, ins);\n      });\n    });\n    // small curls along the hairline, turning alternately; fewer on a small head\n    var cr = Math.max(2.2, 5 * k), step = 0.19 / Math.max(0.4, k);\n    for (var hc = 0, th = -1.05; th <= 0.85; th += step, hc++) {\n      if (Math.abs(th) < bald) continue;\n      var c = onSkull(-Math.PI / 2 + th, hairDepth(th) / k - 5), dirc = hc % 2 ? 1 : -1, cp = [];\n      for (var ct = 0; ct <= 8; ct++) { var ca = -Math.PI / 2 + th + dirc * (0.3 + ct * 0.62); cp.push(P(c.x + Math.cos(ca) * cr, c.y + Math.sin(ca) * cr)); }\n      G.trace(cp, { w: wk(2.0), load: 0.3, taperIn: 0.1, taperOut: 0.6, rough: 0.35 });\n    }\n\n    // the brow sweeping into the nose, whose tip turns with the head\n    G.trace(T.through([Fp(-1.4, -0.95), Fp(-0.8, -1.3), Fp(-0.2, -1.02), Fp(0.02, -0.3), Fp(0.14, 0.5), Fp(0.38, 0.86), Fp(0.12, 0.97), Fp(-0.16, 0.86)], 6),\n      { w: wk(3.6), load: 0.5, taperIn: 0, taperOut: 0.1, rough: 0.3 });\n    G.trace(T.through([Fp(0.35, -1.02), Fp(0.9, -1.3), Fp(1.45, -1.02)], 8), { w: wk(3.1), load: 0.4, taperIn: 0, taperOut: 0.5 });\n    [[-0.82, 7.6], [0.86, 6.2]].forEach(function (eye) {\n      var ec = eye[0];\n      G.trace(T.through([Fp(ec - 0.5, -0.25), Fp(ec, -0.62), Fp(ec + 0.5, -0.3)], 8), { w: wk(4.0), taperIn: 0.15, taperOut: 0.3 });\n      G.trace(T.through([Fp(ec - 0.42, -0.18), Fp(ec, 0.08), Fp(ec + 0.42, -0.22)], 6), { w: wk(1.5), taperIn: 0.3, taperOut: 0.3 });\n      G.trace([Fp(ec + 0.04, -0.38), Fp(ec + 0.06, -0.34)], { w: wk(eye[1]), taperIn: 0, taperOut: 0, rough: 0.15 });\n    });\n    G.trace(T.through([Fp(-0.62, 1.52), Fp(-0.3, 1.38), Fp(0.2, 1.36), Fp(0.55, 1.5)], 6), { w: wk(3.0), load: 0.3, taperIn: 0.1, taperOut: 0.35 });\n    G.trace([Fp(-0.2, 1.72), Fp(0.18, 1.7)], { w: wk(1.6), taperIn: 0.3, taperOut: 0.3 });\n\n    if (o.beard) {\n      [-1, 1].forEach(function (sg) {\n        G.trace(T.through([Fp(0.05 * sg, 1.12), Fp(0.45 * sg, 1.2), Fp(0.85 * sg, 1.5)], 6), { w: wk(2.2), taperIn: 0.2, taperOut: 0.6 });\n      });\n      // a painted mass below the cheekbones, then tiers of hooked curls with light scraped beside them\n      G.matt(function (x, y) {\n        if (!G.inPoly(hd, x, y)) return 0;\n        var q = Fu(x, y), au = Math.abs(q.u), vb = 1.85 - 0.8 * K.ss(0.5, 1.4, au) - (q.u < 0 ? 0.12 * K.ss(0.9, 1.6, au) : 0);\n        return K.ss(vb, vb + 0.6, q.v);\n      }, T.bboxOf(hd), { a: 0.42, drag: 0.12, angle: 1.57 });\n      [[42, 8, 0.9, 14], [29, 10, 1.1, 14], [16, 9, 0.95, 12], [5, 4, 0.35, 12]].forEach(function (tier) {\n        var d = tier[0] * k, n = Math.max(3, Math.round(tier[1] * (0.35 + 0.65 * k))), span = tier[2], L = tier[3] * cs;\n        for (var bd = 0; bd < n; bd++) {\n          var s = (bd + 0.5 * (rh() - 0.5)) / (n - 1), ph = -span * 0.8 + 2 * span * s, ang = POINT + ph;\n          var ox = Math.cos(ang), oy = Math.sin(ang), dir = ph < 0 ? 1 : -1, len = L * (0.8 + 0.4 * rh()) * (1 + 0.4 * Math.exp(-(ph * ph) / 0.1));\n          var rx = ox * (HRX - d), ry = oy * (HRY - d);\n          var at = function (a, b) { b *= cs; return Hp(rx + ox * a - oy * b * dir, ry + oy * a + ox * b * dir); };\n          scrapeIf(T.through([at(2 * cs, -4), at(len * 0.6, -5), at(len * 0.85, -2)], 4), { w: wk(1.5), a: 0.5, rough: 0.5, chip: 0.3, taperIn: 0.3, taperOut: 0.5 });\n          T.traceIn(hd, T.through([at(0, 0), at(len * 0.55, 0), at(len, 3), at(len * 0.95, 8), at(len * 0.7, 9)], 4),\n            { w: wk(2.4), load: 0.35, taperIn: 0.1, taperOut: 0.5, rough: 0.4 }, Math.max(1.5, 4 * k));\n        }\n      });\n    }\n    leadOf(hd);\n    return hd;\n  }\n\n  /**\n   * A standing robed figure, full length. o: x, y (between the feet, on the ground line), h (height px),\n   * facing (+1 turned to our right, -1 to our left), lean (radians, the body toward facing), tilt (the head),\n   * gesture (\"bless\" | \"point\" | \"pray\" | \"book\" | \"rest\"), beard, hair, halo (bool),\n   * colors { tunic, mantle, shoe, halo, flesh, book }, seed.\n   * Returns { head, headCentre, hand } so a scene can aim other figures at them.\n   */\n  function figure(o) {\n    var rf = G.rngFrom(o.seed || 1), H = o.h / 7, fc = o.facing || 1, C = o.colors, lean = o.lean || 0;\n    var s = Math.max(0.5, Math.min(1, H / 90)), cl = Math.cos(lean), sl = Math.sin(lean);\n    /** Figure units (head heights; u toward facing, v up from the feet) to page, leaning. */\n    function Q(u, v) { return P(o.x + fc * (u * H * cl + v * H * sl), o.y - (v * H * cl - u * H * sl)); }\n    function shape(list) { return T.smoothClosed(list.map(function (q) { return Q(q[0], q[1]); }), 5); }\n    function line(list, n) { return T.through(list.map(function (q) { return Q(q[0], q[1]); }), n || 8); }\n    var hc = Q(0.08, 6.5), flesh = C.flesh || \"#ecccbc\";\n\n    if (o.halo) {\n      var hr = 0.74 * H, hcen = Q(0.05, 6.56), ring = K.circle(hcen.x, hcen.y, hr, 90);\n      piece(ring, C.halo, { edge: 0.35, edgeW: 8 });\n      T.edgeMatt(ring, 0.45, 6 + 4 * s);\n      var inner = K.circle(hcen.x, hcen.y, hr - 6 - 3 * s, 90);\n      G.trace(inner.concat([inner[0]]), { w: 2 * s, a: 0.85, taperIn: 0, taperOut: 0, rough: 0.3 });\n      leadOf(ring);\n    }\n\n    // shoes, pointed toward the facing side, under the hem\n    [[[0.2, 0.3], [0.85, 0.28], [1.28, 0.08], [0.9, -0.03], [0.15, 0.0]], [[-0.78, 0.28], [-0.12, 0.28], [-0.02, 0.03], [-0.55, -0.03], [-0.86, 0.1]]].forEach(function (sh) {\n      var p = shape(sh);\n      piece(p, C.shoe, { angle: 0, mottle: 0.5 });\n      T.edgeMatt(p, 0.5, 4);\n      leadOf(p);\n    });\n\n    // tunic to the ankles: matted, long hooked folds, light scraped between them\n    var tunic = shape([[-0.3, 6.08], [0.32, 6.08], [0.86, 5.66], [0.82, 4.8], [0.72, 3.7], [0.92, 1.9], [1.12, 0.3], [0.25, 0.2], [-0.92, 0.3], [-0.98, 1.9], [-0.72, 3.7], [-0.76, 4.8], [-0.88, 5.66]]);\n    piece(tunic, C.tunic, { angle: 1.57, edge: 0.3 });\n    G.matt(T.inside(tunic), T.bboxOf(tunic), { a: 0.4, drag: 0.12, angle: 1.57 });\n    var tf = [];\n    for (var j = 0; j < 5; j++) {\n      var t = j / 4;\n      tf.push(T.hooked(Q(-0.45 + t * 0.95, 4.1), Q(-0.78 + t * 1.75 + (rf() - 0.5) * 0.1, 0.5 + (j % 2) * 0.18), fc * (rf() - 0.5) * 0.25 * H, (j % 2 ? 1 : -1) * fc * 7 * s));\n    }\n    T.foldShadow(tunic, tf, 0.45, 7 * s);\n    tf.forEach(function (l) { T.traceIn(tunic, l, { w: 3 * s, load: 0.5, taperIn: 0.08, taperOut: 0.35, rough: 0.35 }, 5); });\n    for (j = 0; j < 4; j++) {\n      var a0 = tf[j], a1 = tf[j + 1], n = Math.min(a0.length, a1.length);\n      var mid = a0.slice(3, n - 6).map(function (p, i) { var q = a1[i + 3]; return P((p.x + q.x) / 2, (p.y + q.y) / 2); });\n      T.clipRuns(tunic, mid, 6).forEach(function (r) { G.scrape(r, { w: 2.6 * s, a: 0.6, rough: 0.6, chip: 0.4, taperIn: 0.2, taperOut: 0.5 }); });\n    }\n    leadOf(tunic);\n\n    // mantle from the shoulders, open at the neck so the tunic shows under the head, its front edge\n    // swept across to the hip and its hem rising diagonally toward the front. Cut in two pieces along\n    // the fold that falls from the hip to the back, as a glazier would, so the lead draws the drape;\n    // an orphrey band with pearls runs down the front edge and round the hem.\n    // (scene v1: a mantle up to the head read as a hood; scene v2: one flat piece read as a sack.)\n    var mUpper = shape([[-0.9, 5.62], [-0.45, 5.9], [-0.12, 5.82], [0.22, 5.55], [0.3, 5.0], [0.6, 4.1], [0.3, 3.82], [-0.3, 3.48], [-0.95, 3.2], [-0.8, 4.8]]);\n    var mLower = shape([[0.6, 4.1], [0.94, 3.05], [0.98, 2.1], [0.55, 1.9], [0.0, 1.55], [-0.6, 1.1], [-0.96, 1.0], [-0.95, 3.2], [-0.3, 3.48], [0.3, 3.82]]);\n    /** Fold lines traced dark over a soft shadow, each with a ridge of light scraped beside it. */\n    function drape(poly, lines, widths) {\n      T.foldShadow(poly, lines, 0.42, 8 * s);\n      lines.forEach(function (l, i) {\n        T.traceIn(poly, l, { w: widths[i] * s, load: 0.5, taperIn: 0.05, taperOut: 0.35, rough: 0.35 }, 6);\n        var ridge = l.map(function (p, q) {\n          var a = l[Math.max(0, q - 1)], b = l[Math.min(l.length - 1, q + 1)], dx = b.x - a.x, dy = b.y - a.y, dl = Math.hypot(dx, dy) || 1;\n          return P(p.x - dy / dl * 7 * s, p.y + dx / dl * 7 * s);\n        });\n        T.clipRuns(poly, ridge.slice(2, Math.max(3, ridge.length - 4)), 6).forEach(function (r) {\n          G.scrape(r, { w: 2.4 * s, a: 0.5, rough: 0.6, chip: 0.4, taperIn: 0.3, taperOut: 0.5 });\n        });\n      });\n    }\n    piece(mUpper, C.mantle, { angle: 1.3, edge: 0.3 });\n    T.edgeMatt(mUpper, 0.35, 10 * s);\n    var up = [], upw = [];\n    for (j = 0; j < 3; j++) { up.push(line([[-0.85, 5.3 - j * 0.55], [-0.2, 4.78 - j * 0.62], [0.36 + j * 0.2, 5.05 - j * 0.6]], 10)); upw.push(3); }\n    drape(mUpper, up, upw);\n    leadOf(mUpper);\n    piece(mLower, C.mantle, { angle: 1.7, edge: 0.3 });\n    T.edgeMatt(mLower, 0.35, 10 * s);\n    G.matt(T.inside(mLower), T.bboxOf(mLower), { a: 0.12, drag: 0.1, angle: 1.57 });\n    var lo = [], low = [];\n    for (j = 0; j < 4; j++) {\n      // each fold falls from under the split to just above the diagonal hem (v 1.0 at the back, 2.0 at the front)\n      var tt = j / 3, ue = -0.84 + tt * 1.6, ve = 1.05 + (ue + 0.96) / 1.94 + 0.3 + (j % 2) * 0.1;\n      lo.push(T.hooked(Q(-0.8 + tt * 1.3, 3.1 + tt * 0.55 - (j % 2) * 0.2), Q(ue, ve), fc * (6 + rf() * 6) * s, (j % 2 ? -1 : 1) * fc * 8 * s));\n      low.push(3.6);\n    }\n    drape(mLower, lo, low);\n    leadOf(mLower);\n    // the orphrey: down the front edge from the neck, then round the hem to the back\n    var orph = line([[0.2, 5.55], [0.3, 5.0], [0.6, 4.1], [0.94, 3.05], [0.98, 2.1], [0.55, 1.9], [0.0, 1.55], [-0.6, 1.1], [-0.94, 1.0]], 8);\n    var bandW = Math.max(4, 0.07 * H);\n    var band = K.ribbon(orph, function (u) { return u < 0.03 || u > 0.97 ? bandW * 0.6 : bandW; });\n    piece(band, C.band || \"#e2a520\", { angle: 0.5, mottle: 0.5, edge: 0.2, edgeW: 3 });\n    G.matt(T.inside(band), T.bboxOf(band), { a: 0.7, drag: 0.1 });\n    T.pearls(orph, Math.max(8, bandW * 1.6), Math.max(3, bandW * 0.8));\n    leadOf(band, 5);\n\n    // the near arm and hand, by gesture: a sleeve piece, then the hand with finger strokes\n    function limb(sleevePts, handPts, strokes) {\n      var sv = shape(sleevePts);\n      piece(sv, C.tunic, { angle: rf() * 3, edge: 0.3 });\n      G.matt(T.inside(sv), T.bboxOf(sv), { a: 0.35, drag: 0.1, angle: 1.57 });\n      leadOf(sv);\n      var hp = shape(handPts);\n      piece(hp, flesh, { mottle: 0.35, weather: 0.2, edge: 0.2, edgeW: 4 });\n      T.edgeMatt(hp, 0.35, 5);\n      strokes.forEach(function (st) { G.trace(line(st, 4), { w: 1.8 * s, taperIn: 0.1, taperOut: 0.3, rough: 0.3 }); });\n      leadOf(hp);\n      return hp;\n    }\n    var g = o.gesture || \"rest\", hand;\n    if (g === \"bless\") {\n      hand = limb([[0.3, 4.6], [0.9, 4.45], [1.38, 5.28], [1.3, 5.62], [1.0, 5.55], [0.4, 5.05]],\n        [[1.1, 5.5], [1.42, 5.55], [1.52, 5.95], [1.48, 6.4], [1.36, 6.48], [1.2, 6.4], [1.1, 6.1], [0.98, 6.0], [1.02, 5.8]],\n        [[[1.22, 5.8], [1.24, 6.3]], [[1.32, 5.8], [1.36, 6.35]], [[1.42, 5.85], [1.44, 6.3]]]);\n    } else if (g === \"point\") {\n      hand = limb([[0.5, 4.85], [0.9, 4.72], [1.55, 4.92], [1.6, 5.22], [0.95, 5.28], [0.58, 5.22]],\n        [[1.52, 4.9], [1.9, 4.95], [2.3, 5.12], [2.32, 5.22], [1.95, 5.22], [1.8, 5.35], [1.58, 5.3]],\n        [[[1.7, 5.02], [2.2, 5.15]], [[1.68, 5.12], [1.9, 5.14]]]);\n    } else if (g === \"pray\") {\n      // the sleeve crosses the chest from under the mantle, so the joined hands stay attached (scene v1 floated them)\n      hand = limb([[0.2, 4.55], [0.7, 4.45], [1.08, 5.0], [0.9, 5.28], [0.3, 5.05]],\n        [[0.86, 4.95], [1.12, 4.98], [1.36, 5.55], [1.26, 5.74], [1.06, 5.58], [0.84, 5.25]],\n        [[[0.98, 5.12], [1.18, 5.55]], [[0.9, 5.2], [1.08, 5.55]]]);\n    } else if (g === \"book\") {\n      var book = shape([[0.02, 4.25], [0.78, 4.35], [0.74, 5.15], [-0.02, 5.05]]);\n      // a jewelled cover: clear glass in the middle, paint at the edge, a traced cross with a pearl\n      // clasp where its arms meet (window v1's fully matted book read as a blob)\n      piece(book, C.book, { angle: 0.1, edge: 0.4, edgeW: 4 });\n      T.edgeMatt(book, 0.6, 5 * s + 2);\n      var bc = Q(0.38, 4.7), cross = { w: 2.4 * s, a: 0.8, taperIn: 0, taperOut: 0, rough: 0.3 };\n      G.trace([Q(0.38, 4.42), Q(0.37, 4.98)], cross);\n      G.trace([Q(0.12, 4.68), Q(0.64, 4.74)], cross);\n      G.trace([bc, P(bc.x + 0.3, bc.y + 0.2)], { w: 5 * s + 2, a: 0.98, taperIn: 0, taperOut: 0, rough: 0.05, erase: true });\n      leadOf(book);\n      hand = limb([[0.62, 3.95], [0.98, 3.9], [1.05, 4.3], [0.65, 4.35]], [[0.55, 4.12], [0.98, 4.2], [1.0, 4.5], [0.62, 4.52]], [[[0.65, 4.3], [0.92, 4.34]]]);\n    } else {\n      hand = limb([[0.72, 3.5], [1.0, 3.45], [1.08, 3.2], [0.8, 3.15]], [[0.84, 3.18], [1.08, 3.15], [1.12, 2.85], [0.95, 2.72], [0.8, 2.9]],\n        [[[0.9, 3.05], [0.92, 2.8]], [[1.0, 3.05], [1.03, 2.82]]]);\n    }\n\n    var hdp = head({ c: hc, ry: 0.5 * H, facing: fc, tilt: o.tilt == null ? 0.1 : o.tilt, beard: o.beard, hair: o.hair, flesh: flesh, rng: rf });\n    return { head: hdp, headCentre: hc, hand: hand };\n  }\n\n  return { head: head, figure: figure };\n}\n\n// Stained glass: scene settings (sky, hills, trees, towers, a city gate, an arcade, a tiled floor,\n// water in wave bands, a boat of leaded planks, a mast with a bunched sail; for interiors, rooftops, twin\n// arches, a clothed table and the loaves, goblet, jug and fruit bowl that stand on it)\n// for medallion scenes. Prepended after glass.js, geom.js, kit.js and figures.js. Each setting is\n// drawn in the scene medallion's 900px frame (disc centre 450,450, inner radius 382) and mapped\n// into any medallion by S.M, so one composition works full-size or as a compartment of a window.\n// S = { M(x, y) -> page point, sc (page px per frame px), O (page centre), r (page inner radius) }.\n// api = { piece(poly, color, o), lead(poly, w), rng, C (palette) }. Series-local, like the material.\nfunction glassSettings(G, K, T, api) {\n  function P(x, y) { return { x: x, y: y }; }\n  var piece = api.piece, leadOf = api.lead, C = api.C, rng = api.rng;\n\n  /** Clip a polygon to the medallion's disc, densifying its edges first so the clamp follows the circle. */\n  function clip(S, pts) {\n    var dense = [];\n    for (var i = 0; i < pts.length; i++) {\n      var a = pts[i], b = pts[(i + 1) % pts.length], n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / 5));\n      for (var q = 0; q < n; q++) dense.push(P(a.x + (b.x - a.x) * q / n, a.y + (b.y - a.y) * q / n));\n    }\n    return T.clampToDisc(dense, S.O, S.r + 4);\n  }\n  /** Frame points [[x, y], ...] to page points. */\n  function mp(S, list) { return list.map(function (q) { return S.M(q[0], q[1]); }); }\n  /** Stroke widths shrink less than the setting, or small compartments lose their paint. */\n  function sw(S, w) { return w * Math.max(0.55, S.sc); }\n  function arched(cx, top, hw, ht) {\n    var pts = [];\n    for (var q = 0; q <= 10; q++) { var a = Math.PI + Math.PI * q / 10; pts.push([cx + Math.cos(a) * hw, top + hw + Math.sin(a) * hw]); }\n    return pts.concat([[cx + hw, top + ht], [cx - hw, top + ht]]);\n  }\n  function brickWall(S, poly, x0, x1, y0, y1) {\n    var step = S.sc < 0.7 ? 30 : 22, brick = { w: sw(S, 2), a: 0.85, taperIn: 0, taperOut: 0, rough: 0.4 };\n    for (var by = y0 + step, row = 0; by < y1; by += step, row++) {\n      T.traceIn(poly, T.through([S.M(x0 - 6, by), S.M(x1 + 6, by)], 8), brick, 2);\n      for (var bx = x0 + (row % 2) * step * 0.8; bx < x1; bx += step * 1.6) T.traceIn(poly, T.through([S.M(bx, by), S.M(bx, by + step)], 4), brick, 2);\n    }\n  }\n\n  /**\n   * Sky: wedges fanning up from far below the medallion, so the leads rise gently behind the figures.\n   * avoid: frame [x, y] points (heads) each lead must pass at least 70 frame px from, sideways at that\n   * height (scene v3's lead rose straight through the saint's halo).\n   */\n  function sky(S, sd, avoid, o) {\n    var r = G.rngFrom(sd), SC = S.M(450, 1500), n = 7, ang = [], ps = [];\n    for (var i = 0; i <= n; i++) {\n      var t0 = -Math.PI / 2 - 0.5 + i / n + (i > 0 && i < n ? (r() - 0.5) * 0.06 : 0);\n      if (i > 0 && i < n) (avoid || []).forEach(function (a) {\n        var dy = a[1] - 1500, x = 450 + Math.cos(t0) / Math.sin(t0) * dy;\n        if (Math.abs(x - a[0]) < 70) t0 = Math.atan2(dy, a[0] + (x < a[0] ? -70 : 70) - 450);\n      });\n      ang.push(t0);\n    }\n    for (i = 0; i < n; i++) {\n      var wp = [SC];\n      for (var q = 0; q <= 8; q++) { var t = ang[i] + (ang[i + 1] - ang[i]) * q / 8; wp.push(P(SC.x + Math.cos(t) * 1800 * S.sc, SC.y + Math.sin(t) * 1800 * S.sc)); }\n      ps.push(clip(S, wp));\n    }\n    // entry v3 -> v4 (entry-audit-v3.md S1): fanned wedges alone give leads that run the whole height of the roundel like\n    // strings. o.bands cuts them across near frame heights. v4 -> v5: one straight cut across the whole disc lined the\n    // wedges up into a grid of panes, so every wedge is now cut on its own, at its own height and tilt, staggered like\n    // a glazier's offcuts. Without o.bands nothing changes (no random numbers are drawn), so earlier scenes render as before.\n    ((o && o.bands) || []).forEach(function (by) {\n      var out = [];\n      ps.forEach(function (p) {\n        var y = by + (r() - 0.5) * 90, tl = (r() - 0.5) * 50;\n        K.split(p, S.M(0, y - tl), S.M(900, y + tl)).forEach(function (q) { out.push(q); });\n      });\n      ps = out;\n    });\n    ps.forEach(function (p) {\n      piece(p, C.blue, { mottle: 0.8, weather: 0.35, pits: 0.25, seeds: 0.4, edgeW: 18 * S.sc + 4 });\n      G.matt(T.inside(p), T.bboxOf(p), { a: 0.12, drag: 0.1, angle: rng() * 3 });\n    });\n    ps.forEach(function (p) { leadOf(p, 6); });\n  }\n\n  /** A hill: a shadow under the crest, tufts scraped up out of it, dark blades. a, pk, b are frame [x, y]. */\n  function hill(S, a, pk, b, color, sd) {\n    var A = S.M(a[0], a[1]), PK = S.M(pk[0], pk[1]), B = S.M(b[0], b[1]), sc = S.sc;\n    var top = T.through([A, P((A.x + PK.x) / 2, PK.y + (A.y - PK.y) * 0.25), PK, P((PK.x + B.x) / 2, PK.y + (B.y - PK.y) * 0.3), B], 8);\n    var poly = clip(S, top.concat([P(B.x, S.O.y + S.r * 1.3), P(A.x, S.O.y + S.r * 1.3)]));\n    piece(poly, color, { angle: 0.1, mottle: 0.7, grain: 0.35 });\n    var dd = 16 * sc, ww = 120 * sc * sc;\n    G.matt(function (x, y) {\n      if (!G.inPoly(poly, x, y)) return 0;\n      var d = T.nearPath(top, x, y);\n      return Math.exp(-((d - dd) * (d - dd)) / ww);\n    }, T.bboxOf(poly), { a: 0.5, drag: 0.1, angle: 0 });\n    var rt = G.rngFrom(sd);\n    K.every(top, 24 * Math.max(0.6, sc), 14 * sc).forEach(function (p, i) {\n      for (var bl = -1; bl <= 1; bl++) {\n        var bx0 = p.x + bl * 5 * sc, by0 = p.y + (22 + rt() * 10) * sc;\n        G.scrape(T.through([P(bx0, by0), P(bx0 + bl * 3 * sc, by0 - 9 * sc), P(bx0 + bl * 7 * sc, by0 - 17 * sc)], 3), { w: sw(S, 2.4), a: 0.6, rough: 0.5, chip: 0.3, taperIn: 0.2, taperOut: 0.6 });\n      }\n      if (i % 2) T.traceIn(poly, T.through([P(p.x - 4 * sc, p.y + 40 * sc), P(p.x + 2 * sc, p.y + 28 * sc), P(p.x + 9 * sc, p.y + 26 * sc), P(p.x + 11 * sc, p.y + 32 * sc)], 4),\n        { w: sw(S, 2), taperIn: 0.1, taperOut: 0.5, rough: 0.4 }, 3);\n    });\n    leadOf(poly, 6);\n    return poly;\n  }\n\n  /** A tree: an amber trunk forking into branches, each ending in a rounded lobed cluster, rim-shaded, veins scraped. */\n  function tree(S, cr, base, clusters, sd, o) {\n    // o.trunk scales the trunk's width (entry audit T3: a thin trunk read as a lamppost); 1 when absent, as before.\n    var sc = S.sc, crown = S.M(cr[0], cr[1]), tw = (o && o.trunk) || 1;\n    var trunk = clip(S, K.ribbon(T.through([S.M(base[0], base[1]), S.M(base[0] - 14, base[1] - 92), S.M(base[0] + 2, base[1] - 172), crown], 8), function (s) { return Math.max(3, (14 - 5 * s) * sc * tw); }));\n    piece(trunk, C.bark, { angle: 1.57, streak: 0.5 });\n    T.edgeMatt(trunk, 0.4, 7 * sc + 2);\n    leadOf(trunk, 6);\n    clusters.forEach(function (L) {\n      var tip = S.M(L[0], L[1]), mid = S.M((cr[0] + L[0]) / 2 + (L[0] < cr[0] ? -8 : 8), (cr[1] + L[1]) / 2 + 10);\n      var br = clip(S, K.ribbon(T.through([crown, mid, tip], 8), function (s) { return Math.max(2.5, (8 - 4 * s) * sc); }));\n      piece(br, C.bark, { angle: 1.2, streak: 0.5 });\n      leadOf(br, 5);\n    });\n    clusters.forEach(function (L, i) { leaves(S, L, i * 0.9, sd + i); });\n  }\n\n  /**\n   * One rounded lobed leaf cluster, rim-shaded, veins scraped: L = [x, y, r, color] in frame units, ph its lobe phase.\n   * Split out of tree() (unchanged output) so a scene can put a figure between a tree and a cluster in front of it.\n   */\n  function leaves(S, L, ph, sd) {\n    var sc = S.sc, c = S.M(L[0], L[1]), r = L[2] * sc, rim = [];\n    for (var q = 0; q < 70; q++) {\n      var a = q / 70 * 2 * Math.PI, rr = r * (0.86 + 0.14 * Math.abs(Math.sin(a * 3.5 + ph)));\n      rim.push(P(c.x + Math.cos(a) * rr, c.y + Math.sin(a) * rr));\n    }\n    var poly = clip(S, rim);\n    piece(poly, L[3], { angle: ph, mottle: 0.7, grain: 0.4 });\n    T.edgeMatt(poly, 0.5, 14 * sc + 2);\n    var rl = G.rngFrom(sd);\n    for (var v = 0; v < 7; v++) {\n      var va = v / 7 * 2 * Math.PI + ph + (rl() - 0.5) * 0.3;\n      G.scrape(T.through([P(c.x + Math.cos(va) * r * 0.12, c.y + Math.sin(va) * r * 0.12), P(c.x + Math.cos(va + 0.12) * r * 0.45, c.y + Math.sin(va + 0.12) * r * 0.45),\n        P(c.x + Math.cos(va + 0.05) * r * 0.7, c.y + Math.sin(va + 0.05) * r * 0.7)], 4), { w: sw(S, 3), a: 0.6, rough: 0.6, chip: 0.4, taperIn: 0.15, taperOut: 0.6 });\n    }\n    leadOf(poly, 6);\n  }\n\n  /** A tower: brick wall from top to bottom between x0 and x1 (frame), a jewelled band, a scaled red roof; o.door, o.slot. */\n  function tower(S, x0, x1, top, bottom, o) {\n    o = o || {};\n    var cx = (x0 + x1) / 2, sc = S.sc;\n    var wall = clip(S, mp(S, [[x0, top], [x1, top], [x1, bottom], [x0, bottom]]));\n    piece(wall, C.stone, { angle: 0, mottle: 0.45, weather: 0.3, edge: 0.25 });\n    G.matt(T.inside(wall), T.bboxOf(wall), { a: 0.22, drag: 0.1, angle: 0 });\n    brickWall(S, wall, x0, x1, top, bottom);\n    leadOf(wall, 6);\n    if (o.slot) {\n      var slot = clip(S, mp(S, arched(cx, top + 64, 12, 60)));\n      piece(slot, C.night, { angle: 1.57, mottle: 0.5 });\n      leadOf(slot, 5);\n    }\n    if (o.door) {\n      var door = clip(S, mp(S, arched(cx, bottom - 124, Math.min(27, (x1 - x0) * 0.25), 110)));\n      piece(door, C.purple, { angle: 1.57 });\n      G.matt(T.inside(door), T.bboxOf(door), { a: 0.45, drag: 0.12, angle: 1.57 });\n      leadOf(door, 5);\n    }\n    var band = clip(S, mp(S, [[x0 - 12, top - 38], [x1 + 12, top - 38], [x1 + 12, top + 2], [x0 - 12, top + 2]]));\n    piece(band, C.amber, { angle: 0 });\n    G.matt(T.inside(band), T.bboxOf(band), { a: 0.72, drag: 0.1, angle: 0 });\n    T.pearls([S.M(x0 - 6, top - 18), S.M(x1 + 6, top - 18)], Math.max(7, 15 * sc), Math.max(4, 8 * sc));\n    leadOf(band, 6);\n    if (o.roof !== false) {\n      var roofF = [[x0 - 26, top - 36], [x1 + 26, top - 36], [cx, top - 140]], roof = clip(S, mp(S, roofF));\n      piece(roof, C.ruby, { angle: 1.2 });\n      G.matt(T.inside(roof), T.bboxOf(roof), { a: 0.5, drag: 0.1, angle: 1.57 });\n      var rstep = S.sc < 0.7 ? 22 : 16;\n      for (var ry = top - 116, rr = 0; ry < top - 38; ry += rstep, rr++) {\n        var half = (ry - (top - 140)) / 104 * ((x1 - x0) / 2 + 26);\n        for (var rx = cx - half + 9 + (rr % 2) * 8; rx < cx + half - 6; rx += rstep) {\n          G.scrape(T.through([S.M(rx - 6, ry - 4), S.M(rx, ry + 3), S.M(rx + 6, ry - 4)], 3), { w: sw(S, 2.6), a: 0.7, rough: 0.5, chip: 0.3, taperIn: 0.1, taperOut: 0.1 });\n        }\n      }\n      leadOf(roof, 6);\n      var fin = S.M(cx, top - 148), finial = clip(S, K.circle(fin.x, fin.y, Math.max(4, 10 * sc), 24));\n      piece(finial, C.amber, { angle: 0 });\n      leadOf(finial, 5);\n    }\n  }\n\n  /** A city gate: two towers either side of cx (hw apart), a crenellated wall between with a dark arched opening. */\n  function gate(S, cx, top, bottom, hw) {\n    var wall = clip(S, mp(S, [[cx - hw, top + 120], [cx + hw, top + 120], [cx + hw, bottom], [cx - hw, bottom]]));\n    piece(wall, C.stone, { angle: 0, mottle: 0.45, weather: 0.3, edge: 0.25 });\n    G.matt(T.inside(wall), T.bboxOf(wall), { a: 0.22, drag: 0.1, angle: 0 });\n    brickWall(S, wall, cx - hw, cx + hw, top + 120, bottom);\n    leadOf(wall, 6);\n    for (var mx = cx - hw + 8; mx < cx + hw - 20; mx += 34) {\n      var merlon = clip(S, mp(S, [[mx, top + 96], [mx + 20, top + 96], [mx + 20, top + 122], [mx, top + 122]]));\n      piece(merlon, C.stone, { angle: 0, mottle: 0.4 });\n      leadOf(merlon, 5);\n    }\n    var open = clip(S, mp(S, arched(cx, top + 190, hw - 42, bottom - top - 190)));\n    piece(open, C.night, { angle: 1.57, mottle: 0.5 });\n    T.edgeMatt(open, 0.5, 10 * S.sc + 2);\n    leadOf(open, 6);\n    tower(S, cx - hw - 40, cx - hw + 30, top + 20, bottom, { slot: true });\n    tower(S, cx + hw - 30, cx + hw + 40, top, bottom, { slot: true });\n  }\n\n  /** An arcade: two columns with amber capitals carrying an arched ruby band with pearls, a turret on each end. */\n  function arcade(S) {\n    [170, 730].forEach(function (x) {\n      var col = clip(S, mp(S, [[x - 13, 330], [x + 13, 330], [x + 13, 800], [x - 13, 800]]));\n      piece(col, C.stone, { angle: 1.57, mottle: 0.45 });\n      T.edgeMatt(col, 0.35, 5);\n      leadOf(col, 6);\n      var cap = clip(S, mp(S, [[x - 24, 312], [x + 24, 312], [x + 16, 336], [x - 16, 336]]));\n      piece(cap, C.amber, { angle: 0 });\n      G.matt(T.inside(cap), T.bboxOf(cap), { a: 0.6, drag: 0.1 });\n      leadOf(cap, 5);\n    });\n    var arc = [];\n    for (var q = 0; q <= 30; q++) { var a = Math.PI + Math.PI * q / 30; arc.push(S.M(450 + 280 * Math.cos(a), 318 + 190 * Math.sin(a))); }\n    var bandW = Math.max(5, 16 * S.sc), band = clip(S, K.ribbon(arc, function () { return bandW; }));\n    piece(band, C.ruby, { angle: 0 });\n    G.matt(T.inside(band), T.bboxOf(band), { a: 0.7, drag: 0.1 });\n    T.pearls(arc, Math.max(8, 18 * S.sc), Math.max(4, 9 * S.sc));\n    leadOf(band, 6);\n    tower(S, 140, 200, 250, 312, { roof: true });\n    tower(S, 700, 760, 250, 312, { roof: true });\n  }\n\n  /** A tiled floor from frame y down: two rows of amber and white tiles, then plain stone under the ring. */\n  function floor(S, y) {\n    for (var row = 0; row < 2; row++) {\n      for (var x = 40 + row * 23; x < 880; x += 46) {\n        var t = clip(S, mp(S, [[x, y + row * 30], [x + 46, y + row * 30], [x + 46, y + row * 30 + 30], [x, y + row * 30 + 30]]));\n        piece(t, (Math.round(x / 46) + row) % 2 ? C.amber : C.white, { angle: 0, mottle: 0.4 });\n        T.edgeMatt(t, 0.35, 4);\n        leadOf(t, 5);\n      }\n    }\n    var base = clip(S, mp(S, [[20, y + 60], [880, y + 60], [880, 950], [20, 950]]));\n    piece(base, C.purple, { angle: 0 });\n    G.matt(T.inside(base), T.bboxOf(base), { a: 0.35, drag: 0.1 });\n    leadOf(base, 6);\n  }\n\n  /** A wavy line across the frame at height y (amplitude amp, wavelength wl, phase ph), from x0 to x1, as page points. */\n  function wavy(S, y, amp, wl, ph, x0, x1) {\n    var pts = [];\n    for (var x = x0; x <= x1; x += 8) pts.push(S.M(x, y + amp * Math.sin(x / wl * 2 * Math.PI + ph) + amp * 0.35 * Math.sin(x / wl * 4 * Math.PI + ph * 1.7)));\n    return pts;\n  }\n\n  /**\n   * Water (refs 15, 17): stacked wave bands from frame y0 down, each its own piece of glass. Thin white\n   * crest bands between the coloured ones, a painted wave line inside each coloured band.\n   */\n  function water(S, y0, sd) {\n    var r = G.rngFrom(sd), sc = S.sc;\n    var bands = [[C.white, 18], [C.green, 34], [C.blue, 34], [C.white, 16], [C.teal, 36], [C.blue, 240]];\n    var tops = [], y = y0;\n    bands.forEach(function (b) { tops.push({ y: y, ph: r() * 6.28, amp: b[0] === C.white ? 7 : 5 }); y += b[1]; });\n    tops.push({ y: y, ph: 0, amp: 0 });\n    bands.forEach(function (b, i) {\n      var t0 = tops[i], t1 = tops[i + 1];\n      var poly = clip(S, wavy(S, t0.y, t0.amp, 96, t0.ph, -20, 920).concat(wavy(S, t1.y, t1.amp, 96, t1.ph, -20, 920).reverse()));\n      piece(poly, b[0], { angle: 0, mottle: b[0] === C.white ? 0.4 : 0.7, weather: 0.3 });\n      if (b[0] === C.white) T.edgeMatt(poly, 0.45, 5 * sc + 2);\n      else {\n        G.matt(T.inside(poly), T.bboxOf(poly), { a: 0.12, drag: 0.1, angle: 0 });\n        T.traceIn(poly, wavy(S, t0.y + Math.min(b[1], 34) * 0.5, t0.amp, 96, t0.ph + 0.9, -20, 920), { w: sw(S, 2.4), a: 0.8, taperIn: 0.05, taperOut: 0.05, rough: 0.4 }, 3);\n      }\n      leadOf(poly, 6);\n    });\n  }\n\n  /**\n   * A boat (ref 15): a shallow crescent hull of leaded plank strips, one colour per strip from the gunwale\n   * down. The gunwale sags from the raised ends (frame y end) to gun at the middle; the keel dips to keel.\n   * Rivets are painted along each plank. Draw it after the figures in it: it cuts them off at the gunwale.\n   */\n  function boat(S, x0, x1, gun, keel, end, colors) {\n    var cx = (x0 + x1) / 2, sc = S.sc, n = colors.length;\n    /** An edge across the hull at depth t (0 gunwale, 1 keel); the ends step in and down as t grows. */\n    function edge(t) {\n      var xa = x0 + t * 36, xb = x1 - t * 36, ye = end + t * 34, mid = gun + (keel - gun) * t, pts = [];\n      for (var q = 0; q <= 40; q++) {\n        var u = q / 40 * 2 - 1;\n        pts.push(S.M(xa + (xb - xa) * q / 40, ye + (mid - ye) * Math.pow(1 - u * u, 0.7)));\n      }\n      return pts;\n    }\n    var ts = [0];\n    for (var i = 1; i <= n; i++) ts.push(i === 1 ? 0.18 : 0.18 + 0.82 * (i - 1) / (n - 1));\n    for (i = 0; i < n; i++) {\n      var poly = clip(S, edge(ts[i]).concat(edge(ts[i + 1]).reverse()));\n      piece(poly, colors[i], { angle: 0, mottle: 0.6, streak: 0.3 });\n      T.edgeMatt(poly, 0.4, 6 * sc + 2);\n      K.every(edge((ts[i] + ts[i + 1]) / 2), 22 * Math.max(0.6, sc), 26 * sc).forEach(function (p) {\n        if (G.inPoly(poly, p.x, p.y)) G.trace([P(p.x - 2.5 * sc, p.y), P(p.x + 2.5 * sc, p.y + 0.5)], { w: sw(S, 3.2), a: 0.85, taperIn: 0, taperOut: 0, rough: 0.2 });\n      });\n      leadOf(poly, 6);\n    }\n    return { gunwale: edge(0) };\n  }\n\n  /**\n   * A mast (refs 16, 17): an amber pole from frame (x, bottom) up to top, a yard across near the top with a\n   * white sail bunched under it (lobed lower edge, gathers traced), an amber knop on the top.\n   */\n  function mast(S, x, top, bottom, hw) {\n    var sc = S.sc, yardY = top + 40, nS = 5;\n    var pole = clip(S, mp(S, [[x - 7, top], [x + 7, top], [x + 7, bottom], [x - 7, bottom]]));\n    piece(pole, C.amber, { angle: 1.57, streak: 0.4 });\n    T.edgeMatt(pole, 0.4, 4 * sc + 1);\n    leadOf(pole, 5);\n    var sl = [[x - hw, yardY], [x + hw, yardY]];\n    for (var q = 0; q <= nS * 8; q++) { var u = q / (nS * 8); sl.push([x + hw - u * 2 * hw, yardY + 26 + 18 * Math.abs(Math.sin(u * nS * Math.PI))]); }\n    var sail = clip(S, mp(S, sl));\n    piece(sail, C.white, { angle: 0, mottle: 0.4, weather: 0.3 });\n    T.edgeMatt(sail, 0.45, 6 * sc + 2);\n    for (var k = 1; k < nS; k++) {\n      var sx = x - hw + k * 2 * hw / nS;\n      T.traceIn(sail, T.through([S.M(sx, yardY + 2), S.M(sx + 2, yardY + 14), S.M(sx + 4, yardY + 28)], 4), { w: sw(S, 2.6), taperIn: 0.1, taperOut: 0.5, rough: 0.4 }, 2);\n    }\n    leadOf(sail, 5);\n    var yard = clip(S, mp(S, [[x - hw - 12, yardY - 7], [x + hw + 12, yardY - 7], [x + hw + 12, yardY + 7], [x - hw - 12, yardY + 7]]));\n    piece(yard, C.amber, { angle: 0, streak: 0.4 });\n    T.edgeMatt(yard, 0.4, 4 * sc + 1);\n    leadOf(yard, 5);\n    var kn = S.M(x, top - 6), knop = clip(S, K.circle(kn.x, kn.y, Math.max(5, 13 * sc), 24));\n    piece(knop, C.amber, { angle: 0 });\n    leadOf(knop, 5);\n  }\n\n  /** True when frame (x, y) lies inside the medallion's disc, so pieces wholly outside it can be skipped. */\n  function inDisc(S, x, y) { var p = S.M(x, y); return Math.hypot(p.x - S.O.x, p.y - S.O.y) < S.r - 2; }\n  /** A straight frame line as n + 1 page points, dense enough for traceIn's clipping. */\n  function seg(S, xa, ya, xb, yb, n) {\n    var pts = [];\n    for (var q = 0; q <= n; q++) pts.push(S.M(xa + (xb - xa) * q / n, ya + (yb - ya) * q / n));\n    return pts;\n  }\n\n  /**\n   * Rooftops along the top of a roundel (ref 18): a green roof hatched in diamonds and a pale tower with blue\n   * windows, behind a crenellated amber wall whose top is at frame y.\n   */\n  function rooftops(S, y) {\n    var sc = S.sc, hatch = { w: sw(S, 1.8), a: 0.6, taperIn: 0, taperOut: 0, rough: 0.3 };\n    // cana v1 -> v2: a bare white tower read as a box, so it has a red roof; the wall below is thinner.\n    var twRoof = clip(S, mp(S, [[510, y - 78], [600, y - 78], [555, y - 122]]));\n    piece(twRoof, C.ruby, { angle: 1.2 });\n    T.edgeMatt(twRoof, 0.3, 4 * sc + 1);\n    leadOf(twRoof, 5);\n    var tw = clip(S, mp(S, [[520, y - 80], [590, y - 80], [590, y + 4], [520, y + 4]]));\n    piece(tw, C.white, { angle: 1.57, mottle: 0.4 });\n    T.edgeMatt(tw, 0.3, 4 * sc + 1);\n    leadOf(tw, 5);\n    [[541, y - 60], [569, y - 60]].forEach(function (w) {\n      var win = clip(S, mp(S, [[w[0] - 7, w[1]], [w[0] + 7, w[1]], [w[0] + 7, w[1] + 26], [w[0] - 7, w[1] + 26]]));\n      piece(win, C.blue, { angle: 1.57, mottle: 0.5 });\n      leadOf(win, 4);\n    });\n    var roof = clip(S, mp(S, [[290, y + 4], [430, y + 4], [360, y - 76]]));\n    piece(roof, C.green, { angle: 0.5 });\n    for (var hx = 210; hx < 470; hx += 16) {\n      T.traceIn(roof, seg(S, hx, y + 4, hx + 80, y - 76, 12), hatch, 2);\n      T.traceIn(roof, seg(S, hx, y - 76, hx + 80, y + 4, 12), hatch, 2);\n    }\n    leadOf(roof, 5);\n    var wall = clip(S, mp(S, [[0, y], [900, y], [900, y + 20], [0, y + 20]]));\n    piece(wall, C.amber, { angle: 0, mottle: 0.5 });\n    T.edgeMatt(wall, 0.3, 4 * sc + 1);\n    leadOf(wall, 5);\n    for (var mx = 12; mx < 880; mx += 46) {\n      if (!inDisc(S, mx + 12, y - 10)) continue;\n      var m = clip(S, mp(S, [[mx, y - 20], [mx + 24, y - 20], [mx + 24, y + 1], [mx, y + 1]]));\n      piece(m, C.amber, { angle: 0, mottle: 0.5 });\n      leadOf(m, 4);\n    }\n  }\n\n  /** Twin round arches (ref 18): ruby bands springing from amber capitals on white columns at x0, the middle and x1. */\n  function arches(S, x0, x1, spring, bottom) {\n    var sc = S.sc, xm = (x0 + x1) / 2, r = (xm - x0) / 2, bw = Math.max(5, 9 * sc);\n    [x0, xm, x1].forEach(function (x) {\n      var col = clip(S, mp(S, [[x - 10, spring + 16], [x + 10, spring + 16], [x + 10, bottom], [x - 10, bottom]]));\n      piece(col, C.white, { angle: 1.57, mottle: 0.4 });\n      T.edgeMatt(col, 0.3, 4 * sc + 1);\n      leadOf(col, 5);\n    });\n    [x0 + r, xm + r].forEach(function (cx) {\n      var arc = [];\n      for (var q = 0; q <= 30; q++) { var a = Math.PI + Math.PI * q / 30; arc.push(S.M(cx + r * Math.cos(a), spring + r * Math.sin(a))); }\n      var band = clip(S, K.ribbon(arc, function () { return bw; }));\n      piece(band, C.ruby, { angle: 0 });\n      T.edgeMatt(band, 0.3, 3 * sc + 1);\n      leadOf(band, 5);\n    });\n    [x0, xm, x1].forEach(function (x) {\n      var cap = clip(S, mp(S, [[x - 20, spring - 4], [x + 20, spring - 4], [x + 13, spring + 18], [x - 13, spring + 18]]));\n      piece(cap, C.amber, { angle: 0 });\n      T.edgeMatt(cap, 0.3, 4 * sc + 1);\n      leadOf(cap, 5);\n    });\n  }\n\n  /**\n   * A table under a white cloth (ref 18, kept light at Erin's ask): the top face a row of leaded white pieces\n   * painted with a diamond hatch, the front skirt a second row hanging in scalloped U folds. Frame x0..x1, the\n   * top face from y to y + d, the skirt down to y + d + hang. Draw it after the figures: it cuts them at the table.\n   */\n  function table(S, x0, x1, y, d, hang, sd) {\n    var sc = S.sc, r = G.rngFrom(sd), yf = y + d, yb = yf + hang;\n    var hatch = { w: sw(S, 1.5), a: 0.45, taperIn: 0, taperOut: 0, rough: 0.3 }, fold = { w: sw(S, 2.4), a: 0.8, taperIn: 0.15, taperOut: 0.15, rough: 0.35 };\n    var nT = 5, ct = [];\n    for (var i = 0; i <= nT; i++) ct.push(x0 + (x1 - x0) * i / nT + (i > 0 && i < nT ? (r() - 0.5) * 30 : 0));\n    for (i = 0; i < nT; i++) {\n      var tp = clip(S, mp(S, [[ct[i], y], [ct[i + 1], y], [ct[i + 1], yf], [ct[i], yf]]));\n      piece(tp, C.white, { angle: 0, mottle: 0.35, weather: 0.2, edge: 0.1 });\n      for (var hx = ct[i] - d; hx < ct[i + 1]; hx += 16) {\n        T.traceIn(tp, seg(S, hx, yf, hx + d, y, 8), hatch, 1);\n        T.traceIn(tp, seg(S, hx, y, hx + d, yf, 8), hatch, 1);\n      }\n      leadOf(tp, 5);\n    }\n    // cana v1 -> v2: equal lobes with two identical Us each read as a printed pattern, so the lobes vary in\n    // width and depth and the inner fold only appears in some of them.\n    var lobes = 12, lw = (x1 - x0) / lobes, lbx = [x0], dep = [];\n    for (i = 1; i < lobes; i++) lbx.push(x0 + i * lw + (r() - 0.5) * lw * 0.35);\n    lbx.push(x1);\n    for (i = 0; i < lobes; i++) dep.push(9 + r() * 8);\n    /** The skirt's hem: every lobe joins the next at the same height and hangs lowest at its middle, by its own depth. */\n    function hem(x) {\n      var j = 0;\n      while (j < lobes - 1 && x > lbx[j + 1]) j++;\n      var t = Math.max(0, Math.min(1, (x - lbx[j]) / (lbx[j + 1] - lbx[j])));\n      return yb - 16 + dep[j] * Math.sin(Math.PI * t);\n    }\n    for (i = 0; i < lobes; i += 2) {\n      var xa = lbx[i], xb = lbx[i + 2], pts = [[xa, yf], [xb, yf]];\n      for (var q = 0; q <= 24; q++) { var hq = xb - (xb - xa) * q / 24; pts.push([hq, hem(hq)]); }\n      var sk = clip(S, mp(S, pts));\n      piece(sk, C.white, { angle: 1.57, mottle: 0.35, weather: 0.2, edge: 0.1 });\n      T.edgeMatt(sk, 0.25, 5 * sc + 1);\n      for (var l = i; l < i + 2; l++) {\n        var la = lbx[l], lb = lbx[l + 1], lwj = lb - la, lc = la + lwj / 2;\n        T.traceIn(sk, T.through([S.M(la + 5, yf + 6), S.M(lc + (r() - 0.5) * 8, hem(lc) - 12 - r() * 8), S.M(lb - 5, yf + 6)], 8), fold, 2);\n        if (r() < 0.55) T.traceIn(sk, T.through([S.M(la + lwj * 0.32, yf + 4), S.M(lc, yf + (hem(lc) - yf) * (0.35 + r() * 0.2)), S.M(lb - lwj * 0.32, yf + 4)], 6), fold, 2);\n      }\n      leadOf(sk, 5);\n    }\n  }\n\n  // cana v3 -> v4 (Erin: \"Needs more work in the detail like bread and cups, they all appear kind of flat in\n  // placement\"): the things on the table were flat cut-outs on one baseline. They are now modelled (shade on the\n  // side away from the light, which comes from the left as at Chartres; a scraped highlight; foreshortened rims)\n  // and each casts a soft painted shadow on the cloth, so a scene can stand them at different depths.\n\n  /** Model a piece's volume: a matt wash rising toward the side away from the light (dir +1 = right) and toward its foot. */\n  function volume(p, a, dir) {\n    var b = T.bboxOf(p);\n    G.matt(function (x, y) {\n      if (!G.inPoly(p, x, y)) return 0;\n      var u = (x - b.x) / b.w, v = (y - b.y) / b.h, side = dir > 0 ? u : 1 - u;\n      return Math.min(1, Math.max(0, side - 0.45) * 1.8 + Math.max(0, v - 0.65) * 0.9);\n    }, b, { a: a, drag: 0.15, angle: 1.57 });\n  }\n  /** A soft painted shadow on the cloth under something standing at frame (cx, base), rx wide. Draw it before the object. */\n  function shadowOn(S, cx, base, rx) {\n    var c = S.M(cx, base), r = rx * S.sc, ry = Math.max(3, rx * 0.22 * S.sc);\n    G.matt(function (x, y) {\n      var dx = (x - c.x) / r, dy = (y - c.y) / ry, d = dx * dx + dy * dy;\n      return d < 1 ? 1 - d : 0;\n    }, { x: c.x - r - 2, y: c.y - ry - 2, w: 2 * r + 4, h: 2 * ry + 4 }, { a: 0.4, drag: 0.1, angle: 0 });\n  }\n  /** A light scraped out of the paint along frame points: the shine on a rounded thing. */\n  function shine(S, list, w) { G.scrape(T.through(mp(S, list), 5), { w: sw(S, w || 2.6), a: 0.6, rough: 0.4, chip: 0.2, taperIn: 0.3, taperOut: 0.4 }); }\n\n  /** A round loaf (ref 18): domed (a flat underside, a high crust), shaded away from the light, scores that curve over the dome. (cx, cy) is its middle. */\n  function loaf(S, cx, cy, rx, ry, color) {\n    var c = S.M(cx, cy), sc = S.sc, rim = [];\n    shadowOn(S, cx + rx * 0.12, cy + ry * 0.55, rx * 1.05);\n    for (var q = 0; q < 48; q++) {\n      var a = q / 48 * 2 * Math.PI, s = Math.sin(a);\n      rim.push({ x: c.x + Math.cos(a) * rx * sc, y: c.y + s * ry * sc * (s > 0 ? 0.55 : 1.15) });\n    }\n    var p = clip(S, rim);\n    piece(p, color, { angle: 0, mottle: 0.6, grain: 0.4 });\n    T.edgeMatt(p, 0.3, 5 * sc + 2);\n    G.matt(T.inside(p), T.bboxOf(p), { a: 0.15, drag: 0.2, angle: 0 });\n    volume(p, 0.4, 1);\n    var score = { w: sw(S, 2.2), a: 0.8, taperIn: 0.2, taperOut: 0.3, rough: 0.4 };\n    T.traceIn(p, T.through(mp(S, [[cx - rx * 0.55, cy - ry * 0.05], [cx - rx * 0.1, cy - ry * 0.75], [cx + rx * 0.35, cy - ry * 0.6]]), 6), score, 1);\n    T.traceIn(p, T.through(mp(S, [[cx - rx * 0.25, cy - ry * 0.8], [cx + rx * 0.1, cy - ry * 0.4], [cx + rx * 0.55, cy - ry * 0.05]]), 6), score, 1);\n    T.traceIn(p, T.through(mp(S, [[cx - rx * 0.85, cy + ry * 0.12], [cx, cy + ry * 0.36], [cx + rx * 0.85, cy + ry * 0.12]]), 6), { w: sw(S, 1.6), a: 0.55, taperIn: 0.3, taperOut: 0.3, rough: 0.4 }, 1);\n    shine(S, [[cx - rx * 0.7, cy - ry * 0.35], [cx - rx * 0.45, cy - ry * 0.85], [cx - rx * 0.1, cy - ry * 1.0]], 2.6);\n    leadOf(p, 4);\n  }\n\n  /** A small flat round of bread lying on the cloth (ref 18): a pale foreshortened disc with a painted cross. */\n  function wafer(S, cx, cy, rx) {\n    var c = S.M(cx, cy), sc = S.sc, ry = rx * 0.38, p = clip(S, T.ellipse(c.x, c.y, rx * sc, ry * sc, 0, 28));\n    shadowOn(S, cx + 2, cy + ry * 0.7, rx * 1.05);\n    piece(p, C.white, { angle: 0, mottle: 0.3 });\n    T.edgeMatt(p, 0.45, 3 * sc + 1);\n    var cross = { w: sw(S, 1.6), a: 0.8, taperIn: 0, taperOut: 0, rough: 0.3 };\n    T.traceIn(p, seg(S, cx - rx * 0.6, cy, cx + rx * 0.6, cy, 6), cross, 1);\n    T.traceIn(p, seg(S, cx - rx * 0.1, cy - ry * 0.75, cx + rx * 0.1, cy + ry * 0.75, 4), cross, 1);\n    leadOf(p, 4);\n  }\n\n  /** A goblet (ref 18): one white piece cut to an open bowl (its far rim showing), a stem with a knot, a round foot; wine inside. */\n  function goblet(S, cx, base, h) {\n    var sc = S.sc, top = base - h, bw = h * 0.26, ry = h * 0.07, pts = [], q;\n    shadowOn(S, cx + h * 0.05, base, h * 0.26);\n    for (q = 0; q <= 12; q++) { var a = Math.PI + Math.PI * q / 12; pts.push([cx + bw * Math.cos(a), top + ry * Math.sin(a)]); }\n    for (q = 0; q <= 5; q++) { var b = Math.PI * q / 12; pts.push([cx + bw * Math.cos(b), top + h * 0.42 * Math.sin(b)]); }\n    pts.push([cx + h * 0.035, top + h * 0.44], [cx + h * 0.035, base - h * 0.3], [cx + h * 0.07, base - h * 0.26], [cx + h * 0.035, base - h * 0.22], [cx + h * 0.035, base - h * 0.08]);\n    for (q = 0; q <= 8; q++) { var f = Math.PI * q / 8; pts.push([cx + h * 0.2 * Math.cos(f), base - h * 0.04 + h * 0.04 * Math.sin(f)]); }\n    pts.push([cx - h * 0.035, base - h * 0.08], [cx - h * 0.035, base - h * 0.22], [cx - h * 0.07, base - h * 0.26], [cx - h * 0.035, base - h * 0.3], [cx - h * 0.035, top + h * 0.44]);\n    for (q = 7; q <= 12; q++) { var b2 = Math.PI * q / 12; pts.push([cx + bw * Math.cos(b2), top + h * 0.42 * Math.sin(b2)]); }\n    var p = clip(S, mp(S, pts));\n    piece(p, C.white, { angle: 1.57, mottle: 0.3, weather: 0.2 });\n    T.edgeMatt(p, 0.3, 3 * sc + 1);\n    G.matt(T.inside(p), T.bboxOf(p), { a: 0.12, drag: 0.2, angle: 1.57 });\n    volume(p, 0.35, 1);\n    var wc = S.M(cx, top - ry * 0.1), wine = clip(S, T.ellipse(wc.x, wc.y, bw * 0.8 * sc, ry * 0.6 * sc, 0, 24));\n    piece(wine, C.ruby, { angle: 0, mottle: 0.4 });\n    leadOf(wine, 3);\n    var line = { w: sw(S, 1.8), a: 0.8, taperIn: 0, taperOut: 0, rough: 0.3 }, near = [];\n    for (q = 0; q <= 12; q++) { var n = Math.PI * q / 12; near.push(S.M(cx - bw * 0.97 * Math.cos(n), top + ry * Math.sin(n))); }\n    T.traceIn(p, near, line, 1);\n    T.traceIn(p, T.through(mp(S, [[cx - h * 0.16, base - h * 0.035], [cx, base - h * 0.06], [cx + h * 0.16, base - h * 0.035]]), 6), line, 1);\n    shine(S, [[cx - bw * 0.6, top + h * 0.1], [cx - bw * 0.66, top + h * 0.22], [cx - bw * 0.4, top + h * 0.36]], 2.4);\n    leadOf(p, 4);\n  }\n\n  /** A jug (ref 18): a coloured body with an open mouth, neck and spout, a strap handle behind it, modelled and shining. */\n  function jug(S, cx, base, h, color) {\n    var sc = S.sc;\n    shadowOn(S, cx + h * 0.05, base, h * 0.3);\n    var handle = clip(S, K.ribbon(T.through(mp(S, [[cx - h * 0.06, base - h * 0.84], [cx - h * 0.34, base - h * 0.7], [cx - h * 0.2, base - h * 0.42]]), 8), function () { return Math.max(2.5, 5 * sc); }));\n    piece(handle, color, { angle: 1.2 });\n    T.edgeMatt(handle, 0.4, 3 * sc + 1);\n    leadOf(handle, 4);\n    var body = clip(S, T.smoothClosed(mp(S, [[cx - h * 0.09, base - h], [cx + h * 0.16, base - h * 1.02], [cx + h * 0.08, base - h * 0.8], [cx + h * 0.26, base - h * 0.5],\n      [cx + h * 0.2, base - h * 0.1], [cx + h * 0.12, base], [cx - h * 0.12, base], [cx - h * 0.2, base - h * 0.1], [cx - h * 0.26, base - h * 0.5], [cx - h * 0.08, base - h * 0.8]]), 5));\n    piece(body, color, { angle: 1.57, mottle: 0.6 });\n    T.edgeMatt(body, 0.3, 5 * sc + 2);\n    volume(body, 0.45, 1);\n    var line = { w: sw(S, 2), a: 0.8, taperIn: 0, taperOut: 0, rough: 0.3 }, mouth = [];\n    for (var q = 0; q <= 16; q++) { var a = 2 * Math.PI * q / 16; mouth.push(S.M(cx + h * 0.02 + h * 0.1 * Math.cos(a), base - h * 0.96 + h * 0.028 * Math.sin(a))); }\n    T.traceIn(body, mouth, line, 1);\n    T.traceIn(body, T.through(mp(S, [[cx - h * 0.11, base - h * 0.78], [cx, base - h * 0.76], [cx + h * 0.11, base - h * 0.78]]), 6), line, 1);\n    T.traceIn(body, T.through(mp(S, [[cx - h * 0.17, base - h * 0.1], [cx, base - h * 0.07], [cx + h * 0.17, base - h * 0.1]]), 6), line, 1);\n    shine(S, [[cx - h * 0.1, base - h * 0.62], [cx - h * 0.15, base - h * 0.42], [cx - h * 0.11, base - h * 0.2]], 3);\n    leadOf(body, 4);\n  }\n\n  /**\n   * A footed bowl heaped with fruit (ref 18): the dark inside of the bowl within its rim, the fruits sitting in it, then\n   * the bowl's front wall over their lower halves, so the fruit is in the bowl rather than stuck on its face.\n   */\n  function fruitBowl(S, cx, base, w) {\n    var sc = S.sc, rim = base - w * 0.55, ry = w * 0.1, q;\n    shadowOn(S, cx + w * 0.04, base, w * 0.3);\n    var foot = clip(S, mp(S, [[cx - w * 0.07, rim + w * 0.22], [cx + w * 0.07, rim + w * 0.22], [cx + w * 0.07, base - w * 0.1], [cx + w * 0.24, base], [cx - w * 0.24, base], [cx - w * 0.07, base - w * 0.1]]));\n    piece(foot, C.ruby, { angle: 1.57 });\n    T.edgeMatt(foot, 0.3, 3 * sc + 1);\n    volume(foot, 0.35, 1);\n    leadOf(foot, 4);\n    var ic = S.M(cx, rim), inside = clip(S, T.ellipse(ic.x, ic.y, w * 0.5 * sc, ry * sc, 0, 40));\n    piece(inside, C.ruby, { angle: 0 });\n    G.matt(T.inside(inside), T.bboxOf(inside), { a: 0.5, drag: 0.1, angle: 0 });\n    leadOf(inside, 4);\n    [[-0.12, -0.2], [0.16, -0.18], [0.0, -0.32], [-0.26, -0.03], [0.28, -0.01], [0.02, -0.06]].forEach(function (o) {\n      var fx = cx + o[0] * w, fy = rim + o[1] * w, fr = w * 0.12, c = S.M(fx, fy), f = clip(S, K.circle(c.x, c.y, fr * sc, 28));\n      piece(f, C.amber, { angle: 0.5, mottle: 0.5 });\n      T.edgeMatt(f, 0.3, 3 * sc + 1);\n      volume(f, 0.4, 1);\n      shine(S, [[fx - fr * 0.55, fy - fr * 0.1], [fx - fr * 0.45, fy - fr * 0.45], [fx - fr * 0.1, fy - fr * 0.6]], 2.2);\n      leadOf(f, 4);\n    });\n    var pts = [];\n    for (q = 0; q <= 16; q++) { var n = Math.PI * q / 16; pts.push([cx - w * 0.5 * Math.cos(n), rim + ry * Math.sin(n)]); }\n    for (q = 0; q <= 12; q++) { var b = Math.PI * q / 12; pts.push([cx + w * 0.5 * Math.cos(b), rim + w * 0.28 * Math.sin(b)]); }\n    var wall = clip(S, mp(S, pts));\n    piece(wall, C.ruby, { angle: 0 });\n    T.edgeMatt(wall, 0.3, 4 * sc + 1);\n    volume(wall, 0.4, 1);\n    T.traceIn(wall, T.through(mp(S, [[cx - w * 0.44, rim + ry + w * 0.06], [cx, rim + ry * 2 + w * 0.06], [cx + w * 0.44, rim + ry + w * 0.06]]), 8), { w: sw(S, 1.8), a: 0.7, taperIn: 0.2, taperOut: 0.2, rough: 0.3 }, 1);\n    shine(S, [[cx - w * 0.38, rim + ry + w * 0.04], [cx - w * 0.3, rim + ry + w * 0.14], [cx - w * 0.16, rim + ry + w * 0.2]], 2.4);\n    leadOf(wall, 4);\n  }\n\n  /**\n   * A donkey walking right (refs 21, 22), cut as the Laon and Troyes glaziers cut theirs: pale pitted glass; the body one\n   * piece from rump to throat, modelled with paint under the belly and along the haunch, shoulder and throat; legs as\n   * jointed strips (thick at the top, fine in the cannon, a painted hoof), the near foreleg lifted mid-stride; a tail\n   * ending in a tuft; ears as leaves with an inner stroke; the head its own piece, bowed, with an almond eye under a lid,\n   * a nostril curl, mane locks along the crest and a double-lined bridle with a ring. Frame: x0 rump, x1 chest, back and\n   * belly heights, ground under the hooves.\n   * entry v3 -> v4 (entry-audit-v3.md D1-D5): v3's body was a flat slab, its legs straight planks, its head a blocky\n   * profile with a line for an eye, its mane short dashes that read as ribs, its tail a stick.\n   */\n  function donkey(S, x0, x1, back, belly, ground) {\n    var sc = S.sc, L = x1 - x0, u = L / 225, mid = (belly + ground) / 2, sq = Math.max(1, Math.sqrt(u));\n    var line = { w: sw(S, 2) * sq, a: 0.8, taperIn: 0.15, taperOut: 0.3, rough: 0.35 };\n    var fine = { w: sw(S, 1.5) * sq, a: 0.75, taperIn: 0.2, taperOut: 0.5, rough: 0.4 };\n    function strip(list, wFn, n) { return clip(S, K.ribbon(T.through(mp(S, list), n || 8), function (s) { return Math.max(1.5, wFn(s) * sc); })); }\n    function curve(list, n) { return T.through(mp(S, list), n || 6); }\n    /** A leg through [top, joint, fetlock, hoof]: thick at the top, fine in the cannon, a painted hoof; the far pair shaded. */\n    function leg(j, far) {\n      var p = strip(j, function (s) { return (s < 0.35 ? 12 - 14 * s : s < 0.8 ? 7.1 - 1.8 * (s - 0.35) : 6.3 + 6 * (s - 0.8)) * u; });\n      piece(p, C.white, { angle: 1.57, mottle: 0.35, weather: 0.3, pits: 0.3 });\n      T.edgeMatt(p, 0.35, 3 * sc * u + 1);\n      var lb = T.bboxOf(p), hy = S.M(0, j[3][1] - 10 * u).y;\n      G.matt(function (x, y) { return G.inPoly(p, x, y) && y > hy ? 1 : 0; }, lb, { a: 0.7, drag: 0.1, angle: 0 });\n      if (far) G.matt(T.inside(p), lb, { a: 0.3, drag: 0.1, angle: 1.57 });\n      T.traceIn(p, curve([[j[1][0] - 5 * u, j[1][1] - 3 * u], [j[1][0], j[1][1] + 3 * u], [j[1][0] + 5 * u, j[1][1] - 3 * u]], 4), fine, 1);\n      leadOf(p, 4);\n    }\n    // entry v6 -> v7 (entry-audit-v5.md J12): fixed offsets from mid and ground put the hock and fetlock at almost the same\n    // height once the belly deepened, and the legs kinked; every leg's joints are now fractions of its drop.\n    var fall = ground - belly;\n    leg([[x0 + L * 0.22, belly - 10 * u], [x0 + L * 0.14, belly + fall * 0.45], [x0 + L * 0.18, belly + fall * 0.8], [x0 + L * 0.14, ground]], true);\n    leg([[x1 - L * 0.16, belly - 10 * u], [x1 - L * 0.12, belly + fall * 0.45], [x1 - L * 0.14, belly + fall * 0.8], [x1 - L * 0.1, ground]], true);\n    var tail = strip([[x0 + 6 * u, back + 14 * u], [x0 - 12 * u, back + 40 * u], [x0 - 16 * u, belly - 6 * u]], function (s) { return (4 - 2 * s) * u; });\n    piece(tail, C.white, { angle: 1.57, mottle: 0.3 });\n    T.edgeMatt(tail, 0.35, 2 * sc + 1);\n    leadOf(tail, 4);\n    // v4 -> v5 (audit D5): an outlined oval with straight hairs read as a whisk; the tuft is a long lock swinging back and\n    // tapering to a point, its hairs curving with it.\n    var tc = [x0 - 16 * u, belly - 8 * u];\n    var tuft = strip([[tc[0], tc[1]], [tc[0] - 6 * u, tc[1] + 20 * u], [tc[0] + 2 * u, tc[1] + 42 * u]], function (s) { return (2 + 8 * Math.sin(Math.PI * Math.min(1, s * 1.25))) * u * (1 - 0.6 * s); }, 10);\n    piece(tuft, C.white, { angle: 1.57, mottle: 0.3 });\n    T.edgeMatt(tuft, 0.4, 3 * sc + 1);\n    for (var h = -1; h <= 1; h++) T.traceIn(tuft, curve([[tc[0] + h * 3 * u, tc[1] + 4 * u], [tc[0] - 6 * u + h * 4 * u, tc[1] + 22 * u], [tc[0] + 1 * u + h * 1.5 * u, tc[1] + 40 * u]], 6), fine, 1);\n    leadOf(tuft, 4);\n    var body = clip(S, T.smoothClosed(mp(S, [[x0 + L * 0.04, back + 8 * u], [x0 + L * 0.28, back - 4 * u], [x0 + L * 0.6, back + 4 * u], [x1 - L * 0.08, back - 6 * u],\n      [x1 + L * 0.08, back + 6 * u], [x1 + L * 0.18, back + 24 * u], [x1 + L * 0.2, back + 44 * u], [x1 + L * 0.12, back + 66 * u], [x1 + L * 0.02, belly - 22 * u],\n      [x1 - L * 0.1, belly - 4 * u], [x1 - L * 0.35, belly + 6 * u], [x0 + L * 0.3, belly], [x0 + L * 0.06, belly - 18 * u], [x0 - L * 0.04, back + (belly - back) * 0.45]]), 5));\n    piece(body, C.white, { angle: 0, mottle: 0.45, weather: 0.4, pits: 0.45 });\n    T.edgeMatt(body, 0.35, 8 * sc * u + 2);\n    var bb = T.bboxOf(body);\n    G.matt(function (x, y) { return G.inPoly(body, x, y) ? Math.min(1, Math.max(0, (y - bb.y) / bb.h - 0.5) * 2.4) : 0; }, bb, { a: 0.45, drag: 0.15, angle: 0 });\n    var haunch = curve([[x0 + L * 0.05, belly - 24 * u], [x0 + L * 0.15, back + 20 * u], [x0 + L * 0.33, back + 12 * u]], 8);\n    var shoulder = curve([[x1 - L * 0.05, belly - 20 * u], [x1 - L * 0.15, back + 38 * u], [x1 - L * 0.06, back + 10 * u]], 8);\n    var throat = curve([[x1 + L * 0.02, belly - 26 * u], [x1 + L * 0.1, back + 64 * u], [x1 + L * 0.17, back + 46 * u]], 8);\n    T.foldShadow(body, [haunch, shoulder, throat], 0.35, 10 * u * sc);\n    [haunch, shoulder, throat].forEach(function (c) { T.traceIn(body, c, line, 4); });\n    G.scrape(curve([[x0 + L * 0.1, back + 10 * u], [x0 + L * 0.42, back + 6 * u], [x1 - L * 0.22, back + 8 * u]], 8), { w: sw(S, 3) * sq, a: 0.5, rough: 0.5, chip: 0.3, taperIn: 0.3, taperOut: 0.4 });\n    for (var m = 0; m < 9; m++) {\n      var t = m / 8, mx = x1 - L * 0.08 + L * 0.26 * t, my = back - 6 * u + 30 * u * t;\n      T.traceIn(body, curve([[mx - 2 * u, my + 4 * u], [mx + 3 * u, my - 4 * u], [mx + 10 * u, my - 3 * u], [mx + 12 * u, my + 5 * u]], 4), fine, 1);\n    }\n    leadOf(body, 5);\n    leg([[x0 + L * 0.34, belly - 12 * u], [x0 + L * 0.26, belly + fall * 0.45], [x0 + L * 0.32, belly + fall * 0.8], [x0 + L * 0.3, ground]], false);\n    // entry v5 -> v6 (entry-audit-v5.md J11): with a deeper belly the lifted foreleg's fetlock rose level with its knee and\n    // the leg folded flat; its joints are now fractions of the leg's own drop from the belly.\n    var drop = ground - belly;\n    leg([[x1 - L * 0.02, belly - 12 * u], [x1 + L * 0.1, belly + drop * 0.4], [x1 + L * 0.05, belly + drop * 0.72], [x1 + L * 0.11, belly + drop * 0.86]], false);\n    [[[x1 + L * 0.19, back + 30 * u], [x1 + L * 0.14, back + 6 * u], [x1 + L * 0.1, back - 24 * u]], [[x1 + L * 0.25, back + 30 * u], [x1 + L * 0.26, back + 4 * u], [x1 + L * 0.25, back - 26 * u]]].forEach(function (ea) {\n      var ear = strip(ea, function (s) { return 1 + 8 * u * Math.sin(Math.PI * Math.min(1, 0.15 + s * 0.95)); });\n      piece(ear, C.white, { angle: 1.57, mottle: 0.3 });\n      T.edgeMatt(ear, 0.4, 3 * sc + 1);\n      T.traceIn(ear, curve([[ea[0][0], ea[0][1] - 6 * u], ea[1], [ea[2][0], ea[2][1] + 10 * u]], 6), fine, 1);\n      leadOf(ear, 4);\n    });\n    var head = clip(S, T.smoothClosed(mp(S, [[x1 + L * 0.18, back + 22 * u], [x1 + L * 0.26, back + 30 * u], [x1 + L * 0.31, back + 50 * u], [x1 + L * 0.36, back + 78 * u],\n      [x1 + L * 0.4, back + 98 * u], [x1 + L * 0.39, back + 110 * u], [x1 + L * 0.34, back + 118 * u], [x1 + L * 0.28, back + 114 * u], [x1 + L * 0.24, back + 96 * u],\n      [x1 + L * 0.2, back + 70 * u], [x1 + L * 0.14, back + 50 * u]]), 5));\n    piece(head, C.white, { angle: 1.2, mottle: 0.3, weather: 0.25, pits: 0.2 });\n    T.edgeMatt(head, 0.3, 5 * sc * u + 1);\n    G.matt(T.inside(head), T.bboxOf(head), { a: 0.12, drag: 0.2, angle: 1.2 });\n    T.foldShadow(head, [curve([[x1 + L * 0.2, back + 60 * u], [x1 + L * 0.25, back + 92 * u], [x1 + L * 0.3, back + 112 * u]], 6)], 0.3, 8 * u * sc);\n    var ey = [x1 + L * 0.27, back + 50 * u];\n    T.traceIn(head, curve([[ey[0] - 7 * u, ey[1]], [ey[0], ey[1] - 5 * u], [ey[0] + 7 * u, ey[1] + 1 * u]], 5), line, 1);\n    T.traceIn(head, curve([[ey[0] - 7 * u, ey[1]], [ey[0], ey[1] + 3 * u], [ey[0] + 7 * u, ey[1] + 1 * u]], 5), fine, 1);\n    T.traceIn(head, curve([[ey[0] - 6 * u, ey[1] - 7 * u], [ey[0], ey[1] - 9 * u], [ey[0] + 6 * u, ey[1] - 6 * u]], 5), fine, 1);\n    var ep = S.M(ey[0] + 1 * u, ey[1] - 1 * u);\n    G.trace([ep, P(ep.x + 0.5, ep.y + 0.5)], { w: 3.4 * sc * u + 1, a: 0.9, taperIn: 0, taperOut: 0, rough: 0.1 });\n    var ns = [x1 + L * 0.37, back + 104 * u];\n    T.traceIn(head, curve([[ns[0] - 3 * u, ns[1] - 5 * u], [ns[0] + 2 * u, ns[1] - 4 * u], [ns[0] + 3 * u, ns[1] + 2 * u], [ns[0] - 2 * u, ns[1] + 3 * u]], 4), line, 1);\n    // entry v10 -> v11: the mouth curved up like a smile, which read as a cartoon; a short, nearly flat line.\n    T.traceIn(head, curve([[x1 + L * 0.325, back + 112 * u], [x1 + L * 0.35, back + 112.5 * u], [x1 + L * 0.375, back + 112 * u]], 4), fine, 1);\n    // v4 -> v5 (audit D3): two nosebands, the nostril and the mouth crowded the muzzle into a scribble; one noseband, higher.\n    T.traceIn(head, curve([[x1 + L * 0.23, back + 80 * u], [x1 + L * 0.31, back + 78 * u], [x1 + L * 0.39, back + 74 * u]], 6), line, 1);\n    [0, 6].forEach(function (d) {\n      T.traceIn(head, curve([[x1 + L * 0.19 + d * u, back + 34 * u], [x1 + L * 0.23 + d * u, back + 58 * u], [x1 + L * 0.27 + d * u, back + 80 * u]], 6), line, 1);\n    });\n    var rc = S.M(x1 + L * 0.28, back + 80 * u), ring = T.ellipse(rc.x, rc.y, 4 * u * sc + 1, 4 * u * sc + 1, 0, 12);\n    T.traceIn(head, ring.concat([ring[0]]), line, 1);\n    for (var f = 0; f < 3; f++) T.traceIn(head, curve([[x1 + L * (0.2 + f * 0.02), back + 24 * u], [x1 + L * (0.22 + f * 0.02), back + 34 * u], [x1 + L * (0.21 + f * 0.02), back + 42 * u]], 4), fine, 1);\n    leadOf(head, 5);\n  }\n\n  /**\n   * A palm frond (ref 21): one green leaf piece swept from base to tip with a bow, its edge cut into leaflet points,\n   * the rib and leaflets painted. entry v3 -> v4 (audit G3): a smooth outline read as a feather.\n   */\n  function palm(S, base, tip, bow) {\n    var sc = S.sc, a = S.M(base[0], base[1]), b = S.M(tip[0], tip[1]), dx = b.x - a.x, dy = b.y - a.y, dl = Math.hypot(dx, dy) || 1;\n    var axis = T.through([a, P((a.x + b.x) / 2 - dy / dl * bow * sc, (a.y + b.y) / 2 + dx / dl * bow * sc), b], 30);\n    var p = clip(S, K.ribbon(axis, function (s) {\n      var w = 1.5 + 13 * sc * Math.pow(Math.sin(Math.PI * s), 0.7) * Math.min(1, s * 5);\n      return s < 0.15 ? w : w * (0.55 + 0.45 * Math.abs(Math.sin(s * Math.PI * 14)));\n    }));\n    piece(p, C.green, { angle: Math.atan2(dy, dx), streak: 0.4, mottle: 0.5 });\n    T.edgeMatt(p, 0.3, 3 * sc + 1);\n    T.traceIn(p, axis, { w: sw(S, 1.8), a: 0.8, taperIn: 0.1, taperOut: 0.4, rough: 0.3 }, 1);\n    var leaf = { w: sw(S, 1.5), a: 0.7, taperIn: 0.1, taperOut: 0.5, rough: 0.35 };\n    for (var t = 0.2; t < 0.95; t += 0.07) {\n      var q = K.sample(axis, t), tx = q.ny, ty = -q.nx, w = 12 * sc * Math.pow(Math.sin(Math.PI * t), 0.7);\n      [-1, 1].forEach(function (sg) {\n        var ex = q.x + q.nx * w * sg + tx * w * 0.6, ey = q.y + q.ny * w * sg + ty * w * 0.6, pts = [];\n        for (var k = 0; k <= 4; k++) pts.push(P(q.x + (ex - q.x) * k / 4, q.y + (ey - q.y) * k / 4));\n        T.traceIn(p, pts, leaf, 1);\n      });\n    }\n    leadOf(p, 4);\n  }\n\n  /**\n   * A cloak spread on the road (ref 21), cut to read as cloth: a soft outline that wanders between its corners, folds that\n   * curve out from under the hooves (each with its shadow and a light ridge scraped beside it), a small corner turned back\n   * to show the lining. o: lining colour, flap (frame points), folds ([from, to, bow, hook] in frame units), seed.\n   * entry v3 -> v4 (audit C1): a rounded blue blob with wavy lines, against a blue sky, read as a pond. v4 -> v5: straight\n   * edges with straight diagonal folds and a pearled hem read as a red board with wood grain and a ruler.\n   */\n  function cloak(S, list, color, o) {\n    o = o || {};\n    var sc = S.sc, r = G.rngFrom(o.seed || 5), raw = mp(S, list), dense = [];\n    for (var i = 0; i < raw.length; i++) {\n      var a = raw[i], b = raw[(i + 1) % raw.length], n = Math.max(2, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / 10));\n      var nx = -(b.y - a.y), ny = b.x - a.x, nl = Math.hypot(nx, ny) || 1;\n      for (var q = 0; q < n; q++) {\n        var t = q / n, wob = Math.sin(t * Math.PI) * (r() - 0.5) * 7 * sc;\n        dense.push(P(a.x + (b.x - a.x) * t + nx / nl * wob, a.y + (b.y - a.y) * t + ny / nl * wob));\n      }\n    }\n    var p = clip(S, T.smoothClosed(dense, 2));\n    piece(p, color, { angle: 0.3, mottle: 0.6 });\n    T.edgeMatt(p, 0.4, 6 * sc + 2);\n    (o.folds || []).forEach(function (f) {\n      var fl = T.hooked(S.M(f[0][0], f[0][1]), S.M(f[1][0], f[1][1]), f[2] * sc, f[3] * sc);\n      T.foldShadow(p, [fl], 0.35, 7 * sc);\n      T.traceIn(p, fl, { w: sw(S, 2.6), a: 0.85, taperIn: 0.1, taperOut: 0.5, rough: 0.4 }, 3);\n      var ridge = fl.map(function (pt, k) {\n        var a2 = fl[Math.max(0, k - 1)], b2 = fl[Math.min(fl.length - 1, k + 1)], dx = b2.x - a2.x, dy = b2.y - a2.y, dl = Math.hypot(dx, dy) || 1;\n        return P(pt.x - dy / dl * 6 * sc, pt.y + dx / dl * 6 * sc);\n      });\n      T.clipRuns(p, ridge.slice(1, Math.max(2, ridge.length - 2)), 4).forEach(function (run) {\n        G.scrape(run, { w: sw(S, 2.2), a: 0.5, rough: 0.5, chip: 0.3, taperIn: 0.3, taperOut: 0.5 });\n      });\n    });\n    leadOf(p, 5);\n    if (o.flap) {\n      var flap = clip(S, T.smoothClosed(mp(S, o.flap), 5));\n      piece(flap, o.lining || C.amber, { angle: 0.8, mottle: 0.5 });\n      T.edgeMatt(flap, 0.35, 4 * sc + 1);\n      leadOf(flap, 4);\n    }\n  }\n\n  /**\n   * A road across the ground (ref 21's brown strip): a wavy-edged piece with painted pebbles, for a procession to walk on.\n   * entry v3 -> v4 (audit C2): the figures stood on flat grass with four identical hook marks.\n   */\n  function road(S, top, bottom, color, sd) {\n    var r = G.rngFrom(sd), sc = S.sc;\n    var p = clip(S, wavy(S, top, 4, 150, r() * 6, -20, 920).concat(wavy(S, bottom, 5, 170, r() * 6, -20, 920).reverse()));\n    piece(p, color, { angle: 0, mottle: 0.6, grain: 0.4 });\n    T.edgeMatt(p, 0.35, 5 * sc + 2);\n    G.matt(T.inside(p), T.bboxOf(p), { a: 0.15, drag: 0.3, angle: 0 });\n    var peb = { w: sw(S, 1.6), a: 0.7, taperIn: 0, taperOut: 0, rough: 0.4 };\n    for (var i = 0; i < 30; i++) {\n      var px = 40 + r() * 820, py = top + 10 + r() * (bottom - top - 20), rx = 3 + r() * 6, rot = r() * 3;\n      if (!inDisc(S, px, py)) continue;\n      var c = S.M(px, py), e = T.ellipse(c.x, c.y, rx * sc, rx * 0.6 * sc, rot, 12);\n      T.traceIn(p, e.concat([e[0]]), peb, 1);\n    }\n    leadOf(p, 5);\n  }\n\n  /**\n   * A mass of foliage (ref 22's scalloped leaf mass, ref 21's painted leaves): one irregular many-lobed piece with leaves\n   * painted inside it (almond outlines, a light scraped down each), rim-shaded. Frame centre and radii.\n   * entry v3 -> v4 (audit T1): four identical scalloped balls in a row read as broccoli.\n   */\n  function foliage(S, cx, cy, rx, ry, color, sd) {\n    var r = G.rngFrom(sd), sc = S.sc, c = S.M(cx, cy), rim = [], nL = 11, ph = r() * 6.28, ph2 = r() * 6.28;\n    for (var q = 0; q < 140; q++) {\n      // v4 -> v5 (audit T1): lobes deepened; a smooth bean read as a cushion, not a leaf mass.\n      var a = q / 140 * 2 * Math.PI, rr = 0.74 + 0.2 * Math.pow(Math.abs(Math.sin(a * nL / 2 + ph)), 0.5) + 0.06 * Math.sin(a * 3 + ph2);\n      rim.push(P(c.x + Math.cos(a) * rx * sc * rr, c.y + Math.sin(a) * ry * sc * rr));\n    }\n    var p = clip(S, rim);\n    piece(p, color, { angle: r() * 3, mottle: 0.7, grain: 0.4 });\n    T.edgeMatt(p, 0.5, 12 * sc + 2);\n    // v4 -> v5 (audit T1): hollow almond outlines scattered like confetti; the leaves are now painted as filled matt shapes,\n    // smaller and denser, each with its midrib scraped back to light.\n    for (var n = 0; n < 26; n++) {\n      var ang = r() * 6.28, dist = Math.sqrt(r()) * 0.72, lx = cx + Math.cos(ang) * rx * dist, ly = cy + Math.sin(ang) * ry * dist;\n      var dir = r() * 6.28, ll = (10 + r() * 6) * sc, wx = Math.cos(dir), wy = Math.sin(dir), lc = S.M(lx, ly);\n      (function (lc, wx, wy, ll, dir) {\n        G.matt(function (x, y) {\n          if (!G.inPoly(p, x, y)) return 0;\n          var dx = x - lc.x, dy = y - lc.y, al = (dx * wx + dy * wy) / (ll / 2), ac = (-dx * wy + dy * wx) / (ll * 0.28), d = al * al + ac * ac;\n          return d < 1 ? 1 - d * d : 0;\n        }, { x: lc.x - ll, y: lc.y - ll, w: 2 * ll, h: 2 * ll }, { a: 0.5, drag: 0.1, angle: dir });\n      })(lc, wx, wy, ll, dir);\n      G.scrape([P(lc.x - wx * ll * 0.35, lc.y - wy * ll * 0.35), P(lc.x + wx * ll * 0.35, lc.y + wy * ll * 0.35)], { w: sw(S, 1.6), a: 0.45, rough: 0.4, chip: 0.2, taperIn: 0.3, taperOut: 0.3 });\n    }\n    leadOf(p, 6);\n  }\n\n  /**\n   * A cut branch (ref 21): a thin bark stem through frame points with small almond leaves along it, each its own piece.\n   * entry v3 -> v4 (audit T2): the boy held a copy of the palm frond.\n   */\n  function twig(S, list, color) {\n    var sc = S.sc, axis = T.through(mp(S, list), 10);\n    var st = clip(S, K.ribbon(axis, function (s) { return Math.max(1.2, (3 - 1.5 * s) * sc); }));\n    piece(st, C.bark, { angle: 1, streak: 0.4 });\n    leadOf(st, 3);\n    for (var i = 0; i < 6; i++) {\n      var t = 0.3 + i * 0.12, q = K.sample(axis, t), sg = i % 2 ? 1 : -1, tx = q.ny, ty = -q.nx;\n      var len = (14 - i) * sc, dx = q.nx * sg * 0.8 + tx * 0.6, dy = q.ny * sg * 0.8 + ty * 0.6, dl = Math.hypot(dx, dy) || 1;\n      dx /= dl; dy /= dl;\n      var tip = P(q.x + dx * len, q.y + dy * len), m = P((q.x + tip.x) / 2, (q.y + tip.y) / 2), w = len * 0.3;\n      var leaf = clip(S, T.smoothClosed([P(q.x, q.y), P(m.x - dy * w, m.y + dx * w), tip, P(m.x + dy * w, m.y - dx * w)], 4));\n      piece(leaf, color, { angle: Math.atan2(dy, dx), mottle: 0.5 });\n      T.edgeMatt(leaf, 0.3, 2);\n      G.trace([P(q.x, q.y), tip], { w: sw(S, 1.2), a: 0.7, taperIn: 0.2, taperOut: 0.5, rough: 0.3 });\n      leadOf(leaf, 3);\n    }\n  }\n\n  return { clip: clip, sky: sky, hill: hill, tree: tree, leaves: leaves, tower: tower, gate: gate, arcade: arcade, floor: floor,\n    water: water, boat: boat, mast: mast,\n    rooftops: rooftops, arches: arches, table: table, loaf: loaf, wafer: wafer, goblet: goblet, jug: jug, fruitBowl: fruitBowl,\n    donkey: donkey, palm: palm, cloak: cloak, road: road, foliage: foliage, twig: twig };\n}\n\n// Stained glass, scene test: a medallion holding a scene rather than a single\n// figure (Erin 2026-09-13: \"experiment with the series. Ensure compositions and\n// scenes are key\"). Figures come from figures.js, in the v5 grammar Erin\n// accepted. Composition after the scene compartments of a medallion window\n// (Met 24.167, ref 13) and the paired heads of the Cleveland canopy (ref 09).\n//\n// The composition, decided before any piece is cut:\n//   - an elder saint left of centre, larger (hierarchic scale), turned right and\n//     blessing; a young man right of centre, smaller, turned left, leaning in\n//     with his hands joined. The saint's raised hand and the young man's joined\n//     hands stack on one vertical just right of the centre: that gap is the\n//     subject, and the young man's head sits on the diagonal below the hand.\n//   - a brick tower with a red roof behind the young man closes the right side;\n//     a tree behind the saint answers it on the left, lower and softer;\n//   - three green hills make the ground line and run under the ring;\n//   - the blue sky is cut into a few large pieces, so the figures and setting\n//     carry the small ones.\n//\n// v1 -> v2 (own read at 1:1): clamping only corners to the disc skewed the\n// tower and left the hills as planks with sky under them, so pieces are\n// densified before clamping; a straight sky lead ran through the raised hand\n// like a staff, so the sky is cut in wedges radiating from behind the saint;\n// the figures filled too little of the roundel, so both are larger; the tree\n// read as a banana plant, so it is three rounded veined clusters on branches.\nfunction sceneSketch(ctx, state) {\n  var W = state.canvas.width, H = state.canvas.height;\n  var seed = state.seed || 0;\n  var G = glassSheet(ctx, W, H, seed), K = glassGeom();\n  var rng = G.rngFrom(seed * 2654435761 + 7919);\n  var T = glassKit(G, K, rng);\n\n  var C = {\n    blue: \"#2a5bd0\", ruby: \"#c8141e\", teal: \"#1f8a6a\", amber: \"#e2a520\", white: \"#eceee2\", flesh: \"#ecccbc\",\n    maroon: \"#b0303f\", green: \"#4f8f2e\", olive: \"#9aa82e\", purple: \"#7a4a8c\", stone: \"#e6e0cc\", bark: \"#a8742e\", night: \"#1d2f6a\",\n  };\n  var LW = 7, WOB = 0.3, ROUGH = 0.5;\n  function leadOf(p, w, closed) { G.lead(p, { w: w || LW, wobble: WOB, rough: ROUGH, closed: closed !== false }); }\n  function joint(v, w) { G.solder(v.x, v.y, { r: (w || LW) * 0.65, lump: 0.1 }); }\n  function glassPiece(p, color, o) {\n    o = o || {};\n    G.piece(p, {\n      color: color, mottle: o.mottle == null ? 0.7 : o.mottle,\n      streak: color === C.ruby ? 0.6 : (o.streak || 0), angle: o.angle == null ? rng() * 3 : o.angle,\n      pits: o.pits == null ? 0.2 : o.pits, weather: o.weather == null ? 0.3 : o.weather, seeds: o.seeds || 0,\n      edge: o.edge == null ? 0.25 : o.edge, edgeW: o.edgeW || 7, grain: o.grain == null ? 0.25 : o.grain,\n      cloud: o.cloud == null ? 0.5 : o.cloud, relief: o.relief == null ? 1.2 : o.relief,\n    });\n  }\n  function P(x, y) { return { x: x, y: y }; }\n  var F = glassFigures(G, K, T, { piece: glassPiece, lead: function (p, w) { leadOf(p, w || 6); } });\n\n  var O = { x: W / 2, y: H / 2 };\n  var R3 = 430, R2 = 412, R1 = 382;\n  // --- The setting, from settings.js (shared with the window); this medallion's frame is the page ---\n  // v3 -> v4: the sky, tree, tower and hills moved to settings.js, and the sky's leads now keep clear of\n  // both heads (v3's rose straight through the saint's halo).\n  var SET = glassSettings(G, K, T, { piece: glassPiece, lead: function (p, w) { leadOf(p, w || 6); }, rng: rng, C: C });\n  var S = { O: O, r: R1, sc: 1, M: function (x, y) { return P(x, y); } };\n  SET.sky(S, seed + 303, [[379, 210], [545, 305]]);\n  SET.tree(S, [214, 560], [222, 812], [[146, 462, 50, C.green], [228, 382, 58, C.olive], [304, 470, 46, C.teal]], seed + 600);\n  SET.tower(S, 646, 772, 338, 830, { door: true, slot: true });\n  SET.hill(S, [600, 772], [760, 712], [880, 750], C.teal, seed + 71);\n  SET.hill(S, [30, 780], [220, 712], [480, 786], C.green, seed + 72);\n  SET.hill(S, [320, 800], [560, 750], [800, 796], C.olive, seed + 73);\n\n  // --- The figures: the saint first, the young man leaning in over the gap ---\n  F.figure({ x: 372, y: 758, h: 590, facing: 1, lean: 0.02, tilt: 0.1, gesture: \"bless\", beard: true, hair: \"full\", halo: true,\n    colors: { tunic: C.maroon, mantle: C.teal, shoe: C.purple, halo: C.amber, flesh: C.flesh, band: C.amber }, seed: seed + 11 });\n  F.figure({ x: 606, y: 766, h: 500, facing: -1, lean: 0.12, tilt: 0.28, gesture: \"pray\", beard: false, hair: \"short\", halo: false,\n    colors: { tunic: C.amber, mantle: C.ruby, shoe: C.bark, flesh: C.flesh, band: C.white }, seed: seed + 23 });\n\n  // --- Rings, as the figure medallion's: segment ring, then the pearl band ---\n  var nR = 26, pair = 2 * Math.PI / nR, rubyFrac = 62 / 92, offR = rng() * pair, ring = [];\n  for (var i = 0; i < nR; i++) {\n    var b0 = offR + i * pair, b1 = b0 + pair * rubyFrac, b2 = b0 + pair;\n    var red = K.ringSegment(O, R1, R2, b0, b1, 8), wht = K.ringSegment(O, R1, R2, b1, b2, 4);\n    glassPiece(red, C.ruby, { angle: b0 + Math.PI / 2 });\n    glassPiece(wht, C.white, { mottle: 0.4, weather: 0.3, edge: 0.15 });\n    G.matt(function (x, y) {\n      if (!G.inPoly(wht, x, y)) return 0;\n      var r = Math.hypot(x - O.x, y - O.y), e0 = (r - R1) / 5, e1 = (R2 - r) / 5;\n      return Math.exp(-e0 * e0) + Math.exp(-e1 * e1);\n    }, T.bboxOf(wht), { a: 0.6, drag: 0.1, angle: b1 });\n    ring.push(red, wht);\n  }\n  var nP = Math.round(2 * Math.PI * ((R2 + R3) / 2) / 64), stepP = 2 * Math.PI / nP, offP = rng() * stepP, bandP = [];\n  for (i = 0; i < nP; i++) {\n    var ab = offP + i * stepP, seg = K.ringSegment(O, R2, R3, ab, ab + stepP, 10);\n    glassPiece(seg, C.white, { mottle: 0.4, weather: 0.3, edge: 0.15, angle: ab + Math.PI / 2 });\n    G.matt(T.inside(seg), T.bboxOf(seg), { a: 0.78, drag: 0.1, angle: ab + Math.PI / 2 });\n    T.pearls(K.arc(O, (R2 + R3) / 2, ab, ab + stepP, 12), 16, 10);\n    bandP.push(seg);\n  }\n  ring.concat(bandP).forEach(function (p) { leadOf(p); });\n  ring.forEach(function (p) { joint(p[0]); joint(p[Math.floor(p.length / 2) - 1]); });\n  leadOf(K.circle(O.x, O.y, R1, 260), 7);\n  leadOf(K.circle(O.x, O.y, R2, 260), 7);\n  leadOf(K.circle(O.x, O.y, R3, 260), 10);\n\n  G.finish({ halo: 0.15, haloR: 6 });\n}\n\n// Gallery frame (build-works.cjs): the scene was authored at 900px and ships on a 1200px canvas.\nfunction sketch(ctx, state) {\n  var canvas = Object.assign({}, state.canvas, { width: 900, height: 900 });\n  return sceneSketch(ctx, Object.assign({}, state, { canvas: canvas }));\n}\n",
 "layers": []
}
