{
 "genart": "1.2",
 "id": "forest-ride",
 "title": "Forest Ride in Mist",
 "created": "2026-09-11T00:00:00Z",
 "modified": "2026-09-11T21:08:46.746Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1200,
  "height": 1500,
  "pixelDensity": 2
 },
 "parameters": [],
 "colors": [],
 "state": {
  "seed": 1823,
  "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 2: Forest Ride in Mist.\n//\n// A ride cut straight through a conifer plantation, seen from the middle of it:\n// the track runs away from the eye to a vanishing point a little above the\n// centre of the sheet and goes out in mist. The trees stand in two walls, near\n// ones only trunks because their crowns are above the frame, far ones whole\n// cones, the farthest a few ticks (Friedrich's Giant Mountains). The lane closes\n// on a light gap, as Constable's lane at Staunton Harold does, and the whole\n// sheet sits in the top third of the value range except the nearest trunk on\n// each side, which is the one firm thing (Seurat's Trees). The ride itself is\n// paper.\n//\n// One level camera in metres, one-point: every tree, rut and verge agrees on\n// the horizon and on the size its distance gives it. The seed moves the\n// vanishing point, the ride's width, every tree, and how far one can see.\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 + 4243);\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, looking straight down the ride.\n  var EYE = 1.6, F = W * 1.05;\n  var HZ = H * rr(0.41, 0.47);\n  var VX = W / 2 + W * rr(-0.06, 0.06);                  // the ride's vanishing point\n  function at(X, Y, Z) { return { x: VX + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n  var V = rr(32, 60);                                    // how far one can see in the mist, m\n  function vis(Z) { return Math.exp(-Math.pow(Z / V, 1.4)); }\n  var RIDE = rr(2.2, 3.4);                               // half the ride's width, m\n  var ZN = F * EYE / (H - HZ) * 0.85;                    // the ground at the foot of the sheet\n\n  // The light gap where the ride goes out.\n  function glow(x, y) {\n    var dx = (x - VX) / (W * 0.22), dy = (y - HZ + H * 0.04) / (H * 0.16);\n    return Math.exp(-dx * dx - dy * dy);\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  function flick(x, y, h, lean, o) {\n    G.stroke([{ x: x, y: y }, { x: x + Math.sin(lean) * h * 0.4, y: y - h * 0.55 },\n      { x: x + Math.sin(lean) * h, y: y - Math.cos(lean) * h }], o);\n  }\n\n  // ---------------------------------------------------------------------------\n  // Mist and the sky in the gap: level strokes of hard lead used firmly, light\n  // and smooth, thickest along the horizon and thinning into the gap's light.\n  // Hard lead cannot darken what a soft one lays later over it.\n  // ---------------------------------------------------------------------------\n  hatch(rr(-0.02, 0.02), 5, 50, 220, 10, 70, -20, HZ + H * 0.12, function (a, b) {\n    var my = (a.y + b.y) / 2, band = Math.exp(-Math.pow((my - HZ) / (H * 0.12), 2));\n    var g = glow((a.x + b.x) / 2, my);\n    if (rand() > 0.35 + 0.5 * band) return;\n    G.stroke(bowed(a, b, 0.01), { s: 0.12, p: (0.45 + 0.2 * band) * (1 - 0.5 * g), r: 1.3, k: 0.3 + 0.2 * band,\n      tin: 0.15, tout: 0.2 });\n  });\n\n  // ---------------------------------------------------------------------------\n  // The ground: the forest floor under both walls in level strokes, darkening\n  // toward the eye; the ride between them left as paper but for its two ruts,\n  // the grass between them and its verges. Everything thins into the mist.\n  // ---------------------------------------------------------------------------\n  // v1 drew only the floor outside the ride and left the ride as paper; with\n  // the eye in the ride, that was all the near ground, and the bottom half of\n  // the sheet was empty. Now the whole ground is drawn by what it is: the track\n  // between the ruts nearly paper, the grass of the ride, and the floor under\n  // the trees darkest, all in level strokes.\n  function nearness(y) { return Math.pow(smooth(HZ, H, y), 0.7); }\n  // Two bare wheel tracks an axle apart, grass between and beside them. v2\n  // left everything between the ruts bare and the ride read as a paper road.\n  var RUT = rr(0.7, 0.85), WT = 0.2, WIND = rr(-0.1, 0.1);\n  // Each wheel track wanders a little in its line and its width: v3's were\n  // ruled and read as painted lines.\n  function rutAt(Z, sgn) { return sgn * (RUT + 0.1 * (noise(Z / 4, sgn * 3.1, seed + 301) - 0.5)); }\n  function wtAt(Z, sgn) { return WT * (0.75 + 0.5 * noise(Z / 2.5, sgn * 7.7, seed + 302)); }\n  for (var gy = HZ + 2; gy < H + 20;) {\n    var Zg = F * EYE / (gy - HZ), v = vis(Zg), near = nearness(gy), k = F / Zg;\n    var c1 = rutAt(Zg, -1), c2 = rutAt(Zg, 1), h1 = wtAt(Zg, -1), h2 = wtAt(Zg, 1);\n    var w1a = VX + (c1 - h1) * k, w1b = VX + (c1 + h1) * k, w2a = VX + (c2 - h2) * k, w2b = VX + (c2 + h2) * k;\n    var eL = VX - RIDE * k, eR = VX + RIDE * k;\n    var len = 8 + 140 * Math.pow(near, 1.3);\n    for (var gx = -rr(0, len); gx < W + 10; gx += len * rr(0.6, 1.4)) {\n      if (rand() > Math.pow(v, 0.8)) continue;\n      var x0 = gx + rr(-3, 3), x1 = x0 + len * rr(0.6, 1.2), y = gy + rr(-1, 1);\n      // Cut the stroke where it crosses a wheel track, where the hand lifts;\n      // now and then the grass runs over the edge instead.\n      var segs = rand() < 0.15 ? [[x0, x1]]\n        : [[x0, Math.min(x1, w1a)], [Math.max(x0, w1b), Math.min(x1, w2a)], [Math.max(x0, w2b), x1]];\n      segs.forEach(function (seg) {\n        if (seg[1] - seg[0] < 3) return;\n        var mid = (seg[0] + seg[1]) / 2, floor = mid < eL || mid > eR;\n        // Clumped, so the grass reads as tussocks and not an even weave (v3).\n        var pr = (floor ? 0.5 + 0.5 * near : 0.4 + 0.5 * near) * (0.55 + 0.9 * noise(mid / 55, y / 22, seed + 303));\n        G.stroke(bowed({ x: seg[0], y: y }, { x: seg[1], y: y + (seg[1] - seg[0]) * rr(-0.06, 0.06) }, 0.06),\n          { s: 0.4 + 0.55 * near, p: pr * (0.3 + 0.7 * v), r: 0.5 + 1.0 * near, k: 0.55, tin: 0.1, tout: 0.3 });\n        // A tuft: two to four blades fanning up from one root, set a little\n        // off the stroke. v4's single blade rising from each stroke made a \"+\".\n        if (!floor && near > 0.12 && rand() < 0.4) {\n          var tx0 = rr(seg[0], seg[1]), ty0 = y + rr(1, 4) + 6 * near, th = Math.min(34, 5 + 28 * near);\n          for (var bl = 0, nb = 2 + (rand() * 3 | 0); bl < nb; bl++) {\n            flick(tx0 + rr(-2, 2), ty0, th * rr(0.45, 1), WIND + (bl - (nb - 1) / 2) * 0.3 + rr(-0.1, 0.1),\n              { s: 0.4 + 0.5 * 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      });\n      // The wheel tracks take only the odd light mark of hard lead.\n      if (rand() < 0.12) {\n        var tx = rand() < 0.5 ? rr(w1a, w1b) : rr(w2a, w2b);\n        G.stroke([{ x: tx, y: y }, { x: tx + len * rr(0.1, 0.3), y: y }], { s: 0.25, p: 0.4 * v, r: 0.5 + 0.6 * near, k: 0.4 });\n      }\n    }\n    gy += 2.2 + 8 * Math.pow(1 - near, 2);\n  }\n  // The edges of both wheel tracks, running to the vanishing point, broken\n  // only a little.\n  [[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(function (e) {\n    for (var z = ZN * rr(0.9, 1.1); z < V * 2.5;) {\n      var dz = rr(2, 6) * Math.max(1, z / 10), v = vis(z);\n      if (rand() < Math.pow(v, 0.7)) {\n        var pts = [];\n        for (var q = 0; q <= 5; q++) {\n          var zq = z + dz * q / 5, p = at(rutAt(zq, e[0]) + e[1] * wtAt(zq, e[0]) + rr(-0.03, 0.03), 0, zq);\n          pts.push({ x: p.x, y: p.y });\n        }\n        var nr = nearness(pts[0].y);\n        G.stroke(pts, { s: 0.45 + 0.5 * nr, p: (0.35 + 0.45 * nr) * (0.3 + 0.7 * v), r: 0.5 + 1.0 * nr, k: 0.6, tin: 0.1, tout: 0.3 });\n      }\n      z += dz + rr(0.2, 1.8) * Math.max(1, z / 10);\n    }\n  });\n\n  // ---------------------------------------------------------------------------\n  // The two walls of conifers, drawn from the farthest in. Near trees are only\n  // trunks, their crowns above the frame; far ones whole cones of drooping\n  // branches; the farthest a few ticks. With distance the marks get fewer and\n  // the lead harder until nothing is drawn.\n  // ---------------------------------------------------------------------------\n  var trees = [];\n  [-1, 1].forEach(function (side) {\n    // Three rows, spaced: v1's four close rows massed into a stipple curtain.\n    for (var row = 0; row < 3; row++) {\n      for (var Z = rr(2.5, 5) + row; Z < V * 3; Z += rr(3, 6) * (1 + row * 0.3)) {\n        var X = side * (RIDE + 0.6 + row * rr(1.6, 2.4) + rr(0, 1.2)), p = at(X, 0, Z);\n        if (p.x < -W * 0.3 || p.x > W * 1.3) continue;\n        trees.push({ X: X, Z: Z + rr(-0.8, 0.8), side: side, Ht: rr(15, 26), r: rr(0.16, 0.3),\n          cb: rr(0.2, 0.38), cw: rr(0.15, 0.21), seed: (rand() * 1e6) | 0 });\n      }\n    }\n  });\n  trees.sort(function (a, b) { return b.Z - a.Z; });\n  // Only the nearest trunk on each side whose foot is on the sheet is firm.\n  [-1, 1].forEach(function (side) {\n    var best = null;\n    trees.forEach(function (t) {\n      var x = at(t.X, 0, t.Z).x;\n      if (t.side === side && x > W * 0.02 && x < W * 0.98 && (!best || t.Z < best.Z)) best = t;\n    });\n    if (best) best.firm = true;\n  });\n  var lifts = [];\n  function drawConifer(t) {\n    var v = vis(t.Z);\n    if (v < 0.03) return;\n    var rt = G.rngFrom(t.seed);\n    function r2(a, b) { return a + (b - a) * rt(); }\n    var b = at(t.X, 0, t.Z), s = b.s, soft = 0.25 + 0.7 * v;\n    var lean = r2(-0.012, 0.012) * t.Ht * s;\n    function sx(dx, Y) { return b.x + lean * (Y / t.Ht) + dx * s; }\n    function sy(Y) { return b.y - Y * s; }\n    // The trunk, cut at the top of the frame.\n    var yTop = Math.min(t.Ht * 0.97, (b.y + 40) / s), P = [];\n    for (var k = 0; k <= 12; k++) {\n      var Y = yTop * k / 12;\n      P.push({ x: sx(0, Y), y: sy(Y), r: t.r * s * (1 - 0.85 * Y / t.Ht) });\n    }\n    var rp = P[0].r;\n    function side(q) { return P.map(function (p) { return { x: p.x + q * p.r, y: p.y }; }); }\n    var fv = 0.35 + 0.65 * v;\n    if (rp > 1.2) {\n      // Tone first, in hard lead used firmly so it goes smooth, as Seurat\n      // models his trunks; then soft lead down the shaded side and a light rim\n      // on the lit one. v1 filled every trunk with soft grainy lines and the\n      // walls read as stipple.\n      var nL = Math.max(2, Math.ceil(2 * rp / 2.2));\n      for (var j = 0; j < nL; j++) {\n        G.stroke(side(-1 + (2 * j + 1) / nL), { s: t.firm ? 0.3 : 0.2, p: (t.firm ? 0.95 : 0.7) * fv,\n          r: Math.max(0.8, rp / nL * 1.2), k: 0.6, tin: 0.02, tout: 0.05 });\n      }\n      if (t.firm) {\n        // The firm trunk's shade graded across it, darkest at the shaded rim.\n        for (var g = 0; g < 7; g++) {\n          var qg = 0.15 + 0.85 * g / 6;\n          G.stroke(side(qg), { s: 0.9, p: 0.45 + 0.5 * qg, r: Math.max(0.8, rp * 0.1), k: 0.6, tin: 0.02, tout: 0.05 });\n        }\n      }\n      [[0.85, 0.6], [0.95, 0.6], [-0.92, 0.35]].forEach(function (e) {\n        G.stroke(side(e[0]), { s: t.firm ? 0.98 : 0.3 + 0.6 * v, p: (t.firm ? Math.min(1, e[1] + 0.35) : e[1]) * fv,\n          r: Math.max(0.6, rp * 0.08), k: 0.6, tin: 0.02, tout: 0.05 });\n      });\n      if (rp > 6 && t.firm) {\n        // Pine bark: short broken plates across the trunk.\n        for (var by = 0; by < yTop; by += 12 / s) {\n          if (rt() > 0.6) continue;\n          var bw = t.r * (1 - 0.85 * by / t.Ht), q0 = r2(-0.9, 0.2), q1 = q0 + r2(0.25, 0.7);\n          var cx = sx(0, by), cy = sy(by);\n          G.stroke([{ x: cx + q0 * bw * s, y: cy }, { x: cx + (q0 + q1) / 2 * bw * s, y: cy + r2(1, 3) },\n            { x: cx + q1 * bw * s, y: cy }], { s: 0.9, p: (t.firm ? 0.8 : 0.5) * v, r: 0.8, k: 0.5 });\n        }\n      }\n      if (t.firm) lifts.push({ pts: P.map(function (p) { return { x: p.x - 0.45 * p.r, y: p.y }; }), r: Math.max(1, rp * 0.1) });\n    } else {\n      G.stroke(P, { s: soft, p: 0.55 * v, r: Math.max(0.35, rp), k: 0.55, tin: 0.02, tout: 0.2 });\n    }\n    // Dead lower branches below the crown: short twigs angled down and out.\n    var cb = t.cb * t.Ht;\n    for (var dy = 1.2; dy < cb; dy += Math.max(0.4, 8 / s)) {\n      if (rt() > 0.45 * v || sy(dy) < -20) continue;\n      var d = rt() < 0.5 ? -1 : 1, L = r2(0.3, 1.1);\n      G.stroke([{ x: sx(0, dy), y: sy(dy) }, { x: sx(d * L, dy - L * 0.4), y: sy(dy - L * 0.4) }],\n        { s: 0.35, p: 0.5 * v, r: Math.max(0.35, Math.min(0.9, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.4 });\n    }\n    // The crown: tiers of drooping branches, a cone narrowing to the leader,\n    // needles hanging from the near ones as short ticks.\n    // Sparse, as Friedrich's are: v1's close tiers read as ruled lines.\n    var step = Math.max(0.45, 9 / s), nT = s > 30 ? 4 : s > 14 ? 2 : 0;\n    for (var cy2 = cb; cy2 < t.Ht; cy2 += step * r2(0.8, 1.2)) {\n      if (sy(cy2) < -60) break;\n      var R = t.cw * t.Ht * Math.pow(1 - (cy2 - cb) / (t.Ht - cb), 0.85);\n      [-1, 1].forEach(function (d) {\n        if (rt() > 0.75 * Math.pow(v, 0.6)) return;\n        var L = R * r2(0.7, 1.1), droop = r2(0.15, 0.35) * L;\n        var p0 = { x: sx(0, cy2), y: sy(cy2) };\n        var p1 = { x: sx(d * L * 0.5, cy2), y: sy(cy2 - droop * 0.7) };\n        var p2 = { x: sx(d * L, cy2), y: sy(cy2 - droop + 0.1 * L) };\n        var pr = (0.55 + 0.3 * (1 - cy2 / t.Ht)) * (0.3 + 0.7 * v);\n        G.stroke([p0, p1, p2], { s: soft, p: pr, r: Math.max(0.4, Math.min(1.6, 0.05 * s)), k: 0.55, tin: 0.03, tout: 0.3 });\n        for (var n = 0; n < nT; n++) {\n          var f = r2(0.2, 1), hx = p0.x + (p2.x - p0.x) * f, hy = p0.y + (p1.y - p0.y) * Math.min(1, 2 * f) + (p2.y - p1.y) * Math.max(0, 2 * f - 1);\n          var hl = Math.min(18, r2(0.15, 0.35) * s);\n          G.stroke([{ x: hx, y: hy }, { x: hx + d * hl * 0.3, y: hy + hl }],\n            { s: soft, p: pr * 0.85, r: Math.max(0.35, Math.min(1, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.5 });\n        }\n      });\n    }\n    // The leader.\n    if (sy(t.Ht) > -20) {\n      G.stroke([{ x: sx(0, t.Ht * 0.9), y: sy(t.Ht * 0.9) }, { x: sx(0, t.Ht), y: sy(t.Ht) }],\n        { s: soft, p: 0.6 * v, r: Math.max(0.35, Math.min(1, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.4 });\n    }\n  }\n  trees.forEach(drawConifer);\n\n  // ---------------------------------------------------------------------------\n  // The stump through the gap, so the far trees, the mist and the ride meet at\n  // one value; then the kneaded eraser: two faint shafts of light falling\n  // through the mist, and the light side of each firm trunk.\n  // ---------------------------------------------------------------------------\n  G.blend(function (x, y) { return 0.85 * glow(x, y); }, 3);\n  for (var sh = 0, nSh = 1 + (rand() * 2 | 0); sh < nSh; sh++) {\n    var x0 = VX + W * rr(-0.35, 0.1), a = rr(1.0, 1.2), l = H * 0.7;\n    G.erase([{ x: x0, y: -40 }, { x: x0 + Math.cos(a) * l, y: -40 + Math.sin(a) * l }], { r: rr(10, 18), e: 0.2 });\n  }\n  lifts.forEach(function (l) { G.erase(l.pts, { r: l.r, e: 0.3 }); });\n\n  G.finish(pal[0], pal[pal.length - 1]);\n}\n",
 "layers": []
}
