genart.dev
Download GenArt
Entry into Jerusalem

Entry into Jerusalem

Stained Glass · 2026 · 1200×1200 · Plate · Canvas 2D

A procession moves right along a road toward a city tower. A saint rides sideways on a white donkey, blessing, his tunic falling over its flank; the donkey bows its head over a red cloak spread on the road under its hooves. Behind him the followers are one full figure and heads stacked above and behind, and at the gate the greeters are one figure raising a palm on the step before the tower and heads behind him, the crowd suggested with few pieces as the glaziers of Laon and Troyes suggested it. A boy sits in the leaves of a tree above the donkey, holding up a branch he has cut. A seed reglazes the same design; the composition stays.

Technique

one canvas2d algorithm, no layers, because the material is a transmission model the layer stack cannot express (ADR 110 amendment 1): pot-metal glass transmitting colour^d, clouded and banded density, edge corrosion, grisaille trace, matt and scraped highlights, lead with wandering width and solder, light spilling over the lead. The donkey is pale pitted glass modelled with paint under the belly and along the haunch and shoulder, its legs jointed strips with painted hooves, its bowed head carrying an almond eye, a nostril and a double-lined bridle. The rider is clipped under the donkey's back so his draped hem and bare feet hang against its side. Leaves are painted masses with light scraped down each midrib; the cloak is a soft-edged piece with hooked folds; the sky's leads avoid every head and are cut across in staggered pieces so no lead runs the height of the roundel.

Seeds

The same system at three seeds. What moves is the surface and the marks describing it; the composition, the palette and the depth rule stay put.

  1. Entry into Jerusalem, seed 77
  2. Entry into Jerusalem, seed 211211
  3. Entry into Jerusalem, seed 33073307

Layer stack

In paint order. Every mark in the image comes from these; there is no handwritten drawing code.

    Source

    The complete composition. Open it in GenArt to re-render, re-seed, or take it apart.

    entry-into-jerusalem.genart
    {
      "genart": "1.2",
      "id": "entry-into-jerusalem",
      "title": "Entry into Jerusalem",
      "created": "2026-09-13T00:00:00.000Z",
      "modified": "2026-09-14T04:12:05.486Z",
      "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, crowd scene: the Entry into Jerusalem, after the Troyes and Laon panels (refs 21, 22) and the\n// Soissons rider at a city gate (ref 23). Erin 2026-09-13 chose stacked heads for the crowd, a white donkey with\n// the rider sideways, and all four story parts (the gate, palm branches, the boy in the tree, a cloak on the\n// ground), on Cana's lighter leads.\n//\n// The composition, decided before any piece is cut:\n//   - the procession moves right along a road, toward the city tower at the right edge;\n//   - the followers at the left: one full figure in front, two heads stacked above and behind him (a crowd of\n//     heads, only the front one with a body, as at Troyes and Laon);\n//   - the white donkey across the middle, its head bowed over a cloak spread on the road under its hooves; the\n//     saint sits sideways, his tunic falling over its flank and his bare feet hanging below, blessing, haloed;\n//   - a tree rises behind the donkey with a boy in its leaves holding up a cut branch;\n//   - the greeters at the tower: a standing figure raising a palm, two heads stacked behind him.\n// Drawing order: sky, ground, road, tower and step, tree and boy, followers, greeters, cloak, donkey, saint, rings.\n// (The greeters stand on the step further back than the road, so the donkey's head overlaps them: v13.)\n//\n// v1-v3 are recorded in docs/memory/works-glass.md. v3 -> v4 works through entry-audit-v3.md (Erin: \"There's still a\n// lot of issues on the details on this one. Let's be critical, analyze, make notes, and fix.\"): the cloak read as a\n// pond (C1), the ground was bare grass (C2), the saint's hem was a cushion and his feet ovals (R1, R3), the donkey a\n// slab on planks with a blocky head (D1-D5), the tree four balls on a pole (T1, T3), the boy's arm a strip holding a\n// copy of the palm (T2), the palm a feather (G3), the heads lollipops (F1), the sky leads strings (S1), and two big\n// red mantles bookended the scene (K1).\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  /** Lead width scale: Cana's lighter leads (Erin). */\n  var LITE = 0.72, LW = 5, 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.6 : 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.15 : o.pits, weather: o.weather == null ? 0.2 : o.weather, seeds: o.seeds || 0,\n      edge: o.edge == null ? 0.15 : o.edge, edgeW: o.edgeW || 7, grain: o.grain == null ? 0.25 : o.grain,\n      cloud: o.cloud == null ? 0.4 : o.cloud, relief: o.relief == null ? 1.1 : o.relief, clip: o.clip,\n    });\n  }\n  function P(x, y) { return { x: x, y: y }; }\n  var lite = function (p, w) { leadOf(p, (w || 6) * LITE); };\n  var F = glassFigures(G, K, T, { piece: glassPiece, lead: lite });\n  var SET = glassSettings(G, K, T, { piece: glassPiece, lead: lite, rng: rng, C: C });\n  /**\n   * Figures drawn through a copy of the material clipped above frame y h (Cana v3's technique): pieces, leads,\n   * traces, scrapes and matt washes all stop at h. The rider uses it so his legs end under his tunic's drape.\n   */\n  function clippedFigures(h) {\n    var R = { x: 0, y: 0, w: W, h: h };\n    function cl(o) { var c = {}; for (var k in (o || {})) c[k] = o[k]; c.clip = R; return c; }\n    var GC = {};\n    for (var gk in G) GC[gk] = G[gk];\n    GC.trace = function (pts, o) { G.trace(pts, cl(o)); };\n    GC.scrape = function (pts, o) { G.scrape(pts, cl(o)); };\n    GC.matt = function (mask, r, o) { G.matt(function (x, y) { return y < h ? mask(x, y) : 0; }, r, o); };\n    return glassFigures(GC, K, glassKit(GC, K, rng), {\n      piece: function (p, color, o) { glassPiece(p, color, cl(o)); },\n      lead: function (p, w) { G.lead(p, { w: (w || 6) * LITE, wobble: WOB, rough: ROUGH, closed: true, clip: R }); },\n    });\n  }\n\n  var O = { x: W / 2, y: H / 2 };\n  var R3 = 430, R2 = 412, R1 = 382;\n  var S = { O: O, r: R1, sc: 1, M: function (x, y) { return P(x, y); } };\n\n  /**\n   * One of the crowd seen as head and shoulders (refs 21, 22): a drapery piece with two folds, then the head over it.\n   * v3 -> v4 (audit F1): narrow round-topped shoulders under a centred head read as a lollipop on a bag; the shoulders\n   * are now broad and high, meeting the head as a body does.\n   */\n  function bust(x, y, ry, facing, color, o) {\n    o = o || {};\n    var k = ry / 30, fw = Math.max(0.6, k);\n    // v5 -> v6 (entry-audit-v5.md J10): o.long carries the body further down, so a head in a crowd reaches down behind the\n    // figures in front of it (or under the ring) instead of ending in a floating edge.\n    var lo = (o.long || 150) * k;\n    var sh = T.smoothClosed([P(x - 50 * k, y + lo), P(x - 48 * k, y + 50 * k), P(x - 30 * k, y + 20 * k), P(x + 26 * k, y + 18 * k), P(x + 48 * k, y + 44 * k), P(x + 52 * k, y + lo)], 5);\n    glassPiece(sh, color, { angle: 1.57 });\n    G.matt(T.inside(sh), T.bboxOf(sh), { a: 0.3, drag: 0.1, angle: 1.57 });\n    [[10, -6, 2], [-22, -30, -18]].forEach(function (f) {\n      T.traceIn(sh, T.through([P(x + facing * f[0] * k, y + 44 * k), P(x + facing * f[1] * k, y + 92 * k), P(x + facing * f[2] * k, y + 142 * k)], 6),\n        { w: 2.4 * fw, a: 0.8, taperIn: 0.1, taperOut: 0.4, rough: 0.35 }, 4);\n    });\n    lite(sh);\n    return F.head({ c: P(x, y), ry: ry, facing: facing, tilt: o.tilt == null ? 0.1 : o.tilt, beard: o.beard, hair: o.hair || \"short\", flesh: C.flesh, rng: G.rngFrom(o.seed || 1) });\n  }\n\n  // --- Sky, keeping its leads clear of every head and raised hand, cut across so no lead runs the full height ---\n  SET.sky(S, seed + 303, [[150, 400], [185, 352], [234, 384], [433, 325], [550, 175], [687, 380], [765, 405], [805, 455], [637, 408]], { bands: [240, 520] });\n\n  // --- Ground, the road the procession walks on, and the city tower at the right ---\n  SET.hill(S, [20, 772], [440, 752], [880, 745], C.green, seed + 71);\n  SET.road(S, 736, 794, C.bark, seed + 72);\n  SET.tower(S, 730, 830, 380, 765, { door: true, slot: true });\n  // v7 -> v8 (Erin: \"the figures on the right are floating\"; entry-audit-v7.md F1): the greeters stood on a strip of sky,\n  // the road running below their feet and the ring cutting the ground off. A stone threshold before the city gate rises\n  // to meet them; drawn here, the cloak and the donkey still lie over its edge.\n  // v8 -> v9 (entry-audit-v7.md F2): the ring's inner edge runs only ~20 px under x 700, so a sill sloping up to the ring\n  // showed as a sliver and the greeter's shoe tip and hem corner (further left, where it dropped) still met sky. The sill\n  // is now flat across his whole width, and he stands lower, where the disc is deeper.\n  // v9 -> v10 (entry-audit-v7.md F3): at x 600-720 the ring's inner edge runs 744-786, so any ground under a figure standing\n  // near 736 is a sliver and he still stands on the ring. The ground itself rises: a raised step before the gate, its top\n  // near y 700 with 40+ px of stone under the greeter, a darker front face below a traced edge so it reads as a step.\n  var sill = SET.clip(S, [P(596, 704), P(640, 700), P(760, 697), P(900, 694), P(900, 900), P(596, 900)]);\n  glassPiece(sill, C.stone, { angle: 0, mottle: 0.45, weather: 0.3, edge: 0.2 });\n  G.matt(T.inside(sill), T.bboxOf(sill), { a: 0.2, drag: 0.15, angle: 0 });\n  G.matt(function (x, y) { return G.inPoly(sill, x, y) && y > 726 - (x - 596) * 0.02 ? 1 : 0; }, T.bboxOf(sill), { a: 0.35, drag: 0.15, angle: 0 });\n  T.edgeMatt(sill, 0.35, 5);\n  var paving = { w: 2, a: 0.8, taperIn: 0, taperOut: 0, rough: 0.4 };\n  T.traceIn(sill, T.through([P(598, 726), P(760, 723), P(900, 720)], 10), { w: 2.6, a: 0.85, taperIn: 0, taperOut: 0, rough: 0.35 }, 1);\n  [[650, 712], [740, 709], [690, 740]].forEach(function (j) { T.traceIn(sill, [P(j[0], j[1] - 9), P(j[0] + 2, j[1] + 10)], paving, 2); });\n  lite(sill);\n\n  // --- The tree: a thicker trunk, a leaf mass behind the boy, the boy with a bent arm and a cut branch, leaves in front ---\n  // v4 -> v5: at 1.9x the trunk was a flat parallel plank; 1.3x.\n  // v5 -> v6 (entry-audit-v5.md J1): the trunk stopped in the blue gap under the leaves; its top now runs up under the\n  // front leaf masses, so the tree grows into its foliage.\n  SET.tree(S, [548, 250], [520, 560], [], seed + 600, { trunk: 1.3 });\n  SET.foliage(S, 560, 212, 108, 70, C.olive, seed + 610);\n  bust(550, 175, 16, -1, C.ruby, { beard: false, hair: \"short\", tilt: 0.2, seed: seed + 81 });\n  var bArm = K.ribbon(T.through([P(562, 200), P(592, 178), P(584, 142)], 8), function (s) { return 6.5 - 2 * s; });\n  glassPiece(bArm, C.ruby, { angle: 1.2 });\n  T.traceIn(bArm, T.through([P(572, 196), P(588, 182)], 3), { w: 1.8, a: 0.8, taperIn: 0.2, taperOut: 0.3, rough: 0.3 }, 1);\n  lite(bArm);\n  SET.twig(S, [[582, 136], [570, 112], [548, 88]], C.green);\n  var bHand = T.smoothClosed([P(577, 146), P(589, 144), P(592, 134), P(586, 126), P(578, 130)], 4);\n  glassPiece(bHand, C.flesh, { mottle: 0.3, edge: 0.2, edgeW: 3 });\n  G.trace([P(580, 132), P(588, 131)], { w: 1.4, a: 0.8, taperIn: 0.2, taperOut: 0.2, rough: 0.2 });\n  lite(bHand);\n  SET.foliage(S, 480, 270, 70, 42, C.green, seed + 630);\n  SET.foliage(S, 604, 264, 72, 40, C.teal, seed + 640);\n\n  // --- The followers: two heads stacked behind, one full figure in front ---\n  bust(185, 352, 26, 1, C.maroon, { beard: false, hair: \"short\", tilt: 0.18, seed: seed + 62 });\n  bust(150, 400, 26, 1, C.teal, { beard: true, hair: \"full\", tilt: 0.14, long: 300, seed: seed + 61 });\n  F.figure({ x: 215, y: 758, h: 400, facing: 1, lean: 0.04, tilt: 0.15, gesture: \"point\", beard: true, hair: \"tonsure\", halo: false,\n    colors: { tunic: C.amber, mantle: C.ruby, shoe: C.bark, flesh: C.flesh, band: C.white }, seed: seed + 63 });\n\n  // --- The greeters at the tower: two heads behind, one raising a palm, clear of the donkey's head (teal, not a second red: K1) ---\n  // v12 -> v13 (Erin: \"Donkey face is behind figure\"): the greeters stand on the step before the gate, further back than\n  // the road, so they are drawn BEFORE the cloak and the donkey; the donkey's bowed head now overlaps the palm greeter's tunic.\n  // v5 -> v6 (entry-audit-v5.md J3, J6, J10): the palm is drawn before the greeter so his hand closes over its stem (two\n  // grip strokes across it); he stands a little further in so his feet are not lost under the ring; the heads behind\n  // him reach down behind him and the ring, the nearer (purple) one drawn after the farther.\n  bust(765, 405, 22, -1, C.amber, { beard: false, hair: \"full\", tilt: 0.14, long: 300, seed: seed + 72 });\n  bust(805, 455, 21, -1, C.purple, { beard: true, hair: \"full\", tilt: 0.08, long: 240, seed: seed + 71 });\n  // v6 -> v7 (J6): at y 728 his feet were still under the ring; up and in to x 700, y 716, palm and grip with him.\n  // v9 -> v10 (F3): standing on the raised step's top; palm and grip follow.\n  // v10 -> v11: the palm's tip ran into the teal leaf mass; swung right, into the clear sky between the leaves and the roof.\n  // v11 -> v12: the swung tip still touched the leaf mass's lower edge; a little shorter.\n  SET.palm(S, [637, 410], [662, 296], 10);\n  F.figure({ x: 700, y: 702, h: 340, facing: -1, lean: 0.03, tilt: 0.18, gesture: \"bless\", beard: false, hair: \"short\", halo: false,\n    colors: { tunic: C.olive, mantle: C.teal, shoe: C.bark, flesh: C.flesh, band: C.amber }, seed: seed + 73 });\n  [0, 7].forEach(function (d) { G.trace([P(629, 397 + d), P(644, 399 + d)], { w: 1.6, a: 0.85, taperIn: 0.2, taperOut: 0.2, rough: 0.25 }); });\n\n  // --- The cloak on the road under the hooves, the donkey, the saint sideways on it ---\n  // v4 -> v5: the cloak lies on the road as cloth (soft edge, folds curving out from under the front hooves, a small\n  // turned-back corner), not as a board with a ruler along it.\n  SET.cloak(S, [[470, 748], [700, 744], [724, 776], [690, 792], [500, 794], [462, 776]], C.ruby, {\n    lining: C.amber, flap: [[692, 747], [724, 776], [704, 772]], seed: seed + 740,\n    folds: [[[560, 752], [520, 786], 6, 5], [[590, 756], [600, 790], -5, -4], [[620, 752], [668, 786], -6, 6], [[652, 748], [694, 776], 5, -4]],\n  });\n  // v6 -> v7: the deeper belly kinked the hind legs (J12); settings.js donkey now places every leg joint by proportion.\n  // v5 -> v6 (entry-audit-v5.md J2): the saint's feet hung in a strip of sky under the belly, touching nothing. The belly is\n  // deeper, so the hem drapes over it and the feet come out from under the hem against the donkey's side.\n  SET.donkey(S, 290, 560, 530, 682, 765);\n  var FR = clippedFigures(648);\n  FR.figure({ x: 420, y: 715, h: 420, facing: 1, lean: 0.02, tilt: 0.1, gesture: \"bless\", beard: true, hair: \"full\", halo: true,\n    colors: { tunic: C.green, mantle: C.purple, shoe: C.bark, halo: C.amber, flesh: C.flesh, band: C.amber }, seed: seed + 11 });\n  // bare feet hanging at two heights (ref 22), drawn before the tunic so its hem overlaps them (audit R3)\n  [[398, 694, 0], [444, 688, 1]].forEach(function (f) {\n    var fx = f[0], fy = f[1];\n    // v4 -> v5: a fifth smaller; the feet read as large pink blobs. v5 -> v6 (J2): each foot now starts well up under the\n    // hem, an ankle rather than a pill hanging below it.\n    var foot = T.smoothClosed([P(fx - 6, fy - 14), P(fx + 6, fy - 14), P(fx + 8, fy + 6), P(fx + 9, fy + 20), P(fx + 5, fy + 31), P(fx - 5, fy + 32), P(fx - 9, fy + 20), P(fx - 8, fy + 6)], 4);\n    glassPiece(foot, C.flesh, { mottle: 0.3, edge: 0.2, edgeW: 3 });\n    T.edgeMatt(foot, 0.35, 3);\n    for (var t = -1; t <= 1; t++) G.trace([P(fx + t * 3.2, fy + 25), P(fx + t * 3.6, fy + 30)], { w: 1.3, a: 0.8, taperIn: 0.2, taperOut: 0.2, rough: 0.2 });\n    G.trace([P(fx - 4, fy + 13), P(fx + 1, fy + 16)], { w: 1.3, a: 0.6, taperIn: 0.3, taperOut: 0.3, rough: 0.3 });\n    lite(foot);\n  });\n  // the tunic falling over the flank in pleats with a scalloped, bordered hem (ref 22), over the clip line (audit R1)\n  var hemY = function (x) { var fr = ((502 - x) / 52) % 1; return 686 + 14 * Math.sin(Math.PI * fr); };\n  // v4 -> v5: a straight top edge cut across the mantle like an apron; the top now dips and rises with the cloth.\n  var skirt = [P(360, 630), P(392, 621), P(424, 627), P(456, 619), P(486, 626)];\n  for (var q = 0; q <= 30; q++) { var sx = 502 - 156 * q / 30; skirt.push(P(sx, hemY(sx))); }\n  glassPiece(skirt, C.green, { angle: 1.57, edge: 0.3 });\n  G.matt(T.inside(skirt), T.bboxOf(skirt), { a: 0.3, drag: 0.12, angle: 1.57 });\n  var pleat = { w: 2.6, a: 0.8, taperIn: 0.1, taperOut: 0.35, rough: 0.35 };\n  [398, 450].forEach(function (px) { T.traceIn(skirt, T.through([P(px + 8, 616), P(px + 2, 650), P(px, 684)], 6), pleat, 3); });\n  for (var lb = 0; lb < 3; lb++) {\n    var xa = 502 - 52 * lb, xb = xa - 52, xc = (xa + xb) / 2;\n    T.traceIn(skirt, T.through([P(xa - 8, 652), P(xc, hemY(xc) - 12), P(xb + 8, 652)], 6), pleat, 3);\n  }\n  var border = [];\n  for (q = 0; q <= 30; q++) { var bx = 498 - 148 * q / 30; border.push(P(bx, hemY(bx) - 8)); }\n  T.traceIn(skirt, border, { w: 1.8, a: 0.75, taperIn: 0, taperOut: 0, rough: 0.3 }, 2);\n  lite(skirt);\n\n\n  // --- Rings, as Cana's, on the lighter leads ---\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.5, 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.65, 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), 5);\n  leadOf(K.circle(O.x, O.y, R2, 260), 5);\n  leadOf(K.circle(O.x, O.y, R3, 260), 8);\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": []
    }