
Frost Hollow at Dawn
A hollow at first light, after a clear and windless night. The cold air has drained off the slopes and lies in the valley floor as ground mist, level as a lake, and the hedgerow trees stand up out of it with their feet lost. Above the mist line the far slope is clear, its upper fields already catching the light while the hollow is still in shadow. The bank where you are standing is white with frost. The weather here is the cold itself: it is what put the mist in the bottom, took the feet off the trees and rimed the grass at your boots.
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 on the near bank above the mist's top, so the mist line, the contours of the far slope and the height of every tree agree. The mist is the light of the sheet: hard lead laid level in long overlapping lengths so it goes light and smooth, stumped even, drawn up to its top edge with the point, with a lit rim lifted out beneath it, hairlines that shrink and close up toward the far edge so the surface lies flat, and a few softer dashes. Its hard top edge is the dark foot of the land standing on it, woods along the foot in a mass laid with the side of the lead before their stems are ticked in, under a scalloped line of crowns and over one firm line where land meets mist. Here and there a drowned hedge breaks the surface as a run of small touches lying exactly on it. Each tree is cut where it enters the mist and its lead fades out over a metre or two, so the feet go by value rather than by blur; near trunks are filled along their length, branches are single strokes, twigs hairlines, and the far trees are ticks with a little mass on top. The slope above is a mid-tone mass of contour strokes that wander off their own contour, darkest at the foot and lightest where the dawn has reached it, with hedges as touches and a firm ridge line. The near bank is tone under grass clumps of mixed blades, some laid over by the night, with big tussocks in front, a corner worked until it polishes, and dead dock stalks as the darkest strokes. The frost is kneaded-eraser work in three kinds, blades, level drags and points, thickest toward the mist where the hollow is coldest, so the bank's lower edge is lost into the mist by value. The seed moves the horizon, the depth of the mist, the far ridge and the woods on it, the hedgerows and their trees, and the lie of the near bank.
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.
1840
1801
1872
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.
{
"genart": "1.2",
"id": "frost-hollow",
"title": "Frost Hollow at Dawn",
"created": "2026-09-11T00:00:00Z",
"modified": "2026-09-11T22:28:23.723Z",
"renderer": {
"type": "canvas2d",
"version": "1.x"
},
"canvas": {
"width": 1500,
"height": 1200,
"pixelDensity": 2
},
"parameters": [],
"colors": [],
"state": {
"seed": 1840,
"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 4: Frost Hollow at Dawn.\n//\n// A hollow at first light after a clear, still night. The cold air has drained\n// off the slopes and filled the valley floor with ground mist, level as a lake;\n// the hedgerow trees stand up out of it with their feet lost, and the far\n// slope is clear above the mist line, its upper fields catching the first\n// light while the hollow is still in shadow. The near bank, where we stand, is\n// white with frost.\n//\n// One level camera in metres, the eye on the near bank well above the mist's\n// top. The mist is a flat lake of grainy tone from the side of a soft lead,\n// stumped even, with a hard top edge drawn up to with the point and a lit rim\n// lifted out beneath it. What stands in it is lost by value, never by blur.\n// The frost is kneaded-eraser work on the near grass: blades, level drags and\n// points lifted out of dark lead. The seed moves the horizon, the mist's\n// depth, the far ridge and the woods on it, the hedgerows and the near bank.\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 + 1840);\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 /** 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 /** A zig-zag touch: hedges, crowns and seed heads. */\n function scribble(x, y, size, o) {\n var a = rand() * Math.PI * 2, pts = [{ x: x, y: y }], n = 3 + (rand() * 4 | 0), step = size * 0.5;\n for (var q = 0; q < n; q++) {\n a += (q % 2 ? 1 : -1) * rr(2.0, 2.8);\n x += Math.cos(a) * step * rr(0.6, 1.2); y += Math.sin(a) * step * rr(0.6, 1.2);\n pts.push({ x: x, y: y });\n }\n G.stroke(pts, o);\n }\n\n // --- The camera: level, the eye on the near bank above the mist.\n var F = W * 0.95;\n var HZ = H * rr(0.27, 0.4);\n // Shallow mist under tall trees, so they stand well out of it: v1's 5-9 m\n // of mist under 8-15 m trees left them grey stubs.\n var MT = rr(3, 6); // the mist's top over the valley floor, m\n var EYE = MT + rr(9, 16); // the eye over the valley floor, m\n var LIGHT = rand() < 0.5 ? -1 : 1; // the dawn's side: -1 the left\n var HAZE = rr(900, 1600);\n function vis(Z) { return Math.exp(-Math.pow(Z / HAZE, 1.3)); }\n function scr(X, Y, Z) { return { x: W / 2 + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n function mistY(Z) { return HZ + F * (EYE - MT) / Z; } // the mist's top at depth Z, on the sheet\n\n // ---------------------------------------------------------------------------\n // The far slope: the valley floor ends at zf(x) and the land rises beyond it\n // to a crest rh(x) high over a run of DR. Solved per column, so the ridge,\n // the contours and the mist line agree on one camera.\n // ---------------------------------------------------------------------------\n var ZF0 = rr(260, 420), DR = rr(240, 400), RH0 = rr(80, 150), RT = rr(-0.4, 0.4);\n function zf(x) { return ZF0 * (0.85 + 0.3 * noise(x / 420, 2.1, seed + 301)); }\n function rh(x) {\n return RH0 * (1 + RT * (x / W - 0.5) * 2) * (0.75 + 0.5 * noise(x / 300, 5.3, seed + 302)) +\n 5 * (noise(x / 55, 7.7, seed + 303) - 0.5);\n }\n function landD(h, Y) { return DR * 2 / Math.PI * Math.asin(Math.min(1, Y / h)); } // run at which the land reaches Y\n var COLS = Math.ceil(W) + 1;\n var RIDGE = new Float32Array(COLS), YM = new Float32Array(COLS), ZFA = new Float32Array(COLS);\n for (var c0 = 0; c0 < COLS; c0++) {\n var z0 = zf(c0), h0 = rh(c0), best = 1e9;\n for (var q0 = 0; q0 <= 48; q0++) {\n var d0 = 1.6 * DR * q0 / 48, Y0 = h0 * Math.sin(Math.PI / 2 * Math.min(1, d0 / DR));\n best = Math.min(best, HZ + F * (EYE - Y0) / (z0 + d0));\n }\n RIDGE[c0] = best; ZFA[c0] = z0;\n YM[c0] = HZ + F * (EYE - MT) / (z0 + landD(h0, MT));\n }\n function col(A, x) {\n var i = Math.max(0, Math.min(COLS - 1, x)), i0 = Math.floor(i), i1 = Math.min(COLS - 1, i0 + 1);\n return A[i0] + (A[i1] - A[i0]) * (i - i0);\n }\n function ridgeY(x) { return col(RIDGE, x); }\n function topY(x) { return col(YM, x); } // the mist's hard top edge\n function slopeAt(x, t) { var m = topY(x); return m + (ridgeY(x) - m) * t; }\n\n // ---------------------------------------------------------------------------\n // The near bank: its edge, where it goes down into the mist, curves across\n // the sheet, higher on one side.\n // ---------------------------------------------------------------------------\n var NB = rr(0.36, 0.5), TILT = rr(0.08, 0.22) * (rand() < 0.5 ? -1 : 1), CURVE = rr(-0.25, 0.3);\n function bankY(x) {\n var u = x / W - 0.5;\n return HZ + (H - HZ) * (NB + TILT * u + CURVE * u * u + 0.05 * (noise(x / 500, 1.7, seed + 501) - 0.5)) +\n 6 * (noise(x / 40, 3.3, seed + 502) - 0.5);\n }\n function bankNear(x, y) { var b = bankY(x); return y <= b ? 0 : Math.pow(smooth(b, H + 20, y), 0.8); }\n function bankDir(x, y) {\n var a = Math.atan2(bankY(x + 8) - bankY(x - 8), 16);\n return a * (1 - 0.6 * bankNear(x, y));\n }\n\n var MTOP = 1e9, MBOT = -1e9, RTOP = 1e9;\n for (var c1 = 0; c1 <= W; c1 += 4) {\n MTOP = Math.min(MTOP, topY(c1)); MBOT = Math.max(MBOT, bankY(c1)); RTOP = Math.min(RTOP, ridgeY(c1));\n }\n function mistDepth(y) { return clamp01((y - MTOP) / (MBOT - MTOP)); } // 0 the far edge, 1 the near\n /** The pieces of a level stroke x0..x1 at y where ok(x, y) holds. */\n function runs(x0, x1, y, ok) {\n var out = [], st = null, last = x0;\n for (var x = x0; x <= x1; x += 3) {\n if (ok(x, y)) { if (st === null) st = x; last = x; }\n else if (st !== null) { if (last - st > 6) out.push([st, last]); st = null; }\n }\n if (st !== null && last - st > 6) out.push([st, last]);\n return out;\n }\n\n // ---------------------------------------------------------------------------\n // The sky: clear after the night, palest low on the dawn's side. A few level\n // strokes of hard lead, heavier toward the top, and on most seeds a thin\n // bank of dawn cloud drawn with the side of the lead.\n // ---------------------------------------------------------------------------\n var GX = LIGHT > 0 ? W * rr(0.7, 0.95) : W * rr(0.05, 0.3), GY = RTOP - H * 0.02;\n function glow(x, y) { var dx = (x - GX) / (W * 0.35), dy = (y - GY) / (H * 0.2); return Math.exp(-dx * dx - dy * dy); }\n function inSky(p) { return p.y < ridgeY(p.x) - 2; }\n hatch(rr(-0.02, 0.02), 6, 80, 380, 30, 170, -20, H * 0.6, function (a, b) {\n var m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };\n if (!inSky(a) || !inSky(b) || !inSky(m)) return;\n var top = clamp01(1 - m.y / Math.max(1, RTOP));\n if (rand() > (0.12 + 0.6 * top) * (1 - 0.8 * glow(m.x, m.y))) return;\n G.stroke(bowed(a, b, 0.01), { s: rr(0.12, 0.45), p: (0.26 + 0.3 * top) * rr(0.7, 1.2), r: rr(0.6, 2.2), k: 0.35, tin: 0.15, tout: 0.2 });\n });\n if (rand() < 0.75) {\n var BY = RTOP * rr(0.2, 0.7), BX = W * rr(0.15, 0.85), BW = W * rr(0.35, 0.8), BT = H * rr(0.02, 0.06);\n for (var bs = 0; bs < 55; 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 if (by > ridgeY(bx) - 10) continue;\n G.stroke(bowed({ x: bx - bl / 2, y: by }, { x: bx + bl / 2, y: by + rr(-2, 2) }, 0.03),\n { s: 0.85, p: (0.12 + 0.2 * fall) * (1 - 0.6 * glow(bx, by)), r: rr(3, 8), k: 0.3, tin: 0.25, tout: 0.3 });\n }\n }\n\n // ---------------------------------------------------------------------------\n // The far slope, clear above the mist. Its tone follows the contours in hard\n // lead, darkest at the foot and in the folds, lightest up the slope where the\n // dawn has reached it. Fields are drawn by their hedges as touches, woods as\n // close vertical ticks under a scalloped top, the ridge as one firm line.\n // ---------------------------------------------------------------------------\n // Woods along the slope's foot, broken by fields, and on some seeds a hanger\n // higher up: v1's two woods stood alone on a bare slope and read as islands.\n var woods = [];\n for (var wi = 0, nw = rand() < 0.5 ? 1 : 0; wi < nw; wi++) {\n // Kept low and tapered: a tall patch with a scalloped top read as a castle\n // on the skyline (v2, seed 1793).\n woods.push({ c: W * rr(0.1, 0.9), w: W * rr(0.08, 0.18), t: rr(0.15, 0.32), s: seed + 700 + wi });\n }\n var FB = rr(0.1, 0.22), FG = rr(0.35, 0.55); // the foot band's height, and how much fields break it\n /** How high up the slope the woods reach at x, as a share of the slope; 0 outside them. */\n function woodTop(x) {\n var best = FB * smooth(FG - 0.08, FG + 0.08, noise(x / 160, 3.9, seed + 710)) * (0.7 + 0.6 * noise(x / 26, 1.3, seed + 711));\n woods.forEach(function (w) {\n var u = (x - w.c) / w.w;\n if (Math.abs(u) < 1) best = Math.max(best, w.t * Math.pow(Math.cos(u * Math.PI / 2), 1.4) * (0.8 + 0.4 * noise(x / 26, 1.1, w.s)));\n });\n return best;\n }\n /** Darkest at the foot, in the hollow's shadow; the dawn has reached the upper slope and its own side. */\n function slopeShade(x, t) {\n // 🔴 Clamp t. The jittered rows can step past 1, Math.pow(negative, 1.3)\n // is NaN, one dab writes that NaN into the lead, and the next stump's\n // running-sum blur then smears it over every pixel to the right and below:\n // v4 seed 1840 came out with a black quadrant from this one stroke.\n t = clamp01(t);\n var fold = noise(x / 110, t * 2.5, seed + 311);\n var side = LIGHT > 0 ? x / W : 1 - x / W;\n return clamp01(0.35 + 0.5 * Math.pow(1 - t, 1.3) + 0.5 * (fold - 0.5) - 0.3 * side * t - 0.15 * t);\n }\n var band = 0;\n for (var c2 = 0; c2 <= W; c2 += 20) band += (topY(c2) - ridgeY(c2)) / (W / 20 + 1);\n // The slope's body first, with the side of the lead along the contours, so\n // it lies as a mid-tone mass against the pale mist. v2 drew the contour lines\n // alone, the slope stayed bare paper, and the mist's top edge had nothing to\n // be hard against.\n for (var bt = 0.004; bt < 1; bt += 3.2 / Math.max(20, band)) {\n for (var bx0 = -rr(0, 60); bx0 < W + 10;) {\n var bl0 = rr(60, 260), bmid = bx0 + bl0 / 2;\n var btj = bt + rr(-0.5, 0.5) * 3.2 / Math.max(20, band), bsh = slopeShade(bmid, btj);\n if (btj > woodTop(bmid) + 0.01) {\n var bp = [];\n for (var q6 = 0; q6 <= 6; q6++) { var bxx = bx0 + bl0 * q6 / 6; bp.push({ x: bxx, y: slopeAt(bxx, btj) }); }\n G.stroke(bp, { s: 0.75, p: 0.24 + 0.55 * bsh, r: rr(1.6, 3.2), k: 0.32, tin: 0.2, tout: 0.25 });\n }\n bx0 += bl0 + rr(0, 30);\n }\n }\n for (var t = 0.004; t < 1; t += (2.4 + 1.4 * rand()) / Math.max(20, band) * (1 - 0.45 * t)) {\n for (var sx = -rr(0, 80); sx < W + 10;) {\n // Each piece wanders off its own contour and carries its own weight, so\n // the hill is not ruled: v3's evenly spaced rows read as stripes.\n var sl = rr(20, 120), pts = [], mid = sx + sl / 2;\n var tj = t + rr(-0.7, 0.7) * 2.4 / Math.max(20, band), sh = slopeShade(mid, tj);\n if (rand() < 0.3 + 0.7 * sh && tj > woodTop(mid) + 0.02) {\n var wv = 0.05 * (noise(mid / 70, tj * 9, seed + 312) - 0.5);\n for (var q1 = 0; q1 <= 6; q1++) { var xx = sx + sl * q1 / 6; pts.push({ x: xx, y: slopeAt(xx, tj + wv) }); }\n G.stroke(pts, { s: 0.25 + 0.4 * sh, p: (0.3 + 0.55 * sh) * rr(0.75, 1.15), r: rr(0.5, 1.1), k: 0.5, tin: 0.1, tout: 0.15 });\n }\n sx += sl + rr(2, 30);\n }\n }\n // Hedges along the contours and down the slope, as touches that shrink\n // toward the crest; now and then a hedgerow tree as a tick and a tuft.\n function hedgeTouch(x, y, t, dark) {\n if (y > topY(x) - 1 || y < ridgeY(x) + 1) return;\n var sz = 3.2 * (1 - 0.55 * t);\n scribble(x, y, sz, { s: 0.55 + 0.3 * dark, p: (0.45 + 0.4 * dark) * (1 - 0.35 * t), r: 0.55, k: 0.5, tin: 0.05, tout: 0.15 });\n if (rand() < 0.035) {\n var th = sz * rr(2.5, 4.5);\n G.stroke([{ x: x, y: y }, { x: x + rr(-0.5, 0.5), y: y - th }], { s: 0.7, p: 0.7 * (1 - 0.3 * t), r: 0.6, k: 0.6, tin: 0.02, tout: 0.3 });\n for (var k = 0; k < 3; k++) scribble(x + rr(-1, 1) * th * 0.3, y - th * rr(0.7, 1), th * 0.4, { s: 0.6, p: 0.55, r: 0.5, k: 0.5 });\n }\n }\n for (var hc = 0, nhc = 2 + (rand() * 3 | 0); hc < nhc; hc++) {\n var ht = rr(0.1, 0.85), dark = rr(0.2, 0.8);\n for (var hx = rr(-20, W * 0.4); hx < W + 10; hx += 2.2 + 3 * ht) {\n if (rand() < 0.12) { hx += rr(10, 60); continue; }\n if (ht < woodTop(hx) + 0.03) continue;\n hedgeTouch(hx, slopeAt(hx, ht + 0.01 * (noise(hx / 60, hc, seed + 320) - 0.5)) + rr(-0.6, 0.6), ht, dark);\n }\n }\n for (var hd = 0, nhd = 3 + (rand() * 4 | 0); hd < nhd; hd++) {\n var x0d = rr(0, W), drift = rr(-60, 60), t1 = rr(0.35, 1), dk = rr(0.2, 0.8);\n for (var td = 0; td < t1; td += 2.4 / Math.max(20, band)) {\n if (rand() < 0.1) continue;\n var xd = x0d + drift * td;\n if (td < woodTop(xd) + 0.02) continue;\n hedgeTouch(xd + rr(-0.8, 0.8), slopeAt(xd, td), td, dk);\n }\n }\n // The woods: a mass laid first with the side of the lead along the contours\n // (Seurat: one dark body, not scribble), then close vertical ticks for the\n // stems, dark at the foot where they stand on the mist, and a scalloped line\n // of crowns. v1 laid ticks alone and they read as salt and pepper.\n var wmax = 0;\n for (var c3 = 0; c3 <= W; c3 += 3) wmax = Math.max(wmax, woodTop(c3));\n for (var wt0 = 0; wt0 < wmax; wt0 += 2.2 / Math.max(20, band)) {\n for (var wx0 = -rr(0, 40); wx0 < W + 10;) {\n var wl0 = rr(20, 80);\n runs(wx0, wx0 + wl0, wt0, function (x, t) { return t < woodTop(x) - 0.004; }).forEach(function (rn) {\n var wp = [];\n for (var q = 0; q <= 5; q++) { var xx = rn[0] + (rn[1] - rn[0]) * q / 5; wp.push({ x: xx, y: Math.min(topY(xx) - 1.2, slopeAt(xx, wt0)) }); }\n G.stroke(wp, { s: 0.9, p: 0.9 + 0.1 * (1 - wt0 / wmax), r: 1.8, k: 0.75, tin: 0.1, tout: 0.15 });\n });\n wx0 += wl0 + rr(0, 4);\n }\n }\n for (var tx0 = 0; tx0 < W; tx0 += rr(0.9, 2.2)) {\n var wt = woodTop(tx0);\n if (wt < 0.01) continue;\n for (var wn = 0, nwn = Math.ceil(wt * band / 5); wn < nwn; wn++) {\n var tt = wt * rand(), wy = Math.min(topY(tx0) - 0.6, slopeAt(tx0, tt)), wfoot = 1 - tt / wt;\n G.stroke([{ x: tx0, y: wy }, { x: tx0 + rr(-0.4, 0.4), y: wy - rr(2, 5) * (1 - 0.4 * tt) }],\n { s: 0.85 + 0.15 * wfoot, p: 0.9 + 0.1 * wfoot, r: rr(0.45, 0.8), k: 0.7, tin: 0.05, tout: 0.3 });\n }\n }\n for (var cx = 0; cx < W; cx += rr(3, 6)) {\n var ct = woodTop(cx);\n if (ct < 0.02) continue;\n scribble(cx, slopeAt(cx, ct) + 1.5, rr(3.5, 6) * (1 - 0.4 * ct), { s: 0.85, p: 0.9, r: 0.6, k: 0.7, tin: 0.05, tout: 0.15 });\n }\n // Where the land meets the mist, one firm line along the whole sheet: the\n // lake's hard top edge is this dark foot standing on it, heaviest under the\n // woods. v3 left the edge as fuzzy grey and the mist had nothing to end at.\n for (var ex0 = -10; ex0 < W + 10;) {\n var el0 = rr(30, 140), ep0 = [], wtm = woodTop(ex0 + el0 / 2);\n for (var q7 = 0; q7 <= 6; q7++) { var exx0 = ex0 + el0 * q7 / 6; ep0.push({ x: exx0, y: topY(exx0) - 0.8 }); }\n if (rand() < (wtm > 0.02 ? 0.95 : 0.55)) {\n G.stroke(ep0, { s: 0.9, p: wtm > 0.02 ? 1 : 0.7, r: wtm > 0.02 ? 1.3 : 0.7, k: 0.85, tin: 0.05, tout: 0.08 });\n }\n ex0 += el0 + rr(0, 8);\n }\n // The ridge: a firm line of soft lead, heavier where the crest turns from\n // the light, gone over twice in places; a few trees on the skyline.\n for (var ox = -10; ox < W + 10;) {\n var ol = rr(40, 160), rp = [];\n for (var q2 = 0; q2 <= 8; q2++) { var rx = ox + ol * q2 / 8; rp.push({ x: rx, y: ridgeY(rx) }); }\n var turn = clamp01(-LIGHT * (rp[8].y - rp[0].y) / ol * 4 + 0.35);\n if (rand() < 0.93) {\n G.stroke(rp, { s: 0.7, p: 0.55 + 0.3 * turn, r: 0.6 + 0.8 * turn, k: 0.6, tin: 0.06, tout: 0.1 });\n if (rand() < 0.35) G.stroke(rp.map(function (p) { return { x: p.x, y: p.y + 0.9 }; }), { s: 0.75, p: 0.5, r: 0.55, k: 0.5, tin: 0.2, tout: 0.2 });\n }\n ox += ol + rr(0, 6);\n }\n if (rand() < 0.6) {\n for (var st = 0, nst = 2 + (rand() * 5 | 0), sc = rr(0.1, 0.9); st < nst; st++) {\n var tx = W * (sc + rr(-0.08, 0.08)), ty = ridgeY(tx), tsz = rr(3, 7);\n G.stroke([{ x: tx, y: ty }, { x: tx, y: ty - tsz * 1.2 }], { s: 0.65, p: 0.6, r: 0.55, k: 0.6, tin: 0.02, tout: 0.3 });\n for (var k1 = 0; k1 < 3; k1++) scribble(tx + rr(-1, 1) * tsz * 0.4, ty - tsz * rr(1, 1.8), tsz * 0.5, { s: 0.55, p: 0.5, r: 0.5, k: 0.5 });\n }\n }\n\n // ---------------------------------------------------------------------------\n // The mist: a flat lake of pale tone, the light of the sheet as it is in the\n // references (Bury Hill, Friedrich's Madonna). Hard lead laid level in long\n // overlapping lengths, so it goes light and smooth, then stumped; hard-lead\n // hairlines across it that shrink and close up toward its far edge, so it\n // lies flat. Its hard top edge is the dark slope foot standing on it. v1\n // laid soft lead and it went to grey static darker than the slope.\n // ---------------------------------------------------------------------------\n function lap(x, y) { return 1 - smooth(bankY(x) - 4, bankY(x) + 16, y); }\n function inMist(x, y) { return y > topY(x) + 0.5 && lap(x, y) > 0.02; }\n for (var my = MTOP + 1; my < MBOT + 18; my += rr(2.8, 4.2) * (0.55 + 0.6 * mistDepth(my))) {\n var dep = mistDepth(my);\n for (var mx = -rr(0, 300); mx < W + 10;) {\n var ml = rr(120, 420) * (0.45 + 0.55 * dep);\n runs(mx, mx + ml, my + rr(-0.6, 0.6), inMist).forEach(function (rn) {\n var dmin = 1e9, x;\n for (x = rn[0]; x <= rn[1]; x += 6) dmin = Math.min(dmin, my - topY(x));\n var r = Math.min(rr(5, 8), Math.max(0.6, dmin * 0.85)), xm = (rn[0] + rn[1]) / 2;\n var mott = noise(xm / 380, my / 70, seed + 601) - 0.5;\n G.stroke(bowed({ x: rn[0], y: my }, { x: rn[1], y: my + rr(-0.5, 0.5) }, 0.004),\n { s: 0.15, p: (0.32 + 0.08 * dep + 0.08 * mott) * (0.4 + 0.6 * lap(xm, my)), r: r, k: 0.2, tin: 0.2, tout: 0.25 });\n });\n mx += ml + rr(5, 50);\n }\n }\n // Up to the edge with the point: three close lines following it.\n [0.8, 2.1, 3.5].forEach(function (off) {\n for (var ex = -10; ex < W + 10;) {\n var el = rr(40, 160), ep = [];\n for (var q3 = 0; q3 <= 8; q3++) { var exx = ex + el * q3 / 8; ep.push({ x: exx, y: topY(exx) + off }); }\n G.stroke(ep, { s: 0.2, p: 0.38 - 0.03 * off, r: 0.65, k: 0.4, tin: 0.04, tout: 0.06 });\n ex += el + rr(0, 3);\n }\n });\n // Celmins's hairlines: close and short at the far edge, long and open near.\n for (var hy = MTOP + 3; hy < MBOT; hy += (1.8 + 9 * mistDepth(hy)) * rr(0.7, 1.3)) {\n var hdp = mistDepth(hy), hl = 15 + 190 * Math.pow(hdp, 1.3);\n for (var hx2 = -rr(0, hl); hx2 < W + 10; hx2 += hl * rr(1.1, 2.2)) {\n if (rand() > 0.55) continue;\n runs(hx2, hx2 + hl * rr(0.6, 1.2), hy, inMist).forEach(function (rn) {\n G.stroke([{ x: rn[0], y: hy }, { x: rn[1], y: hy + rr(-0.3, 0.3) }], { s: 0.15, p: 0.55 + 0.2 * hdp, r: 0.45, k: 0.5, tin: 0.1, tout: 0.2 });\n });\n }\n }\n // A few softer dashes, so the surface is not one weave.\n for (var sd = 0; sd < 140; sd++) {\n var dy = rr(MTOP + 4, MBOT), ddp = mistDepth(dy), dx = rr(-20, W), dl = rr(8, 40) * (0.3 + ddp);\n if (!inMist(dx, dy) || !inMist(dx + dl, dy)) continue;\n G.stroke([{ x: dx, y: dy }, { x: dx + dl, y: dy + rr(-0.3, 0.3) }], { s: 0.55, p: 0.3 + 0.1 * ddp, r: 0.8, k: 0.45, tin: 0.2, tout: 0.3 });\n }\n // The stump through the body of the lake, clear of its top edge, so the tone\n // goes even while the grain still shows.\n G.blend(function (x, y) {\n var e = topY(x);\n var m = 0.8 * smooth(e + 3, e + 9, y) * (1 - smooth(bankY(x) - 10, bankY(x), y));\n // A lighter pass over the slope turns its tone from line toward mass.\n var sl = 0.35 * smooth(ridgeY(x) - 2, ridgeY(x) + 10, y) * (1 - smooth(e - 6, e - 1, y));\n return Math.max(m, sl);\n }, 2.5);\n\n // ---------------------------------------------------------------------------\n // The hedgerow trees: in lines across the valley floor, standing up out of\n // the mist. Below its top a tree is lost: its lead fades to nothing over a\n // metre or two, so its foot goes by value into the tone around it.\n // ---------------------------------------------------------------------------\n var trees = [];\n function addTree(X, Z, big) {\n var p = scr(X, 0, Z);\n if (p.x < -80 || p.x > W + 80) return;\n if (mistY(Z) > bankY(p.x) - 3 || Z > col(ZFA, p.x) - 15) return;\n trees.push({ X: X, Z: Z, h: big ? rr(16, 24) : rr(11, 18), big: !!big, seed: (rand() * 1e6) | 0, lean: rr(-0.08, 0.08) });\n }\n var hedges = [];\n for (var hi = 0, nh = 2 + (rand() < 0.6 ? 1 : 0); hi < nh; hi++) {\n var across = hi === 0 ? rand() < 0.5 : rand() < 0.3;\n var HZ0 = rr(70, 300), HX0 = rr(-0.45, 0.45) * HZ0 * W / F, ang = across ? rr(1.35, 1.75) : rr(-0.65, 0.65);\n hedges.push({ Z0: HZ0, X0: HX0, ang: ang });\n // Wide spacing and frequent gaps: an even step read as a planted orchard.\n for (var ts = -700; ts < 700; ts += rr(8, 46)) {\n if (rand() < 0.3) continue;\n var tz = HZ0 + ts * Math.cos(ang);\n if (tz > 25) addTree(HX0 + ts * Math.sin(ang), tz, false);\n }\n }\n if (rand() < 0.85) {\n // One tree nearer than the rest, on a third, tried a few times over: a\n // single attempt often fell behind the bank's edge and was dropped, and\n // the sheet lost its foreground tree altogether (v4, seed 1793).\n for (var la = 0, before = trees.length; la < 8 && trees.length === before; la++) {\n var lx = W * (rand() < 0.5 ? rr(0.16, 0.38) : rr(0.62, 0.84));\n var lz = F * (EYE - MT) / (bankY(lx) - HZ) * rr(1.05, 1.4);\n addTree((lx - W / 2) * lz / F, lz, true);\n }\n }\n trees.sort(function (a, b) { return b.Z - a.Z; });\n // The hedges themselves are drowned, but here and there a crest just breaks\n // the surface as a broken line of small touches lying exactly on it. It is\n // what the photographs of a fog lake show, and it gives the flat middle of\n // the sheet something to read as well as proving the surface is level.\n hedges.forEach(function (hg) {\n if (rand() < 0.35) return;\n for (var t = -700; t < 700; t += rr(2, 9)) {\n var cz = hg.Z0 + t * Math.cos(hg.ang), cxw = hg.X0 + t * Math.sin(hg.ang);\n if (cz < 30) continue;\n var cp = scr(cxw, 0, cz);\n if (cp.x < -20 || cp.x > W + 20) continue;\n var cy = mistY(cz);\n if (cy > bankY(cp.x) - 3 || cy < topY(cp.x) + 2) continue;\n if (rand() < 0.45) continue; // mostly under, breaking through in runs\n var cl = Math.max(1.2, 0.5 * cp.s), cv = vis(cz);\n G.stroke([{ x: cp.x, y: cy + rr(-0.3, 0.3) }, { x: cp.x + cl, y: cy + rr(-0.3, 0.3) }],\n { s: 0.5, p: (0.3 + 0.25 * cv) * rr(0.6, 1.2), r: Math.max(0.4, 0.08 * cp.s), k: 0.45, tin: 0.15, tout: 0.2 });\n }\n });\n\n function growTree(rnd, height, lean, depthMax) {\n function r2(a, b) { return a + (b - a) * rnd(); }\n var br = [];\n function grow(x, y, a, len, r, depth) {\n var n = Math.max(3, Math.round(len / 0.22)), pts = [{ x: x, y: y, r: r }];\n var bend = (rnd() - 0.5) * 0.06, sweep = 0.02 + 0.05 * depth / depthMax, rEnd = r * 0.78;\n for (var k = 1; k <= n; k++) {\n a += bend + (rnd() - 0.5) * 0.14 + (Math.PI / 2 - a) * sweep;\n x += Math.cos(a) * len / n; y += Math.sin(a) * len / n;\n pts.push({ x: x, y: y, r: r + (rEnd - r) * k / n });\n }\n br.push({ pts: pts, depth: depth });\n if (depth >= depthMax || rEnd < 0.008) return;\n var nl = depth === 0 ? 0 : 1 + (rnd() < 0.6 ? 1 : 0) + (depth > 2 && rnd() < 0.5 ? 1 : 0);\n for (var j = 0; j < nl; j++) {\n var kk = Math.floor(n * r2(0.35, 0.8)), p = pts[kk], side = rnd() < 0.5 ? -1 : 1;\n var dir = Math.atan2(pts[kk + 1].y - p.y, pts[kk + 1].x - p.x);\n grow(p.x, p.y, dir + side * r2(0.6, 1.0), len * r2(0.4, 0.6), p.r * 0.5, depth + 1);\n }\n var spread = r2(0.35, 0.7), share = r2(0.5, 0.7), tilt = (rnd() - 0.5) * 0.3;\n grow(x, y, a + spread * (1 - share) + tilt, len * r2(0.65, 0.85), rEnd * Math.sqrt(share), depth + 1);\n grow(x, y, a - spread * share + tilt, len * r2(0.6, 0.8), rEnd * Math.sqrt(1 - share), depth + 1);\n }\n grow(0, 0, Math.PI / 2 + lean, height * 0.34, height * 0.028, 0);\n var top = 0;\n br.forEach(function (b) { b.pts.forEach(function (p) { if (p.y > top) top = p.y; }); });\n var kk = height / top;\n br.forEach(function (b) { b.pts.forEach(function (p) { p.x *= kk; p.y *= kk; p.r *= kk; }); });\n return br;\n }\n function offsetLine(P, q) {\n return P.map(function (p, i) {\n var a = P[Math.max(0, i - 1)], b = P[Math.min(P.length - 1, i + 1)];\n var tx = b.x - a.x, ty = b.y - a.y, l = Math.hypot(tx, ty) || 1;\n return { x: p.x - ty / l * q * p.r, y: p.y + tx / l * q * p.r };\n });\n }\n var lifts = []; // eraser work, done last\n // Dawn shadows first: each tree lays a faint lane across the mist's top,\n // away from the light, which is what says the top is flat.\n trees.forEach(function (tr) {\n var s = F / tr.Z, hc = (tr.h - MT) * s, ey = mistY(tr.Z), ex = scr(tr.X, 0, tr.Z).x;\n if (hc < 4) return;\n var sl = hc * rr(1.6, 2.8);\n for (var k = 0; k < 3; k++) {\n G.stroke([{ x: ex, y: ey + 1 + k }, { x: ex - LIGHT * sl, y: ey + 1 + k + hc * 0.1 }],\n { s: 0.8, p: 0.14 * vis(tr.Z), r: Math.max(1, Math.min(6, hc * 0.06)), k: 0.3, tin: 0.05, tout: 0.6 });\n }\n });\n trees.forEach(function (tr) {\n // The dawn is behind the hollow, so even a far tree keeps most of its\n // weight: v3's haze took the mid-ground trees down to pale scrub.\n // The near tree is the firmest thing on the sheet and the only one drawn\n // at full pressure (Seurat: only the nearest trunk firm).\n var base = scr(tr.X, 0, tr.Z), s = base.s, near = clamp01((260 - tr.Z) / 200);\n var v = (0.55 + 0.45 * vis(tr.Z)) * (tr.big ? 1.15 : 1);\n var dm = tr.Z < 110 ? 7 : tr.Z < 250 ? 6 : 5;\n // Far trees are ticks with a little mass on top, not drawn trees: at this\n // size v2's branches and twig haze read as scrub (Friedrich's far conifers).\n var crown = (tr.h - MT) * s;\n if (crown < 26) {\n var ty0 = mistY(tr.Z);\n G.stroke([{ x: base.x, y: ty0 + 1 }, { x: base.x + rr(-1, 1), y: ty0 - crown }],\n { s: 0.8, p: 0.8 * v, r: Math.max(0.4, crown * 0.035), k: 0.6, tin: 0.02, tout: 0.25 });\n for (var ks = 0, nks = 2 + (rand() * 3 | 0); ks < nks; ks++) {\n scribble(base.x + rr(-0.3, 0.3) * crown, ty0 - crown * rr(0.45, 0.95), crown * rr(0.18, 0.33),\n { s: 0.8, p: 0.7 * v, r: Math.max(0.4, crown * 0.03), k: 0.55, tin: 0.05, tout: 0.2 });\n }\n return;\n }\n growTree(G.rngFrom(tr.seed), tr.h, tr.lean, dm).forEach(function (b) {\n var P = [];\n b.pts.forEach(function (p) {\n var f = smooth(MT - 1.6, MT + 0.8, p.y);\n if (f > 0.02 || P.length) P.push({ x: base.x + p.x * s, y: base.y - p.y * s, r: p.r * s, f: f });\n });\n if (P.length < 2) return;\n var r0 = P[0].r, f0 = P[0].f, f1 = P[P.length - 1].f;\n if (r0 > 1.3) {\n // Trunk and big limbs filled along their length, darker at the rims\n // and on the side away from the dawn.\n var nL = Math.ceil(2 * r0 / 0.9);\n for (var j = 0; j < nL; j++) {\n // A silhouette against the lit mist, and the firmest thing on the\n // sheet: the near trunk is where the drawing earns its darks.\n var q = -1 + (2 * j + 1) / nL, pr = (1 + 0.1 * Math.pow(Math.abs(q), 1.5) + 0.1 * Math.max(0, -LIGHT * q)) * v;\n G.stroke(offsetLine(P, q), { s: 1, p: pr * f0, p1: pr * f1, r: 0.8, k: 0.75, tin: 0.02, tout: 0.05 });\n }\n if (b.depth <= 1) lifts.push({ pts: offsetLine(P, 0.5 * LIGHT), r: Math.max(0.8, r0 * 0.12), e: 0.3 });\n } else if (r0 > 0.5) {\n var pb = Math.max(0.6, 1 - 0.05 * b.depth) * v;\n G.stroke(P, { s: Math.max(0.4, 0.6 + 0.4 * near - 0.05 * b.depth), p: pb * f0, p1: pb * f1,\n r: Math.max(0.4, Math.min(1.3, r0 * 0.9)), k: 0.65, tin: 0.03, tout: 0.3 });\n } else {\n // Twigs: hairlines of harder lead.\n G.stroke(P, { s: 0.3, p: 0.72 * v * f0, p1: 0.72 * v * f1, r: 0.35, k: 0.55, tin: 0.05, tout: 0.3 });\n }\n // The twig haze at the crown's edge, in light touches.\n if (b.depth >= dm - 1 && rand() < 0.25 && f1 > 0.5 && 0.45 * s > 3) {\n var e = P[P.length - 1];\n scribble(e.x, e.y, Math.max(1.5, Math.min(7, 0.45 * s)), { s: 0.55, p: 0.22 * v, r: 0.4, k: 0.45, tin: 0.1, tout: 0.2 });\n }\n });\n // The mist's top wrapping the trunk where it goes in.\n var ey = mistY(tr.Z), ew = 0.9 * s + 3;\n lifts.push({ pts: [{ x: base.x - ew, y: ey - 0.5 }, { x: base.x + ew, y: ey - 0.5 }], r: Math.max(0.8, Math.min(3, 0.25 * s)), e: 0.3 });\n });\n\n // ---------------------------------------------------------------------------\n // The near bank: rough grass going down into the mist, drawn in sweeps along\n // the fall of the bank, tufts of two to four blades, a dark foot worked until\n // it polishes, and a few dead dock stalks. It fades into the mist at its\n // lower edge by value.\n // ---------------------------------------------------------------------------\n function bankFade(x, y) { var b = bankY(x); return smooth(b - 2, b + 14 + 30 * bankNear(x, y), y); }\n function bankPass(o) {\n for (var gy = MTOP + (MBOT - MTOP) * 0.3; gy < H + 20;) {\n var gxStep = 0;\n for (var gx = -rr(0, 60); gx < W + 10; gx += gxStep) {\n var x = gx + rr(-3, 3), y = gy + rr(-1, 1), near = bankNear(x, y);\n var len = o.len0 + o.len1 * Math.pow(near, 1.3);\n gxStep = len * rr(o.g0, o.g1);\n if (y <= bankY(x)) continue;\n // Direction wanders with the lie of the ground: v1's level rows of\n // dashes read as a ruled, planted field.\n var fd = bankFade(x, y), l = len * rr(0.7, 1.2);\n var a = bankDir(x, y) + 0.5 * (noise(x / 200, y / 120, seed + 520) - 0.5) + rr(-o.jit, o.jit);\n G.stroke(bowed({ x: x, y: y + rr(-o.yj, o.yj) }, { x: x + Math.cos(a) * l, y: y + Math.sin(a) * l }, 0.08),\n { s: o.s0 + o.s1 * near, p: (o.p0 + o.p1 * near) * fd, r: o.r0 + o.r1 * near, k: o.k, tin: 0.1, tout: 0.3 });\n }\n gy += o.sp0 + o.sp1 * Math.pow(1 - bankNear(W / 2, gy), 2);\n }\n }\n // A mid tone under the grass, laid broad and overlapping, so the frost has\n // something to be lifted out of: v1's pale bank took no lift-outs.\n bankPass({ len0: 40, len1: 260, sp0: 5, sp1: 6, g0: 0.35, g1: 0.8, s0: 0.6, s1: 0.3, p0: 0.18, p1: 0.35, r0: 2, r1: 4, k: 0.3, jit: 0.2, yj: 3 });\n bankPass({ len0: 8, len1: 160, sp0: 3.5, sp1: 5, g0: 1.0, g1: 2.4, s0: 0.35, s1: 0.6, p0: 0.3, p1: 0.5, r0: 0.5, r1: 1.2, k: 0.6, jit: 0.3, yj: 3 });\n // The dark foot, on the side away from the dawn, graded over a wide span.\n // Weighted to the foot of the sheet and to the side away from the dawn.\n // 🔴 Graded on the sheet, never on the bank's edge: v7 followed the curve of\n // bankY and the dark read as a dome sitting in the foreground (the same\n // mound Field Edge's v4 drew). v2 graded it only downward and it lay across\n // the foot as a stripe.\n var CX = W * rr(0.45, 0.95);\n function foot(x, y) {\n var down = smooth(H * 0.62, H * 1.02, y + 30 * (noise(x / 320, 2.7, seed + 530) - 0.5));\n var side = LIGHT > 0 ? smooth(CX, 0, x) : smooth(W - CX, W, x);\n return Math.pow(down, 1.25) * (0.35 + 0.65 * side);\n }\n // Worked over three times until the lead saturates and polishes: this corner\n // is the one place the sheet goes as dark as graphite goes, which is not\n // black. Erin on v6: \"Too faint. Make some of it stand out.\"\n [0, 0.12, -0.1].forEach(function (off) {\n for (var fy = MTOP; fy < H + 10; fy += 1.8) {\n for (var fx = -10; fx < W + 10; fx += rr(18, 45)) {\n var fc = foot(fx, fy);\n if (fc < 0.03 || fy <= bankY(fx) + 2 || rand() > Math.pow(fc, 0.4)) continue;\n var fdir = bankDir(fx, fy) + off + rr(-0.15, 0.15), fl = rr(40, 110);\n G.stroke(bowed({ x: fx, y: fy + rr(-2, 2) }, { x: fx + Math.cos(fdir) * fl, y: fy + Math.sin(fdir) * fl }, 0.05),\n { s: 1, p: 0.7 + 0.3 * fc, r: 1.4, k: 0.9, tin: 0.1, tout: 0.3 });\n }\n }\n });\n /**\n * A clump: one to six blades of mixed length from one root, some laid over\n * by the night. v1's even two-to-four-blade fans stood in rows like seedlings.\n */\n function tuft(x, y, len, o, fn) {\n var n = 1 + (rand() * 6 | 0), lean = rr(-0.3, 0.3);\n for (var b = 0; b < n; b++) {\n var a = -Math.PI / 2 + lean + rr(-0.5, 0.5);\n if (rand() < 0.25) a += (rand() < 0.5 ? -1 : 1) * rr(0.6, 1.1);\n var l = len * rr(0.4, 1.3), bend = rr(-0.4, 0.4) + (a + Math.PI / 2) * 0.4;\n var m = { x: x + Math.cos(a) * l * 0.55, y: y + Math.sin(a) * l * 0.55 };\n var e = { x: x + Math.cos(a + bend) * l, y: y + Math.sin(a + bend) * l };\n fn([{ x: x, y: y }, m, e], o);\n }\n }\n for (var tf = 0; tf < 2600; tf++) {\n var tx2 = rr(-10, W + 10), bY = bankY(tx2), ty2 = bY + (H + 10 - bY) * Math.pow(rand(), 0.85);\n var tn = bankNear(tx2, ty2), tfd = bankFade(tx2, ty2);\n // Paper between the marks up the bank, where the ground turns away: v5\n // covered the whole bank evenly and it read as a thicket.\n if (tfd < 0.1 || rand() > 0.35 + 0.65 * tn) continue;\n tuft(tx2, ty2, (3 + 46 * Math.pow(tn, 1.3)) * rr(0.5, 1.3), { s: 0.55 + 0.45 * tn, p: (0.5 + 0.55 * tn) * tfd, r: 0.4 + 0.6 * tn, k: 0.6, tin: 0.02, tout: 0.4 }, G.stroke);\n }\n // Big tussocks in the front: without a change of scale toward the eye the\n // whole bank reads as one carpet of small marks (v3).\n for (var tg = 0, ntg = 90 + (rand() * 60 | 0); tg < ntg; tg++) {\n var gx0 = rr(-20, W + 20), gb = bankY(gx0), gy0 = gb + (H + 20 - gb) * Math.pow(rand(), 0.5);\n var gn = bankNear(gx0, gy0);\n if (gn < 0.45) continue;\n tuft(gx0, gy0, (30 + 70 * gn) * rr(0.7, 1.4), { s: 0.9, p: 0.9 + 0.1 * gn, r: 0.8 + 1.2 * gn, k: 0.8, tin: 0.02, tout: 0.45 }, G.stroke);\n }\n // Dead dock stalks, the darkest single strokes on the sheet, each with its\n // seed heads in clusters up the top half.\n for (var dk2 = 0, nd = rand() < 0.9 ? 2 + (rand() * 4 | 0) : 0, dcx = rr(0.1, 0.9); dk2 < nd; dk2++) {\n var dxx = W * clamp01(dcx + rr(-0.12, 0.12)), dbY = bankY(dxx), dyy = dbY + (H + 30 - dbY) * rr(0.55, 1);\n var dn = bankNear(dxx, Math.min(H, dyy)), dh = rr(90, 260) * dn, dl2 = rr(-0.12, 0.12), stem = [];\n for (var q4 = 0; q4 <= 10; q4++) {\n var f4 = q4 / 10;\n stem.push({ x: dxx + Math.sin(dl2) * dh * f4 + 6 * Math.sin(f4 * 3 + dk2) * f4, y: dyy - dh * f4 });\n }\n var dr = Math.max(0.6, 1.6 * dn);\n G.stroke(stem, { s: 1, p: 1, r: dr, k: 0.85, tin: 0.02, tout: 0.2 });\n for (var sb = 0; sb < 9; sb++) {\n var at = stem[4 + (rand() * 6 | 0)], sa = -Math.PI / 2 + (rand() < 0.5 ? -1 : 1) * rr(0.3, 0.7), sl2 = rr(0.05, 0.12) * dh;\n var se = { x: at.x + Math.cos(sa) * sl2, y: at.y + Math.sin(sa) * sl2 };\n G.stroke([at, se], { s: 0.95, p: 0.92, r: dr * 0.5, k: 0.7, tin: 0.02, tout: 0.3 });\n for (var sh2 = 0; sh2 < 3; sh2++) scribble(se.x + rr(-2, 2), se.y + rr(-3, 4), rr(2, 4) * (0.5 + dn), { s: 0.95, p: 0.9, r: 0.6, k: 0.7 });\n }\n lifts.push({ pts: stem.map(function (p) { return { x: p.x + LIGHT * dr * 0.4, y: p.y }; }), r: Math.max(0.5, dr * 0.35), e: 0.55 });\n }\n\n // ---------------------------------------------------------------------------\n // The stump along the bank's edge, so bank and mist meet at one value.\n // ---------------------------------------------------------------------------\n G.blend(function (x, y) { var b = (y - bankY(x)) / 14; return 0.65 * Math.exp(-b * b); }, 2.5);\n\n // ---------------------------------------------------------------------------\n // The kneaded eraser. A lit rim under the mist's top edge and a few level\n // drifts of light across the lake; the light side of the near trunks; then\n // the frost: blades, level drags and points lifted out of the grass, thickest\n // toward the mist where the hollow is coldest.\n // ---------------------------------------------------------------------------\n for (var rx2 = -10; rx2 < W + 10;) {\n var rl = rr(60, 240), rpts = [];\n for (var q5 = 0; q5 <= 10; q5++) { var rxx = rx2 + rl * q5 / 10; rpts.push({ x: rxx, y: topY(rxx) + 2.6 }); }\n if (rand() < 0.85) G.erase(rpts, { r: 1.5, e: 0.45 });\n rx2 += rl + rr(0, 30);\n }\n for (var dr2 = 0, ndr = 5 + (rand() * 8 | 0); dr2 < ndr; dr2++) {\n var dry = rr(MTOP + 8, MBOT - 10), ddp2 = mistDepth(dry), drl = rr(80, 400) * (0.4 + ddp2), drx = rr(-100, W);\n runs(drx, drx + drl, dry, inMist).forEach(function (rn) {\n G.erase([{ x: rn[0], y: dry }, { x: rn[1], y: dry + rr(-0.4, 0.4) }], { r: 1 + 2.5 * ddp2, e: 0.22 });\n });\n }\n lifts.forEach(function (l) { G.erase(l.pts, { r: l.r, e: l.e }); });\n for (var fr = 0; fr < 3200; fr++) {\n var fx2 = rr(-10, W + 10), fbY = bankY(fx2), fy2 = fbY + (H + 10 - fbY) * Math.pow(rand(), 1.1);\n var fn2 = bankNear(fx2, fy2), cold = 1 - 0.45 * fn2;\n if (rand() > cold) continue;\n var kind = rand();\n if (kind < 0.55) {\n tuft(fx2, fy2, (4 + 55 * Math.pow(fn2, 1.2)) * rr(0.5, 1.3), { r: 0.6 + 1.1 * fn2, e: 0.85 }, G.erase);\n } else if (kind < 0.85) {\n var fa = bankDir(fx2, fy2) + rr(-0.1, 0.1), fl2 = rr(6, 30) * (0.4 + fn2);\n G.erase([{ x: fx2, y: fy2 }, { x: fx2 + Math.cos(fa) * fl2, y: fy2 + Math.sin(fa) * fl2 }], { r: 0.8 + 1.6 * fn2, e: 0.4 });\n } else {\n G.erase([{ x: fx2, y: fy2 }, { x: fx2 + rr(1, 3), y: fy2 - rr(0, 1) }], { r: 0.7 + 0.5 * fn2, e: 0.9 });\n }\n }\n\n G.finish(pal[0], pal[pal.length - 1]);\n}\n",
"layers": []
}