genart.dev
Download GenArt
Field Edge in Low Cloud

Field Edge in Low Cloud

Weather Book · 2026 · 1500×1000 · Canvas 2D

The edge of a field on a day when the cloud has come down to the ground. A hedge runs away from where you stand on a long diagonal and goes into the cloud; one bare tree stands in it on the left, near enough to see its bark. Past the tree the hedge breaks into separate touches, then into nothing, and the far half of the sheet is fog. The ground in front is rough pasture, darkest at your feet. The drawing is darkest where you are and lightest where the hedge goes out. Nothing in it is black.

Technique

one canvas2d algorithm lays the whole sheet in graphite, raster by raster. The paper is a height field; a pencil tip rides at a height set by its pressure and its grade and lays lead only on the grain above it, so soft lead used lightly is grainy and hard lead used firmly is smooth. Each grain holds a cap set by the grade, so the lead saturates, a harder pencil cannot darken what a softer one laid, and the darkest the sheet goes is a dense grey. Where the lead is heaviest it polishes and lifts slightly toward silver. A worn tip leaves fine striations along each stroke. The scene is placed through one level camera in metres: the tree sets the framing, the hedge runs through its foot to a vanishing point, and the field is drawn in sweeps that lie along the ground toward that point. Fog is visibility, not grey: past a few dozen metres the marks thin out and the lead turns hard until nothing is drawn. The sky is low stratus in side-of-the-lead masses and nearly level lines, the fog is level strokes of hard lead, a stump softens where the far hedge, the fog and the cloud meet, and a kneaded eraser lifts mist across the far hedge, the light side of the trunk and stalks in the near grass. The seed moves the horizon, the tree and its branching, the angle and profile of the hedge, and how far one can see.

Seeds

The same system at three seeds. The composition itself re-cuts: the geometry is derived from the seed, so masses, edges and placement all move, while the palette and the drawing language stay put.

  1. Field Edge in Low Cloud, seed 18211821
  2. Field Edge in Low Cloud, seed 18171817
  3. Field Edge in Low Cloud, seed 18231823

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.

    field-edge.genart
    {
      "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": []
    }