{
 "genart": "1.2",
 "id": "field-edge",
 "title": "Field Edge in Low Cloud",
 "created": "2026-09-11T00:00:00Z",
 "modified": "2026-09-11T20:24:27.463Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1500,
  "height": 1000,
  "pixelDensity": 2
 },
 "parameters": [],
 "colors": [],
 "state": {
  "seed": 1821,
  "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 1: Field Edge in Low Cloud.\n//\n// A field's edge on a day of low cloud. A hedge runs away from the eye on a\n// diagonal into the cloud, one bare tree stands in it on the left third, and\n// the far half of the sheet is lost in fog. The weather is the subject; the\n// land is what it acts on.\n//\n// Everything is placed through one level camera, in metres, so the hedge, the\n// tree and the rows of the field agree on a horizon and on the size their\n// distance gives them. The seed moves the horizon, the tree and its branching,\n// the hedge's angle and profile, and how far one can see.\n//\n// Distance is fewer marks, never greyer ones: the fog thins the drawing by\n// dropping marks and changing to harder lead, and an edge is lost where two\n// values meet, never by blurring. The darks are earned in the near field and\n// the tree; the light is the fog where the hedge goes out.\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 + 7919);\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  // --- The camera: level, the eye 1.6 m up, a low horizon so the cloud has room.\n  var EYE = 1.6, F = W * 0.78;\n  var HZ = H * rr(0.55, 0.66);\n  function at(X, Y, Z) { return { x: W / 2 + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n  var V = rr(70, 140);                                   // how far one can see, m\n  function vis(Z) { return Math.exp(-Math.pow(Z / V, 1.5)); }\n\n  // --- The tree fixes the framing: its foot on the left third, its crown just\n  // under the top edge. Its distance follows from those two.\n  // Ranges set wide enough that each seed moves the composition (qc measured\n  // 0.0245 structural difference with narrower ones, floor 0.03), and narrow\n  // enough that the tree stays on the left third.\n  var treeH = rr(10, 15), treeTopY = H * rr(0.03, 0.14), treeX = W * rr(0.24, 0.40);\n  var ZT = F * (treeH - EYE) / (HZ - treeTopY);\n  var XT = (treeX - W / 2) * ZT / F;\n\n  // --- The hedge: a straight field boundary through the tree's foot, coming in\n  // at the left edge and running away to the right.\n  var ANG = rr(0.18, 0.46), TA = Math.tan(ANG);\n  function hedgeX(Z) { return XT + 0.5 + (Z - ZT) * TA; }\n  // Start the hedge well outside the left edge (v3 began it in frame and left a notch).\n  var ZH0 = Math.max(3, 0.7 * (XT + 0.5 - ZT * TA) / (-(W / 2) / F - TA));\n  var ZH1 = V * 3;\n  var VPX = W / 2 + F * TA;                              // where the hedge and the rows vanish\n  function hedgeTop(Z) {\n    var u = (Z - ZH0) / Math.cos(ANG);\n    return 2.1 + 1.1 * (noise(u / 6, 3.7, seed + 101) - 0.5) + 0.7 * (noise(u / 1.7, 9.1, seed + 102) - 0.5);\n  }\n\n  // The lightest place on the sheet: the fog where the hedge goes out.\n  var GX = Math.min(W * 0.9, VPX + W * 0.04), GY = HZ - H * 0.03;\n  function glow(x, y) {\n    var dx = (x - GX) / (W * 0.34), dy = (y - GY) / (H * 0.22);\n    return Math.exp(-dx * dx - dy * dy);\n  }\n\n  /**\n   * Parallel strokes at `angle` over the band y0..y1, broken into lengths with\n   * gaps, each handed to fn(a, b) with its two ends.\n   */\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);\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\n  // ---------------------------------------------------------------------------\n  // Low cloud: stratus, so nearly level, rising a little with the wind. Soft\n  // lead used lightly, so the sky is grain; the cloud's bodies are stretched\n  // level and heaviest overhead, and crossed only where they are thick. v1's\n  // steep all-over diagonals read as rain.\n  // ---------------------------------------------------------------------------\n  function cloud(x, y) {\n    var top = Math.pow(clamp01(1 - y / HZ), 0.7);\n    var m = noise(x / 300, y / 90, seed + 201) * 0.6 + noise(x / 110, y / 40, seed + 202) * 0.4;\n    var mass = smooth(0.3, 0.75, m);\n    return clamp01(0.25 * top + 0.75 * mass * (0.3 + 0.7 * top)) * (1 - 0.85 * glow(x, y));\n  }\n  var SKY = -rr(0.05, 0.14);\n  // The cloud's bodies first, with the side of the lead: v3 built the whole sky\n  // from fine level lines and it read as ruled scan lines, not cloud.\n  for (var cb = 0; cb < 110; cb++) {\n    var cx0 = rr(-200, W), cy0 = rr(-20, HZ * 0.85), c0 = cloud(cx0, cy0);\n    if (c0 < 0.2) continue;\n    var cl = rr(150, 500);\n    G.stroke(bowed({ x: cx0, y: cy0 }, { x: cx0 + Math.cos(SKY) * cl, y: cy0 + Math.sin(SKY) * cl }, 0.05),\n      { s: 0.85, p: 0.12 + 0.4 * c0, r: rr(5, 11), k: 0.35, tin: 0.2, tout: 0.3 });\n  }\n  hatch(SKY, 5.5, 60, 260, 12, 80, -20, HZ + H * 0.03, function (a, b) {\n    var ca = cloud(a.x, a.y), cz = cloud(b.x, b.y);\n    if (ca + cz < 0.08) return;\n    G.stroke(bowed(a, b, 0.02), { s: 0.72, p: 0.08 + 0.55 * ca, p1: 0.08 + 0.55 * cz, r: 1.1, k: 0.5, tin: 0.08, tout: 0.15 });\n  });\n  hatch(SKY - 0.38, 4.5, 40, 160, 10, 60, -20, HZ, function (a, b) {\n    var c = cloud((a.x + b.x) / 2, (a.y + b.y) / 2);\n    if (c < 0.4 || rand() > c) return;\n    G.stroke(bowed(a, b, 0.03), { s: 0.8, p: 0.5 * c, r: 1.0, k: 0.45 });\n  });\n\n  // ---------------------------------------------------------------------------\n  // Fog: level strokes of hard lead used firmly, so it is light and smooth,\n  // thickest in a band along the horizon. Hard lead cannot darken what a soft\n  // one laid, so it lies over the near hedge and changes nothing there.\n  // ---------------------------------------------------------------------------\n  hatch(rr(-0.02, 0.02), 3.2, 60, 260, 10, 70, HZ - H * 0.2, HZ + H * 0.16, function (a, b) {\n    var band = Math.exp(-Math.pow(((a.y + b.y) / 2 - HZ) / (H * 0.1), 2));\n    if (rand() > 0.45 + 0.55 * band) return;\n    G.stroke(bowed(a, b, 0.01), { s: 0.12, p: 0.5 + 0.2 * band, r: 1.3, k: 0.3 + 0.2 * band, tin: 0.15, tout: 0.2 });\n  });\n\n  // ---------------------------------------------------------------------------\n  // The field: rough pasture in front of the hedge, drawn in sweeps that lie\n  // along the ground toward the vanishing point, flattened as a hand flattens\n  // them. v1 ruled the rows out to the vanishing point and they read as\n  // streaks. Near sweeps are long, soft and heavy; far ones short, hard and few.\n  // ---------------------------------------------------------------------------\n  function nearness(y) { return Math.pow(smooth(HZ, H, y), 0.6); }\n  function depthAt(y) { return y > HZ + 0.5 ? F * EYE / (y - HZ) : 1e6; }\n  /** Where the hedge's foot is at screen x, or -1 right of the vanishing point. */\n  var C0 = XT + 0.5 - ZT * TA;\n  function hedgeFootY(x) {\n    var den = (x - W / 2) / F - TA;\n    if (den >= 0) return -1;\n    var Z = C0 / den;\n    return Z > 0 ? HZ + F * EYE / Z : -1;\n  }\n  function onGround(x, y) { var f = hedgeFootY(x); return y > HZ + 1 && (f < 0 || y > f); }\n  /**\n   * One direction for the whole field, rising to the right: toward the\n   * vanishing point on the left, easing to nearly level past it. v2 folded the\n   * direction about the vanishing point and drew a chevron in the near right.\n   */\n  function groundDir(x, y) {\n    var a = Math.atan2(HZ - y, VPX - x);\n    if (a < -Math.PI / 2) a += Math.PI;\n    var w = smooth(VPX - W * 0.35, VPX + W * 0.05, x);\n    return (1 - w) * a * 0.5 - w * 0.07;\n  }\n  var WIND = rr(0.18, 0.32);                             // from the west: everything leans right\n  function groundPass(o) {\n    for (var gy = HZ + 2; gy < H + 20;) {\n      var near = nearness(gy), v = vis(depthAt(gy));\n      var len = o.len0 + o.len1 * Math.pow(near, 1.3);\n      for (var gx = -rr(0, len); gx < W + 10; gx += len * rr(o.g0, o.g1)) {\n        var x = gx + rr(-3, 3), y = gy + rr(-1, 1);\n        if (!onGround(x, y) || rand() > Math.pow(v, 0.7)) continue;\n        var a = groundDir(x, y) + rr(-0.08, 0.08), l = len * rr(0.7, 1.2);\n        var b = { x: x + Math.cos(a) * l, y: y + Math.sin(a) * l };\n        G.stroke(bowed({ x: x, y: y }, b, 0.04), { s: o.s0 + o.s1 * near, p: (o.p0 + o.p1 * near) * (0.35 + 0.65 * v),\n          r: o.r0 + o.r1 * near, k: o.k, tin: 0.1, tout: 0.3 });\n        if (o.stubble && near > 0.25 && rand() < 0.35) {\n          var hh = Math.min(34, 6 + 26 * near) * rr(0.5, 1), le = WIND + rr(-0.15, 0.15), sx = x + rr(0, l * 0.8);\n          var sy = y + Math.sin(a) * (sx - x) / Math.max(0.2, Math.cos(a));\n          G.stroke([{ x: sx, y: sy }, { x: sx + Math.sin(le) * hh * 0.4, y: sy - hh * 0.55 },\n            { x: sx + Math.sin(le) * hh, y: sy - Math.cos(le) * hh }],\n            { s: 0.5 + 0.45 * near, p: (0.35 + 0.5 * near) * v, r: 0.45 + 0.5 * near, k: 0.5, tin: 0.02, tout: 0.5 });\n        }\n      }\n      gy += o.sp0 + o.sp1 * Math.pow(1 - near, 2);\n    }\n  }\n  // The ground's tone first, broad and light, with the side of the lead: v2's\n  // heavier tone flooded the grain and the near field read as static.\n  groundPass({ len0: 30, len1: 160, sp0: 7, sp1: 10, g0: 0.5, g1: 1.1, s0: 0.7, s1: 0.2, p0: 0.12, p1: 0.25,\n    r0: 2, r1: 4, k: 0.3 });\n  // Then the sweeps, firm enough to fill the grain along their length so each\n  // one reads as a stroke, with paper between them, and the stubble in them.\n  groundPass({ len0: 10, len1: 150, sp0: 2.6, sp1: 8, g0: 0.9, g1: 1.8, s0: 0.35, s1: 0.6, p0: 0.35, p1: 0.55,\n    r0: 0.55, r1: 0.9, k: 0.7, stubble: true });\n\n  // ---------------------------------------------------------------------------\n  // The hedge: massed zig-zag touches, dark at the foot, broken at the top.\n  // With distance the touches get fewer and the lead harder, until they are\n  // separate touches and then nothing (Constable's lane at Staunton Harold).\n  // ---------------------------------------------------------------------------\n  // The mass under the touches: the side of the lead run ALONG the hedge at a\n  // few heights, in overlapping lengths whose width follows the distance, so\n  // the near hedge reads as one dark body (Seurat) and not loose scribble. v5\n  // laid it as vertical strokes per column; they met in pale seams and the\n  // hedge read as a row of panels.\n  // Each length's heights are jittered and its strokes wide enough to overlap\n  // their neighbours: v7's six fixed heights read as ruled stripes.\n  for (var hz0 = ZH0; hz0 < ZH1; hz0 *= rr(1.10, 1.16)) {\n    var hz1 = hz0 * 1.28, hv = vis(hz0 * 1.1);\n    if (hv < 0.06) break;\n    var hs = at(0, 0, hz0 * 1.1).s, hr = Math.max(1, Math.min(14, 0.24 * hs));\n    [0.08, 0.22, 0.36, 0.5, 0.64, 0.78].forEach(function (f0) {\n      if (rand() > hv) return;\n      var fy = f0 + rr(-0.07, 0.07), line = [];\n      for (var q = 0; q <= 6; q++) {\n        var z = hz0 + (hz1 - hz0) * q / 6, p = at(hedgeX(z), fy * hedgeTop(z) * rr(0.85, 1.1), z);\n        line.push({ x: p.x, y: p.y });\n      }\n      G.stroke(line, { s: 0.2 + 0.75 * hv, p: (0.95 - 0.3 * fy) * hv, r: hr * rr(0.8, 1.15), k: 0.5, tin: 0.15, tout: 0.2 });\n    });\n  }\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  for (var Z = ZH0; Z < ZH1; Z *= 1.011) {\n    var v = vis(Z);\n    if (v < 0.04) break;\n    var top = hedgeTop(Z), base = at(hedgeX(Z), 0, Z);\n    var size = Math.max(2.2, Math.min(24, 0.28 * base.s));\n    var n = Math.ceil(top * base.s / (0.5 * size));\n    for (var q = 0; q < n; q++) {\n      if (rand() > Math.pow(v, 0.8)) continue;\n      var fy = (q + rand()) / n;                         // 0 at the foot, 1 at the top\n      var pt = at(hedgeX(Z) + rr(-0.6, 0.6), fy * top, Z + rr(-0.6, 0.6));\n      var sz = size * (fy > 0.8 ? 0.7 : 1);\n      scribble(pt.x, pt.y, sz, { s: 0.25 + 0.65 * v, p: (0.6 + 0.35 * (1 - fy)) * (0.25 + 0.75 * v) * (fy > 0.85 ? 0.6 : 1),\n        r: Math.max(0.5, Math.min(1.6, sz * 0.07)), k: 0.5, tin: 0.05, tout: 0.15 });\n    }\n    // The dark verge in the hedge's shadow at its foot.\n    if (rand() < v) {\n      var gh = Math.min(30, rr(0.25, 0.5) * base.s);\n      G.stroke([{ x: base.x, y: base.y + 1 }, { x: base.x + Math.sin(WIND) * gh, y: base.y + 1 - Math.cos(WIND) * gh }],\n        { s: 0.9 * v + 0.1, p: 0.8 * v, r: Math.max(0.5, Math.min(1.4, 0.012 * base.s)), k: 0.6, tin: 0.02, tout: 0.5 });\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // Trees. Grown in metres, then scaled to the height asked for.\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;   // ash: the tips sweep up\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      // The fork: the thicker child keeps closer to the parent's line.\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  /** Offset a screen polyline sideways by q of its own half-width. */\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  /**\n   * Draw a tree. Near, its trunk and limbs are filled with strokes along their\n   * length, darker at the rims and on the side away from the light; far, they\n   * are open outlines only (Friedrich, Constable's far trees).\n   */\n  function drawTree(br, bx, by, s, height, v, open) {\n    br.forEach(function (b) {\n      var P = b.pts.map(function (p) { return { x: bx + p.x * s, y: by - p.y * s, r: p.r * s, hy: p.y }; });\n      var r0 = P[0].r;\n      // The crown's top goes a little into the low cloud.\n      var fade = v * (1 - 0.35 * smooth(0.55, 1, P[P.length - 1].hy / height));\n      if (r0 > 1.3 && !open) {\n        var nL = Math.ceil(2 * r0 / 0.9);\n        for (var j = 0; j < nL; j++) {\n          var q = -1 + (2 * j + 1) / nL;\n          // The nearest trunk is the one firm thing on the sheet (Seurat's Trees).\n          G.stroke(offsetLine(P, q), { s: 0.98, p: (0.85 + 0.15 * Math.pow(Math.abs(q), 1.5) + 0.1 * Math.max(0, q)) * fade,\n            r: 0.8, k: 0.55, tin: 0.02, tout: 0.05 });\n        }\n        if (b.depth <= 1) {\n          // Bark: short broken ticks across the trunk and the big limbs.\n          for (var t = 0; t < P.length - 1; t += 1) {\n            if (rand() > 0.5) continue;\n            var p0 = P[t], p1 = P[t + 1], tx = p1.x - p0.x, ty = p1.y - p0.y, l = Math.hypot(tx, ty) || 1;\n            var nx = -ty / l, ny = tx / l, q0 = rr(-0.9, 0), q1 = q0 + rr(0.3, 0.9), sag = rr(1, 3);\n            G.stroke([{ x: p0.x + nx * q0 * p0.r, y: p0.y + ny * q0 * p0.r },\n              { x: p0.x + nx * (q0 + q1) / 2 * p0.r, y: p0.y + ny * (q0 + q1) / 2 * p0.r + sag },\n              { x: p0.x + nx * q1 * p0.r, y: p0.y + ny * q1 * p0.r }], { s: 0.9, p: 0.65 * fade, r: 0.7, k: 0.5 });\n          }\n          lifts.push({ pts: offsetLine(P, -0.5), r: Math.max(0.8, r0 * 0.12), e: 0.3 });\n        }\n      } else if (r0 > 1.3) {\n        [-1, 1].forEach(function (q) {\n          G.stroke(offsetLine(P, q), { s: 0.4 + 0.4 * v, p: 0.6 * fade, r: 0.6, k: 0.5, tin: 0.05, tout: 0.1 });\n        });\n      } else {\n        G.stroke(P, { s: Math.max(0.35, 0.88 - 0.07 * b.depth), p: Math.max(0.4, 0.85 - 0.06 * b.depth) * fade,\n          r: Math.max(0.35, Math.min(1.3, r0 * 0.9)), k: 0.55, tin: 0.03, tout: 0.3 });\n      }\n    });\n  }\n  // Two hedgerow trees further along, fewer marks and harder lead with each.\n  [rr(2.0, 2.6), rr(3.6, 4.8)].forEach(function (m, i) {\n    var Zt = ZT * m, vt = vis(Zt);\n    if (vt < 0.12) return;\n    var ht = rr(8, 12), b = at(hedgeX(Zt), 0, Zt);\n    drawTree(growTree(G.rngFrom(seed * 97 + 13 + i), ht, -rr(0.02, 0.1), 5), b.x, b.y, b.s, ht, vt * 0.85, true);\n  });\n  // The near tree.\n  var tb = at(XT, 0, ZT);\n  drawTree(growTree(G.rngFrom(seed * 31 + 5), treeH, -rr(0.02, 0.08), 7), tb.x, tb.y, tb.s, treeH,\n    0.55 + 0.45 * vis(ZT), false);\n\n  // ---------------------------------------------------------------------------\n  // The near corner: soft lead worked hard across the rows until it will take\n  // no more, so it polishes to the sheen. The one place the drawing is black\n  // as graphite goes, which is not black.\n  // ---------------------------------------------------------------------------\n  // Graded toward the foot and the left over a wide span: v4's tighter mask\n  // gave the dark a hard diagonal top and it read as a mound.\n  var CX = W * rr(0.75, 1.3);                            // how far right the near dark reaches\n  function corner(x, y) { return Math.pow(smooth(HZ + H * 0.08, H, y), 1.5) * smooth(CX, 0, x); }\n  [0, 0.12].forEach(function (off) {\n    for (var gy = H * 0.64; gy < H + 10; gy += 2.4) {\n      for (var gx = -10; gx < W * 0.8; gx += rr(30, 70)) {\n        var c = corner(gx, gy);\n        if (c < 0.1 || rand() > Math.sqrt(c) || !onGround(gx, gy)) continue;\n        var dir = groundDir(gx, gy) + off + rr(-0.08, 0.08), ln = rr(40, 110);\n        G.stroke(bowed({ x: gx, y: gy }, { x: gx + Math.cos(dir) * ln, y: gy + Math.sin(dir) * ln }, 0.03),\n          { s: 1, p: 0.7 + 0.3 * c, r: 1.4, k: 0.8, tin: 0.1, tout: 0.3 });\n      }\n    }\n  });\n\n  // ---------------------------------------------------------------------------\n  // The stump through the fog band, so the far hedge, the fog and the cloud\n  // meet at one value and their edges go.\n  // ---------------------------------------------------------------------------\n  // A lighter pass over the upper sky turns its lines toward tone, as the\n  // cloud in Snižina is mottled tone and not line.\n  G.blend(function (x, y) {\n    var b = (y - GY) / (H * 0.12);\n    return Math.max(0.85 * Math.exp(-b * b) * smooth(W * 0.25, W * 0.8, x), 0.35 * smooth(HZ * 0.9, HZ * 0.5, y));\n  }, 3);\n\n  // ---------------------------------------------------------------------------\n  // The kneaded eraser: mist lying across the far hedge, the light side of the\n  // trunk, and stalks in the dark corner picked out as they catch the light.\n  // ---------------------------------------------------------------------------\n  var mist = [];\n  for (Z = V * 0.5; Z < V * 2; Z *= 1.05) { var mp = at(hedgeX(Z) + 1, rr(0.4, 0.9), Z); mist.push({ x: mp.x, y: mp.y }); }\n  G.erase(mist, { r: 4, e: 0.35 });\n  lifts.forEach(function (l) { G.erase(l.pts, { r: l.r, e: l.e }); });\n  for (var fs = 0; fs < 110; fs++) {\n    var sx = rr(0, W * 0.8), sy = rr(H * 0.72, H);\n    if (corner(sx, sy) < 0.25) continue;\n    var sl = rr(12, 40), le = WIND + rr(-0.2, 0.2);\n    G.erase([{ x: sx, y: sy }, { x: sx + Math.sin(le) * sl * 0.45, y: sy - sl * 0.5 },\n      { x: sx + Math.sin(le) * sl, y: sy - Math.cos(le) * sl }], { r: 0.6, e: 0.75 });\n  }\n\n  G.finish(pal[0], pal[pal.length - 1]);\n}\n",
 "layers": []
}
