
Forest Ride in Mist
A grass ride cut straight through a pine plantation, on a morning of mist. You stand between the wheel tracks and they run away from you to a gap in the trees a little above the middle of the sheet, where the mist is lightest. The pines stand in two walls. The near ones are only trunks, their crowns far above the frame; further in you can see their lower branches; the furthest are a few upright strokes, and then nothing. One trunk on each side is drawn firmly, in modelled tone darkening to its shaded edge. Everything else stays pale, and the gap at the end of the ride is the lightest thing on the sheet.
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 looks straight down the ride, so every tree, wheel track and verge shares one vanishing point. The ground is drawn by what it is: level strokes clumped into tussocks on the grass, darker under the trees, and two bare wheel tracks an axle apart that wander a little in line and width. Trunks take a smooth tone of hard lead first and soft lead down the shaded side, as Seurat models his trees; only the nearest trunk on each side is firm. Branches are drooping strokes with needles hanging from the nearer ones, sparse the way Friedrich draws conifers. With distance the marks thin and the lead hardens until nothing is drawn; hard lead laid level makes the mist, a stump softens the gap, and a kneaded eraser lifts two faint shafts of light and the light side of the firm trunks. The seed moves the vanishing point, the ride's width, every tree, and how far one can see.
Seeds
The same system at three seeds. The composition itself re-cuts: the geometry is derived from the seed, so masses, edges and placement all move, while the palette and the drawing language stay put.
1823
1810
1884
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": "forest-ride",
"title": "Forest Ride in Mist",
"created": "2026-09-11T00:00:00Z",
"modified": "2026-09-11T21:08:46.746Z",
"renderer": {
"type": "canvas2d",
"version": "1.x"
},
"canvas": {
"width": 1200,
"height": 1500,
"pixelDensity": 2
},
"parameters": [],
"colors": [],
"state": {
"seed": 1823,
"params": {},
"colorPalette": [
"#e8e3d7",
"#d2cec4",
"#bbb8b0",
"#a5a39d",
"#8e8d89",
"#787876",
"#616262",
"#4b4d4f",
"#34373b"
]
},
"algorithm": "// Weather Book: the graphite material. The builder prepends this file to each\n// sheet's algorithm, so every sheet in the series draws with the same lead on\n// the same paper. Series-local on purpose (plan section 7): it moves to a\n// shared package only when a second series needs it, and that move gets an ADR.\n//\n// The model, after Costa Sousa & Buchanan (2000):\n// - The paper is a height field (its tooth). A pencil tip rides at a height\n// set by its pressure and its grade and lays lead only on grain above it,\n// so soft lead used lightly catches the peaks (light and grainy) and hard\n// lead used firmly reaches the valleys (light and smooth).\n// - Each grain holds a cap set by the grade. Build-up saturates, a harder\n// lead can never darken what a softer one laid, and even the softest lead\n// stops short of black.\n// - Where the lead lies heaviest the platelets polish, and the value lifts\n// slightly toward silver (the sheen).\n// - A stump pushes lead into the valleys; a kneaded eraser lifts it, more\n// from the peaks than from the valleys.\n//\n// Marks are placed in logical px and laid in device px, so the grain is the\n// same physical size at --scale 1 and at --scale 2.\nfunction graphiteSheet(ctx, W, H, seed) {\n var PW = ctx.canvas.width, PH = ctx.canvas.height;\n var D = PW / W;\n var N = PW * PH;\n var tooth = new Float32Array(N); // paper height, 0 in a valley .. 1 on a peak\n var lead = new Float32Array(N); // darkness laid, 0 bare paper .. ~0.92\n var strokes = 0; // gives each stroke its own tip\n\n function rngFrom(a) {\n return function () {\n a |= 0; a = a + 0x6D2B79F5 | 0;\n var t = Math.imul(a ^ a >>> 15, 1 | a);\n t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;\n return ((t ^ t >>> 14) >>> 0) / 4294967296;\n };\n }\n function hash2(ix, iy, salt) {\n var h = Math.imul(ix | 0, 374761393) ^ Math.imul(iy | 0, 668265263) ^ Math.imul(salt | 0, 1597334677);\n h = Math.imul(h ^ h >>> 13, 1274126177);\n return ((h ^ h >>> 16) >>> 0) / 4294967296;\n }\n /** Smooth value noise, 0..1. */\n function noise(x, y, salt) {\n var ix = Math.floor(x), iy = Math.floor(y);\n var fx = x - ix, fy = y - iy;\n fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy);\n var a = hash2(ix, iy, salt), b = hash2(ix + 1, iy, salt);\n var c = hash2(ix, iy + 1, salt), d = hash2(ix + 1, iy + 1, salt);\n return (a + (b - a) * fx) * (1 - fy) + (c + (d - c) * fx) * fy;\n }\n\n // --- The paper -------------------------------------------------------------\n // A fine tooth, a coarser felt under it, and faint laid lines across the\n // sheet. Then equalised, so a tip riding at height t touches the top 1 - t\n // of the grain whatever the noise's own distribution.\n var BINS = 1024, hist = new Uint32Array(BINS);\n var s1 = seed * 3 + 11, s2 = seed * 3 + 12, s3 = seed * 3 + 13;\n for (var py = 0; py < PH; py++) {\n var y = (py + 0.5) / D;\n for (var px = 0; px < PW; px++) {\n var x = (px + 0.5) / D;\n // Mostly fine tooth. v4 weighted a 4.5 px felt at 0.4 and the threshold\n // drew its contours as camouflage blobs; v3's salt and pepper came from a\n // switch-like contact, not from the fine scale, and the ramp below fixes that.\n var v = 0.62 * noise(x / 1.25, y / 1.25, s1) + 0.23 * noise(x / 3.2, y / 2.8, s2) +\n 0.15 * noise(x / 18, y / 1.6, s3);\n tooth[py * PW + px] = v;\n hist[Math.min(BINS - 1, (v * BINS) | 0)]++;\n }\n }\n var cdf = new Float32Array(BINS), acc = 0;\n for (var b = 0; b < BINS; b++) { acc += hist[b]; cdf[b] = acc / N; }\n for (var i = 0; i < N; i++) tooth[i] = cdf[Math.min(BINS - 1, (tooth[i] * BINS) | 0)];\n\n // --- The lead ----------------------------------------------------------------\n /**\n * One touch of the tip, in device px. s is softness, 0 (2H) .. 1 (6B); p is\n * pressure 0..1; k is the share of the way to the grain's cap it lays.\n * (nx, ny) is the stroke's normal and salt the stroke's own tip: a worn lead\n * is not round, so it lays fine striations along the stroke's direction.\n */\n function dab(cx, cy, r, s, p, k, nx, ny, salt) {\n var t = 1.0 - p * (0.95 + 0.3 * (1 - s)); // how low the tip reaches into the grain\n var cap = 0.30 + 0.62 * s; // the darkest this grade can go\n var smear = p * p * 0.18; // pressed lead smears a little into the valleys\n var x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(PW - 1, Math.ceil(cx + r));\n var y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(PH - 1, Math.ceil(cy + r));\n var r2 = r * r;\n for (var yy = y0; yy <= y1; yy++) {\n var dy = yy + 0.5 - cy, row = yy * PW;\n for (var xx = x0; xx <= x1; xx++) {\n var dx = xx + 0.5 - cx, d2 = dx * dx + dy * dy;\n if (d2 >= r2) continue;\n var j = row + xx, h = tooth[j];\n // A graded contact, not a switch: v3's steep ramp made every pixel\n // all-or-nothing.\n var c = (h - t) * 3;\n if (c < smear) c = smear;\n if (c <= 0) continue;\n var qq = (dx * nx + dy * ny) * 0.8 + salt, iq = Math.floor(qq), fq = qq - iq;\n fq = fq * fq * (3 - 2 * fq);\n var ha = hash2(iq, 0, 77), hb = hash2(iq + 1, 0, 77);\n c *= 0.45 + 0.75 * (ha + (hb - ha) * fq);\n if (c > 1) c = 1;\n var target = cap * (0.8 + 0.2 * h), cur = lead[j];\n if (cur < target) lead[j] = cur + (target - cur) * k * c * (1 - d2 / r2);\n }\n }\n }\n /** A kneaded eraser's touch: lifts a share e of the lead, more from the peaks. */\n function lift(cx, cy, r, e) {\n var x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(PW - 1, Math.ceil(cx + r));\n var y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(PH - 1, Math.ceil(cy + r));\n var r2 = r * r;\n for (var yy = y0; yy <= y1; yy++) {\n var dy = yy + 0.5 - cy, row = yy * PW;\n for (var xx = x0; xx <= x1; xx++) {\n var dx = xx + 0.5 - cx, d2 = dx * dx + dy * dy;\n if (d2 >= r2) continue;\n var j = row + xx;\n lead[j] *= 1 - e * (1 - d2 / r2) * (0.5 + 0.5 * tooth[j]);\n }\n }\n }\n /**\n * Walk a polyline (logical px) in steps, calling touch(x, y, t, ux, uy) in\n * device px, with (ux, uy) the unit direction of travel.\n */\n function walk(pts, stepDev, touch) {\n var L = 0, j;\n for (j = 1; j < pts.length; j++) L += Math.hypot(pts[j].x - pts[j - 1].x, pts[j].y - pts[j - 1].y);\n if (!(L > 0)) return;\n var step = stepDev / D, done = 0, carry = 0;\n for (j = 1; j < pts.length; j++) {\n var a = pts[j - 1], b = pts[j], l = Math.hypot(b.x - a.x, b.y - a.y);\n var ux = l > 0 ? (b.x - a.x) / l : 1, uy = l > 0 ? (b.y - a.y) / l : 0;\n var u = carry;\n for (; u < l; u += step) {\n var f = u / l;\n touch((a.x + (b.x - a.x) * f) * D, (a.y + (b.y - a.y) * f) * D, (done + u) / L, ux, uy);\n }\n carry = u - l; done += l;\n }\n }\n /**\n * One pencil stroke along a polyline in logical px. o.s softness, o.p the\n * pressure at the start and o.p1 at the end, o.r the tip radius in logical\n * px, o.k how much a pass lays (default 0.5), o.tin / o.tout the share of the\n * stroke over which the pencil lands and lifts.\n */\n function stroke(pts, o) {\n if (pts.length < 2) return;\n var s = o.s, p0 = o.p, p1 = o.p1 == null ? o.p : o.p1;\n var rDev = Math.max(0.75, o.r * D);\n var tin = Math.max(1e-3, o.tin == null ? 0.08 : o.tin), tout = Math.max(1e-3, o.tout == null ? 0.2 : o.tout);\n var stepDev = Math.max(0.5, rDev * 0.4);\n // Per touch, so that one pass lays about k whatever the step.\n var kk = Math.min(1, (o.k == null ? 0.6 : o.k) * stepDev / rDev);\n var salt = (++strokes * 7.31) % 1000;\n walk(pts, stepDev, function (x, y, t, ux, uy) {\n var env = Math.min(1, t / tin, (1 - t) / tout);\n if (env <= 0) return;\n dab(x, y, rDev * (0.55 + 0.45 * env), s, (p0 + (p1 - p0) * t) * Math.sqrt(env), kk, -uy, ux, salt);\n });\n }\n /** A kneaded-eraser stroke: o.r radius in logical px, o.e strength 0..1. */\n function erase(pts, o) {\n var rDev = Math.max(0.75, o.r * D), stepDev = Math.max(0.5, rDev * 0.4);\n var ee = Math.min(1, o.e * stepDev / rDev);\n walk(pts, stepDev, function (x, y, t) {\n var env = Math.min(1, t / 0.15, (1 - t) / 0.3);\n if (env > 0) lift(x, y, rDev, ee * env);\n });\n }\n /**\n * The stump: blur the lead over `radius` logical px and mix it back by\n * mask(x, y) in logical px. Lead leaves the peaks for the valleys, so the\n * grain closes and the tone goes smooth.\n */\n function blend(mask, radius) {\n var R = Math.max(1, Math.round(radius * D)), tmp = new Float32Array(N), out = new Float32Array(N);\n var xx, yy, sum, j, w = 2 * R + 1;\n for (yy = 0; yy < PH; yy++) {\n var row = yy * PW;\n sum = 0;\n for (xx = -R; xx <= R; xx++) sum += lead[row + Math.min(PW - 1, Math.max(0, xx))];\n for (xx = 0; xx < PW; xx++) {\n tmp[row + xx] = sum / w;\n sum += lead[row + Math.min(PW - 1, xx + R + 1)] - lead[row + Math.max(0, xx - R)];\n }\n }\n for (xx = 0; xx < PW; xx++) {\n sum = 0;\n for (yy = -R; yy <= R; yy++) sum += tmp[Math.min(PH - 1, Math.max(0, yy)) * PW + xx];\n for (yy = 0; yy < PH; yy++) {\n out[yy * PW + xx] = sum / w;\n sum += tmp[Math.min(PH - 1, yy + R + 1) * PW + xx] - tmp[Math.max(0, yy - R) * PW + xx];\n }\n }\n for (yy = 0; yy < PH; yy++) {\n for (xx = 0; xx < PW; xx++) {\n var m = mask((xx + 0.5) / D, (yy + 0.5) / D);\n if (m > 0) { j = yy * PW + xx; lead[j] += (out[j] - lead[j]) * Math.min(1, m); }\n }\n }\n }\n function rgb(hex) {\n var n = parseInt(hex.slice(1), 16);\n return [n >> 16 & 255, n >> 8 & 255, n & 255];\n }\n /**\n * Lay the sheet down. Every pixel sits on the one line from the paper to the\n * darkest the lead can go, so the palette explains the whole frame.\n */\n function finish(paperHex, leadHex) {\n var P = rgb(paperHex), G = rgb(leadHex);\n var img = ctx.createImageData(PW, PH), out = img.data;\n for (var j = 0, o = 0; j < N; j++, o += 4) {\n var h = tooth[j], L = lead[j];\n // The tooth shows a little in the bare paper, as it does under raking light.\n var k = L + 0.03 * (1 - h) * (1 - L);\n // Sheen: the heaviest lead polishes and silvers, the peaks most.\n var sh = (L - 0.70) / 0.18;\n if (sh > 0) { if (sh > 1) sh = 1; k -= 0.09 * sh * sh * (0.4 + 0.6 * h); }\n if (k < 0) k = 0; else if (k > 1) k = 1;\n out[o] = P[0] + (G[0] - P[0]) * k;\n out[o + 1] = P[1] + (G[1] - P[1]) * k;\n out[o + 2] = P[2] + (G[2] - P[2]) * k;\n out[o + 3] = 255;\n }\n ctx.putImageData(img, 0, 0);\n }\n\n return { D: D, rngFrom: rngFrom, noise: noise, stroke: stroke, erase: erase, blend: blend, finish: finish };\n}\n\n// Weather Book 2: Forest Ride in Mist.\n//\n// A ride cut straight through a conifer plantation, seen from the middle of it:\n// the track runs away from the eye to a vanishing point a little above the\n// centre of the sheet and goes out in mist. The trees stand in two walls, near\n// ones only trunks because their crowns are above the frame, far ones whole\n// cones, the farthest a few ticks (Friedrich's Giant Mountains). The lane closes\n// on a light gap, as Constable's lane at Staunton Harold does, and the whole\n// sheet sits in the top third of the value range except the nearest trunk on\n// each side, which is the one firm thing (Seurat's Trees). The ride itself is\n// paper.\n//\n// One level camera in metres, one-point: every tree, rut and verge agrees on\n// the horizon and on the size its distance gives it. The seed moves the\n// vanishing point, the ride's width, every tree, and how far one can see.\nfunction sketch(ctx, state) {\n var W = state.canvas.width, H = state.canvas.height;\n var seed = state.seed || 0;\n var pal = state.colorPalette;\n var G = graphiteSheet(ctx, W, H, seed);\n var rand = G.rngFrom(seed * 2654435761 + 4243);\n var noise = G.noise;\n function rr(a, b) { return a + (b - a) * rand(); }\n function clamp01(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }\n function smooth(e0, e1, x) { var t = clamp01((x - e0) / (e1 - e0)); return t * t * (3 - 2 * t); }\n\n // --- The camera: level, the eye 1.6 m up, looking straight down the ride.\n var EYE = 1.6, F = W * 1.05;\n var HZ = H * rr(0.41, 0.47);\n var VX = W / 2 + W * rr(-0.06, 0.06); // the ride's vanishing point\n function at(X, Y, Z) { return { x: VX + F * X / Z, y: HZ + F * (EYE - Y) / Z, s: F / Z }; }\n var V = rr(32, 60); // how far one can see in the mist, m\n function vis(Z) { return Math.exp(-Math.pow(Z / V, 1.4)); }\n var RIDE = rr(2.2, 3.4); // half the ride's width, m\n var ZN = F * EYE / (H - HZ) * 0.85; // the ground at the foot of the sheet\n\n // The light gap where the ride goes out.\n function glow(x, y) {\n var dx = (x - VX) / (W * 0.22), dy = (y - HZ + H * 0.04) / (H * 0.16);\n return Math.exp(-dx * dx - dy * dy);\n }\n /** Parallel strokes at `angle` over the band y0..y1, each handed to fn(a, b). */\n function hatch(angle, spacing, len0, len1, gap0, gap1, y0, y1, fn) {\n var dx = Math.cos(angle), dy = Math.sin(angle), nx = -dy, ny = dx;\n var nmin = 1e9, nmax = -1e9, tmin = 1e9, tmax = -1e9;\n [[0, y0], [W, y0], [0, y1], [W, y1]].forEach(function (c) {\n var n = c[0] * nx + c[1] * ny, t = c[0] * dx + c[1] * dy;\n nmin = Math.min(nmin, n); nmax = Math.max(nmax, n); tmin = Math.min(tmin, t); tmax = Math.max(tmax, t);\n });\n for (var n = nmin; n <= nmax; n += spacing * rr(0.7, 1.3)) {\n for (var t = tmin - rr(0, len1); t < tmax;) {\n var len = rr(len0, len1);\n var a = { x: n * nx + t * dx, y: n * ny + t * dy };\n var b = { x: a.x + len * dx, y: a.y + len * dy };\n var my = (a.y + b.y) / 2, mx = (a.x + b.x) / 2;\n if (my > y0 && my < y1 && mx > -len / 2 && mx < W + len / 2) fn(a, b);\n t += len + rr(gap0, gap1);\n }\n }\n }\n /** A hand's stroke is never ruled: bow it a little. */\n function bowed(a, b, amt) {\n var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2, l = Math.hypot(b.x - a.x, b.y - a.y) || 1;\n var off = (rand() - 0.5) * amt * l;\n return [a, { x: mx - (b.y - a.y) / l * off, y: my + (b.x - a.x) / l * off }, b];\n }\n function flick(x, y, h, lean, o) {\n G.stroke([{ x: x, y: y }, { x: x + Math.sin(lean) * h * 0.4, y: y - h * 0.55 },\n { x: x + Math.sin(lean) * h, y: y - Math.cos(lean) * h }], o);\n }\n\n // ---------------------------------------------------------------------------\n // Mist and the sky in the gap: level strokes of hard lead used firmly, light\n // and smooth, thickest along the horizon and thinning into the gap's light.\n // Hard lead cannot darken what a soft one lays later over it.\n // ---------------------------------------------------------------------------\n hatch(rr(-0.02, 0.02), 5, 50, 220, 10, 70, -20, HZ + H * 0.12, function (a, b) {\n var my = (a.y + b.y) / 2, band = Math.exp(-Math.pow((my - HZ) / (H * 0.12), 2));\n var g = glow((a.x + b.x) / 2, my);\n if (rand() > 0.35 + 0.5 * band) return;\n G.stroke(bowed(a, b, 0.01), { s: 0.12, p: (0.45 + 0.2 * band) * (1 - 0.5 * g), r: 1.3, k: 0.3 + 0.2 * band,\n tin: 0.15, tout: 0.2 });\n });\n\n // ---------------------------------------------------------------------------\n // The ground: the forest floor under both walls in level strokes, darkening\n // toward the eye; the ride between them left as paper but for its two ruts,\n // the grass between them and its verges. Everything thins into the mist.\n // ---------------------------------------------------------------------------\n // v1 drew only the floor outside the ride and left the ride as paper; with\n // the eye in the ride, that was all the near ground, and the bottom half of\n // the sheet was empty. Now the whole ground is drawn by what it is: the track\n // between the ruts nearly paper, the grass of the ride, and the floor under\n // the trees darkest, all in level strokes.\n function nearness(y) { return Math.pow(smooth(HZ, H, y), 0.7); }\n // Two bare wheel tracks an axle apart, grass between and beside them. v2\n // left everything between the ruts bare and the ride read as a paper road.\n var RUT = rr(0.7, 0.85), WT = 0.2, WIND = rr(-0.1, 0.1);\n // Each wheel track wanders a little in its line and its width: v3's were\n // ruled and read as painted lines.\n function rutAt(Z, sgn) { return sgn * (RUT + 0.1 * (noise(Z / 4, sgn * 3.1, seed + 301) - 0.5)); }\n function wtAt(Z, sgn) { return WT * (0.75 + 0.5 * noise(Z / 2.5, sgn * 7.7, seed + 302)); }\n for (var gy = HZ + 2; gy < H + 20;) {\n var Zg = F * EYE / (gy - HZ), v = vis(Zg), near = nearness(gy), k = F / Zg;\n var c1 = rutAt(Zg, -1), c2 = rutAt(Zg, 1), h1 = wtAt(Zg, -1), h2 = wtAt(Zg, 1);\n var w1a = VX + (c1 - h1) * k, w1b = VX + (c1 + h1) * k, w2a = VX + (c2 - h2) * k, w2b = VX + (c2 + h2) * k;\n var eL = VX - RIDE * k, eR = VX + RIDE * k;\n var len = 8 + 140 * Math.pow(near, 1.3);\n for (var gx = -rr(0, len); gx < W + 10; gx += len * rr(0.6, 1.4)) {\n if (rand() > Math.pow(v, 0.8)) continue;\n var x0 = gx + rr(-3, 3), x1 = x0 + len * rr(0.6, 1.2), y = gy + rr(-1, 1);\n // Cut the stroke where it crosses a wheel track, where the hand lifts;\n // now and then the grass runs over the edge instead.\n var segs = rand() < 0.15 ? [[x0, x1]]\n : [[x0, Math.min(x1, w1a)], [Math.max(x0, w1b), Math.min(x1, w2a)], [Math.max(x0, w2b), x1]];\n segs.forEach(function (seg) {\n if (seg[1] - seg[0] < 3) return;\n var mid = (seg[0] + seg[1]) / 2, floor = mid < eL || mid > eR;\n // Clumped, so the grass reads as tussocks and not an even weave (v3).\n var pr = (floor ? 0.5 + 0.5 * near : 0.4 + 0.5 * near) * (0.55 + 0.9 * noise(mid / 55, y / 22, seed + 303));\n G.stroke(bowed({ x: seg[0], y: y }, { x: seg[1], y: y + (seg[1] - seg[0]) * rr(-0.06, 0.06) }, 0.06),\n { s: 0.4 + 0.55 * near, p: pr * (0.3 + 0.7 * v), r: 0.5 + 1.0 * near, k: 0.55, tin: 0.1, tout: 0.3 });\n // A tuft: two to four blades fanning up from one root, set a little\n // off the stroke. v4's single blade rising from each stroke made a \"+\".\n if (!floor && near > 0.12 && rand() < 0.4) {\n var tx0 = rr(seg[0], seg[1]), ty0 = y + rr(1, 4) + 6 * near, th = Math.min(34, 5 + 28 * near);\n for (var bl = 0, nb = 2 + (rand() * 3 | 0); bl < nb; bl++) {\n flick(tx0 + rr(-2, 2), ty0, th * rr(0.45, 1), WIND + (bl - (nb - 1) / 2) * 0.3 + rr(-0.1, 0.1),\n { s: 0.4 + 0.5 * near, p: (0.35 + 0.5 * near) * v, r: 0.45 + 0.5 * near, k: 0.5, tin: 0.02, tout: 0.5 });\n }\n }\n });\n // The wheel tracks take only the odd light mark of hard lead.\n if (rand() < 0.12) {\n var tx = rand() < 0.5 ? rr(w1a, w1b) : rr(w2a, w2b);\n G.stroke([{ x: tx, y: y }, { x: tx + len * rr(0.1, 0.3), y: y }], { s: 0.25, p: 0.4 * v, r: 0.5 + 0.6 * near, k: 0.4 });\n }\n }\n gy += 2.2 + 8 * Math.pow(1 - near, 2);\n }\n // The edges of both wheel tracks, running to the vanishing point, broken\n // only a little.\n [[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(function (e) {\n for (var z = ZN * rr(0.9, 1.1); z < V * 2.5;) {\n var dz = rr(2, 6) * Math.max(1, z / 10), v = vis(z);\n if (rand() < Math.pow(v, 0.7)) {\n var pts = [];\n for (var q = 0; q <= 5; q++) {\n var zq = z + dz * q / 5, p = at(rutAt(zq, e[0]) + e[1] * wtAt(zq, e[0]) + rr(-0.03, 0.03), 0, zq);\n pts.push({ x: p.x, y: p.y });\n }\n var nr = nearness(pts[0].y);\n G.stroke(pts, { s: 0.45 + 0.5 * nr, p: (0.35 + 0.45 * nr) * (0.3 + 0.7 * v), r: 0.5 + 1.0 * nr, k: 0.6, tin: 0.1, tout: 0.3 });\n }\n z += dz + rr(0.2, 1.8) * Math.max(1, z / 10);\n }\n });\n\n // ---------------------------------------------------------------------------\n // The two walls of conifers, drawn from the farthest in. Near trees are only\n // trunks, their crowns above the frame; far ones whole cones of drooping\n // branches; the farthest a few ticks. With distance the marks get fewer and\n // the lead harder until nothing is drawn.\n // ---------------------------------------------------------------------------\n var trees = [];\n [-1, 1].forEach(function (side) {\n // Three rows, spaced: v1's four close rows massed into a stipple curtain.\n for (var row = 0; row < 3; row++) {\n for (var Z = rr(2.5, 5) + row; Z < V * 3; Z += rr(3, 6) * (1 + row * 0.3)) {\n var X = side * (RIDE + 0.6 + row * rr(1.6, 2.4) + rr(0, 1.2)), p = at(X, 0, Z);\n if (p.x < -W * 0.3 || p.x > W * 1.3) continue;\n trees.push({ X: X, Z: Z + rr(-0.8, 0.8), side: side, Ht: rr(15, 26), r: rr(0.16, 0.3),\n cb: rr(0.2, 0.38), cw: rr(0.15, 0.21), seed: (rand() * 1e6) | 0 });\n }\n }\n });\n trees.sort(function (a, b) { return b.Z - a.Z; });\n // Only the nearest trunk on each side whose foot is on the sheet is firm.\n [-1, 1].forEach(function (side) {\n var best = null;\n trees.forEach(function (t) {\n var x = at(t.X, 0, t.Z).x;\n if (t.side === side && x > W * 0.02 && x < W * 0.98 && (!best || t.Z < best.Z)) best = t;\n });\n if (best) best.firm = true;\n });\n var lifts = [];\n function drawConifer(t) {\n var v = vis(t.Z);\n if (v < 0.03) return;\n var rt = G.rngFrom(t.seed);\n function r2(a, b) { return a + (b - a) * rt(); }\n var b = at(t.X, 0, t.Z), s = b.s, soft = 0.25 + 0.7 * v;\n var lean = r2(-0.012, 0.012) * t.Ht * s;\n function sx(dx, Y) { return b.x + lean * (Y / t.Ht) + dx * s; }\n function sy(Y) { return b.y - Y * s; }\n // The trunk, cut at the top of the frame.\n var yTop = Math.min(t.Ht * 0.97, (b.y + 40) / s), P = [];\n for (var k = 0; k <= 12; k++) {\n var Y = yTop * k / 12;\n P.push({ x: sx(0, Y), y: sy(Y), r: t.r * s * (1 - 0.85 * Y / t.Ht) });\n }\n var rp = P[0].r;\n function side(q) { return P.map(function (p) { return { x: p.x + q * p.r, y: p.y }; }); }\n var fv = 0.35 + 0.65 * v;\n if (rp > 1.2) {\n // Tone first, in hard lead used firmly so it goes smooth, as Seurat\n // models his trunks; then soft lead down the shaded side and a light rim\n // on the lit one. v1 filled every trunk with soft grainy lines and the\n // walls read as stipple.\n var nL = Math.max(2, Math.ceil(2 * rp / 2.2));\n for (var j = 0; j < nL; j++) {\n G.stroke(side(-1 + (2 * j + 1) / nL), { s: t.firm ? 0.3 : 0.2, p: (t.firm ? 0.95 : 0.7) * fv,\n r: Math.max(0.8, rp / nL * 1.2), k: 0.6, tin: 0.02, tout: 0.05 });\n }\n if (t.firm) {\n // The firm trunk's shade graded across it, darkest at the shaded rim.\n for (var g = 0; g < 7; g++) {\n var qg = 0.15 + 0.85 * g / 6;\n G.stroke(side(qg), { s: 0.9, p: 0.45 + 0.5 * qg, r: Math.max(0.8, rp * 0.1), k: 0.6, tin: 0.02, tout: 0.05 });\n }\n }\n [[0.85, 0.6], [0.95, 0.6], [-0.92, 0.35]].forEach(function (e) {\n G.stroke(side(e[0]), { s: t.firm ? 0.98 : 0.3 + 0.6 * v, p: (t.firm ? Math.min(1, e[1] + 0.35) : e[1]) * fv,\n r: Math.max(0.6, rp * 0.08), k: 0.6, tin: 0.02, tout: 0.05 });\n });\n if (rp > 6 && t.firm) {\n // Pine bark: short broken plates across the trunk.\n for (var by = 0; by < yTop; by += 12 / s) {\n if (rt() > 0.6) continue;\n var bw = t.r * (1 - 0.85 * by / t.Ht), q0 = r2(-0.9, 0.2), q1 = q0 + r2(0.25, 0.7);\n var cx = sx(0, by), cy = sy(by);\n G.stroke([{ x: cx + q0 * bw * s, y: cy }, { x: cx + (q0 + q1) / 2 * bw * s, y: cy + r2(1, 3) },\n { x: cx + q1 * bw * s, y: cy }], { s: 0.9, p: (t.firm ? 0.8 : 0.5) * v, r: 0.8, k: 0.5 });\n }\n }\n if (t.firm) lifts.push({ pts: P.map(function (p) { return { x: p.x - 0.45 * p.r, y: p.y }; }), r: Math.max(1, rp * 0.1) });\n } else {\n G.stroke(P, { s: soft, p: 0.55 * v, r: Math.max(0.35, rp), k: 0.55, tin: 0.02, tout: 0.2 });\n }\n // Dead lower branches below the crown: short twigs angled down and out.\n var cb = t.cb * t.Ht;\n for (var dy = 1.2; dy < cb; dy += Math.max(0.4, 8 / s)) {\n if (rt() > 0.45 * v || sy(dy) < -20) continue;\n var d = rt() < 0.5 ? -1 : 1, L = r2(0.3, 1.1);\n G.stroke([{ x: sx(0, dy), y: sy(dy) }, { x: sx(d * L, dy - L * 0.4), y: sy(dy - L * 0.4) }],\n { s: 0.35, p: 0.5 * v, r: Math.max(0.35, Math.min(0.9, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.4 });\n }\n // The crown: tiers of drooping branches, a cone narrowing to the leader,\n // needles hanging from the near ones as short ticks.\n // Sparse, as Friedrich's are: v1's close tiers read as ruled lines.\n var step = Math.max(0.45, 9 / s), nT = s > 30 ? 4 : s > 14 ? 2 : 0;\n for (var cy2 = cb; cy2 < t.Ht; cy2 += step * r2(0.8, 1.2)) {\n if (sy(cy2) < -60) break;\n var R = t.cw * t.Ht * Math.pow(1 - (cy2 - cb) / (t.Ht - cb), 0.85);\n [-1, 1].forEach(function (d) {\n if (rt() > 0.75 * Math.pow(v, 0.6)) return;\n var L = R * r2(0.7, 1.1), droop = r2(0.15, 0.35) * L;\n var p0 = { x: sx(0, cy2), y: sy(cy2) };\n var p1 = { x: sx(d * L * 0.5, cy2), y: sy(cy2 - droop * 0.7) };\n var p2 = { x: sx(d * L, cy2), y: sy(cy2 - droop + 0.1 * L) };\n var pr = (0.55 + 0.3 * (1 - cy2 / t.Ht)) * (0.3 + 0.7 * v);\n G.stroke([p0, p1, p2], { s: soft, p: pr, r: Math.max(0.4, Math.min(1.6, 0.05 * s)), k: 0.55, tin: 0.03, tout: 0.3 });\n for (var n = 0; n < nT; n++) {\n var f = r2(0.2, 1), hx = p0.x + (p2.x - p0.x) * f, hy = p0.y + (p1.y - p0.y) * Math.min(1, 2 * f) + (p2.y - p1.y) * Math.max(0, 2 * f - 1);\n var hl = Math.min(18, r2(0.15, 0.35) * s);\n G.stroke([{ x: hx, y: hy }, { x: hx + d * hl * 0.3, y: hy + hl }],\n { s: soft, p: pr * 0.85, r: Math.max(0.35, Math.min(1, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.5 });\n }\n });\n }\n // The leader.\n if (sy(t.Ht) > -20) {\n G.stroke([{ x: sx(0, t.Ht * 0.9), y: sy(t.Ht * 0.9) }, { x: sx(0, t.Ht), y: sy(t.Ht) }],\n { s: soft, p: 0.6 * v, r: Math.max(0.35, Math.min(1, 0.02 * s)), k: 0.5, tin: 0.05, tout: 0.4 });\n }\n }\n trees.forEach(drawConifer);\n\n // ---------------------------------------------------------------------------\n // The stump through the gap, so the far trees, the mist and the ride meet at\n // one value; then the kneaded eraser: two faint shafts of light falling\n // through the mist, and the light side of each firm trunk.\n // ---------------------------------------------------------------------------\n G.blend(function (x, y) { return 0.85 * glow(x, y); }, 3);\n for (var sh = 0, nSh = 1 + (rand() * 2 | 0); sh < nSh; sh++) {\n var x0 = VX + W * rr(-0.35, 0.1), a = rr(1.0, 1.2), l = H * 0.7;\n G.erase([{ x: x0, y: -40 }, { x: x0 + Math.cos(a) * l, y: -40 + Math.sin(a) * l }], { r: rr(10, 18), e: 0.2 });\n }\n lifts.forEach(function (l) { G.erase(l.pts, { r: l.r, e: 0.3 }); });\n\n G.finish(pal[0], pal[pal.length - 1]);\n}\n",
"layers": []
}