{
 "genart": "1.2",
 "id": "frost-hollow",
 "title": "Frost Hollow at Dawn",
 "created": "2026-09-11T00:00:00Z",
 "modified": "2026-09-11T22:28:23.723Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1500,
  "height": 1200,
  "pixelDensity": 2
 },
 "parameters": [],
 "colors": [],
 "state": {
  "seed": 1840,
  "params": {},
  "colorPalette": [
   "#e8e3d7",
   "#d2cec4",
   "#bbb8b0",
   "#a5a39d",
   "#8e8d89",
   "#787876",
   "#616262",
   "#4b4d4f",
   "#34373b"
  ]
 },
 "algorithm": "// Weather Book: the graphite material. The builder prepends this file to each\n// sheet's algorithm, so every sheet in the series draws with the same lead on\n// the same paper. Series-local on purpose (plan section 7): it moves to a\n// shared package only when a second series needs it, and that move gets an ADR.\n//\n// The model, after Costa Sousa & Buchanan (2000):\n//   - The paper is a height field (its tooth). A pencil tip rides at a height\n//     set by its pressure and its grade and lays lead only on grain above it,\n//     so soft lead used lightly catches the peaks (light and grainy) and hard\n//     lead used firmly reaches the valleys (light and smooth).\n//   - Each grain holds a cap set by the grade. Build-up saturates, a harder\n//     lead can never darken what a softer one laid, and even the softest lead\n//     stops short of black.\n//   - Where the lead lies heaviest the platelets polish, and the value lifts\n//     slightly toward silver (the sheen).\n//   - A stump pushes lead into the valleys; a kneaded eraser lifts it, more\n//     from the peaks than from the valleys.\n//\n// Marks are placed in logical px and laid in device px, so the grain is the\n// same physical size at --scale 1 and at --scale 2.\nfunction graphiteSheet(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 tooth = new Float32Array(N);   // paper height, 0 in a valley .. 1 on a peak\n  var lead = new Float32Array(N);    // darkness laid, 0 bare paper .. ~0.92\n  var strokes = 0;                   // gives each stroke its own tip\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\n  // --- The paper -------------------------------------------------------------\n  // A fine tooth, a coarser felt under it, and faint laid lines across the\n  // sheet. Then equalised, so a tip riding at height t touches the top 1 - t\n  // of the grain whatever the noise's own distribution.\n  var BINS = 1024, hist = new Uint32Array(BINS);\n  var s1 = seed * 3 + 11, s2 = seed * 3 + 12, s3 = seed * 3 + 13;\n  for (var py = 0; py < PH; py++) {\n    var y = (py + 0.5) / D;\n    for (var px = 0; px < PW; px++) {\n      var x = (px + 0.5) / D;\n      // Mostly fine tooth. v4 weighted a 4.5 px felt at 0.4 and the threshold\n      // drew its contours as camouflage blobs; v3's salt and pepper came from a\n      // switch-like contact, not from the fine scale, and the ramp below fixes that.\n      var v = 0.62 * noise(x / 1.25, y / 1.25, s1) + 0.23 * noise(x / 3.2, y / 2.8, s2) +\n        0.15 * noise(x / 18, y / 1.6, s3);\n      tooth[py * PW + px] = v;\n      hist[Math.min(BINS - 1, (v * BINS) | 0)]++;\n    }\n  }\n  var cdf = new Float32Array(BINS), acc = 0;\n  for (var b = 0; b < BINS; b++) { acc += hist[b]; cdf[b] = acc / N; }\n  for (var i = 0; i < N; i++) tooth[i] = cdf[Math.min(BINS - 1, (tooth[i] * BINS) | 0)];\n\n  // --- The lead ----------------------------------------------------------------\n  /**\n   * One touch of the tip, in device px. s is softness, 0 (2H) .. 1 (6B); p is\n   * pressure 0..1; k is the share of the way to the grain's cap it lays.\n   * (nx, ny) is the stroke's normal and salt the stroke's own tip: a worn lead\n   * is not round, so it lays fine striations along the stroke's direction.\n   */\n  function dab(cx, cy, r, s, p, k, nx, ny, salt) {\n    var t = 1.0 - p * (0.95 + 0.3 * (1 - s));       // how low the tip reaches into the grain\n    var cap = 0.30 + 0.62 * s;                        // the darkest this grade can go\n    var smear = p * p * 0.18;                         // pressed lead smears a little into the valleys\n    var x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(PW - 1, Math.ceil(cx + r));\n    var y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(PH - 1, Math.ceil(cy + r));\n    var r2 = r * r;\n    for (var yy = y0; yy <= y1; yy++) {\n      var dy = yy + 0.5 - cy, row = yy * PW;\n      for (var xx = x0; xx <= x1; xx++) {\n        var dx = xx + 0.5 - cx, d2 = dx * dx + dy * dy;\n        if (d2 >= r2) continue;\n        var j = row + xx, h = tooth[j];\n        // A graded contact, not a switch: v3's steep ramp made every pixel\n        // all-or-nothing.\n        var c = (h - t) * 3;\n        if (c < smear) c = smear;\n        if (c <= 0) continue;\n        var qq = (dx * nx + dy * ny) * 0.8 + salt, iq = Math.floor(qq), fq = qq - iq;\n        fq = fq * fq * (3 - 2 * fq);\n        var ha = hash2(iq, 0, 77), hb = hash2(iq + 1, 0, 77);\n        c *= 0.45 + 0.75 * (ha + (hb - ha) * fq);\n        if (c > 1) c = 1;\n        var target = cap * (0.8 + 0.2 * h), cur = lead[j];\n        if (cur < target) lead[j] = cur + (target - cur) * k * c * (1 - d2 / r2);\n      }\n    }\n  }\n  /** A kneaded eraser's touch: lifts a share e of the lead, more from the peaks. */\n  function lift(cx, cy, r, e) {\n    var x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(PW - 1, Math.ceil(cx + r));\n    var y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(PH - 1, Math.ceil(cy + r));\n    var r2 = r * r;\n    for (var yy = y0; yy <= y1; yy++) {\n      var dy = yy + 0.5 - cy, row = yy * PW;\n      for (var xx = x0; xx <= x1; xx++) {\n        var dx = xx + 0.5 - cx, d2 = dx * dx + dy * dy;\n        if (d2 >= r2) continue;\n        var j = row + xx;\n        lead[j] *= 1 - e * (1 - d2 / r2) * (0.5 + 0.5 * tooth[j]);\n      }\n    }\n  }\n  /**\n   * Walk a polyline (logical px) in steps, calling touch(x, y, t, ux, uy) in\n   * device px, with (ux, uy) the unit direction of travel.\n   */\n  function walk(pts, stepDev, touch) {\n    var L = 0, j;\n    for (j = 1; j < pts.length; j++) L += Math.hypot(pts[j].x - pts[j - 1].x, pts[j].y - pts[j - 1].y);\n    if (!(L > 0)) return;\n    var step = stepDev / D, done = 0, carry = 0;\n    for (j = 1; j < pts.length; j++) {\n      var a = pts[j - 1], b = pts[j], l = Math.hypot(b.x - a.x, b.y - a.y);\n      var ux = l > 0 ? (b.x - a.x) / l : 1, uy = l > 0 ? (b.y - a.y) / l : 0;\n      var u = carry;\n      for (; u < l; u += step) {\n        var f = u / l;\n        touch((a.x + (b.x - a.x) * f) * D, (a.y + (b.y - a.y) * f) * D, (done + u) / L, ux, uy);\n      }\n      carry = u - l; done += l;\n    }\n  }\n  /**\n   * One pencil stroke along a polyline in logical px. o.s softness, o.p the\n   * pressure at the start and o.p1 at the end, o.r the tip radius in logical\n   * px, o.k how much a pass lays (default 0.5), o.tin / o.tout the share of the\n   * stroke over which the pencil lands and lifts.\n   */\n  function stroke(pts, o) {\n    if (pts.length < 2) return;\n    var s = o.s, p0 = o.p, p1 = o.p1 == null ? o.p : o.p1;\n    var rDev = Math.max(0.75, o.r * D);\n    var tin = Math.max(1e-3, o.tin == null ? 0.08 : o.tin), tout = Math.max(1e-3, o.tout == null ? 0.2 : o.tout);\n    var stepDev = Math.max(0.5, rDev * 0.4);\n    // Per touch, so that one pass lays about k whatever the step.\n    var kk = Math.min(1, (o.k == null ? 0.6 : o.k) * stepDev / rDev);\n    var salt = (++strokes * 7.31) % 1000;\n    walk(pts, stepDev, function (x, y, t, ux, uy) {\n      var env = Math.min(1, t / tin, (1 - t) / tout);\n      if (env <= 0) return;\n      dab(x, y, rDev * (0.55 + 0.45 * env), s, (p0 + (p1 - p0) * t) * Math.sqrt(env), kk, -uy, ux, salt);\n    });\n  }\n  /** A kneaded-eraser stroke: o.r radius in logical px, o.e strength 0..1. */\n  function erase(pts, o) {\n    var rDev = Math.max(0.75, o.r * D), stepDev = Math.max(0.5, rDev * 0.4);\n    var ee = Math.min(1, o.e * stepDev / rDev);\n    walk(pts, stepDev, function (x, y, t) {\n      var env = Math.min(1, t / 0.15, (1 - t) / 0.3);\n      if (env > 0) lift(x, y, rDev, ee * env);\n    });\n  }\n  /**\n   * The stump: blur the lead over `radius` logical px and mix it back by\n   * mask(x, y) in logical px. Lead leaves the peaks for the valleys, so the\n   * grain closes and the tone goes smooth.\n   */\n  function blend(mask, radius) {\n    var R = Math.max(1, Math.round(radius * D)), tmp = new Float32Array(N), out = new Float32Array(N);\n    var xx, yy, sum, j, w = 2 * R + 1;\n    for (yy = 0; yy < PH; yy++) {\n      var row = yy * PW;\n      sum = 0;\n      for (xx = -R; xx <= R; xx++) sum += lead[row + Math.min(PW - 1, Math.max(0, xx))];\n      for (xx = 0; xx < PW; xx++) {\n        tmp[row + xx] = sum / w;\n        sum += lead[row + Math.min(PW - 1, xx + R + 1)] - lead[row + Math.max(0, xx - R)];\n      }\n    }\n    for (xx = 0; xx < PW; xx++) {\n      sum = 0;\n      for (yy = -R; yy <= R; yy++) sum += tmp[Math.min(PH - 1, Math.max(0, yy)) * PW + xx];\n      for (yy = 0; yy < PH; yy++) {\n        out[yy * PW + xx] = sum / w;\n        sum += tmp[Math.min(PH - 1, yy + R + 1) * PW + xx] - tmp[Math.max(0, yy - R) * PW + xx];\n      }\n    }\n    for (yy = 0; yy < PH; yy++) {\n      for (xx = 0; xx < PW; xx++) {\n        var m = mask((xx + 0.5) / D, (yy + 0.5) / D);\n        if (m > 0) { j = yy * PW + xx; lead[j] += (out[j] - lead[j]) * Math.min(1, m); }\n      }\n    }\n  }\n  function rgb(hex) {\n    var n = parseInt(hex.slice(1), 16);\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n  /**\n   * Lay the sheet down. Every pixel sits on the one line from the paper to the\n   * darkest the lead can go, so the palette explains the whole frame.\n   */\n  function finish(paperHex, leadHex) {\n    var P = rgb(paperHex), G = rgb(leadHex);\n    var img = ctx.createImageData(PW, PH), out = img.data;\n    for (var j = 0, o = 0; j < N; j++, o += 4) {\n      var h = tooth[j], L = lead[j];\n      // The tooth shows a little in the bare paper, as it does under raking light.\n      var k = L + 0.03 * (1 - h) * (1 - L);\n      // Sheen: the heaviest lead polishes and silvers, the peaks most.\n      var sh = (L - 0.70) / 0.18;\n      if (sh > 0) { if (sh > 1) sh = 1; k -= 0.09 * sh * sh * (0.4 + 0.6 * h); }\n      if (k < 0) k = 0; else if (k > 1) k = 1;\n      out[o] = P[0] + (G[0] - P[0]) * k;\n      out[o + 1] = P[1] + (G[1] - P[1]) * k;\n      out[o + 2] = P[2] + (G[2] - P[2]) * k;\n      out[o + 3] = 255;\n    }\n    ctx.putImageData(img, 0, 0);\n  }\n\n  return { D: D, rngFrom: rngFrom, noise: noise, stroke: stroke, erase: erase, blend: blend, finish: finish };\n}\n\n// Weather Book 4: Frost Hollow at Dawn.\n//\n// A hollow at first light after a clear, still night. The cold air has drained\n// off the slopes and filled the valley floor with ground mist, level as a lake;\n// the hedgerow trees stand up out of it with their feet lost, and the far\n// slope is clear above the mist line, its upper fields catching the first\n// light while the hollow is still in shadow. The near bank, where we stand, is\n// white with frost.\n//\n// One level camera in metres, the eye on the near bank well above the mist's\n// top. The mist is a flat lake of grainy tone from the side of a soft lead,\n// stumped even, with a hard top edge drawn up to with the point and a lit rim\n// lifted out beneath it. What stands in it is lost by value, never by blur.\n// The frost is kneaded-eraser work on the near grass: blades, level drags and\n// points lifted out of dark lead. The seed moves the horizon, the mist's\n// depth, the far ridge and the woods on it, the hedgerows and the near bank.\nfunction sketch(ctx, state) {\n  var W = state.canvas.width, H = state.canvas.height;\n  var seed = state.seed || 0;\n  var pal = state.colorPalette;\n  var G = graphiteSheet(ctx, W, H, seed);\n  var rand = G.rngFrom(seed * 2654435761 + 1840);\n  var noise = G.noise;\n  function rr(a, b) { return a + (b - a) * rand(); }\n  function clamp01(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }\n  function smooth(e0, e1, x) { var t = clamp01((x - e0) / (e1 - e0)); return t * t * (3 - 2 * t); }\n\n  /** Parallel strokes at `angle` over the band y0..y1, each handed to fn(a, b). */\n  function hatch(angle, spacing, len0, len1, gap0, gap1, y0, y1, fn) {\n    var dx = Math.cos(angle), dy = Math.sin(angle), nx = -dy, ny = dx;\n    var nmin = 1e9, nmax = -1e9, tmin = 1e9, tmax = -1e9;\n    [[0, y0], [W, y0], [0, y1], [W, y1]].forEach(function (c) {\n      var n = c[0] * nx + c[1] * ny, t = c[0] * dx + c[1] * dy;\n      nmin = Math.min(nmin, n); nmax = Math.max(nmax, n); tmin = Math.min(tmin, t); tmax = Math.max(tmax, t);\n    });\n    for (var n = nmin; n <= nmax; n += spacing * rr(0.7, 1.3)) {\n      for (var t = tmin - rr(0, len1); t < tmax;) {\n        var len = rr(len0, len1);\n        var a = { x: n * nx + t * dx, y: n * ny + t * dy };\n        var b = { x: a.x + len * dx, y: a.y + len * dy };\n        var my = (a.y + b.y) / 2, mx = (a.x + b.x) / 2;\n        if (my > y0 && my < y1 && mx > -len / 2 && mx < W + len / 2) fn(a, b);\n        t += len + rr(gap0, gap1);\n      }\n    }\n  }\n  /** A hand's stroke is never ruled: bow it a little. */\n  function bowed(a, b, amt) {\n    var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2, l = Math.hypot(b.x - a.x, b.y - a.y) || 1;\n    var off = (rand() - 0.5) * amt * l;\n    return [a, { x: mx - (b.y - a.y) / l * off, y: my + (b.x - a.x) / l * off }, b];\n  }\n  /** A zig-zag touch: hedges, crowns and seed heads. */\n  function scribble(x, y, size, o) {\n    var a = rand() * Math.PI * 2, pts = [{ x: x, y: y }], n = 3 + (rand() * 4 | 0), step = size * 0.5;\n    for (var q = 0; q < n; q++) {\n      a += (q % 2 ? 1 : -1) * rr(2.0, 2.8);\n      x += Math.cos(a) * step * rr(0.6, 1.2); y += Math.sin(a) * step * rr(0.6, 1.2);\n      pts.push({ x: x, y: y });\n    }\n    G.stroke(pts, o);\n  }\n\n  // --- The camera: level, the eye on the near bank above the mist.\n  var F = W * 0.95;\n  var HZ = H * rr(0.27, 0.4);\n  // Shallow mist under tall trees, so they stand well out of it: v1's 5-9 m\n  // of mist under 8-15 m trees left them grey stubs.\n  var MT = rr(3, 6);                                     // the mist's top over the valley floor, m\n  var EYE = MT + rr(9, 16);                              // the eye over the valley floor, m\n  var LIGHT = rand() < 0.5 ? -1 : 1;                     // the dawn's side: -1 the left\n  var HAZE = rr(900, 1600);\n  function vis(Z) { return Math.exp(-Math.pow(Z / HAZE, 1.3)); }\n  function scr(X, Y, Z) { return { x: W / 2 + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n  function mistY(Z) { return HZ + F * (EYE - MT) / Z; }  // the mist's top at depth Z, on the sheet\n\n  // ---------------------------------------------------------------------------\n  // The far slope: the valley floor ends at zf(x) and the land rises beyond it\n  // to a crest rh(x) high over a run of DR. Solved per column, so the ridge,\n  // the contours and the mist line agree on one camera.\n  // ---------------------------------------------------------------------------\n  var ZF0 = rr(260, 420), DR = rr(240, 400), RH0 = rr(80, 150), RT = rr(-0.4, 0.4);\n  function zf(x) { return ZF0 * (0.85 + 0.3 * noise(x / 420, 2.1, seed + 301)); }\n  function rh(x) {\n    return RH0 * (1 + RT * (x / W - 0.5) * 2) * (0.75 + 0.5 * noise(x / 300, 5.3, seed + 302)) +\n      5 * (noise(x / 55, 7.7, seed + 303) - 0.5);\n  }\n  function landD(h, Y) { return DR * 2 / Math.PI * Math.asin(Math.min(1, Y / h)); }   // run at which the land reaches Y\n  var COLS = Math.ceil(W) + 1;\n  var RIDGE = new Float32Array(COLS), YM = new Float32Array(COLS), ZFA = new Float32Array(COLS);\n  for (var c0 = 0; c0 < COLS; c0++) {\n    var z0 = zf(c0), h0 = rh(c0), best = 1e9;\n    for (var q0 = 0; q0 <= 48; q0++) {\n      var d0 = 1.6 * DR * q0 / 48, Y0 = h0 * Math.sin(Math.PI / 2 * Math.min(1, d0 / DR));\n      best = Math.min(best, HZ + F * (EYE - Y0) / (z0 + d0));\n    }\n    RIDGE[c0] = best; ZFA[c0] = z0;\n    YM[c0] = HZ + F * (EYE - MT) / (z0 + landD(h0, MT));\n  }\n  function col(A, x) {\n    var i = Math.max(0, Math.min(COLS - 1, x)), i0 = Math.floor(i), i1 = Math.min(COLS - 1, i0 + 1);\n    return A[i0] + (A[i1] - A[i0]) * (i - i0);\n  }\n  function ridgeY(x) { return col(RIDGE, x); }\n  function topY(x) { return col(YM, x); }                // the mist's hard top edge\n  function slopeAt(x, t) { var m = topY(x); return m + (ridgeY(x) - m) * t; }\n\n  // ---------------------------------------------------------------------------\n  // The near bank: its edge, where it goes down into the mist, curves across\n  // the sheet, higher on one side.\n  // ---------------------------------------------------------------------------\n  var NB = rr(0.36, 0.5), TILT = rr(0.08, 0.22) * (rand() < 0.5 ? -1 : 1), CURVE = rr(-0.25, 0.3);\n  function bankY(x) {\n    var u = x / W - 0.5;\n    return HZ + (H - HZ) * (NB + TILT * u + CURVE * u * u + 0.05 * (noise(x / 500, 1.7, seed + 501) - 0.5)) +\n      6 * (noise(x / 40, 3.3, seed + 502) - 0.5);\n  }\n  function bankNear(x, y) { var b = bankY(x); return y <= b ? 0 : Math.pow(smooth(b, H + 20, y), 0.8); }\n  function bankDir(x, y) {\n    var a = Math.atan2(bankY(x + 8) - bankY(x - 8), 16);\n    return a * (1 - 0.6 * bankNear(x, y));\n  }\n\n  var MTOP = 1e9, MBOT = -1e9, RTOP = 1e9;\n  for (var c1 = 0; c1 <= W; c1 += 4) {\n    MTOP = Math.min(MTOP, topY(c1)); MBOT = Math.max(MBOT, bankY(c1)); RTOP = Math.min(RTOP, ridgeY(c1));\n  }\n  function mistDepth(y) { return clamp01((y - MTOP) / (MBOT - MTOP)); }   // 0 the far edge, 1 the near\n  /** The pieces of a level stroke x0..x1 at y where ok(x, y) holds. */\n  function runs(x0, x1, y, ok) {\n    var out = [], st = null, last = x0;\n    for (var x = x0; x <= x1; x += 3) {\n      if (ok(x, y)) { if (st === null) st = x; last = x; }\n      else if (st !== null) { if (last - st > 6) out.push([st, last]); st = null; }\n    }\n    if (st !== null && last - st > 6) out.push([st, last]);\n    return out;\n  }\n\n  // ---------------------------------------------------------------------------\n  // The sky: clear after the night, palest low on the dawn's side. A few level\n  // strokes of hard lead, heavier toward the top, and on most seeds a thin\n  // bank of dawn cloud drawn with the side of the lead.\n  // ---------------------------------------------------------------------------\n  var GX = LIGHT > 0 ? W * rr(0.7, 0.95) : W * rr(0.05, 0.3), GY = RTOP - H * 0.02;\n  function glow(x, y) { var dx = (x - GX) / (W * 0.35), dy = (y - GY) / (H * 0.2); return Math.exp(-dx * dx - dy * dy); }\n  function inSky(p) { return p.y < ridgeY(p.x) - 2; }\n  hatch(rr(-0.02, 0.02), 6, 80, 380, 30, 170, -20, H * 0.6, function (a, b) {\n    var m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };\n    if (!inSky(a) || !inSky(b) || !inSky(m)) return;\n    var top = clamp01(1 - m.y / Math.max(1, RTOP));\n    if (rand() > (0.12 + 0.6 * top) * (1 - 0.8 * glow(m.x, m.y))) return;\n    G.stroke(bowed(a, b, 0.01), { s: rr(0.12, 0.45), p: (0.26 + 0.3 * top) * rr(0.7, 1.2), r: rr(0.6, 2.2), k: 0.35, tin: 0.15, tout: 0.2 });\n  });\n  if (rand() < 0.75) {\n    var BY = RTOP * rr(0.2, 0.7), BX = W * rr(0.15, 0.85), BW = W * rr(0.35, 0.8), BT = H * rr(0.02, 0.06);\n    for (var bs = 0; bs < 55; bs++) {\n      var u = rr(-1, 1), by = BY + rr(-1, 1) * BT * (1 - 0.5 * u * u), bx = BX + u * BW / 2;\n      var fall = 1 - Math.abs(u), bl = rr(80, 260) * (0.5 + fall);\n      if (by > ridgeY(bx) - 10) continue;\n      G.stroke(bowed({ x: bx - bl / 2, y: by }, { x: bx + bl / 2, y: by + rr(-2, 2) }, 0.03),\n        { s: 0.85, p: (0.12 + 0.2 * fall) * (1 - 0.6 * glow(bx, by)), r: rr(3, 8), k: 0.3, tin: 0.25, tout: 0.3 });\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // The far slope, clear above the mist. Its tone follows the contours in hard\n  // lead, darkest at the foot and in the folds, lightest up the slope where the\n  // dawn has reached it. Fields are drawn by their hedges as touches, woods as\n  // close vertical ticks under a scalloped top, the ridge as one firm line.\n  // ---------------------------------------------------------------------------\n  // Woods along the slope's foot, broken by fields, and on some seeds a hanger\n  // higher up: v1's two woods stood alone on a bare slope and read as islands.\n  var woods = [];\n  for (var wi = 0, nw = rand() < 0.5 ? 1 : 0; wi < nw; wi++) {\n    // Kept low and tapered: a tall patch with a scalloped top read as a castle\n    // on the skyline (v2, seed 1793).\n    woods.push({ c: W * rr(0.1, 0.9), w: W * rr(0.08, 0.18), t: rr(0.15, 0.32), s: seed + 700 + wi });\n  }\n  var FB = rr(0.1, 0.22), FG = rr(0.35, 0.55);          // the foot band's height, and how much fields break it\n  /** How high up the slope the woods reach at x, as a share of the slope; 0 outside them. */\n  function woodTop(x) {\n    var best = FB * smooth(FG - 0.08, FG + 0.08, noise(x / 160, 3.9, seed + 710)) * (0.7 + 0.6 * noise(x / 26, 1.3, seed + 711));\n    woods.forEach(function (w) {\n      var u = (x - w.c) / w.w;\n      if (Math.abs(u) < 1) best = Math.max(best, w.t * Math.pow(Math.cos(u * Math.PI / 2), 1.4) * (0.8 + 0.4 * noise(x / 26, 1.1, w.s)));\n    });\n    return best;\n  }\n  /** Darkest at the foot, in the hollow's shadow; the dawn has reached the upper slope and its own side. */\n  function slopeShade(x, t) {\n    // 🔴 Clamp t. The jittered rows can step past 1, Math.pow(negative, 1.3)\n    // is NaN, one dab writes that NaN into the lead, and the next stump's\n    // running-sum blur then smears it over every pixel to the right and below:\n    // v4 seed 1840 came out with a black quadrant from this one stroke.\n    t = clamp01(t);\n    var fold = noise(x / 110, t * 2.5, seed + 311);\n    var side = LIGHT > 0 ? x / W : 1 - x / W;\n    return clamp01(0.35 + 0.5 * Math.pow(1 - t, 1.3) + 0.5 * (fold - 0.5) - 0.3 * side * t - 0.15 * t);\n  }\n  var band = 0;\n  for (var c2 = 0; c2 <= W; c2 += 20) band += (topY(c2) - ridgeY(c2)) / (W / 20 + 1);\n  // The slope's body first, with the side of the lead along the contours, so\n  // it lies as a mid-tone mass against the pale mist. v2 drew the contour lines\n  // alone, the slope stayed bare paper, and the mist's top edge had nothing to\n  // be hard against.\n  for (var bt = 0.004; bt < 1; bt += 3.2 / Math.max(20, band)) {\n    for (var bx0 = -rr(0, 60); bx0 < W + 10;) {\n      var bl0 = rr(60, 260), bmid = bx0 + bl0 / 2;\n      var btj = bt + rr(-0.5, 0.5) * 3.2 / Math.max(20, band), bsh = slopeShade(bmid, btj);\n      if (btj > woodTop(bmid) + 0.01) {\n        var bp = [];\n        for (var q6 = 0; q6 <= 6; q6++) { var bxx = bx0 + bl0 * q6 / 6; bp.push({ x: bxx, y: slopeAt(bxx, btj) }); }\n        G.stroke(bp, { s: 0.75, p: 0.24 + 0.55 * bsh, r: rr(1.6, 3.2), k: 0.32, tin: 0.2, tout: 0.25 });\n      }\n      bx0 += bl0 + rr(0, 30);\n    }\n  }\n  for (var t = 0.004; t < 1; t += (2.4 + 1.4 * rand()) / Math.max(20, band) * (1 - 0.45 * t)) {\n    for (var sx = -rr(0, 80); sx < W + 10;) {\n      // Each piece wanders off its own contour and carries its own weight, so\n      // the hill is not ruled: v3's evenly spaced rows read as stripes.\n      var sl = rr(20, 120), pts = [], mid = sx + sl / 2;\n      var tj = t + rr(-0.7, 0.7) * 2.4 / Math.max(20, band), sh = slopeShade(mid, tj);\n      if (rand() < 0.3 + 0.7 * sh && tj > woodTop(mid) + 0.02) {\n        var wv = 0.05 * (noise(mid / 70, tj * 9, seed + 312) - 0.5);\n        for (var q1 = 0; q1 <= 6; q1++) { var xx = sx + sl * q1 / 6; pts.push({ x: xx, y: slopeAt(xx, tj + wv) }); }\n        G.stroke(pts, { s: 0.25 + 0.4 * sh, p: (0.3 + 0.55 * sh) * rr(0.75, 1.15), r: rr(0.5, 1.1), k: 0.5, tin: 0.1, tout: 0.15 });\n      }\n      sx += sl + rr(2, 30);\n    }\n  }\n  // Hedges along the contours and down the slope, as touches that shrink\n  // toward the crest; now and then a hedgerow tree as a tick and a tuft.\n  function hedgeTouch(x, y, t, dark) {\n    if (y > topY(x) - 1 || y < ridgeY(x) + 1) return;\n    var sz = 3.2 * (1 - 0.55 * t);\n    scribble(x, y, sz, { s: 0.55 + 0.3 * dark, p: (0.45 + 0.4 * dark) * (1 - 0.35 * t), r: 0.55, k: 0.5, tin: 0.05, tout: 0.15 });\n    if (rand() < 0.035) {\n      var th = sz * rr(2.5, 4.5);\n      G.stroke([{ x: x, y: y }, { x: x + rr(-0.5, 0.5), y: y - th }], { s: 0.7, p: 0.7 * (1 - 0.3 * t), r: 0.6, k: 0.6, tin: 0.02, tout: 0.3 });\n      for (var k = 0; k < 3; k++) scribble(x + rr(-1, 1) * th * 0.3, y - th * rr(0.7, 1), th * 0.4, { s: 0.6, p: 0.55, r: 0.5, k: 0.5 });\n    }\n  }\n  for (var hc = 0, nhc = 2 + (rand() * 3 | 0); hc < nhc; hc++) {\n    var ht = rr(0.1, 0.85), dark = rr(0.2, 0.8);\n    for (var hx = rr(-20, W * 0.4); hx < W + 10; hx += 2.2 + 3 * ht) {\n      if (rand() < 0.12) { hx += rr(10, 60); continue; }\n      if (ht < woodTop(hx) + 0.03) continue;\n      hedgeTouch(hx, slopeAt(hx, ht + 0.01 * (noise(hx / 60, hc, seed + 320) - 0.5)) + rr(-0.6, 0.6), ht, dark);\n    }\n  }\n  for (var hd = 0, nhd = 3 + (rand() * 4 | 0); hd < nhd; hd++) {\n    var x0d = rr(0, W), drift = rr(-60, 60), t1 = rr(0.35, 1), dk = rr(0.2, 0.8);\n    for (var td = 0; td < t1; td += 2.4 / Math.max(20, band)) {\n      if (rand() < 0.1) continue;\n      var xd = x0d + drift * td;\n      if (td < woodTop(xd) + 0.02) continue;\n      hedgeTouch(xd + rr(-0.8, 0.8), slopeAt(xd, td), td, dk);\n    }\n  }\n  // The woods: a mass laid first with the side of the lead along the contours\n  // (Seurat: one dark body, not scribble), then close vertical ticks for the\n  // stems, dark at the foot where they stand on the mist, and a scalloped line\n  // of crowns. v1 laid ticks alone and they read as salt and pepper.\n  var wmax = 0;\n  for (var c3 = 0; c3 <= W; c3 += 3) wmax = Math.max(wmax, woodTop(c3));\n  for (var wt0 = 0; wt0 < wmax; wt0 += 2.2 / Math.max(20, band)) {\n    for (var wx0 = -rr(0, 40); wx0 < W + 10;) {\n      var wl0 = rr(20, 80);\n      runs(wx0, wx0 + wl0, wt0, function (x, t) { return t < woodTop(x) - 0.004; }).forEach(function (rn) {\n        var wp = [];\n        for (var q = 0; q <= 5; q++) { var xx = rn[0] + (rn[1] - rn[0]) * q / 5; wp.push({ x: xx, y: Math.min(topY(xx) - 1.2, slopeAt(xx, wt0)) }); }\n        G.stroke(wp, { s: 0.9, p: 0.9 + 0.1 * (1 - wt0 / wmax), r: 1.8, k: 0.75, tin: 0.1, tout: 0.15 });\n      });\n      wx0 += wl0 + rr(0, 4);\n    }\n  }\n  for (var tx0 = 0; tx0 < W; tx0 += rr(0.9, 2.2)) {\n    var wt = woodTop(tx0);\n    if (wt < 0.01) continue;\n    for (var wn = 0, nwn = Math.ceil(wt * band / 5); wn < nwn; wn++) {\n      var tt = wt * rand(), wy = Math.min(topY(tx0) - 0.6, slopeAt(tx0, tt)), wfoot = 1 - tt / wt;\n      G.stroke([{ x: tx0, y: wy }, { x: tx0 + rr(-0.4, 0.4), y: wy - rr(2, 5) * (1 - 0.4 * tt) }],\n        { s: 0.85 + 0.15 * wfoot, p: 0.9 + 0.1 * wfoot, r: rr(0.45, 0.8), k: 0.7, tin: 0.05, tout: 0.3 });\n    }\n  }\n  for (var cx = 0; cx < W; cx += rr(3, 6)) {\n    var ct = woodTop(cx);\n    if (ct < 0.02) continue;\n    scribble(cx, slopeAt(cx, ct) + 1.5, rr(3.5, 6) * (1 - 0.4 * ct), { s: 0.85, p: 0.9, r: 0.6, k: 0.7, tin: 0.05, tout: 0.15 });\n  }\n  // Where the land meets the mist, one firm line along the whole sheet: the\n  // lake's hard top edge is this dark foot standing on it, heaviest under the\n  // woods. v3 left the edge as fuzzy grey and the mist had nothing to end at.\n  for (var ex0 = -10; ex0 < W + 10;) {\n    var el0 = rr(30, 140), ep0 = [], wtm = woodTop(ex0 + el0 / 2);\n    for (var q7 = 0; q7 <= 6; q7++) { var exx0 = ex0 + el0 * q7 / 6; ep0.push({ x: exx0, y: topY(exx0) - 0.8 }); }\n    if (rand() < (wtm > 0.02 ? 0.95 : 0.55)) {\n      G.stroke(ep0, { s: 0.9, p: wtm > 0.02 ? 1 : 0.7, r: wtm > 0.02 ? 1.3 : 0.7, k: 0.85, tin: 0.05, tout: 0.08 });\n    }\n    ex0 += el0 + rr(0, 8);\n  }\n  // The ridge: a firm line of soft lead, heavier where the crest turns from\n  // the light, gone over twice in places; a few trees on the skyline.\n  for (var ox = -10; ox < W + 10;) {\n    var ol = rr(40, 160), rp = [];\n    for (var q2 = 0; q2 <= 8; q2++) { var rx = ox + ol * q2 / 8; rp.push({ x: rx, y: ridgeY(rx) }); }\n    var turn = clamp01(-LIGHT * (rp[8].y - rp[0].y) / ol * 4 + 0.35);\n    if (rand() < 0.93) {\n      G.stroke(rp, { s: 0.7, p: 0.55 + 0.3 * turn, r: 0.6 + 0.8 * turn, k: 0.6, tin: 0.06, tout: 0.1 });\n      if (rand() < 0.35) G.stroke(rp.map(function (p) { return { x: p.x, y: p.y + 0.9 }; }), { s: 0.75, p: 0.5, r: 0.55, k: 0.5, tin: 0.2, tout: 0.2 });\n    }\n    ox += ol + rr(0, 6);\n  }\n  if (rand() < 0.6) {\n    for (var st = 0, nst = 2 + (rand() * 5 | 0), sc = rr(0.1, 0.9); st < nst; st++) {\n      var tx = W * (sc + rr(-0.08, 0.08)), ty = ridgeY(tx), tsz = rr(3, 7);\n      G.stroke([{ x: tx, y: ty }, { x: tx, y: ty - tsz * 1.2 }], { s: 0.65, p: 0.6, r: 0.55, k: 0.6, tin: 0.02, tout: 0.3 });\n      for (var k1 = 0; k1 < 3; k1++) scribble(tx + rr(-1, 1) * tsz * 0.4, ty - tsz * rr(1, 1.8), tsz * 0.5, { s: 0.55, p: 0.5, r: 0.5, k: 0.5 });\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // The mist: a flat lake of pale tone, the light of the sheet as it is in the\n  // references (Bury Hill, Friedrich's Madonna). Hard lead laid level in long\n  // overlapping lengths, so it goes light and smooth, then stumped; hard-lead\n  // hairlines across it that shrink and close up toward its far edge, so it\n  // lies flat. Its hard top edge is the dark slope foot standing on it. v1\n  // laid soft lead and it went to grey static darker than the slope.\n  // ---------------------------------------------------------------------------\n  function lap(x, y) { return 1 - smooth(bankY(x) - 4, bankY(x) + 16, y); }\n  function inMist(x, y) { return y > topY(x) + 0.5 && lap(x, y) > 0.02; }\n  for (var my = MTOP + 1; my < MBOT + 18; my += rr(2.8, 4.2) * (0.55 + 0.6 * mistDepth(my))) {\n    var dep = mistDepth(my);\n    for (var mx = -rr(0, 300); mx < W + 10;) {\n      var ml = rr(120, 420) * (0.45 + 0.55 * dep);\n      runs(mx, mx + ml, my + rr(-0.6, 0.6), inMist).forEach(function (rn) {\n        var dmin = 1e9, x;\n        for (x = rn[0]; x <= rn[1]; x += 6) dmin = Math.min(dmin, my - topY(x));\n        var r = Math.min(rr(5, 8), Math.max(0.6, dmin * 0.85)), xm = (rn[0] + rn[1]) / 2;\n        var mott = noise(xm / 380, my / 70, seed + 601) - 0.5;\n        G.stroke(bowed({ x: rn[0], y: my }, { x: rn[1], y: my + rr(-0.5, 0.5) }, 0.004),\n          { s: 0.15, p: (0.32 + 0.08 * dep + 0.08 * mott) * (0.4 + 0.6 * lap(xm, my)), r: r, k: 0.2, tin: 0.2, tout: 0.25 });\n      });\n      mx += ml + rr(5, 50);\n    }\n  }\n  // Up to the edge with the point: three close lines following it.\n  [0.8, 2.1, 3.5].forEach(function (off) {\n    for (var ex = -10; ex < W + 10;) {\n      var el = rr(40, 160), ep = [];\n      for (var q3 = 0; q3 <= 8; q3++) { var exx = ex + el * q3 / 8; ep.push({ x: exx, y: topY(exx) + off }); }\n      G.stroke(ep, { s: 0.2, p: 0.38 - 0.03 * off, r: 0.65, k: 0.4, tin: 0.04, tout: 0.06 });\n      ex += el + rr(0, 3);\n    }\n  });\n  // Celmins's hairlines: close and short at the far edge, long and open near.\n  for (var hy = MTOP + 3; hy < MBOT; hy += (1.8 + 9 * mistDepth(hy)) * rr(0.7, 1.3)) {\n    var hdp = mistDepth(hy), hl = 15 + 190 * Math.pow(hdp, 1.3);\n    for (var hx2 = -rr(0, hl); hx2 < W + 10; hx2 += hl * rr(1.1, 2.2)) {\n      if (rand() > 0.55) continue;\n      runs(hx2, hx2 + hl * rr(0.6, 1.2), hy, inMist).forEach(function (rn) {\n        G.stroke([{ x: rn[0], y: hy }, { x: rn[1], y: hy + rr(-0.3, 0.3) }], { s: 0.15, p: 0.55 + 0.2 * hdp, r: 0.45, k: 0.5, tin: 0.1, tout: 0.2 });\n      });\n    }\n  }\n  // A few softer dashes, so the surface is not one weave.\n  for (var sd = 0; sd < 140; sd++) {\n    var dy = rr(MTOP + 4, MBOT), ddp = mistDepth(dy), dx = rr(-20, W), dl = rr(8, 40) * (0.3 + ddp);\n    if (!inMist(dx, dy) || !inMist(dx + dl, dy)) continue;\n    G.stroke([{ x: dx, y: dy }, { x: dx + dl, y: dy + rr(-0.3, 0.3) }], { s: 0.55, p: 0.3 + 0.1 * ddp, r: 0.8, k: 0.45, tin: 0.2, tout: 0.3 });\n  }\n  // The stump through the body of the lake, clear of its top edge, so the tone\n  // goes even while the grain still shows.\n  G.blend(function (x, y) {\n    var e = topY(x);\n    var m = 0.8 * smooth(e + 3, e + 9, y) * (1 - smooth(bankY(x) - 10, bankY(x), y));\n    // A lighter pass over the slope turns its tone from line toward mass.\n    var sl = 0.35 * smooth(ridgeY(x) - 2, ridgeY(x) + 10, y) * (1 - smooth(e - 6, e - 1, y));\n    return Math.max(m, sl);\n  }, 2.5);\n\n  // ---------------------------------------------------------------------------\n  // The hedgerow trees: in lines across the valley floor, standing up out of\n  // the mist. Below its top a tree is lost: its lead fades to nothing over a\n  // metre or two, so its foot goes by value into the tone around it.\n  // ---------------------------------------------------------------------------\n  var trees = [];\n  function addTree(X, Z, big) {\n    var p = scr(X, 0, Z);\n    if (p.x < -80 || p.x > W + 80) return;\n    if (mistY(Z) > bankY(p.x) - 3 || Z > col(ZFA, p.x) - 15) return;\n    trees.push({ X: X, Z: Z, h: big ? rr(16, 24) : rr(11, 18), big: !!big, seed: (rand() * 1e6) | 0, lean: rr(-0.08, 0.08) });\n  }\n  var hedges = [];\n  for (var hi = 0, nh = 2 + (rand() < 0.6 ? 1 : 0); hi < nh; hi++) {\n    var across = hi === 0 ? rand() < 0.5 : rand() < 0.3;\n    var HZ0 = rr(70, 300), HX0 = rr(-0.45, 0.45) * HZ0 * W / F, ang = across ? rr(1.35, 1.75) : rr(-0.65, 0.65);\n    hedges.push({ Z0: HZ0, X0: HX0, ang: ang });\n    // Wide spacing and frequent gaps: an even step read as a planted orchard.\n    for (var ts = -700; ts < 700; ts += rr(8, 46)) {\n      if (rand() < 0.3) continue;\n      var tz = HZ0 + ts * Math.cos(ang);\n      if (tz > 25) addTree(HX0 + ts * Math.sin(ang), tz, false);\n    }\n  }\n  if (rand() < 0.85) {\n    // One tree nearer than the rest, on a third, tried a few times over: a\n    // single attempt often fell behind the bank's edge and was dropped, and\n    // the sheet lost its foreground tree altogether (v4, seed 1793).\n    for (var la = 0, before = trees.length; la < 8 && trees.length === before; la++) {\n      var lx = W * (rand() < 0.5 ? rr(0.16, 0.38) : rr(0.62, 0.84));\n      var lz = F * (EYE - MT) / (bankY(lx) - HZ) * rr(1.05, 1.4);\n      addTree((lx - W / 2) * lz / F, lz, true);\n    }\n  }\n  trees.sort(function (a, b) { return b.Z - a.Z; });\n  // The hedges themselves are drowned, but here and there a crest just breaks\n  // the surface as a broken line of small touches lying exactly on it. It is\n  // what the photographs of a fog lake show, and it gives the flat middle of\n  // the sheet something to read as well as proving the surface is level.\n  hedges.forEach(function (hg) {\n    if (rand() < 0.35) return;\n    for (var t = -700; t < 700; t += rr(2, 9)) {\n      var cz = hg.Z0 + t * Math.cos(hg.ang), cxw = hg.X0 + t * Math.sin(hg.ang);\n      if (cz < 30) continue;\n      var cp = scr(cxw, 0, cz);\n      if (cp.x < -20 || cp.x > W + 20) continue;\n      var cy = mistY(cz);\n      if (cy > bankY(cp.x) - 3 || cy < topY(cp.x) + 2) continue;\n      if (rand() < 0.45) continue;                       // mostly under, breaking through in runs\n      var cl = Math.max(1.2, 0.5 * cp.s), cv = vis(cz);\n      G.stroke([{ x: cp.x, y: cy + rr(-0.3, 0.3) }, { x: cp.x + cl, y: cy + rr(-0.3, 0.3) }],\n        { s: 0.5, p: (0.3 + 0.25 * cv) * rr(0.6, 1.2), r: Math.max(0.4, 0.08 * cp.s), k: 0.45, tin: 0.15, tout: 0.2 });\n    }\n  });\n\n  function growTree(rnd, height, lean, depthMax) {\n    function r2(a, b) { return a + (b - a) * rnd(); }\n    var br = [];\n    function grow(x, y, a, len, r, depth) {\n      var n = Math.max(3, Math.round(len / 0.22)), pts = [{ x: x, y: y, r: r }];\n      var bend = (rnd() - 0.5) * 0.06, sweep = 0.02 + 0.05 * depth / depthMax, rEnd = r * 0.78;\n      for (var k = 1; k <= n; k++) {\n        a += bend + (rnd() - 0.5) * 0.14 + (Math.PI / 2 - a) * sweep;\n        x += Math.cos(a) * len / n; y += Math.sin(a) * len / n;\n        pts.push({ x: x, y: y, r: r + (rEnd - r) * k / n });\n      }\n      br.push({ pts: pts, depth: depth });\n      if (depth >= depthMax || rEnd < 0.008) return;\n      var nl = depth === 0 ? 0 : 1 + (rnd() < 0.6 ? 1 : 0) + (depth > 2 && rnd() < 0.5 ? 1 : 0);\n      for (var j = 0; j < nl; j++) {\n        var kk = Math.floor(n * r2(0.35, 0.8)), p = pts[kk], side = rnd() < 0.5 ? -1 : 1;\n        var dir = Math.atan2(pts[kk + 1].y - p.y, pts[kk + 1].x - p.x);\n        grow(p.x, p.y, dir + side * r2(0.6, 1.0), len * r2(0.4, 0.6), p.r * 0.5, depth + 1);\n      }\n      var spread = r2(0.35, 0.7), share = r2(0.5, 0.7), tilt = (rnd() - 0.5) * 0.3;\n      grow(x, y, a + spread * (1 - share) + tilt, len * r2(0.65, 0.85), rEnd * Math.sqrt(share), depth + 1);\n      grow(x, y, a - spread * share + tilt, len * r2(0.6, 0.8), rEnd * Math.sqrt(1 - share), depth + 1);\n    }\n    grow(0, 0, Math.PI / 2 + lean, height * 0.34, height * 0.028, 0);\n    var top = 0;\n    br.forEach(function (b) { b.pts.forEach(function (p) { if (p.y > top) top = p.y; }); });\n    var kk = height / top;\n    br.forEach(function (b) { b.pts.forEach(function (p) { p.x *= kk; p.y *= kk; p.r *= kk; }); });\n    return br;\n  }\n  function offsetLine(P, q) {\n    return P.map(function (p, i) {\n      var a = P[Math.max(0, i - 1)], b = P[Math.min(P.length - 1, i + 1)];\n      var tx = b.x - a.x, ty = b.y - a.y, l = Math.hypot(tx, ty) || 1;\n      return { x: p.x - ty / l * q * p.r, y: p.y + tx / l * q * p.r };\n    });\n  }\n  var lifts = [];                                        // eraser work, done last\n  // Dawn shadows first: each tree lays a faint lane across the mist's top,\n  // away from the light, which is what says the top is flat.\n  trees.forEach(function (tr) {\n    var s = F / tr.Z, hc = (tr.h - MT) * s, ey = mistY(tr.Z), ex = scr(tr.X, 0, tr.Z).x;\n    if (hc < 4) return;\n    var sl = hc * rr(1.6, 2.8);\n    for (var k = 0; k < 3; k++) {\n      G.stroke([{ x: ex, y: ey + 1 + k }, { x: ex - LIGHT * sl, y: ey + 1 + k + hc * 0.1 }],\n        { s: 0.8, p: 0.14 * vis(tr.Z), r: Math.max(1, Math.min(6, hc * 0.06)), k: 0.3, tin: 0.05, tout: 0.6 });\n    }\n  });\n  trees.forEach(function (tr) {\n    // The dawn is behind the hollow, so even a far tree keeps most of its\n    // weight: v3's haze took the mid-ground trees down to pale scrub.\n    // The near tree is the firmest thing on the sheet and the only one drawn\n    // at full pressure (Seurat: only the nearest trunk firm).\n    var base = scr(tr.X, 0, tr.Z), s = base.s, near = clamp01((260 - tr.Z) / 200);\n    var v = (0.55 + 0.45 * vis(tr.Z)) * (tr.big ? 1.15 : 1);\n    var dm = tr.Z < 110 ? 7 : tr.Z < 250 ? 6 : 5;\n    // Far trees are ticks with a little mass on top, not drawn trees: at this\n    // size v2's branches and twig haze read as scrub (Friedrich's far conifers).\n    var crown = (tr.h - MT) * s;\n    if (crown < 26) {\n      var ty0 = mistY(tr.Z);\n      G.stroke([{ x: base.x, y: ty0 + 1 }, { x: base.x + rr(-1, 1), y: ty0 - crown }],\n        { s: 0.8, p: 0.8 * v, r: Math.max(0.4, crown * 0.035), k: 0.6, tin: 0.02, tout: 0.25 });\n      for (var ks = 0, nks = 2 + (rand() * 3 | 0); ks < nks; ks++) {\n        scribble(base.x + rr(-0.3, 0.3) * crown, ty0 - crown * rr(0.45, 0.95), crown * rr(0.18, 0.33),\n          { s: 0.8, p: 0.7 * v, r: Math.max(0.4, crown * 0.03), k: 0.55, tin: 0.05, tout: 0.2 });\n      }\n      return;\n    }\n    growTree(G.rngFrom(tr.seed), tr.h, tr.lean, dm).forEach(function (b) {\n      var P = [];\n      b.pts.forEach(function (p) {\n        var f = smooth(MT - 1.6, MT + 0.8, p.y);\n        if (f > 0.02 || P.length) P.push({ x: base.x + p.x * s, y: base.y - p.y * s, r: p.r * s, f: f });\n      });\n      if (P.length < 2) return;\n      var r0 = P[0].r, f0 = P[0].f, f1 = P[P.length - 1].f;\n      if (r0 > 1.3) {\n        // Trunk and big limbs filled along their length, darker at the rims\n        // and on the side away from the dawn.\n        var nL = Math.ceil(2 * r0 / 0.9);\n        for (var j = 0; j < nL; j++) {\n          // A silhouette against the lit mist, and the firmest thing on the\n          // sheet: the near trunk is where the drawing earns its darks.\n          var q = -1 + (2 * j + 1) / nL, pr = (1 + 0.1 * Math.pow(Math.abs(q), 1.5) + 0.1 * Math.max(0, -LIGHT * q)) * v;\n          G.stroke(offsetLine(P, q), { s: 1, p: pr * f0, p1: pr * f1, r: 0.8, k: 0.75, tin: 0.02, tout: 0.05 });\n        }\n        if (b.depth <= 1) lifts.push({ pts: offsetLine(P, 0.5 * LIGHT), r: Math.max(0.8, r0 * 0.12), e: 0.3 });\n      } else if (r0 > 0.5) {\n        var pb = Math.max(0.6, 1 - 0.05 * b.depth) * v;\n        G.stroke(P, { s: Math.max(0.4, 0.6 + 0.4 * near - 0.05 * b.depth), p: pb * f0, p1: pb * f1,\n          r: Math.max(0.4, Math.min(1.3, r0 * 0.9)), k: 0.65, tin: 0.03, tout: 0.3 });\n      } else {\n        // Twigs: hairlines of harder lead.\n        G.stroke(P, { s: 0.3, p: 0.72 * v * f0, p1: 0.72 * v * f1, r: 0.35, k: 0.55, tin: 0.05, tout: 0.3 });\n      }\n      // The twig haze at the crown's edge, in light touches.\n      if (b.depth >= dm - 1 && rand() < 0.25 && f1 > 0.5 && 0.45 * s > 3) {\n        var e = P[P.length - 1];\n        scribble(e.x, e.y, Math.max(1.5, Math.min(7, 0.45 * s)), { s: 0.55, p: 0.22 * v, r: 0.4, k: 0.45, tin: 0.1, tout: 0.2 });\n      }\n    });\n    // The mist's top wrapping the trunk where it goes in.\n    var ey = mistY(tr.Z), ew = 0.9 * s + 3;\n    lifts.push({ pts: [{ x: base.x - ew, y: ey - 0.5 }, { x: base.x + ew, y: ey - 0.5 }], r: Math.max(0.8, Math.min(3, 0.25 * s)), e: 0.3 });\n  });\n\n  // ---------------------------------------------------------------------------\n  // The near bank: rough grass going down into the mist, drawn in sweeps along\n  // the fall of the bank, tufts of two to four blades, a dark foot worked until\n  // it polishes, and a few dead dock stalks. It fades into the mist at its\n  // lower edge by value.\n  // ---------------------------------------------------------------------------\n  function bankFade(x, y) { var b = bankY(x); return smooth(b - 2, b + 14 + 30 * bankNear(x, y), y); }\n  function bankPass(o) {\n    for (var gy = MTOP + (MBOT - MTOP) * 0.3; gy < H + 20;) {\n      var gxStep = 0;\n      for (var gx = -rr(0, 60); gx < W + 10; gx += gxStep) {\n        var x = gx + rr(-3, 3), y = gy + rr(-1, 1), near = bankNear(x, y);\n        var len = o.len0 + o.len1 * Math.pow(near, 1.3);\n        gxStep = len * rr(o.g0, o.g1);\n        if (y <= bankY(x)) continue;\n        // Direction wanders with the lie of the ground: v1's level rows of\n        // dashes read as a ruled, planted field.\n        var fd = bankFade(x, y), l = len * rr(0.7, 1.2);\n        var a = bankDir(x, y) + 0.5 * (noise(x / 200, y / 120, seed + 520) - 0.5) + rr(-o.jit, o.jit);\n        G.stroke(bowed({ x: x, y: y + rr(-o.yj, o.yj) }, { x: x + Math.cos(a) * l, y: y + Math.sin(a) * l }, 0.08),\n          { s: o.s0 + o.s1 * near, p: (o.p0 + o.p1 * near) * fd, r: o.r0 + o.r1 * near, k: o.k, tin: 0.1, tout: 0.3 });\n      }\n      gy += o.sp0 + o.sp1 * Math.pow(1 - bankNear(W / 2, gy), 2);\n    }\n  }\n  // A mid tone under the grass, laid broad and overlapping, so the frost has\n  // something to be lifted out of: v1's pale bank took no lift-outs.\n  bankPass({ len0: 40, len1: 260, sp0: 5, sp1: 6, g0: 0.35, g1: 0.8, s0: 0.6, s1: 0.3, p0: 0.18, p1: 0.35, r0: 2, r1: 4, k: 0.3, jit: 0.2, yj: 3 });\n  bankPass({ len0: 8, len1: 160, sp0: 3.5, sp1: 5, g0: 1.0, g1: 2.4, s0: 0.35, s1: 0.6, p0: 0.3, p1: 0.5, r0: 0.5, r1: 1.2, k: 0.6, jit: 0.3, yj: 3 });\n  // The dark foot, on the side away from the dawn, graded over a wide span.\n  // Weighted to the foot of the sheet and to the side away from the dawn.\n  // 🔴 Graded on the sheet, never on the bank's edge: v7 followed the curve of\n  // bankY and the dark read as a dome sitting in the foreground (the same\n  // mound Field Edge's v4 drew). v2 graded it only downward and it lay across\n  // the foot as a stripe.\n  var CX = W * rr(0.45, 0.95);\n  function foot(x, y) {\n    var down = smooth(H * 0.62, H * 1.02, y + 30 * (noise(x / 320, 2.7, seed + 530) - 0.5));\n    var side = LIGHT > 0 ? smooth(CX, 0, x) : smooth(W - CX, W, x);\n    return Math.pow(down, 1.25) * (0.35 + 0.65 * side);\n  }\n  // Worked over three times until the lead saturates and polishes: this corner\n  // is the one place the sheet goes as dark as graphite goes, which is not\n  // black. Erin on v6: \"Too faint. Make some of it stand out.\"\n  [0, 0.12, -0.1].forEach(function (off) {\n    for (var fy = MTOP; fy < H + 10; fy += 1.8) {\n      for (var fx = -10; fx < W + 10; fx += rr(18, 45)) {\n        var fc = foot(fx, fy);\n        if (fc < 0.03 || fy <= bankY(fx) + 2 || rand() > Math.pow(fc, 0.4)) continue;\n        var fdir = bankDir(fx, fy) + off + rr(-0.15, 0.15), fl = rr(40, 110);\n        G.stroke(bowed({ x: fx, y: fy + rr(-2, 2) }, { x: fx + Math.cos(fdir) * fl, y: fy + Math.sin(fdir) * fl }, 0.05),\n          { s: 1, p: 0.7 + 0.3 * fc, r: 1.4, k: 0.9, tin: 0.1, tout: 0.3 });\n      }\n    }\n  });\n  /**\n   * A clump: one to six blades of mixed length from one root, some laid over\n   * by the night. v1's even two-to-four-blade fans stood in rows like seedlings.\n   */\n  function tuft(x, y, len, o, fn) {\n    var n = 1 + (rand() * 6 | 0), lean = rr(-0.3, 0.3);\n    for (var b = 0; b < n; b++) {\n      var a = -Math.PI / 2 + lean + rr(-0.5, 0.5);\n      if (rand() < 0.25) a += (rand() < 0.5 ? -1 : 1) * rr(0.6, 1.1);\n      var l = len * rr(0.4, 1.3), bend = rr(-0.4, 0.4) + (a + Math.PI / 2) * 0.4;\n      var m = { x: x + Math.cos(a) * l * 0.55, y: y + Math.sin(a) * l * 0.55 };\n      var e = { x: x + Math.cos(a + bend) * l, y: y + Math.sin(a + bend) * l };\n      fn([{ x: x, y: y }, m, e], o);\n    }\n  }\n  for (var tf = 0; tf < 2600; tf++) {\n    var tx2 = rr(-10, W + 10), bY = bankY(tx2), ty2 = bY + (H + 10 - bY) * Math.pow(rand(), 0.85);\n    var tn = bankNear(tx2, ty2), tfd = bankFade(tx2, ty2);\n    // Paper between the marks up the bank, where the ground turns away: v5\n    // covered the whole bank evenly and it read as a thicket.\n    if (tfd < 0.1 || rand() > 0.35 + 0.65 * tn) continue;\n    tuft(tx2, ty2, (3 + 46 * Math.pow(tn, 1.3)) * rr(0.5, 1.3), { s: 0.55 + 0.45 * tn, p: (0.5 + 0.55 * tn) * tfd, r: 0.4 + 0.6 * tn, k: 0.6, tin: 0.02, tout: 0.4 }, G.stroke);\n  }\n  // Big tussocks in the front: without a change of scale toward the eye the\n  // whole bank reads as one carpet of small marks (v3).\n  for (var tg = 0, ntg = 90 + (rand() * 60 | 0); tg < ntg; tg++) {\n    var gx0 = rr(-20, W + 20), gb = bankY(gx0), gy0 = gb + (H + 20 - gb) * Math.pow(rand(), 0.5);\n    var gn = bankNear(gx0, gy0);\n    if (gn < 0.45) continue;\n    tuft(gx0, gy0, (30 + 70 * gn) * rr(0.7, 1.4), { s: 0.9, p: 0.9 + 0.1 * gn, r: 0.8 + 1.2 * gn, k: 0.8, tin: 0.02, tout: 0.45 }, G.stroke);\n  }\n  // Dead dock stalks, the darkest single strokes on the sheet, each with its\n  // seed heads in clusters up the top half.\n  for (var dk2 = 0, nd = rand() < 0.9 ? 2 + (rand() * 4 | 0) : 0, dcx = rr(0.1, 0.9); dk2 < nd; dk2++) {\n    var dxx = W * clamp01(dcx + rr(-0.12, 0.12)), dbY = bankY(dxx), dyy = dbY + (H + 30 - dbY) * rr(0.55, 1);\n    var dn = bankNear(dxx, Math.min(H, dyy)), dh = rr(90, 260) * dn, dl2 = rr(-0.12, 0.12), stem = [];\n    for (var q4 = 0; q4 <= 10; q4++) {\n      var f4 = q4 / 10;\n      stem.push({ x: dxx + Math.sin(dl2) * dh * f4 + 6 * Math.sin(f4 * 3 + dk2) * f4, y: dyy - dh * f4 });\n    }\n    var dr = Math.max(0.6, 1.6 * dn);\n    G.stroke(stem, { s: 1, p: 1, r: dr, k: 0.85, tin: 0.02, tout: 0.2 });\n    for (var sb = 0; sb < 9; sb++) {\n      var at = stem[4 + (rand() * 6 | 0)], sa = -Math.PI / 2 + (rand() < 0.5 ? -1 : 1) * rr(0.3, 0.7), sl2 = rr(0.05, 0.12) * dh;\n      var se = { x: at.x + Math.cos(sa) * sl2, y: at.y + Math.sin(sa) * sl2 };\n      G.stroke([at, se], { s: 0.95, p: 0.92, r: dr * 0.5, k: 0.7, tin: 0.02, tout: 0.3 });\n      for (var sh2 = 0; sh2 < 3; sh2++) scribble(se.x + rr(-2, 2), se.y + rr(-3, 4), rr(2, 4) * (0.5 + dn), { s: 0.95, p: 0.9, r: 0.6, k: 0.7 });\n    }\n    lifts.push({ pts: stem.map(function (p) { return { x: p.x + LIGHT * dr * 0.4, y: p.y }; }), r: Math.max(0.5, dr * 0.35), e: 0.55 });\n  }\n\n  // ---------------------------------------------------------------------------\n  // The stump along the bank's edge, so bank and mist meet at one value.\n  // ---------------------------------------------------------------------------\n  G.blend(function (x, y) { var b = (y - bankY(x)) / 14; return 0.65 * Math.exp(-b * b); }, 2.5);\n\n  // ---------------------------------------------------------------------------\n  // The kneaded eraser. A lit rim under the mist's top edge and a few level\n  // drifts of light across the lake; the light side of the near trunks; then\n  // the frost: blades, level drags and points lifted out of the grass, thickest\n  // toward the mist where the hollow is coldest.\n  // ---------------------------------------------------------------------------\n  for (var rx2 = -10; rx2 < W + 10;) {\n    var rl = rr(60, 240), rpts = [];\n    for (var q5 = 0; q5 <= 10; q5++) { var rxx = rx2 + rl * q5 / 10; rpts.push({ x: rxx, y: topY(rxx) + 2.6 }); }\n    if (rand() < 0.85) G.erase(rpts, { r: 1.5, e: 0.45 });\n    rx2 += rl + rr(0, 30);\n  }\n  for (var dr2 = 0, ndr = 5 + (rand() * 8 | 0); dr2 < ndr; dr2++) {\n    var dry = rr(MTOP + 8, MBOT - 10), ddp2 = mistDepth(dry), drl = rr(80, 400) * (0.4 + ddp2), drx = rr(-100, W);\n    runs(drx, drx + drl, dry, inMist).forEach(function (rn) {\n      G.erase([{ x: rn[0], y: dry }, { x: rn[1], y: dry + rr(-0.4, 0.4) }], { r: 1 + 2.5 * ddp2, e: 0.22 });\n    });\n  }\n  lifts.forEach(function (l) { G.erase(l.pts, { r: l.r, e: l.e }); });\n  for (var fr = 0; fr < 3200; fr++) {\n    var fx2 = rr(-10, W + 10), fbY = bankY(fx2), fy2 = fbY + (H + 10 - fbY) * Math.pow(rand(), 1.1);\n    var fn2 = bankNear(fx2, fy2), cold = 1 - 0.45 * fn2;\n    if (rand() > cold) continue;\n    var kind = rand();\n    if (kind < 0.55) {\n      tuft(fx2, fy2, (4 + 55 * Math.pow(fn2, 1.2)) * rr(0.5, 1.3), { r: 0.6 + 1.1 * fn2, e: 0.85 }, G.erase);\n    } else if (kind < 0.85) {\n      var fa = bankDir(fx2, fy2) + rr(-0.1, 0.1), fl2 = rr(6, 30) * (0.4 + fn2);\n      G.erase([{ x: fx2, y: fy2 }, { x: fx2 + Math.cos(fa) * fl2, y: fy2 + Math.sin(fa) * fl2 }], { r: 0.8 + 1.6 * fn2, e: 0.4 });\n    } else {\n      G.erase([{ x: fx2, y: fy2 }, { x: fx2 + rr(1, 3), y: fy2 - rr(0, 1) }], { r: 0.7 + 0.5 * fn2, e: 0.9 });\n    }\n  }\n\n  G.finish(pal[0], pal[pal.length - 1]);\n}\n",
 "layers": []
}
