genart.dev
Download GenArt
Reed Shore in Still Air

Reed Shore in Still Air

Weather Book · 2026 · 1800×900 · Canvas 2D

A lake on a day without a breath of wind. Reeds stand thick along the near bank on one side, their plumes hanging; across the open water a low hill lies on the far shore, and the water gives it back upside down, whole. Most of the sheet is sky. The stillness is the weather here. Nothing on the water moves but a few level glints where the light catches it, and the marks on the water get smaller and paler toward the far shore until you can't tell where the lake ends.

Technique

one canvas2d algorithm lays the whole sheet in graphite, with the series' material: paper as a height field, lead grades with their own caps, a graded contact, striations from a worn tip, and a sheen where the lead is heaviest. One level camera in metres, the eye 1.6 m over the water. Reeds are vertical marks and water horizontal ones, and neither is one kind of line. Reeds come heavy, common or hairline, the nearest heavy stems drawn by their two edges with a pale centre, and now and then one broken over at a kink; each has a leaf or two and a drooping plume, is reflected below its foot in broken vertical pieces, and the bed's dark reflection is laid into the water beneath it. The water mixes long hairlines of hard lead, softer dashes and, nearer the eye, heavier rippled strokes that swell and thin. The hill's outline is a firm line of soft lead, heavier on the slope turned from the light and gone over twice in places, over a shoreline drawn right across the sheet with a dark band of shore trees at the hill's foot. Its body is a mid-tone mass of level strokes in harder lead with a few broken contours that fade as they go down, and on most seeds a soft bank of low stratus lies across part of the sky. Its reflection is laid in level marks inside its mirrored shape rather than copied. Water marks shrink and pale toward the horizon. A stump softens the far shoreline so land and water meet at one value, and a kneaded eraser lifts level glints across the reflections. The main reed bed and the hill take opposite sides of the sheet; on most seeds a small second bed stands at the far edge of the hill's side. The seed moves the horizon, the hill, which side the reeds stand on, how far they reach and how tall they grow.

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. Reed Shore in Still Air, seed 18061806
  2. Reed Shore in Still Air, seed 19681968
  3. Reed Shore in Still Air, seed 18351835

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.

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