{
 "genart": "1.2",
 "id": "reed-shore",
 "title": "Reed Shore in Still Air",
 "created": "2026-09-11T00:00:00Z",
 "modified": "2026-09-11T21:31:27.612Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1800,
  "height": 900,
  "pixelDensity": 2
 },
 "parameters": [],
 "colors": [],
 "state": {
  "seed": 1806,
  "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 3: Reed Shore in Still Air.\n//\n// A lake on a day without wind. Reeds stand thick at the near bank on one side\n// of the sheet; across open water a low hill lies on the far shore, and the\n// water, perfectly still, gives the hill back upside down. Most of the sheet\n// is sky, as in Friedrich's Tollense-See. The weather is the stillness: the\n// reflections are whole, broken only by the few level glints a kneaded eraser\n// lifts out of them.\n//\n// One level camera in metres, the eye 1.6 m above the water. Reeds are\n// vertical marks, water horizontal ones; each reflection is drawn in the marks\n// of the water, not copied from what it reflects. Water marks shrink and pale\n// toward the horizon until it is lost (Celmins's Ocean). The reeds and the hill\n// take opposite sides so they balance across the water. The seed moves the\n// horizon, the hill, which side the reeds take and how far they reach.\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 + 1806);\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 over the water.\n  var EYE = 1.6, F = W * 0.9;\n  // Ranges wide enough that each seed moves the composition: v3's measured\n  // 0.018 structural difference against the 0.03 floor.\n  var HZ = H * rr(0.46, 0.6);\n  function at(X, Y, Z) { return { x: W / 2 + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n  var HAZE = rr(900, 1600);                              // still-air haze over the lake, m\n  function vis(Z) { return Math.exp(-Math.pow(Z / HAZE, 1.2)); }\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\n  // The reeds take one side, the hill the other.\n  var SIDE = rand() < 0.5 ? -1 : 1;                      // -1: reeds on the left\n  var REACH = rr(0.28, 0.65);                            // how far across the reeds come, of the width\n  // How tall the bed stands, and on most seeds a small second bed at the far\n  // edge of the hill's side: v5's seeds differed only in which side and how\n  // far, and two of three measured alike (structure 0.0257, floor 0.03).\n  var HS = rr(0.7, 1.35), SECOND = rand() < 0.6 ? rr(0.08, 0.2) : 0;\n\n  // ---------------------------------------------------------------------------\n  // The sky: nearly empty. A few level strokes of hard lead, more of them and\n  // heavier toward the top, and the odd drag of soft lead; the paper is the\n  // light (Friedrich's Tollense-See).\n  // ---------------------------------------------------------------------------\n  hatch(rr(-0.02, 0.02), 6, 80, 420, 30, 180, -20, HZ - H * 0.03, function (a, b) {\n    var top = clamp01(1 - (a.y + b.y) / 2 / HZ);\n    if (rand() > 0.12 + 0.55 * top) return;\n    G.stroke(bowed(a, b, 0.01), { s: rr(0.12, 0.45), p: 0.35 + 0.35 * top, r: rr(0.6, 1.8), k: 0.35, tin: 0.15, tout: 0.2 });\n  });\n  // On most seeds a soft bank of low stratus lies across part of the sky,\n  // its height, place and breadth set by the seed: the weather differs from\n  // sheet to sheet, and v8's seeds, which differed only in reeds and hill,\n  // measured 0.0298 against the 0.03 structure floor.\n  if (rand() < 0.75) {\n    var BY = HZ * rr(0.15, 0.6), BX = W * rr(0.1, 0.9), BW = W * rr(0.35, 0.8), BT = H * rr(0.05, 0.12);\n    for (var bs = 0; bs < 60; bs++) {\n      var u = rr(-1, 1), by = BY + rr(-1, 1) * BT * (1 - 0.5 * u * u), bx = BX + u * BW / 2;\n      var fall = 1 - Math.abs(u), bl = rr(80, 260) * (0.5 + fall);\n      G.stroke(bowed({ x: bx - bl / 2, y: by }, { x: bx + bl / 2, y: by + rr(-3, 3) }, 0.03),\n        { s: 0.85, p: 0.14 + 0.2 * fall, r: rr(5, 11), k: 0.3, tin: 0.25, tout: 0.3 });\n    }\n  }\n  for (var cb = 0; cb < 10; cb++) {\n    var cx0 = rr(-200, W), cy0 = rr(0, HZ * 0.35), cl = rr(300, 800);\n    G.stroke(bowed({ x: cx0, y: cy0 }, { x: cx0 + cl, y: cy0 + rr(-10, 10) }, 0.03),\n      { s: 0.75, p: rr(0.12, 0.22), r: rr(6, 10), k: 0.3, tin: 0.25, tout: 0.3 });\n  }\n\n  // ---------------------------------------------------------------------------\n  // The far shore: a flat line of shore with a low hill on the side away from\n  // the reeds. Drawn mostly by its outline, the body in a few short strokes\n  // falling from the ridge (Friedrich: far hills are line, not fill).\n  // ---------------------------------------------------------------------------\n  // Tall and near enough to hold its place across the water, as Friedrich's\n  // does (about a tenth of the sheet): v2's 18-45 m at 500-900 m was a thin\n  // line of ticks at the horizon.\n  // Low and long, never a cone: v6's 75 m over 22% of the width drew a steep\n  // peak that read as Fuji, not Friedrich's low hill.\n  var HILL_Z = rr(380, 620), HILL_H = rr(35, 60), SHORE_H = rr(2, 4);\n  var HC = SIDE < 0 ? rr(0.5, 0.85) : rr(0.15, 0.5), HW = rr(0.3, 0.48);\n  var shoreY = HZ + F * EYE / HILL_Z, pxm = F / HILL_Z, hv = vis(HILL_Z);\n  function landH(x) {\n    var u = (x / W - HC) / HW, hump = Math.abs(u) < 1 ? Math.pow(Math.cos(u * Math.PI / 2), 1.6) : 0;\n    return SHORE_H + HILL_H * hump * (0.85 + 0.3 * noise(x / 90, 1.3, seed + 401)) +\n      1.2 * (noise(x / 25, 4.1, seed + 402) - 0.5);\n  }\n  function ridgeY(x) { return shoreY - landH(x) * pxm; }\n  // The outline, defined: a near-continuous line of soft lead along the ridge,\n  // heavier where the slope turns from the light (the light is from the left)\n  // and thin on the lit side, gone over twice in places as a hand goes back\n  // over a line. Erin on v7: \"outline could be more defined\".\n  for (var ox = -10; ox < W + 10;) {\n    var ol = rr(40, 160), pts = [];\n    for (var q = 0; q <= 8; q++) { var xx = ox + ol * q / 8; pts.push({ x: xx, y: ridgeY(xx) }); }\n    var shade = clamp01((pts[8].y - pts[0].y) / ol * 4 + 0.3);\n    if (rand() < 0.92) {\n      G.stroke(pts, { s: 0.75, p: (0.7 + 0.25 * shade) * hv + 0.25, r: 0.7 + 0.9 * shade, k: 0.65, tin: 0.06, tout: 0.1 });\n      if (rand() < 0.35) {\n        G.stroke(pts.map(function (p) { return { x: p.x, y: p.y + 0.8 }; }), { s: 0.8, p: 0.6 * hv + 0.2, r: 0.6, k: 0.5, tin: 0.2, tout: 0.2 });\n      }\n    }\n    ox += ol + rr(0, 6);\n  }\n  // The shoreline across the whole width, firm and level, and at the hill's\n  // foot a dark band of shore trees in close ticks.\n  for (var sx0 = -10; sx0 < W + 10;) {\n    var sl0 = rr(60, 300);\n    if (rand() < 0.9) G.stroke([{ x: sx0, y: shoreY }, { x: sx0 + sl0, y: shoreY + rr(-0.4, 0.4) }],\n      { s: 0.6, p: 0.7 * hv + 0.25, r: 0.9, k: 0.6, tin: 0.05, tout: 0.08 });\n    sx0 += sl0 + rr(0, 8);\n  }\n  for (var tx = 0; tx < W; tx += rr(1.5, 6)) {\n    var foot = shoreY - ridgeY(tx) > SHORE_H * pxm * 1.5;\n    if (rand() > (foot ? 0.9 : 0.45)) continue;\n    var ty = foot ? shoreY : ridgeY(tx), th = foot ? rr(2, 7) : rr(1.5, 5);\n    G.stroke([{ x: tx, y: ty }, { x: tx + rr(-0.5, 0.5), y: ty - th }], { s: foot ? 0.75 : 0.4, p: (foot ? 0.75 : 0.5) * hv + 0.1,\n      r: foot ? 0.8 : 0.6, k: 0.55, tin: 0.05, tout: 0.4 });\n  }\n  // The hill's tone: level strokes of harder lead through its body, so it\n  // lies as a mid-tone mass against the pale sky, as Friedrich's does. v4 drew\n  // it by its contours alone and it read as a faint outline.\n  for (var hy = HZ - HILL_H * pxm * 1.3; hy < shoreY; hy += rr(1.8, 3)) {\n    for (var hx = -rr(0, 60); hx < W + 10;) {\n      var hl = rr(20, 90), hm = hx + hl / 2;\n      if (hy > ridgeY(hm) + 1 && shoreY - ridgeY(hm) > SHORE_H * pxm * 1.5 && rand() < 0.85) {\n        var x0h = hx, x1h = hx + hl;\n        while (x0h < x1h && hy < ridgeY(x0h)) x0h += 2;\n        while (x1h > x0h && hy < ridgeY(x1h)) x1h -= 2;\n        if (x1h - x0h > 4) {\n          G.stroke([{ x: x0h, y: hy }, { x: x1h, y: hy + rr(-0.3, 0.3) }], { s: rr(0.22, 0.55), p: 0.5 * hv + rr(0.15, 0.35),\n            r: rr(0.6, 1.3), k: 0.5, tin: 0.1, tout: 0.15 });\n        }\n      }\n      hx += hl + rr(0, 12);\n    }\n  }\n  // The hill's body: two to four broken contours below the ridge, following\n  // its form and converging at its ends, each lighter than the last\n  // (Friedrich: ridges thin and space out to one line). v3's vertical strokes\n  // down from the ridge read as a curtain of rain.\n  for (var c = 1, nc = 2 + (rand() * 3 | 0); c <= nc; c++) {\n    var off = c * rr(0.12, 0.2);                         // of the local height below the ridge\n    for (var cx2 = -10; cx2 < W + 10;) {\n      var cl2 = rr(20, 90), cp = [];\n      for (var q2 = 0; q2 <= 5; q2++) {\n        var xx2 = cx2 + cl2 * q2 / 5;\n        cp.push({ x: xx2, y: ridgeY(xx2) + (shoreY - ridgeY(xx2)) * off });\n      }\n      if (shoreY - ridgeY(cx2 + cl2 / 2) > SHORE_H * pxm * 2 && rand() < 0.7) {\n        G.stroke(cp, { s: 0.45, p: (0.6 - 0.1 * c) * hv + 0.1, r: 0.8, k: 0.5, tin: 0.1, tout: 0.2 });\n      }\n      cx2 += cl2 + rr(5, 40);\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // The water: level strokes that shrink, thin and pale toward the horizon until\n  // it is lost (Celmins), darkening toward the eye where the water gives back\n  // the higher sky. The hill's reflection is laid in the same level marks,\n  // denser, inside the hill's mirrored shape.\n  // ---------------------------------------------------------------------------\n  function nearness(y) { return Math.pow(smooth(shoreY, H, y), 0.8); }\n  function reflDepth(x) { return shoreY - ridgeY(x); }   // the mirrored land's depth below the shore, px\n  // Where the reed bed stands, so the water under it can carry its dark\n  // reflection: v2 had no darks and nothing seated the reeds in the water.\n  function bedZone(side, reach, x) {\n    return side < 0 ? smooth(reach * W, reach * W * 0.55, x) : smooth((1 - reach) * W, (1 - reach * 0.55) * W, x);\n  }\n  function reedZone(x) {\n    return Math.max(bedZone(SIDE, REACH, x), SECOND ? bedZone(-SIDE, SECOND, x) : 0);\n  }\n  /**\n   * One mark on the open water, of three kinds so the water is not one weave\n   * (Erin on v7: more variation in the line types): a long hairline of hard\n   * lead, a softer dash, and nearer the eye a heavier rippled stroke that\n   * swells and thins. rz is how far the reed bed's dark reflection reaches here.\n   */\n  function waterMark(x, y, l, near, rz) {\n    var kind = rand();\n    if (kind < 0.1 + 0.3 * near) {\n      var amp = 0.6 + 2.2 * near, ph = rand() * 6.28, w = [];\n      for (var q = 0; q <= 8; q++) w.push({ x: x + l * 1.3 * q / 8, y: y + Math.sin(ph + q * 1.1) * amp });\n      G.stroke(w, { s: 0.55 + 0.4 * near, p: 0.45 + 0.45 * near + 0.3 * rz * near, r: 0.8 + 1.4 * near, k: 0.55, tin: 0.2, tout: 0.3 });\n    } else if (kind < 0.55) {\n      G.stroke([{ x: x, y: y }, { x: x + l * 1.6, y: y + rr(-0.3, 0.3) }], { s: 0.12, p: 0.55 + 0.3 * near, r: 0.45, k: 0.5, tin: 0.1, tout: 0.2 });\n    } else {\n      G.stroke([{ x: x, y: y }, { x: x + l, y: y + rr(-0.4, 0.4) }], { s: 0.35 + 0.4 * near + 0.45 * rz * near,\n        p: 0.38 + 0.45 * near + 0.55 * rz * near, r: 0.7 + 1.0 * near, k: 0.5, tin: 0.1, tout: 0.25 });\n    }\n  }\n  for (var gy = shoreY + 1.5; gy < H + 10;) {\n    // Far marks paler and smaller, never absent: v1's were too light to\n    // register and the far water was blank paper.\n    var near = nearness(gy), len = 12 + 130 * Math.pow(near, 1.4);\n    for (var gx = -rr(0, len); gx < W + 10; gx += len * rr(0.9, 1.8)) {\n      var l = len * rr(0.6, 1.2), mid = gx + l / 2, inRefl = gy - shoreY < reflDepth(mid) * 0.95;\n      if (!inRefl && rand() > 0.45 + 0.45 * near) continue;\n      if (inRefl) {\n        G.stroke([{ x: gx, y: gy }, { x: gx + l, y: gy + rr(-0.4, 0.4) }],\n          { s: rr(0.3, 0.6), p: 0.6 * hv + 0.2, r: rr(0.5, 1.1) + 0.5 * near, k: 0.55, tin: 0.1, tout: 0.2 });\n      } else {\n        waterMark(gx, gy, l, near, reedZone(mid));\n      }\n    }\n    gy += 1.6 + 7 * Math.pow(1 - near, 2) * rr(0.7, 1.3);\n  }\n\n  // ---------------------------------------------------------------------------\n  // The reeds: a thick bed at the near bank on one side, thinning into the open\n  // water. Each a tapered stem with a strap leaf or two and, on most, a\n  // drooping plume; each reflected below its foot in broken vertical marks.\n  // ---------------------------------------------------------------------------\n  var reeds = [];\n  // 6-26 m out: a band along the near bank whose tops reach about the\n  // horizon. v1 set them at 2.6-16 m; their feet fell below the frame and they\n  // stood the full height of half the sheet, hiding the hill and the horizon.\n  for (var i = 0; i < 520; i++) {\n    // A few as near as 4 m, entering from the foot of the sheet thick and dark:\n    // v3 had no true dark and failed the value-range floor at 0.298.\n    var Z = 4 + 22 * Math.pow(rand(), 1.3), sd = SIDE, rc = REACH;\n    if (SECOND && rand() < 0.22) { sd = -SIDE; rc = SECOND; }    // the small second bed\n    var sx = sd < 0 ? rr(-0.05, rc) : rr(1 - rc, 1.05);\n    // Thickest at the sheet's edge, thinning toward the open water.\n    var edge = sd < 0 ? 1 - sx / rc : (sx - (1 - rc)) / rc;\n    if (rand() > Math.pow(clamp01(edge), 0.7)) continue;\n    reeds.push({ X: (sx * W - W / 2) * Z / F, Z: Z, h: rr(1.3, 2.6) * HS, lean: rr(-0.06, 0.06) + 0.03 * sd,\n      plume: rand() < 0.65, seed: (rand() * 1e6) | 0,\n      // A heavy stem, a common one, or a hairline; and now and then one broken\n      // over at a kink. v7's stems were all one weight.\n      wt: rand() < 0.2 ? 1.8 : rand() < 0.62 ? 1 : 0.5, kink: rand() < 0.12 ? rr(0.55, 0.85) : 0,\n      ka: (rand() < 0.5 ? -1 : 1) * rr(0.4, 1.0) });\n  }\n  reeds.sort(function (a, b) { return b.Z - a.Z; });\n  var lifts = [];\n  reeds.forEach(function (rd) {\n    var rt = G.rngFrom(rd.seed);\n    function r2(a, b) { return a + (b - a) * rt(); }\n    var base = at(rd.X, 0, rd.Z), s = base.s, near = clamp01((26 - rd.Z) / 22);\n    var bend = r2(-0.08, 0.08), stem = [], refl = [];\n    for (var k = 0; k <= 10; k++) {\n      var f = k / 10, Y = rd.h * f, dx = (rd.lean * f + bend * f * f) * rd.h;\n      if (rd.kink && f > rd.kink) {\n        // Past the kink the top droops over, short: v8's fell nearly flat at\n        // full length and cut long diagonals across the open water.\n        var over = (f - rd.kink) * rd.h * 0.6, y0k = rd.kink * rd.h;\n        dx = (rd.lean * rd.kink + bend * rd.kink * rd.kink) * rd.h + over * Math.sin(rd.ka);\n        Y = y0k + over * Math.cos(rd.ka);\n      }\n      stem.push({ x: base.x + dx * s, y: base.y - Y * s });\n      refl.push({ x: base.x + dx * s, y: base.y + Y * s });\n    }\n    var rw = Math.max(0.35, Math.min(3.2, 0.005 * s * rd.wt));\n    var soft = Math.min(1, 0.6 + 0.38 * near + (rd.wt > 1 ? 0.1 : rd.wt < 1 ? -0.25 : 0));\n    var pr = Math.min(1, 0.6 + 0.38 * near + (rd.wt > 1 ? 0.12 : rd.wt < 1 ? -0.12 : 0));\n    if (rd.wt > 1 && rw > 1.4) {\n      // The nearest heavy stems are drawn by their two edges with a pale\n      // centre, as a reed is round and catches light down its middle.\n      [-1, 1].forEach(function (e) {\n        G.stroke(stem.map(function (p) { return { x: p.x + e * rw, y: p.y }; }), { s: soft, p: pr, r: rw * 0.45, k: 0.65, tin: 0.02, tout: 0.25 });\n      });\n      G.stroke(stem, { s: 0.3, p: pr * 0.5, r: rw * 0.5, k: 0.4, tin: 0.02, tout: 0.25 });\n    } else {\n      G.stroke(stem, { s: soft, p: pr, r: rw, k: 0.6, tin: 0.02, tout: 0.25 });\n    }\n    // A leaf or two: a long blade rising up and out from the stem, its tip\n    // arching over a little. v1 drew them out-up-then-down as \"^\" chevrons\n    // that read as birds.\n    for (var lf = 0, nl = 1 + (rt() < 0.5 ? 1 : 0); lf < nl; lf++) {\n      var at0 = stem[3 + (rt() * 4 | 0)], d = rt() < 0.5 ? -1 : 1, ll = r2(0.3, 0.55) * s;\n      G.stroke([at0, { x: at0.x + d * ll * 0.3, y: at0.y - ll * 0.7 }, { x: at0.x + d * ll * 0.75, y: at0.y - ll * 0.82 }],\n        { s: soft, p: pr * 0.85, r: Math.max(0.35, rw * 0.7), k: 0.55, tin: 0.05, tout: 0.4 });\n    }\n    // The plume: a dense tuft of short strands hanging to one side from the\n    // tip. v1's seven long strands splayed up and read as birds in the sky.\n    if (rd.plume) {\n      var tip = stem[10], pd = rd.lean + bend >= 0 ? 1 : -1;\n      for (var pf = 0; pf < 12; pf++) {\n        var ang = r2(0.25, 1.35), pl = r2(0.08, 0.2) * s, o = r2(0, 0.12) * s;\n        var ps = { x: tip.x + pd * o * 0.3, y: tip.y + o };\n        G.stroke([ps, { x: ps.x + pd * Math.cos(ang) * pl, y: ps.y + Math.sin(ang) * pl }],\n          { s: soft, p: pr * 0.9, r: Math.max(0.35, rw * 0.5), k: 0.5, tin: 0.05, tout: 0.5 });\n      }\n    }\n    // The reflection: the same stem mirrored in the still water, lighter and\n    // in broken pieces.\n    for (var k2 = 0; k2 < 10;) {\n      var n2 = 1 + (rt() * 3 | 0);\n      if (rt() < 0.7) G.stroke(refl.slice(k2, Math.min(10, k2 + n2) + 1), { s: soft * 0.9, p: pr * 0.7, r: rw * 0.9, k: 0.5, tin: 0.05, tout: 0.1 });\n      k2 += n2 + (rt() < 0.3 ? 1 : 0);\n    }\n  });\n\n  // ---------------------------------------------------------------------------\n  // The stump along the far shore, so the land and its reflection meet the\n  // water at one value there; then the kneaded eraser: level glints across the\n  // reflections, the only thing that says the water is water.\n  // ---------------------------------------------------------------------------\n  G.blend(function (x, y) { var b = (y - shoreY) / (H * 0.05); return 0.6 * Math.exp(-b * b); }, 2);\n  for (var g = 0; g < 90; g++) {\n    var gy2 = rr(shoreY + 2, H), nr = nearness(gy2), gl = rr(20, 60) + 220 * nr * rr(0.3, 1), gx2 = rr(-40, W);\n    G.erase([{ x: gx2, y: gy2 }, { x: gx2 + gl, y: gy2 + rr(-0.5, 0.5) }], { r: 0.6 + 1.2 * nr, e: 0.6 });\n  }\n\n  G.finish(pal[0], pal[pal.length - 1]);\n}\n",
 "layers": []
}
