
Mouths of a River
A river delta in plan, engraved as a survey chart. The river comes down out of a valley between bluffs, meanders across the plain behind the old coast, and at the coast divides into channels that divide again as they run out across the land they have built into the sea. A wide channel is drawn with two banks and a line of lining inside each; the narrowest with a single line. Loops the river has cut off lie beside it as crescents of still water. Between the channels the ground is too low to drain and carries the marsh sign: a short level rule with fine strokes standing up from it. The banks of the channels stand a little higher and carry grass. Where the sea works the delta front, old shorelines lie one behind another as fine lines on stippled sand. Ponds and lagoons are ruled across with level lines. The bluffs are hachured, and streams come down off the upland and lose themselves in the marsh. Offshore, flats are stippled out to a dotted low-water line, parted where each channel runs on across them and widened over the bar at each mouth. The sea beyond is water-lined, with soundings in fathoms and dotted depth curves that swing out round the delta. The seed decides how far the river builds against the sea: a few long fingers, each channel carried out between its own banks; a rounded lobe of many mouths; or a broad smooth arc with ridges across it. It also sets the bearing of the coast, the course of the valley, and every channel.
Technique
the sketch lays out the river as vectors first. The trunk is a sine-generated meander (Langbein and Leopold). The distributaries grow from the apex as a branching walk in which each channel steers toward a bearing of its own and hands half its span of bearings to each branch. The channels are rasterised into a distance field that carves them into the ground and raises banks beside them, and the bluffs are eroded as in Relief of a Coast, by filled, routed stream power. The sketch then engraves the sheet itself: banks and lining traced by marching squares, the old shorelines as contours of an exact Euclidean distance from the open sea, hachures as tapered wedges on the bluffs, the marsh and grass signs, stipple, and soundings. The dotted low-water line is a `painting:flow-lines` layer reading a channel the sketch publishes on the algorithm data bridge, and the graduated neat line is a `shapes:path` layer.
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.
29
14
12
Layer stack
In paint order. Every mark in the image comes from these; there is no handwritten drawing code.
painting:flow-linesLow-Water Line — Dottedshapes:pathNeat Line — Graduated Bordershapes:pathNeat Line — Outer Ruleshapes:pathNeat Line — Inner Rulefilter:grainPlate Tone
Source
The complete composition. Open it in GenArt to re-render, re-seed, or take it apart.
{
"genart": "1.2",
"id": "mouths-of-a-river",
"title": "Mouths of a River",
"created": "2026-09-11T00:00:00Z",
"modified": "2026-09-11T16:58:03.939Z",
"renderer": {
"type": "canvas2d",
"version": "1.x"
},
"canvas": {
"width": 1400,
"height": 1000
},
"parameters": [],
"colors": [],
"dataChannels": [
{
"name": "drying",
"type": "vector",
"cols": 480,
"rows": 360
}
],
"state": {
"seed": 29,
"params": {},
"colorPalette": [
"#1d2327",
"#3b474f",
"#6c7a82",
"#b3bab8",
"#ebe8e0"
]
},
"algorithm": "// Mouths of a River. The sketch builds the ground, runs the river down its\n// valley and out across the delta it has built into the sea, and engraves the\n// chart: channel banks, water-lining, marsh, beach ridges, grass, the stipple\n// of sand and flats, hachures on the bluffs, streams, depth curves and\n// soundings. Only the dotted low-water line is left to a plugin layer, reading\n// a map the sketch publishes on the ADR 062 data bridge.\n//\n// The river comes out between bluffs, meanders across its valley floor and\n// divides at the old coast into channels that each divide again, carrying the\n// land out into the sea on their banks. Between the channels the ground is too\n// low to drain and is marsh; where the sea works the delta front it throws up\n// ridges of sand parallel to the shore. The seed decides how far the river\n// builds against the waves: a long-fingered delta whose channels run far out\n// between their own banks, a rounded one, or a smooth arc with ridges across\n// it and few mouths.\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 K = W / 1400;\n\n var COLS = Math.round(240 * W / 700), ROWS = Math.round(180 * H / 500);\n var N = COLS * ROWS;\n // The field maps onto the inset plate: the build script uses the same box.\n var PX = W * 0.075, PY = H * 0.075, PW = W * 0.85, PH = H * 0.81;\n var ASPECT = PW / PH;\n var DX = ASPECT / (COLS - 1), DY = 1 / (ROWS - 1); // cell size, plate heights\n var CW = PW / (COLS - 1), CH = PH / (ROWS - 1); // cell size, px\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 var rand = rngFrom(seed * 2654435761 + 104729);\n\n /** A lattice of random values, bilinearly sampled over [0,1]. */\n function lattice(nx, ny) {\n var v = new Float32Array(nx * ny);\n for (var k = 0; k < nx * ny; k++) v[k] = rand() * 2 - 1;\n return function (u, t) {\n // 🔴 Clamp: an out-of-range sample becomes a NaN in a published channel\n // and takes the render down on a frame the CLI still exits 0 for.\n if (u < 0) u = 0; else if (u > 1) u = 1;\n if (t < 0) t = 0; else if (t > 1) t = 1;\n var x = u * (nx - 1), y = t * (ny - 1);\n var x0 = Math.floor(x), y0 = Math.floor(y);\n var x1 = x0 + 1 > nx - 1 ? nx - 1 : x0 + 1, y1 = y0 + 1 > ny - 1 ? ny - 1 : y0 + 1;\n var fx = x - x0, fy = y - y0;\n fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy);\n var a = v[y0 * nx + x0], b = v[y0 * nx + x1], c = v[y1 * nx + x0], d = v[y1 * nx + x1];\n return (a + (b - a) * fx) * (1 - fy) + (c + (d - c) * fx) * fy;\n };\n }\n function smooth(e0, e1, x) {\n var t = (x - e0) / (e1 - e0);\n if (t < 0) t = 0; else if (t > 1) t = 1;\n return t * t * (3 - 2 * t);\n }\n /** A fixed random value per cell, for marks that must not move between passes. */\n function hash(i) {\n var h = Math.imul(i ^ (seed * 374761393), 668265263);\n h = Math.imul(h ^ h >>> 13, 1274126177);\n return ((h ^ h >>> 16) >>> 0) / 4294967296;\n }\n\n // --- The lie of the land ------------------------------------------------\n // s runs from the sea toward the land and t along the coast. The old coast,\n // before the river built out from it, is s = 0; the plate is placed so it\n // lies well back and the delta has open sea in front of it.\n var th = (rand() < 0.6 ? -Math.PI / 2 : Math.PI / 2) + (rand() * 2 - 1) * 0.3;\n var nx0 = Math.cos(th), ny0 = Math.sin(th); // from sea toward land\n var px0 = -ny0, py0 = nx0; // along the coast\n var cx = ASPECT / 2, cy = 0.5;\n var off = -(0.20 + rand() * 0.06);\n function toS(x, y) { return (x - cx) * nx0 + (y - cy) * ny0 + off; }\n function toT(x, y) { return (x - cx) * px0 + (y - cy) * py0; }\n function toX(s, t) { return cx + (s - off) * nx0 + t * px0; }\n function toY(s, t) { return cy + (s - off) * ny0 + t * py0; }\n var sMaxPlate = Math.max(toS(0, 0), toS(ASPECT, 0), toS(0, 1), toS(ASPECT, 1));\n var ph = [];\n for (var i = 0; i < 12; i++) ph.push(rand() * Math.PI * 2);\n\n // How far the river wins against the waves. Low: few long channels, each\n // run far out between its own banks. High: a broad smooth front, ridged by\n // the sea, with few mouths.\n var wv = rand();\n var maxGen = wv > 0.72 ? 2 : wv < 0.3 ? 3 : 4;\n var E1 = 0.10 + rand() * 0.04; // the foot of the bluffs\n var tA = (rand() * 2 - 1) * 0.26; // where the valley comes down\n function axis(s) { return tA + 0.05 * Math.sin(3.3 * s + ph[7]) + 0.02 * Math.sin(7.1 * s + ph[8]); }\n function coastOff(t) { return 0.03 * Math.sin(2.3 * t + ph[0]) + 0.016 * Math.sin(5.9 * t + ph[1]) + 0.006 * Math.sin(13.1 * t + ph[4]); }\n function escOff(t) { return 0.05 * Math.sin(1.7 * t + ph[2]) + 0.02 * Math.sin(4.3 * t + ph[3]); }\n // The valley's width wanders, so its sides are not ruled.\n var lvw = lattice(14, 2);\n function valW(s) { return 0.06 * (1 + 0.3 * lvw(Math.max(0, Math.min(1, s / 0.6)), 0.5)); }\n\n // The envelope of the delta: how far out in front of the old coast the land\n // has been built, at each point along it. Longshore drift carries the lobe a\n // little to one side of the river.\n var R = 0.14 + 0.18 * wv + rand() * 0.06;\n var Wd = 1.25 * (0.30 + 0.25 * wv);\n var tAp = axis(0);\n var tc = tAp + (rand() * 2 - 1) * 0.08 * wv;\n function front(t) {\n var q = (t - tc) / Wd;\n if (q <= -1 || q >= 1) return 0;\n // 🔴 An exponent above one, so the front leaves the old coast at a\n // tangent. Below it, the delta met the coast in a hard corner.\n return R * Math.pow(1 - q * q, 1.3) * (1 + 0.09 * Math.sin(3.1 * q + ph[5]) + 0.05 * Math.sin(7.3 * q + ph[6]));\n }\n\n // --- The river ----------------------------------------------------------\n // The trunk meanders as a sine-generated curve (Langbein and Leopold): its\n // heading swings back and forth with distance along the channel, which is\n // the shape of a real meander rather than a sine wave laid on the ground.\n // A pull toward the valley's axis keeps it on the valley floor.\n var ST = 0.003;\n var HW0 = 0.0095 + rand() * 0.002; // the trunk's half-width, plate heights\n var HWC = 0.0027; // narrower than this, drawn as a line\n var chans = []; // {p: [x, y, hw, ...], ext, parent, mouth, gen}\n var lm = lattice(30, 3);\n var LAM = 0.10 + rand() * 0.04, OM = 1.5 + rand() * 0.3, phm = rand() * Math.PI * 2;\n var sAp = 0.012;\n var s = sMaxPlate + 0.04, t = axis(s), a = 0, phase = 0, bends = [];\n var trunk = { p: [], ext: [], parent: -1, mouth: false, gen: 0 }, prevTh = 0, prevD = 0;\n chans.push(trunk);\n for (var it = 0; it < 4000 && s > sAp; it++) {\n phase += ST / LAM * (1 + 0.35 * lm(Math.min(1, it / 900), 0.5));\n var thm = OM * (0.4 + 0.6 * smooth(E1 + 0.03, E1 - 0.05, s)) * Math.sin(2 * Math.PI * phase + phm);\n var corr = Math.max(-0.5, Math.min(0.5, (axis(s) - t) * 5));\n a = thm + corr;\n s -= Math.cos(a) * ST; t += Math.sin(a) * ST;\n trunk.p.push(toX(s, t), toY(s, t), HW0);\n // The apex of each bend, where an old loop may have been cut off.\n var dth = thm - prevTh;\n if (prevD > 0 && dth <= 0 || prevD < 0 && dth >= 0) bends.push(trunk.p.length / 3 - 1);\n prevD = dth; prevTh = thm;\n }\n\n // The distributaries. Each channel runs out from the apex, turning slowly\n // toward its own radial bearing so the channels fan, and divides after a\n // run; each branch takes a share of the water and is narrower for it. A\n // channel that runs into another ends there. One that passes the envelope\n // of the delta carries its banks out with it as a finger of land, until it\n // opens into the sea.\n var OCC = 0.01, occ = {};\n function occKey(x, y) { return Math.floor(x / OCC) + ',' + Math.floor(y / OCC); }\n function finger(hw) { return 0.006 + Math.pow(1 - wv, 1.5) * 0.20 * Math.sqrt(hw / HW0); }\n var bars = [];\n // Each channel steers toward a bearing of its own and hands half of its\n // span of bearings to each branch, so the channels fan across the delta\n // instead of running together down one side of it.\n var queue = [{ s: s, t: t, a: Math.max(-0.9, Math.min(0.9, a)), hw: HW0, gen: 0, parent: 0,\n tb: 0.6 * Math.atan2(tc - tAp, R), span: 1.3 + 0.5 * wv }];\n // The trunk's cells are occupied too, so no distributary turns back into it.\n for (var q0 = 0; q0 < trunk.p.length; q0 += 3) occ[occKey(trunk.p[q0], trunk.p[q0 + 1])] = 0;\n function grow(b) {\n var id = chans.length;\n var ch = { p: [], ext: [], parent: b.parent, mouth: false, gen: b.gen };\n chans.push(ch);\n var s = b.s, t = b.t, a = b.a, hw = b.hw, since = 0, tot = 0;\n var Lb = (0.03 + rand() * 0.05) * (1 + 0.6 * (1 - wv)) * (b.gen === 0 ? 1.2 : 1);\n var mph = rand() * Math.PI * 2, mlam = 0.05 + rand() * 0.04, wnd = lattice(16, 1);\n ch.p.push(toX(s, t), toY(s, t), hw);\n for (var it = 0; it < 1500; it++) {\n // The bearing a channel holds wanders with distance, so no channel runs\n // out across the delta as a straight spoke.\n a += 0.08 * (b.tb + 0.6 * wnd(Math.min(1, tot / 0.5), 0) - a);\n if (a > 1.25) a = 1.25; else if (a < -1.25) a = -1.25;\n var am = a + 0.32 * Math.sin(2 * Math.PI * tot / mlam + mph);\n s -= Math.cos(am) * ST; t += Math.sin(am) * ST; tot += ST; since += ST;\n var x = toX(s, t), y = toY(s, t);\n ch.p.push(x, y, hw);\n if (x < -0.03 || y < -0.03 || x > ASPECT + 0.03 || y > 1.03) return;\n var beyond = -(s + coastOff(t)) - front(t);\n if (beyond > finger(hw)) {\n // The mouth. The channel runs on a little way across the flats it\n // has laid down, and a bar of sand builds in front of it.\n ch.mouth = true;\n var ex = x, ey = y, dxm = toX(s - Math.cos(am), t + Math.sin(am)) - toX(s, t), dym = toY(s - Math.cos(am), t + Math.sin(am)) - toY(s, t);\n // The line across the mouth: past it the channel raises no banks.\n ch.cut = [ex, ey, dxm, dym];\n ch.ext.push(ex, ey, hw);\n for (var e = 1; e <= 8; e++) ch.ext.push(ex + dxm * e * 0.003, ey + dym * e * 0.003, hw * (1 - 0.06 * e));\n bars.push([ex + dxm * 0.03, ey + dym * 0.03, 0.010 + hw * 1.6]);\n return;\n }\n if (tot > 0.02) {\n var n = ch.p.length, hx = x + (x - ch.p[n - 6]) / (2 * ST) * 0.016, hy = y + (y - ch.p[n - 5]) / (2 * ST) * 0.016;\n var o = occ[occKey(hx, hy)];\n if (o !== undefined && o !== id && !(tot < 0.05 && (o === b.parent || chans[o].parent === b.parent))) return;\n }\n occ[occKey(x, y)] = id;\n if (since > Lb && b.gen < maxGen && hw > HWC * 1.15 && beyond < -0.02) {\n var r = 0.35 + rand() * 0.3;\n queue.push({ s: s, t: t, a: a - 0.15 - rand() * 0.2, hw: hw * Math.sqrt(r) * 1.08, gen: b.gen + 1, parent: id, tb: b.tb - b.span / 4, span: b.span / 2 });\n queue.push({ s: s, t: t, a: a + 0.15 + rand() * 0.2, hw: hw * Math.sqrt(1 - r) * 1.08, gen: b.gen + 1, parent: id, tb: b.tb + b.span / 4, span: b.span / 2 });\n return;\n }\n }\n }\n for (var qi = 0; qi < queue.length && qi < 80; qi++) grow(queue[qi]);\n // A branch that ran into another within a step or two of its split left a\n // scrap of bank beside the channel, read as a stray tick. None of them can\n // have branches of their own: a split needs a run of Lb first.\n chans = chans.filter(function (ch, ci) { return ci === 0 || ch.mouth || ch.p.length / 3 * ST >= 0.015; });\n\n // Cut-off meanders: the loop of an old bend, left standing beside the river\n // on the valley floor as a crescent of still water.\n var oxbows = [], kb = Math.round(0.32 * LAM / ST);\n for (var bi = 0; bi < bends.length && oxbows.length < 3; bi++) {\n var ib = bends[bi], tp = trunk.p;\n if (ib - kb < 0 || ib + kb >= tp.length / 3) continue;\n var sb = toS(tp[ib * 3], tp[ib * 3 + 1]);\n if (sb < 0.012 || rand() > 0.7) continue;\n // Out from the bend, on the side away from its chord.\n var mx = (tp[(ib - kb) * 3] + tp[(ib + kb) * 3]) / 2, my = (tp[(ib - kb) * 3 + 1] + tp[(ib + kb) * 3 + 1]) / 2;\n var ox = tp[ib * 3] - mx, oy = tp[ib * 3 + 1] - my, ol = Math.hypot(ox, oy) || 1;\n var sh = 0.022 + rand() * 0.01, pts = [], ok = true;\n for (var j = -kb; j <= kb; j++) {\n var px = tp[(ib + j) * 3] + ox / ol * sh, py = tp[(ib + j) * 3 + 1] + oy / ol * sh;\n for (var j2 = 0; j2 < tp.length && ok; j2 += 9) if (Math.hypot(tp[j2] - px, tp[j2 + 1] - py) < HW0 + 0.004) ok = false;\n var spx = toS(px, py);\n if (spx + coastOff(toT(px, py)) < 0.008 || spx > E1 && Math.abs(toT(px, py) - axis(spx)) > valW(spx) * 0.55) ok = false;\n pts.push(px, py, 0.0034 * Math.pow(Math.sin(Math.PI * (j + kb) / (2 * kb)), 0.5));\n }\n if (ok) oxbows.push({ p: pts, ext: [], parent: -1, mouth: false, gen: 9, ox: true });\n }\n chans = chans.concat(oxbows);\n\n // --- The channels on the grid ---------------------------------------------\n // For every cell: how far it lies outside the nearest carved bank (negative\n // inside the water), that channel's half-width, and the distance to the\n // nearest creek too narrow to carve and to the runs across the flats.\n var bankD = new Float32Array(N).fill(1), bankHW = new Float32Array(N), bankOx = new Uint8Array(N);\n var bankCut = new Float32Array(N).fill(-1); // how far behind its mouth, for a channel that has one\n var creekD = new Float32Array(N).fill(1), extD = new Float32Array(N).fill(1);\n function raster(x0, y0, h0, x1, y1, h1, reach, kind, ox, cut) {\n var c0 = Math.max(0, Math.floor((Math.min(x0, x1) - reach) / DX)), c1 = Math.min(COLS - 1, Math.ceil((Math.max(x0, x1) + reach) / DX));\n var r0 = Math.max(0, Math.floor((Math.min(y0, y1) - reach) / DY)), r1 = Math.min(ROWS - 1, Math.ceil((Math.max(y0, y1) + reach) / DY));\n var vx = x1 - x0, vy = y1 - y0, L2 = vx * vx + vy * vy || 1e-12;\n for (var r = r0; r <= r1; r++) for (var c = c0; c <= c1; c++) {\n var px = c * DX, py = r * DY;\n // 🔴 Past the mouth. The levee is a ring round the channel's end cap,\n // and it closed every mouth: the fingers ended in sealed loops.\n var behind = cut ? (px - cut[0]) * cut[2] + (py - cut[1]) * cut[3] : -1;\n if (behind > 0) continue;\n var u = ((px - x0) * vx + (py - y0) * vy) / L2;\n if (u < 0) u = 0; else if (u > 1) u = 1;\n var hw = h0 + (h1 - h0) * u;\n var d = Math.hypot(px - x0 - vx * u, py - y0 - vy * u), p = r * COLS + c;\n if (kind === 0) { if (d - hw < bankD[p]) { bankD[p] = d - hw; bankHW[p] = hw; bankOx[p] = ox ? 1 : 0; bankCut[p] = behind; } }\n else if (kind === 1) { if (d < creekD[p]) creekD[p] = d; }\n else if (d - hw < extD[p]) extD[p] = d - hw;\n }\n }\n chans.forEach(function (ch) {\n var p = ch.p;\n for (var j = 0; j + 5 < p.length; j += 3) {\n var carved = p[j + 2] >= HWC;\n raster(p[j], p[j + 1], p[j + 2], p[j + 3], p[j + 4], p[j + 5], p[j + 2] * 2 + 0.03, carved ? 0 : 1, ch.ox, ch.cut);\n }\n for (var k = 0; k + 5 < ch.ext.length; k += 3) raster(ch.ext[k], ch.ext[k + 1], ch.ext[k + 2], ch.ext[k + 3], ch.ext[k + 4], ch.ext[k + 5], 0.02, 2, false);\n });\n var barF = new Float32Array(N);\n bars.forEach(function (b) {\n var c0 = Math.max(0, Math.floor((b[0] - 3 * b[2]) / DX)), c1 = Math.min(COLS - 1, Math.ceil((b[0] + 3 * b[2]) / DX));\n var r0 = Math.max(0, Math.floor((b[1] - 3 * b[2]) / DY)), r1 = Math.min(ROWS - 1, Math.ceil((b[1] + 3 * b[2]) / DY));\n for (var r = r0; r <= r1; r++) for (var c = c0; c <= c1; c++) {\n var d = Math.hypot(c * DX - b[0], r * DY - b[1]) / b[2];\n barF[r * COLS + c] = Math.max(barF[r * COLS + c], Math.exp(-d * d));\n }\n });\n\n // --- The ground -----------------------------------------------------------\n var w1x = lattice(6, 5), w1y = lattice(6, 5);\n var n1 = lattice(7, 5), n2 = lattice(15, 11), n3 = lattice(33, 25), nE = lattice(24, 18);\n // 🔴 Ponds and lagoons are drawn out along the coast, as the low ground\n // between old shorelines lies. Off a round lattice every one was a blob.\n var lagA = lattice(7, 18), lag2 = lattice(29, 21);\n function pondF(sp, tp, u, v, x, y) {\n var edge = Math.min(x, ASPECT - x, y, 1 - y);\n return lagA(0.5 + tp / 2.2, (sp + 0.8) / 2.2) + 0.4 * lag2(u, v) + 0.12 * n3(u, v) - 0.9 * (1 - smooth(0.02, 0.07, edge));\n }\n /**\n * The mainland: a plain barely above the sea behind the old coast, then a\n * short steep rise of bluffs to an upland that climbs gently away inland.\n * The plain is kept level so it takes marsh and grass and no hachures.\n */\n function mainland(s, t, u, v) {\n var sc = s + coastOff(t), se = s + escOff(t);\n var plain = 0.003 + 0.005 * smooth(0, E1, sc);\n var up = smooth(E1 - 0.014, E1 + 0.014, se);\n var vw = valW(s), dv = Math.abs(t - axis(s));\n // The upland falls toward the valley as well as the sea, and rolls, so its\n // streams gather into branching courses. On a plane they ran side by side.\n var h = plain + up * (0.11 + 0.07 * Math.max(0, se - E1) + 0.016 * n1(u, v) + 0.003 * n2(u, v) - 0.03 * Math.exp(-dv / 0.12));\n // The valley: a flat floor between steep sides, cut down to the plain.\n var V = smooth(vw * 1.3, vw * 0.6, dv);\n var floor = plain + 0.012 * Math.max(0, se - E1);\n return floor + (h - floor) * (1 - V) + 0.0008 * n3(u, v) * (1 - up);\n }\n var hmap = new Float32Array(N), kind = new Uint8Array(N); // kind: 0 land, 1 sea, 2 channel, 3 still water\n for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) {\n var p = r * COLS + c, x = c * DX, y = r * DY, u = x / ASPECT, v = y;\n // A gentle warp of the whole ground, so no line on the sheet is ruled.\n var xw = x + 0.02 * w1x(u, v), yw = y + 0.02 * w1y(u, v);\n var sp = toS(xw, yw), tp2 = toT(xw, yw), sc = sp + coastOff(tp2);\n var h;\n if (sc >= 0) {\n h = mainland(sp, tp2, u, v);\n // Ponds on the plain behind the old coast.\n if (pondF(sp, tp2, u, v, x, y) - 0.7 * (1 - smooth(0.012, 0.03, bankD[p])) - 0.7 * smooth(E1 * 0.4, E1 * 0.8, sc) - 0.7 * (1 - smooth(0.004, 0.02, sc)) > 0.66) { h = -0.002; kind[p] = 3; }\n } else {\n var F = front(tp2) * (1 + 0.07 * nE(u, v));\n // The banks thin to a point at the mouth. Cut off square, each finger\n // ended in a flat-topped stub.\n var inEnv = -sc < F, levee = !bankOx[p] && bankD[p] < (bankHW[p] + 0.004) * smooth(0, 0.022, -bankCut[p]);\n if (inEnv || levee) {\n // The banks stand a little above the ground between the channels.\n h = 0.0014 + 0.0026 * Math.exp(-Math.max(0, bankD[p]) / 0.007) + 0.0005 * n3(u, v);\n // Lagoons, in the low ground well away from the channels.\n var lsc = pondF(sp, tp2, u, v, x, y) - 0.7 * (1 - smooth(0.012, 0.03, bankD[p])) - 0.7 * smooth(0.55, 0.85, -sc / F) - 0.7 * (1 - smooth(0.004, 0.02, -sc));\n if (inEnv && lsc > 0.64) { h = -0.002; kind[p] = 3; }\n } else { h = -0.01; kind[p] = 1; }\n }\n if (bankD[p] < 0) { h = -0.003; kind[p] = bankOx[p] ? 3 : 2; }\n hmap[p] = h;\n }\n // Specks of land left inside the water, where two limbs of a meander ran\n // together or two banks nearly met, are drowned: each read as a fleck of\n // dirt in the channel.\n var comp = new Int32Array(N).fill(-1), stackQ = new Int32Array(N);\n for (var p0 = 0; p0 < N; p0++) {\n if (hmap[p0] <= 0 || comp[p0] >= 0) continue;\n var qn = 0, members = [], edge = false, wetK = 1;\n comp[p0] = p0; stackQ[qn++] = p0;\n while (qn) {\n var cq = stackQ[--qn], ccq = cq % COLS, rcq = (cq - ccq) / COLS;\n members.push(cq);\n if (ccq === 0 || rcq === 0 || ccq === COLS - 1 || rcq === ROWS - 1) edge = true;\n var nbs = [cq - 1, cq + 1, cq - COLS, cq + COLS];\n for (var nq = 0; nq < 4; nq++) {\n var q = nbs[nq];\n if ((nq === 0 && ccq === 0) || (nq === 1 && ccq === COLS - 1) || q < 0 || q >= N) continue;\n if (hmap[q] <= 0) { if (kind[q] !== 1) wetK = kind[q]; continue; }\n if (comp[q] < 0) { comp[q] = p0; stackQ[qn++] = q; }\n }\n }\n if (!edge && members.length < 20) members.forEach(function (m) { hmap[m] = -0.003; kind[m] = wetK; });\n }\n // And the other way: a pond of a few cells took a shoreline loop so tight it\n // printed as a black speck. It is filled back to the ground round it.\n var compW = new Int32Array(N).fill(-1);\n for (var w0 = 0; w0 < N; w0++) {\n if (kind[w0] !== 3 || bankOx[w0] || compW[w0] >= 0) continue;\n var wn = 0, wm = [];\n compW[w0] = w0; stackQ[wn++] = w0;\n while (wn) {\n var cw = stackQ[--wn], ccw = cw % COLS;\n wm.push(cw);\n var nbw = [ccw > 0 ? cw - 1 : -1, ccw < COLS - 1 ? cw + 1 : -1, cw - COLS, cw + COLS];\n for (var nw = 0; nw < 4; nw++) {\n var qw = nbw[nw];\n if (qw < 0 || qw >= N || kind[qw] !== 3 || compW[qw] >= 0) continue;\n compW[qw] = w0; stackQ[wn++] = qw;\n }\n }\n if (wm.length < 25) wm.forEach(function (m) { hmap[m] = 0.0016; kind[m] = 0; });\n }\n\n function at(a, c, r) {\n if (c < 0) c = 0; else if (c > COLS - 1) c = COLS - 1;\n if (r < 0) r = 0; else if (r > ROWS - 1) r = ROWS - 1;\n return a[r * COLS + c];\n }\n /** Bilinear sample of a map at fractional cell coordinates. */\n function bil(a, x, y) {\n if (x < 0) x = 0; else if (x > COLS - 1.001) x = COLS - 1.001;\n if (y < 0) y = 0; else if (y > ROWS - 1.001) y = ROWS - 1.001;\n var x0 = Math.floor(x), y0 = Math.floor(y), fx = x - x0, fy = y - y0, p = y0 * COLS + x0;\n return (a[p] * (1 - fx) + a[p + 1] * fx) * (1 - fy) + (a[p + COLS] * (1 - fx) + a[p + COLS + 1] * fx) * fy;\n }\n var NB = [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]];\n\n /** Two-pass chamfer distance (in cells) from every source cell, with the nearest source. */\n function chamfer(isSrc) {\n var d = new Float32Array(N), src = new Int32Array(N);\n for (var p = 0; p < N; p++) { d[p] = isSrc(p) ? 0 : 1e6; src[p] = d[p] === 0 ? p : -1; }\n var D2 = Math.SQRT2;\n function relax(p, q, w) { if (d[q] + w < d[p]) { d[p] = d[q] + w; src[p] = src[q]; } }\n for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) {\n var p = r * COLS + c;\n if (c > 0) relax(p, p - 1, 1);\n if (r > 0) {\n relax(p, p - COLS, 1);\n if (c > 0) relax(p, p - COLS - 1, D2);\n if (c < COLS - 1) relax(p, p - COLS + 1, D2);\n }\n }\n for (var r2 = ROWS - 1; r2 >= 0; r2--) for (var c2 = COLS - 1; c2 >= 0; c2--) {\n var p2 = r2 * COLS + c2;\n if (c2 < COLS - 1) relax(p2, p2 + 1, 1);\n if (r2 < ROWS - 1) {\n relax(p2, p2 + COLS, 1);\n if (c2 < COLS - 1) relax(p2, p2 + COLS + 1, D2);\n if (c2 > 0) relax(p2, p2 + COLS - 1, D2);\n }\n }\n return { d: d, src: src };\n }\n /** Box-blur a copy of a map, `passes` times, radius 2. */\n function blur(a, passes, cap) {\n var s = new Float32Array(N), tmp = new Float32Array(N);\n for (var p = 0; p < N; p++) s[p] = cap !== undefined ? Math.min(a[p], cap) : a[p];\n for (var pass = 0; pass < passes; pass++) {\n for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) {\n var acc = 0, cnt = 0;\n for (var oy = -2; oy <= 2; oy++) for (var ox = -2; ox <= 2; ox++) {\n var cc = c + ox, rr = r + oy;\n if (cc < 0 || rr < 0 || cc >= COLS || rr >= ROWS) continue;\n acc += s[rr * COLS + cc]; cnt++;\n }\n tmp[r * COLS + c] = acc / cnt;\n }\n var sw = s; s = tmp; tmp = sw;\n }\n return s;\n }\n /**\n * Exact Euclidean distance (in cells) from every source cell, carrying the\n * index of the nearest source (Felzenszwalb and Huttenlocher's two-pass\n * lower envelope). 🔴 A chamfer's distance steps in eighths of a turn, and\n * water-lining laid on its bands came out octagonal round Relief of a Coast's stacks.\n */\n function edt(isSrc) {\n var INF = 1e12, M = Math.max(COLS, ROWS);\n var colD = new Float64Array(N), colS = new Int32Array(N);\n var f = new Float64Array(M), out = new Float64Array(M), arg = new Int32Array(M);\n var v = new Int32Array(M), z = new Float64Array(M + 1);\n function pass1(n) {\n var k = 0; v[0] = 0; z[0] = -INF; z[1] = INF;\n for (var q = 1; q < n; q++) {\n var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);\n while (s <= z[k]) { k--; s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); }\n k++; v[k] = q; z[k] = s; z[k + 1] = INF;\n }\n k = 0;\n for (var q2 = 0; q2 < n; q2++) {\n while (z[k + 1] < q2) k++;\n out[q2] = (q2 - v[k]) * (q2 - v[k]) + f[v[k]]; arg[q2] = v[k];\n }\n }\n for (var c = 0; c < COLS; c++) {\n for (var r = 0; r < ROWS; r++) f[r] = isSrc(r * COLS + c) ? 0 : INF;\n pass1(ROWS);\n for (var r2 = 0; r2 < ROWS; r2++) { colD[r2 * COLS + c] = out[r2]; colS[r2 * COLS + c] = arg[r2]; }\n }\n var d = new Float32Array(N), src = new Int32Array(N);\n for (var r3 = 0; r3 < ROWS; r3++) {\n for (var c2 = 0; c2 < COLS; c2++) f[c2] = colD[r3 * COLS + c2];\n pass1(COLS);\n for (var c3 = 0; c3 < COLS; c3++) {\n var p = r3 * COLS + c3;\n d[p] = out[c3] >= INF * 0.5 ? 1e6 : Math.sqrt(out[c3]);\n src[p] = out[c3] >= INF * 0.5 ? -1 : colS[r3 * COLS + arg[c3]] * COLS + arg[c3];\n }\n }\n return { d: d, src: src };\n }\n\n // --- Drainage and erosion -------------------------------------------------\n // The bluffs are eroded as Relief of a Coast's escarpment is: hollows filled\n // to their spill point, water routed downhill, and each cell cut toward its\n // receiver by the square root of the water passing through it. The river's\n // channels are already water, so the gullies find their way down to them.\n var heapK = new Float64Array(N), heapV = new Int32Array(N), heapN = 0;\n function hpush(key, val) {\n var i = heapN++;\n while (i > 0) {\n var pa = (i - 1) >> 1;\n if (heapK[pa] <= key) break;\n heapK[i] = heapK[pa]; heapV[i] = heapV[pa]; i = pa;\n }\n heapK[i] = key; heapV[i] = val;\n }\n function hpop() {\n var top = heapV[0], key = heapK[--heapN], val = heapV[heapN], i = 0;\n for (;;) {\n var l = 2 * i + 1;\n if (l >= heapN) break;\n if (l + 1 < heapN && heapK[l + 1] < heapK[l]) l++;\n if (heapK[l] >= key) break;\n heapK[i] = heapK[l]; heapV[i] = heapV[l]; i = l;\n }\n heapK[i] = key; heapV[i] = val;\n return top;\n }\n var seen = new Uint8Array(N);\n /** Fill every hollow on the land to its spill point, with a hair of fall. */\n function fill() {\n seen.fill(0); heapN = 0;\n for (var p = 0; p < N; p++) {\n var c = p % COLS, r = (p - c) / COLS;\n if (hmap[p] <= 0 || c === 0 || r === 0 || c === COLS - 1 || r === ROWS - 1) { seen[p] = 1; hpush(hmap[p], p); }\n }\n while (heapN) {\n var cur = hpop(), cc = cur % COLS, rc = (cur - cc) / COLS;\n for (var nb = 0; nb < 8; nb++) {\n var c2 = cc + NB[nb][0], r2 = rc + NB[nb][1];\n if (c2 < 0 || r2 < 0 || c2 >= COLS || r2 >= ROWS) continue;\n var q = r2 * COLS + c2;\n if (seen[q]) continue;\n seen[q] = 1;\n if (hmap[q] <= hmap[cur] + 1e-6) hmap[q] = hmap[cur] + 1e-6;\n hpush(hmap[q], q);\n }\n }\n }\n var rcv = new Int32Array(N), acc = new Float32Array(N), landIdx = [];\n /**\n * Route water downhill and accumulate it. The diagonal fall is weighed by a\n * fixed random per cell (Fairfield and Leymarie's rho-8), because a plain\n * steepest-of-eight rule runs every stream on a plane slope as a ruled\n * line at a multiple of 45 degrees.\n */\n function route() {\n rcv.fill(-1); acc.fill(0); landIdx = [];\n for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) {\n var p = r * COLS + c;\n if (hmap[p] <= 0) continue;\n landIdx.push(p);\n var best = 0, diag = 2 - hash(p * 7 + 3);\n for (var nb = 0; nb < 8; nb++) {\n var c2 = c + NB[nb][0], r2 = r + NB[nb][1];\n if (c2 < 0 || r2 < 0 || c2 >= COLS || r2 >= ROWS) continue;\n var q = r2 * COLS + c2;\n var drop = (hmap[p] - hmap[q]) / (NB[nb][0] && NB[nb][1] ? diag : 1);\n if (drop > best) { best = drop; rcv[p] = q; }\n }\n }\n landIdx.sort(function (a, b) { return hmap[b] - hmap[a]; });\n for (var li = 0; li < landIdx.length; li++) {\n var pl = landIdx[li];\n acc[pl] += 1;\n if (rcv[pl] >= 0) acc[rcv[pl]] += acc[pl];\n }\n }\n // Gentler than Relief of a Coast's. On a bluff this short the deep gullies\n // were too small to draw as streams, and the hachures ran into each one.\n var KF = 0.03, ROUNDS = 10;\n var lap = new Float32Array(N);\n for (var round = 0; round < ROUNDS; round++) {\n fill(); route();\n // Implicit stream-power step, receivers first (Braun and Willett), so it\n // is stable however hard it cuts.\n for (var li2 = landIdx.length - 1; li2 >= 0; li2--) {\n var pe = landIdx[li2], rq = rcv[pe];\n if (rq < 0) continue;\n // Only water gathered into a channel cuts. Cutting from the first cell\n // down, every hillside grew a gully, and the hachures drew a feather\n // down each one so Relief of a Coast's escarpment read as streaks.\n var F = KF * Math.sqrt(acc[pe]) * smooth(15, 70, acc[pe]);\n var hn = (hmap[pe] + F * Math.max(hmap[rq], 0)) / (1 + F);\n hmap[pe] = Math.max(0.001, Math.min(hmap[pe], hn));\n }\n // A little hillslope creep, so valley sides are slopes and not steps.\n for (var pd = 0; pd < N; pd++) {\n lap[pd] = 0;\n if (hmap[pd] <= 0) continue;\n var cd = pd % COLS, rd = (pd - cd) / COLS, sumL = 0, nL = 0;\n if (cd > 0 && hmap[pd - 1] > 0) { sumL += hmap[pd - 1]; nL++; }\n if (cd < COLS - 1 && hmap[pd + 1] > 0) { sumL += hmap[pd + 1]; nL++; }\n if (rd > 0 && hmap[pd - COLS] > 0) { sumL += hmap[pd - COLS]; nL++; }\n if (rd < ROWS - 1 && hmap[pd + COLS] > 0) { sumL += hmap[pd + COLS]; nL++; }\n if (nL) lap[pd] = sumL / nL - hmap[pd];\n }\n for (var pd2 = 0; pd2 < N; pd2++) if (hmap[pd2] > 0) hmap[pd2] = Math.max(0.001, hmap[pd2] + 0.18 * lap[pd2]);\n }\n fill(); route();\n // Ground the last fill raised to its spill point: a flat a stream crosses\n // as a ruled line. The pen lifts over it.\n var levelled = new Uint8Array(N), pre = Float32Array.from(hmap);\n fill(); route();\n for (var pv = 0; pv < N; pv++) if (hmap[pv] - pre[pv] > 1e-5) levelled[pv] = 1;\n // A gully that gathers enough water is drawn as a stream, but only on ground\n // that drains. Across the marsh the same routing runs dead straight over\n // ground filled level, so a stream is traced until it reaches the marsh and\n // stops there, as a stream coming off high ground loses itself in one.\n var A1 = Math.round(N * 0.0014);\n var isStream = new Uint8Array(N);\n for (var ps = 0; ps < N; ps++) if (hmap[ps] > 0.006 && acc[ps] >= A1) isStream[ps] = 1;\n var toStream = chamfer(function (p) { return isStream[p] === 1; });\n\n // --- Slope and light ------------------------------------------------------\n var slope = new Float32Array(N), facing = new Float32Array(N);\n var LX = -Math.SQRT1_2, LY = -Math.SQRT1_2; // toward the light, north-west\n var samples = [];\n for (var r2 = 0; r2 < ROWS; r2++) {\n for (var c2 = 0; c2 < COLS; c2++) {\n var n = r2 * COLS + c2;\n var gx = (at(hmap, c2 + 1, r2) - at(hmap, c2 - 1, r2)) / (2 * DX);\n var gy = (at(hmap, c2, r2 + 1) - at(hmap, c2, r2 - 1)) / (2 * DY);\n var g = Math.sqrt(gx * gx + gy * gy);\n slope[n] = g;\n facing[n] = g > 1e-6 ? (-gx * LX - gy * LY) / g : 0; // +1 faces the light\n // 🔴 Read off the high ground only. Most of this sheet is level, and a\n // percentile taken over all of it set the scale by the flat ground, so\n // the banks of the channels took hachures.\n if (hmap[n] > 0.03 && (r2 * 7 + c2) % 11 === 0) samples.push(g);\n }\n }\n samples.sort(function (a, b) { return a - b; });\n var smax = samples.length ? samples[Math.floor(samples.length * 0.92)] : 1;\n /** The weight of engraving a piece of ground takes: steepness, then the light. */\n function toneOf(sl, f) {\n var sn = Math.min(1.25, sl / smax);\n var T = 0.04 + 0.46 * Math.pow(sn, 0.85) + 0.38 * sn * (f < 0 ? -f : 0) - 0.14 * sn * (f > 0 ? f : 0);\n return T < 0 ? 0 : T > 1 ? 1 : T;\n }\n var tone = new Float32Array(N);\n for (var m = 0; m < N; m++) if (hmap[m] > 0) tone[m] = toneOf(slope[m], facing[m]);\n var hsm = blur(hmap, 1), gxT = new Float32Array(N), gyT = new Float32Array(N);\n for (var r8 = 0; r8 < ROWS; r8++) for (var c8 = 0; c8 < COLS; c8++) {\n gxT[r8 * COLS + c8] = at(hsm, c8 + 1, r8) - at(hsm, c8 - 1, r8);\n gyT[r8 * COLS + c8] = at(hsm, c8, r8 + 1) - at(hsm, c8, r8 - 1);\n }\n\n var fromLand = edt(function (p) { return hmap[p] > 0; }); // distance out into the water\n var fromSea = edt(function (p) { return hmap[p] <= 0; }); // distance inland\n var fromOpen = edt(function (p) { return kind[p] === 1; }); // distance from the open sea only\n\n // The sea bed shelves away from whatever land is nearest, so the depth\n // curves swing out round the delta front.\n for (var pb = 0; pb < N; pb++) {\n if (kind[pb] !== 1) continue;\n var cb = pb % COLS, rb = (pb - cb) / COLS, dp0 = fromLand.d[pb] * DY;\n hmap[pb] = -Math.min(0.6, (0.004 + 0.6 * Math.pow(dp0, 1.5)) * (1 + 0.22 * n1(cb / (COLS - 1), rb / (ROWS - 1))));\n }\n\n // How gentle the shore is, read off the ground just inside it.\n var gentle = new Float32Array(N);\n for (var pg = 0; pg < N; pg++) {\n if (hmap[pg] <= 0 || fromSea.d[pg] > 3) continue;\n var sum = 0, cnt = 0, cg = pg % COLS, rg = Math.floor(pg / COLS);\n for (var oy = -3; oy <= 3; oy++) for (var ox2 = -3; ox2 <= 3; ox2++) {\n var cc2 = cg + ox2, rr2 = rg + oy;\n if (cc2 < 0 || rr2 < 0 || cc2 >= COLS || rr2 >= ROWS) continue;\n var q2 = rr2 * COLS + cc2;\n if (hmap[q2] <= 0) continue;\n sum += Math.min(1.5, slope[q2] / smax); cnt++;\n }\n gentle[pg] = 1 - smooth(0.22, 0.65, cnt ? sum / cnt : 1);\n }\n // The flats in front of the shore, in cells, wider off the delta and wider\n // again over the bar at each mouth.\n var FLAT = 5 + 3 * wv, BAR = 9;\n var flatW = new Float32Array(N);\n for (var pf = 0; pf < N; pf++) {\n if (kind[pf] !== 1) continue;\n var sl = fromLand.src[pf];\n flatW[pf] = (sl >= 0 ? FLAT * gentle[sl] : 0) + BAR * barF[pf];\n }\n\n // --- The water ------------------------------------------------------------\n // The sea is water-lined from the low-water line outward. In a channel or a\n // lagoon the lining is a single line inside each bank: the distance is\n // capped short of the second band. Still water keeps only its bank; lined,\n // every pond read as a hole cut in the sheet.\n var fromLow = new Float32Array(N);\n for (var pl2 = 0; pl2 < N; pl2++) {\n if (hmap[pl2] > 0) fromLow[pl2] = 0;\n else if (kind[pl2] === 1) fromLow[pl2] = fromLand.d[pl2] - flatW[pl2];\n else if (kind[pl2] === 2) fromLow[pl2] = Math.min(fromLand.d[pl2], 2.9);\n else fromLow[pl2] = 0;\n }\n // Traced on a smoothed copy in the open sea, so the lining does not carry\n // every notch of the shore out with it; raw in the channels, which a blur\n // would close up.\n var smB = blur(fromLow, 3, 70), sm = new Float32Array(N);\n for (var pm = 0; pm < N; pm++) {\n var wb = kind[pm] === 1 ? smooth(3, 9, fromLand.d[pm]) : 0;\n sm[pm] = fromLow[pm] + (smB[pm] - fromLow[pm]) * wb;\n }\n var BANDS = [1.5];\n for (var k2 = 1; k2 <= 14; k2++) BANDS.push(1.3 + 1.9 * Math.pow(k2, 1.3));\n function nearBand(d, tol) {\n for (var b = 0; b < BANDS.length; b++) if (Math.abs(d - BANDS[b]) < tol) return true;\n return false;\n }\n\n // --- The ground's cover -----------------------------------------------------\n // Behind the open shore the sea has thrown up ridges of sand, as deep a belt\n // of them as the waves have the better of the river. Behind them, and\n // between the channels, the ground is too low to drain and is marsh. The\n // banks of the channels stand a little higher and carry grass, as do the\n // valley floor and the high ground wherever it is too gentle for hachures.\n var RD = 3 + 15 * wv;\n var shoreDir = new Float32Array(N), drying = new Float32Array(N), stipple = new Float32Array(N);\n var sand = new Float32Array(N), marsh = new Float32Array(N), grass = new Float32Array(N), ridgeM = new Uint8Array(N);\n var dune = lattice(26, 20), mpatch = lattice(14, 10), meadow = lattice(18, 13);\n for (var r6 = 0; r6 < ROWS; r6++) for (var c6 = 0; c6 < COLS; c6++) {\n var i6 = r6 * COLS + c6, u6 = c6 / (COLS - 1), v6 = r6 / (ROWS - 1);\n var gx6 = (at(sm, c6 + 1, r6) - at(sm, c6 - 1, r6)) / 2;\n var gy6 = (at(sm, c6, r6 + 1) - at(sm, c6, r6 - 1)) / 2;\n shoreDir[i6] = Math.atan2(gy6, gx6) + Math.PI / 2;\n var h6 = hmap[i6];\n if (h6 > 0) {\n var rz = h6 < 0.009 && bankD[i6] > 0.002 ? 1 - smooth(RD - 2, RD, fromOpen.d[i6]) : 0;\n if (rz > 0.5 && fromOpen.d[i6] > 1.2) ridgeM[i6] = 1;\n sand[i6] = rz * 0.42 * (0.7 + 0.3 * dune(u6, v6));\n var offLev = smooth(0.003, 0.009, bankD[i6]) * smooth(0.002, 0.005, creekD[i6]);\n marsh[i6] = (1 - smooth(0.0042, 0.0065, h6)) * offLev * (1 - rz) * (0.3 + 0.7 * smooth(-0.4, 0.4, mpatch(u6, v6)));\n var flatT = 1 - smooth(0.10, 0.30, tone[i6]);\n var lev = (1 - smooth(0.004, 0.008, Math.max(0, bankD[i6]))) * 0.9;\n var gz = Math.max(lev, smooth(0.0062, 0.009, h6) * flatT * 0.6, smooth(0.03, 0.05, h6) * flatT * 0.5);\n var patch = 0.3 + 0.7 * smooth(-0.35, 0.45, meadow(u6, v6));\n if (toStream.d[i6] > 2.5) grass[i6] = gz * (1 - rz) * patch;\n continue;\n }\n if (kind[i6] !== 1) continue;\n var d6 = fromLand.d[i6], fw = flatW[i6];\n // Flats, stippled, thinning toward the low-water line and parted where a\n // channel runs on across them.\n if (d6 < fw) stipple[i6] = 0.9 * Math.pow(1 - d6 / fw, 0.7) * smooth(0, 0.004, extD[i6]);\n if (fw > 2.5 && Math.abs(fromLow[i6]) < 0.6 && extD[i6] > 0.002) drying[i6] = 1;\n }\n\n // --- Publish ------------------------------------------------------------\n var gl = (typeof globalThis !== 'undefined') ? globalThis : window;\n gl.__genart_data = gl.__genart_data || {};\n gl.__genart_data.cols = COLS;\n gl.__genart_data.rows = ROWS;\n function pack(name, mag, ang) {\n var f32 = new Float32Array(N * 3);\n for (var p = 0; p < N; p++) {\n var an = ang[p], mg = mag[p];\n if (!isFinite(an)) an = 0;\n if (!isFinite(mg)) mg = 0;\n f32[p * 3] = Math.cos(an);\n f32[p * 3 + 1] = Math.sin(an);\n f32[p * 3 + 2] = mg;\n }\n gl.__genart_data[name] = f32;\n }\n pack('drying', drying, shoreDir);\n\n // --- Tracing ------------------------------------------------------------\n // Contour lines of a map at one level, by marching squares, chained into\n // polylines (flat arrays of cell coordinates). Edges are numbered: the\n // horizontal edge right of cell p is p, the vertical edge below it N + p.\n var eA = new Int32Array(2 * N), eB = new Int32Array(2 * N), eStamp = new Int32Array(2 * N);\n var ex = new Float32Array(2 * N), ey = new Float32Array(2 * N), eUsed = new Uint8Array(2 * N);\n var stamp = 0;\n function contour(a, L) {\n stamp++;\n var touched = [], e = [0, 0, 0, 0], ne;\n function cross(va, vb, id, xa, ya, xb, yb) {\n if ((va < L) === (vb < L)) return;\n if (eStamp[id] !== stamp) {\n eStamp[id] = stamp; eA[id] = -1; eB[id] = -1; eUsed[id] = 0;\n var t = (L - va) / (vb - va);\n ex[id] = xa + (xb - xa) * t; ey[id] = ya + (yb - ya) * t;\n touched.push(id);\n }\n e[ne++] = id;\n }\n function link(i, j) {\n if (eA[i] === -1) eA[i] = j; else eB[i] = j;\n if (eA[j] === -1) eA[j] = i; else eB[j] = i;\n }\n for (var r = 0; r < ROWS - 1; r++) for (var c = 0; c < COLS - 1; c++) {\n var p = r * COLS + c;\n var v0 = a[p], v1 = a[p + 1], v2 = a[p + COLS + 1], v3 = a[p + COLS];\n var b0 = v0 < L, b1 = v1 < L, b2 = v2 < L, b3 = v3 < L;\n if (b0 === b1 && b1 === b2 && b2 === b3) continue;\n ne = 0;\n cross(v0, v1, p, c, r, c + 1, r);\n cross(v1, v2, N + p + 1, c + 1, r, c + 1, r + 1);\n cross(v3, v2, p + COLS, c, r + 1, c + 1, r + 1);\n cross(v0, v3, N + p, c, r, c, r + 1);\n if (ne === 2) link(e[0], e[1]);\n else if (ne === 4) { link(e[0], e[1]); link(e[2], e[3]); }\n }\n var lines = [];\n for (var ti = 0; ti < touched.length; ti++) {\n var id0 = touched[ti];\n if (eUsed[id0]) continue;\n // Walk back to an end if the line has one, so it is traced in one piece.\n var s0 = id0, prev = -1, guard = 0;\n while (eB[s0] !== -1 && guard++ < touched.length) {\n var nx = eA[s0] === prev ? eB[s0] : eA[s0];\n if (nx === id0) break;\n prev = s0; s0 = nx;\n }\n var pts = [], cur = s0, pv = -1;\n while (cur !== -1 && !eUsed[cur]) {\n eUsed[cur] = 1; pts.push(ex[cur], ey[cur]);\n var l1 = eA[cur], l2 = eB[cur];\n var nxt = (l1 !== -1 && l1 !== pv && !eUsed[l1]) ? l1 : (l2 !== -1 && l2 !== pv && !eUsed[l2]) ? l2 : -1;\n pv = cur; cur = nxt;\n }\n if (pts.length >= 4) lines.push(pts);\n }\n return lines;\n }\n /** Corner-cutting, so a traced line does not show the grid it came from. */\n function chaikin(p, rounds) {\n for (var it = 0; it < rounds; it++) {\n var q = [p[0], p[1]];\n for (var j = 0; j < p.length - 2; j += 2) {\n q.push(0.75 * p[j] + 0.25 * p[j + 2], 0.75 * p[j + 1] + 0.25 * p[j + 3],\n 0.25 * p[j] + 0.75 * p[j + 2], 0.25 * p[j + 1] + 0.75 * p[j + 3]);\n }\n q.push(p[p.length - 2], p[p.length - 1]);\n p = q;\n }\n return p;\n }\n function X(c) { return PX + c * CW; }\n function Y(r) { return PY + r * CH; }\n function lineLen(p) {\n var s = 0;\n for (var j = 2; j < p.length; j += 2) s += Math.hypot((p[j] - p[j - 2]) * CW, (p[j + 1] - p[j - 1]) * CH);\n return s;\n }\n function strokeLine(p) {\n ctx.moveTo(X(p[0]), Y(p[1]));\n for (var j = 2; j < p.length; j += 2) ctx.lineTo(X(p[j]), Y(p[j + 1]));\n }\n /** Cell coordinates of a point given in plate heights. */\n function CX(x) { return PX + x / DX * CW; }\n function CY(y) { return PY + y / DY * CH; }\n\n // --- The sheet ----------------------------------------------------------\n // The layers draw over this, so the sheet is laid here: the paper colour\n // with a faint, slow mottle, as a hand-made sheet has.\n ctx.fillStyle = pal[4];\n ctx.fillRect(0, 0, W, H);\n var m1 = lattice(44, 32), m2 = lattice(140, 100);\n var MS = 3 * K;\n for (var my = 0; my < H; my += MS) for (var mx = 0; mx < W; mx += MS) {\n var mv = 0.5 + 0.35 * m1(mx / W, my / H) + 0.25 * m2(mx / W, my / H);\n if (mv <= 0.45) continue;\n ctx.fillStyle = 'rgba(60,70,76,' + (0.05 * (mv - 0.45)).toFixed(3) + ')';\n ctx.fillRect(mx, my, MS + 0.5, MS + 0.5);\n }\n\n // Everything engraved from here on is held inside the plate. 🔴 Unclipped,\n // hachures begun on the plate's edge ran on past it and hung below the\n // neat line as black wedges.\n ctx.save();\n ctx.beginPath();\n ctx.rect(PX, PY, PW, PH);\n ctx.clip();\n\n // --- Sand and flats -------------------------------------------------------\n // Stipple, dot by dot on a jittered grid so it lies as evenly as a\n // roulette's, with the chance of a dot set by how sandy the ground is. The\n // flats in front of the shore are stippled darker than the sand behind it.\n // 🔴 Drawn by flow-line layers at two or three steps each, the \"dots\" were\n // two-pixel blobs that bunched inside each cell and read as dirt.\n var sg = rngFrom(seed * 6151 + 5), SG = 2.6 * K, dotsN = 0;\n ctx.save();\n ctx.globalAlpha = 0.9;\n [[sand, pal[1], 0.6], [stipple, pal[0], 0.58]].forEach(function (z) {\n ctx.fillStyle = z[1];\n ctx.beginPath();\n for (var y = PY + SG / 2; y < PY + PH; y += SG) for (var x = PX + SG / 2; x < PX + PW; x += SG) {\n var jx = x + (sg() - 0.5) * SG, jy = y + (sg() - 0.5) * SG, roll = sg(), rs = sg();\n if (roll >= bil(z[0], (jx - PX) / CW, (jy - PY) / CH)) continue;\n var rad = z[2] * K * (0.8 + 0.4 * rs);\n ctx.moveTo(jx + rad, jy);\n ctx.arc(jx, jy, rad, 0, Math.PI * 2);\n dotsN++;\n }\n ctx.fill();\n });\n ctx.restore();\n\n // --- Marsh ----------------------------------------------------------------\n // The engraver's marsh sign: a short rule laid level, with a few fine\n // strokes standing up from it, set in loose staggered rows. Like every\n // symbol on a map it is square to the page, not to the ground.\n var mr = rngFrom(seed * 7717 + 3), MSX = 13 * K, MSY = 7.5 * K, marshN = 0;\n ctx.save();\n ctx.strokeStyle = pal[1];\n ctx.lineCap = 'round';\n ctx.lineWidth = 0.6 * K;\n ctx.globalAlpha = 0.85;\n ctx.beginPath();\n for (var my2 = PY + MSY * 0.5, row = 0; my2 < PY + PH - 3 * K; my2 += MSY, row++) {\n for (var mx2 = PX + MSX * (row % 2 ? 1 : 0.5); mx2 < PX + PW - 3 * K; mx2 += MSX) {\n var jx2 = mx2 + (mr() - 0.5) * MSX * 0.45, jy2 = my2 + (mr() - 0.5) * MSY * 0.3;\n var roll2 = mr(), len2 = (5.5 + mr() * 3.5) * K, nt = 2 + Math.floor(mr() * 3);\n var mc = (jx2 - PX) / CW, mrr = (jy2 - PY) / CH;\n if (mc < 1 || mrr < 1 || mc > COLS - 2 || mrr > ROWS - 2) continue;\n if (roll2 > 0.95 * bil(marsh, mc, mrr)) continue;\n // Clear of the water on either side, so no sign sits on a bank.\n if (bil(fromSea.d, mc - len2 / CW * 0.5, mrr) < 1.2 || bil(fromSea.d, mc + len2 / CW * 0.5, mrr) < 1.2) continue;\n ctx.moveTo(jx2 - len2 / 2, jy2);\n ctx.lineTo(jx2 + len2 / 2, jy2);\n for (var j6 = 0; j6 < nt; j6++) {\n var bx6 = jx2 + (j6 - (nt - 1) / 2) * 1.5 * K + (mr() - 0.5) * 0.6 * K;\n var tall = (2.2 + mr() * 1.6) * K, lean = (j6 - (nt - 1) / 2) * 0.5 * K;\n ctx.moveTo(bx6, jy2 - 0.9 * K);\n ctx.lineTo(bx6 + lean, jy2 - 0.9 * K - tall);\n }\n marshN++;\n }\n }\n ctx.stroke();\n ctx.restore();\n\n // --- Grass ----------------------------------------------------------------\n var gr = rngFrom(seed * 104723 + 7), GS = 15 * K, tufts = 0;\n ctx.save();\n ctx.strokeStyle = pal[1];\n ctx.lineCap = 'round';\n ctx.lineWidth = 0.6 * K;\n ctx.globalAlpha = 0.85;\n ctx.beginPath();\n for (var ty = PY + GS * 0.5; ty < PY + PH - 4 * K; ty += GS * 0.72) {\n for (var tx = PX + GS * 0.5; tx < PX + PW - 4 * K; tx += GS) {\n var gx3 = tx + (gr() - 0.5) * GS * 0.9, gy3 = ty + (gr() - 0.5) * GS * 0.6;\n var roll = gr(), nb3 = 3 + Math.floor(gr() * 3), sz = (4.2 + gr() * 2.2) * K;\n var gc = (gx3 - PX) / CW, grr = (gy3 - PY) / CH;\n if (gc < 1 || grr < 1 || gc > COLS - 2 || grr > ROWS - 2) continue;\n if (bil(hmap, gc, grr) <= 0 || bil(fromSea.d, gc, grr) < 1.3 || roll > 0.95 * bil(grass, gc, grr)) continue;\n for (var j3 = 0; j3 < nb3; j3++) {\n var fan = (j3 / (nb3 - 1) - 0.5) * 1.15 + (hash(tufts * 7 + j3) - 0.5) * 0.2;\n var len = sz * (1 - 0.4 * Math.abs(fan));\n var bx = gx3 + (j3 - (nb3 - 1) / 2) * 0.45 * K;\n ctx.moveTo(bx, gy3);\n ctx.quadraticCurveTo(bx + Math.sin(fan) * len * 0.4, gy3 - len * 0.55, bx + Math.sin(fan) * len, gy3 - Math.cos(fan) * len);\n }\n tufts++;\n }\n }\n ctx.stroke();\n ctx.restore();\n\n // --- Hachures on the bluffs -------------------------------------------------\n // As on Relief of a Coast: strokes set along each contour at an even\n // spacing, each run down the fall line to the next contour below.\n var hs = [];\n for (var ph0 = 0; ph0 < N; ph0 += 3) if (hmap[ph0] > 0) hs.push(hmap[ph0]);\n hs.sort(function (a, b) { return a - b; });\n var TIER = smax * (5.5 * K) / PH;\n var hmax = hs.length ? hs[hs.length - 1] : 0;\n var hr = rngFrom(seed * 31337 + 11);\n var STEP = 0.35, MAXLEN = 34 * K, strokes = 0;\n ctx.save();\n ctx.fillStyle = pal[0];\n ctx.globalAlpha = 0.9;\n for (var lv = 1; lv * TIER < hmax; lv++) {\n var L = lv * TIER, floorH = L - 0.86 * TIER;\n var lines = contour(hmap, L);\n for (var li3 = 0; li3 < lines.length; li3++) {\n var pl = lines[li3];\n var next = hr() * 4 * K, run = 0;\n for (var j4 = 0; j4 < pl.length - 2; j4 += 2) {\n var ax = pl[j4], ay = pl[j4 + 1], bx2 = pl[j4 + 2], by2 = pl[j4 + 3];\n var seg = Math.hypot((bx2 - ax) * CW, (by2 - ay) * CH);\n while (run + seg >= next) {\n var tt2 = (next - run) / seg;\n var sx = ax + (bx2 - ax) * tt2, sy = ay + (by2 - ay) * tt2;\n var T = toneOf(bil(slope, sx, sy), bil(facing, sx, sy));\n // Closer on steep ground and in shadow, as well as heavier.\n next += (2.1 + 5.2 * (1 - T)) * K * (0.9 + 0.2 * hr());\n // Only the face of the bluffs is cut. The shoulders above and below\n // it took long faint strokes, and the band read as a fringe of grass.\n if (T < 0.22) continue;\n var wd = (0.24 + 1.05 * Math.pow(T, 1.35)) * K * (0.9 + 0.2 * hr());\n // Trace down the fall line.\n var path = [sx, sy], x = sx, y = sy, lenPx = 0;\n for (var stp = 0; stp < 120; stp++) {\n var gxs = bil(gxT, x, y), gys = bil(gyT, x, y), gm = Math.hypot(gxs, gys);\n if (gm < 1e-5) break;\n x -= gxs / gm * STEP; y -= gys / gm * STEP;\n lenPx += STEP * CW;\n var hh = bil(hmap, x, y);\n if (hh < floorH || hh <= 0 || lenPx > MAXLEN || bil(toStream.d, x, y) < 1.0) break;\n if (x < 1 || y < 1 || x > COLS - 2 || y > ROWS - 2) break;\n path.push(x, y);\n }\n if (path.length < 6) continue;\n // A wedge: full width at the top of the course, lifting to a point\n // at the bottom, as a burin stroke does.\n var npt = path.length / 2, left = [], right = [];\n for (var q3 = 0; q3 < npt; q3++) {\n var qa = Math.max(0, q3 - 1), qb = Math.min(npt - 1, q3 + 1);\n var tx2 = X(path[qb * 2]) - X(path[qa * 2]), ty2 = Y(path[qb * 2 + 1]) - Y(path[qa * 2 + 1]);\n var tl = Math.hypot(tx2, ty2) || 1;\n var hw = wd * 0.5 * (1 - 0.5 * q3 / (npt - 1));\n left.push(X(path[q3 * 2]) - ty2 / tl * hw, Y(path[q3 * 2 + 1]) + tx2 / tl * hw);\n right.push(X(path[q3 * 2]) + ty2 / tl * hw, Y(path[q3 * 2 + 1]) - tx2 / tl * hw);\n }\n ctx.beginPath();\n ctx.moveTo(left[0], left[1]);\n for (var q4 = 2; q4 < left.length; q4 += 2) ctx.lineTo(left[q4], left[q4 + 1]);\n for (var q5 = right.length - 2; q5 >= 0; q5 -= 2) ctx.lineTo(right[q5], right[q5 + 1]);\n ctx.closePath();\n ctx.fill();\n strokes++;\n }\n run += seg;\n }\n }\n }\n ctx.restore();\n\n // --- Beach ridges -----------------------------------------------------------\n // Old shorelines, one behind another, each left inland as the delta built\n // out past it: fine lines laid parallel to the open shore, broken where a\n // channel or a lagoon cuts through.\n var foS = blur(fromOpen.d, 2, 40), ridges = 0;\n ctx.save();\n ctx.strokeStyle = pal[1];\n ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n for (var lvR = 2.2; lvR < RD - 0.5; lvR += 2.0) {\n ctx.lineWidth = 0.55 * K;\n ctx.globalAlpha = 0.7 * (1 - 0.5 * lvR / RD);\n ctx.beginPath();\n contour(foS, lvR).forEach(function (ln) {\n var p = chaikin(ln, 2), run = [];\n for (var j = 0; j <= p.length; j += 2) {\n var ok = j < p.length && bil(ridgeM, p[j], p[j + 1]) > 0.75;\n if (ok) run.push(p[j], p[j + 1]);\n if ((!ok || j === p.length) && run.length) {\n if (run.length >= 12) { strokeLine(run); ridges++; }\n run = [];\n }\n }\n });\n ctx.stroke();\n }\n ctx.restore();\n\n ctx.save();\n ctx.strokeStyle = pal[0];\n ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n ctx.globalAlpha = 0.82;\n for (var bb = 0; bb < BANDS.length; bb++) {\n var lk = contour(sm, BANDS[bb]);\n ctx.lineWidth = (0.95 - 0.045 * bb) * K;\n ctx.beginPath();\n lk.forEach(function (ln) {\n // A small closed ring round a speck of land read as an eye. The land\n // keeps its own shoreline and the lining passes it by.\n var n = ln.length, closed = Math.hypot(ln[0] - ln[n - 2], ln[1] - ln[n - 1]) < 1.5;\n if (closed && lineLen(ln) < 90 * K) return;\n strokeLine(chaikin(ln, 2));\n });\n ctx.stroke();\n }\n ctx.restore();\n\n // --- Still water ------------------------------------------------------------\n // Ponds, lagoons and cut-off loops are ruled across with fine level lines,\n // square to the page, as an engraved map rules a lake. Left blank, each one\n // read as a hole cut in the sheet.\n var still = new Float32Array(N);\n for (var pk = 0; pk < N; pk++) still[pk] = kind[pk] === 3 ? 1 : 0;\n var RS = 2.8 * K, RX = 1.5 * K;\n ctx.save();\n ctx.strokeStyle = pal[1];\n ctx.lineWidth = 0.4 * K;\n ctx.globalAlpha = 0.62;\n ctx.beginPath();\n for (var ry = PY + RS / 2; ry < PY + PH; ry += RS) {\n var runX = -1;\n for (var rx = PX; rx <= PX + PW + RX; rx += RX) {\n var rc2 = (rx - PX) / CW, rr4 = (ry - PY) / CH;\n var wet = rx <= PX + PW && bil(still, rc2, rr4) > 0.5 && bil(fromLand.d, rc2, rr4) > 0.9;\n if (wet && runX < 0) runX = rx;\n else if (!wet && runX >= 0) {\n if (rx - runX > 2 * RX) { ctx.moveTo(runX, ry); ctx.lineTo(rx - RX, ry); }\n runX = -1;\n }\n }\n }\n ctx.stroke();\n ctx.restore();\n\n // Depth curves at five, ten and twenty fathoms, dotted, as a survey chart draws them.\n var dep = new Float32Array(N);\n for (var pd3 = 0; pd3 < N; pd3++) dep[pd3] = -hmap[pd3];\n function fathoms(dp) { return Math.max(1, Math.round(46 * Math.pow(Math.max(0, dp) / 0.30, 1.6))); }\n var CURVES = [5, 10, 20], curveN = 0;\n ctx.save();\n ctx.strokeStyle = pal[1];\n ctx.lineCap = 'round';\n ctx.lineWidth = 1.15 * K;\n ctx.globalAlpha = 0.75;\n if (ctx.setLineDash) ctx.setLineDash([0.01, 3.6 * K]);\n CURVES.forEach(function (fm) {\n var lines = contour(dep, 0.30 * Math.pow(fm / 46, 1 / 1.6));\n ctx.beginPath();\n lines.forEach(function (ln) { if (ln.length > 24) { strokeLine(chaikin(ln, 2)); curveN++; } });\n ctx.stroke();\n });\n ctx.restore();\n\n // --- The shoreline --------------------------------------------------------\n ctx.save();\n ctx.strokeStyle = pal[0];\n ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n ctx.lineWidth = 1.25 * K;\n ctx.globalAlpha = 0.92;\n ctx.beginPath();\n contour(hmap, 0).forEach(function (ln) { strokeLine(chaikin(ln, 2)); });\n ctx.stroke();\n ctx.restore();\n\n // --- Creeks -----------------------------------------------------------------\n // A channel too narrow to draw with two banks is drawn as one line, tapering\n // as it goes.\n var creeks = 0;\n ctx.save();\n ctx.strokeStyle = pal[0];\n ctx.lineCap = 'round';\n ctx.globalAlpha = 0.9;\n chans.forEach(function (ch) {\n var p = ch.p, drew = false;\n for (var j = 0; j + 5 < p.length; j += 3) {\n if (p[j + 2] >= HWC) continue;\n var cc = p[j] / DX, rr = p[j + 1] / DY;\n if (bil(hmap, cc, rr) <= 0 && bil(fromLand.d, cc, rr) > 1) continue;\n ctx.lineWidth = (0.35 + 0.8 * p[j + 2] / HWC) * K;\n ctx.beginPath();\n ctx.moveTo(CX(p[j]), CY(p[j + 1]));\n ctx.lineTo(CX(p[j + 3]), CY(p[j + 4]));\n ctx.stroke();\n drew = true;\n }\n if (drew) creeks++;\n });\n ctx.restore();\n\n // --- Streams ----------------------------------------------------------------\n var hasUp = new Uint8Array(N), done = new Uint8Array(N), streamLines = 0;\n for (var pu = 0; pu < N; pu++) if (isStream[pu] && rcv[pu] >= 0) hasUp[rcv[pu]] = 1;\n var heads = [];\n for (var ph2 = 0; ph2 < N; ph2++) if (isStream[ph2] && !hasUp[ph2]) heads.push(ph2);\n heads.sort(function (a, b) { return hmap[a] - hmap[b]; });\n ctx.save();\n ctx.strokeStyle = pal[0];\n ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n ctx.globalAlpha = 0.9;\n var wbx = lattice(60, 45), wby = lattice(60, 45);\n heads.forEach(function (hd) {\n var pts = [], ac = [], cur = hd, guard = 0, visited = [], inked = 0;\n while (cur >= 0 && guard++ < 4000) {\n var c9 = cur % COLS, r9 = Math.floor(cur / COLS), u9 = c9 / (COLS - 1), v9 = r9 / (ROWS - 1);\n pts.push(c9 + 0.8 * wbx(u9, v9), r9 + 0.8 * wby(u9, v9)); ac.push(acc[cur]);\n if (hmap[cur] <= 0 || done[cur] || marsh[cur] > 0.3) break;\n if (c9 < 2 || r9 < 2 || c9 > COLS - 3 || r9 > ROWS - 3) break;\n done[cur] = 1; visited.push(cur);\n if (!levelled[cur]) inked++;\n cur = rcv[cur];\n }\n // 🔴 A gully head at the foot of the bluffs, a few cells long, printed as\n // a stray tick. Too short to draw: its cells are handed back, so a stream\n // from higher up that reaches them runs on through instead of stopping.\n if (inked < 12) { visited.forEach(function (v) { done[v] = 0; }); return; }\n var sp2 = chaikin(pts, 2), n2 = sp2.length / 2;\n for (var j5 = 0; j5 < n2 - 1; j5++) {\n var a5 = ac[Math.min(ac.length - 1, Math.floor(j5 / (n2 - 1) * (ac.length - 1)))];\n if (levelled[Math.round(sp2[j5 * 2 + 1]) * COLS + Math.round(sp2[j5 * 2])]) continue;\n ctx.lineWidth = Math.min(1.35, 0.3 + 0.2 * Math.log2(a5 / A1 + 1)) * K;\n ctx.beginPath();\n ctx.moveTo(X(sp2[j5 * 2]), Y(sp2[j5 * 2 + 1]));\n ctx.lineTo(X(sp2[j5 * 2 + 2]), Y(sp2[j5 * 2 + 3]));\n ctx.stroke();\n }\n streamLines++;\n });\n ctx.restore();\n\n // --- Soundings --------------------------------------------------------------\n // Depth figures in fathoms on an open grid over the sea, only in the gaps\n // between water-lines, clear of the depth curves and the runs of the\n // channels across the flats.\n var SP = 18, sound = [];\n var sr = rngFrom(seed * 7919 + 31);\n for (var gy2 = SP * 0.6; gy2 < ROWS - 4; gy2 += SP * 0.87) {\n var shift = (Math.round(gy2 / (SP * 0.87)) % 2) * SP * 0.5;\n for (var gx2 = SP * 0.4 + shift; gx2 < COLS - 4; gx2 += SP) {\n var sc2 = Math.round(gx2 + (sr() - 0.5) * SP * 0.5), sr2 = Math.round(gy2 + (sr() - 0.5) * SP * 0.5);\n if (sc2 < 5 || sr2 < 4 || sc2 > COLS - 6 || sr2 > ROWS - 5) continue;\n var ps2 = sr2 * COLS + sc2;\n if (kind[ps2] !== 1 || fromLand.d[ps2] < 5 || sm[ps2] < 3 || extD[ps2] < 0.01) continue;\n if (sm[ps2] < BANDS[BANDS.length - 1] + 2 && nearBand(sm[ps2], 1.7)) continue;\n var dp = dep[ps2];\n var gdx = (dep[ps2 + 1] - dep[ps2 - 1]) / 2, gdy = (dep[ps2 + COLS] - dep[ps2 - COLS]) / 2;\n var gm2 = Math.sqrt(gdx * gdx + gdy * gdy) || 1e-6;\n if (CURVES.some(function (fm) { return Math.abs(dp - 0.30 * Math.pow(fm / 46, 1 / 1.6)) / gm2 < 2.4; })) continue;\n sound.push([sc2, sr2, fathoms(dp)]);\n }\n }\n ctx.save();\n ctx.fillStyle = pal[1];\n ctx.globalAlpha = 0.92;\n ctx.font = 'italic ' + (8.8 * K).toFixed(2) + 'px Georgia, \"Times New Roman\", serif';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n sound.forEach(function (s) { ctx.fillText(String(s[2]), X(s[0]), Y(s[1])); });\n ctx.restore();\n\n ctx.restore(); // the plate clip\n\n var landN = 0; for (var pz = 0; pz < N; pz++) if (hmap[pz] > 0) landN++;\n var mouths = 0; chans.forEach(function (ch) { if (ch.mouth) mouths++; });\n gl.__genart_data.debug = {\n wv: +wv.toFixed(2), land: +(landN / N).toFixed(3), chans: chans.length, mouths: mouths, oxbows: oxbows.length,\n creeks: creeks, streams: streamLines, marsh: marshN, tufts: tufts, ridges: ridges, strokes: strokes,\n soundings: sound.length, curves: curveN, smax: +smax.toFixed(2), tier: +TIER.toFixed(4),\n };\n}\n",
"layers": [
{
"id": "drying",
"type": "painting:flow-lines",
"name": "Low-Water Line — Dotted",
"visible": true,
"locked": false,
"opacity": 1,
"blendMode": "normal",
"transform": {
"x": 105,
"y": 75,
"width": 1190,
"height": 810,
"rotation": 0,
"scaleX": 1,
"scaleY": 1,
"anchorX": 0,
"anchorY": 0
},
"properties": {
"field": "algorithm:drying",
"fieldCols": 480,
"fieldRows": 360,
"minMagnitude": 0.5,
"seed": 42,
"lineCount": 5000,
"seedDistribution": "grid-jittered",
"lineLength": 3,
"stepSize": 1.2,
"lineWeight": 1.24,
"lineWeightVariation": 0.35,
"taper": "none",
"color": "#3b474f",
"colorVariation": 0.05,
"opacity": 0.6,
"paintMode": "multiply",
"depthScale": false,
"horizonY": 0,
"depthWeightRange": "[1, 1]",
"depthOpacityRange": "[1, 1]",
"maskCenterY": -1,
"maskSpread": 0.25
}
},
{
"id": "graduation",
"type": "shapes:path",
"name": "Neat Line — Graduated Border",
"visible": true,
"locked": false,
"opacity": 0.8,
"blendMode": "normal",
"transform": {
"x": 0,
"y": 0,
"width": 1400,
"height": 1000,
"rotation": 0,
"scaleX": 1,
"scaleY": 1,
"anchorX": 0,
"anchorY": 0
},
"properties": {
"fillColor": "#1d2327",
"fillEnabled": true,
"strokeColor": "#000000",
"strokeWidth": 0,
"strokeEnabled": false,
"d": "M 105.00 69.00 L 149.07 69.00 L 149.07 75.00 L 105.00 75.00 Z M 193.15 69.00 L 237.22 69.00 L 237.22 75.00 L 193.15 75.00 Z M 281.30 69.00 L 325.37 69.00 L 325.37 75.00 L 281.30 75.00 Z M 369.44 69.00 L 413.52 69.00 L 413.52 75.00 L 369.44 75.00 Z M 457.59 69.00 L 501.67 69.00 L 501.67 75.00 L 457.59 75.00 Z M 545.74 69.00 L 589.81 69.00 L 589.81 75.00 L 545.74 75.00 Z M 633.89 69.00 L 677.96 69.00 L 677.96 75.00 L 633.89 75.00 Z M 722.04 69.00 L 766.11 69.00 L 766.11 75.00 L 722.04 75.00 Z M 810.19 69.00 L 854.26 69.00 L 854.26 75.00 L 810.19 75.00 Z M 898.33 69.00 L 942.41 69.00 L 942.41 75.00 L 898.33 75.00 Z M 986.48 69.00 L 1030.56 69.00 L 1030.56 75.00 L 986.48 75.00 Z M 1074.63 69.00 L 1118.70 69.00 L 1118.70 75.00 L 1074.63 75.00 Z M 1162.78 69.00 L 1206.85 69.00 L 1206.85 75.00 L 1162.78 75.00 Z M 1250.93 69.00 L 1295.00 69.00 L 1295.00 75.00 L 1250.93 75.00 Z M 105.00 885.00 L 149.07 885.00 L 149.07 891.00 L 105.00 891.00 Z M 193.15 885.00 L 237.22 885.00 L 237.22 891.00 L 193.15 891.00 Z M 281.30 885.00 L 325.37 885.00 L 325.37 891.00 L 281.30 891.00 Z M 369.44 885.00 L 413.52 885.00 L 413.52 891.00 L 369.44 891.00 Z M 457.59 885.00 L 501.67 885.00 L 501.67 891.00 L 457.59 891.00 Z M 545.74 885.00 L 589.81 885.00 L 589.81 891.00 L 545.74 891.00 Z M 633.89 885.00 L 677.96 885.00 L 677.96 891.00 L 633.89 891.00 Z M 722.04 885.00 L 766.11 885.00 L 766.11 891.00 L 722.04 891.00 Z M 810.19 885.00 L 854.26 885.00 L 854.26 891.00 L 810.19 891.00 Z M 898.33 885.00 L 942.41 885.00 L 942.41 891.00 L 898.33 891.00 Z M 986.48 885.00 L 1030.56 885.00 L 1030.56 891.00 L 986.48 891.00 Z M 1074.63 885.00 L 1118.70 885.00 L 1118.70 891.00 L 1074.63 891.00 Z M 1162.78 885.00 L 1206.85 885.00 L 1206.85 891.00 L 1162.78 891.00 Z M 1250.93 885.00 L 1295.00 885.00 L 1295.00 891.00 L 1250.93 891.00 Z M 99.00 75.00 L 105.00 75.00 L 105.00 120.00 L 99.00 120.00 Z M 99.00 165.00 L 105.00 165.00 L 105.00 210.00 L 99.00 210.00 Z M 99.00 255.00 L 105.00 255.00 L 105.00 300.00 L 99.00 300.00 Z M 99.00 345.00 L 105.00 345.00 L 105.00 390.00 L 99.00 390.00 Z M 99.00 435.00 L 105.00 435.00 L 105.00 480.00 L 99.00 480.00 Z M 99.00 525.00 L 105.00 525.00 L 105.00 570.00 L 99.00 570.00 Z M 99.00 615.00 L 105.00 615.00 L 105.00 660.00 L 99.00 660.00 Z M 99.00 705.00 L 105.00 705.00 L 105.00 750.00 L 99.00 750.00 Z M 99.00 795.00 L 105.00 795.00 L 105.00 840.00 L 99.00 840.00 Z M 1295.00 75.00 L 1301.00 75.00 L 1301.00 120.00 L 1295.00 120.00 Z M 1295.00 165.00 L 1301.00 165.00 L 1301.00 210.00 L 1295.00 210.00 Z M 1295.00 255.00 L 1301.00 255.00 L 1301.00 300.00 L 1295.00 300.00 Z M 1295.00 345.00 L 1301.00 345.00 L 1301.00 390.00 L 1295.00 390.00 Z M 1295.00 435.00 L 1301.00 435.00 L 1301.00 480.00 L 1295.00 480.00 Z M 1295.00 525.00 L 1301.00 525.00 L 1301.00 570.00 L 1295.00 570.00 Z M 1295.00 615.00 L 1301.00 615.00 L 1301.00 660.00 L 1295.00 660.00 Z M 1295.00 705.00 L 1301.00 705.00 L 1301.00 750.00 L 1295.00 750.00 Z M 1295.00 795.00 L 1301.00 795.00 L 1301.00 840.00 L 1295.00 840.00 Z M 99.00 69.00 L 105.00 69.00 L 105.00 75.00 L 99.00 75.00 Z M 1295.00 69.00 L 1301.00 69.00 L 1301.00 75.00 L 1295.00 75.00 Z M 99.00 885.00 L 105.00 885.00 L 105.00 891.00 L 99.00 891.00 Z M 1295.00 885.00 L 1301.00 885.00 L 1301.00 891.00 L 1295.00 891.00 Z",
"scaleToFit": false
}
},
{
"id": "neat-outer",
"type": "shapes:path",
"name": "Neat Line — Outer Rule",
"visible": true,
"locked": false,
"opacity": 0.85,
"blendMode": "normal",
"transform": {
"x": 0,
"y": 0,
"width": 1400,
"height": 1000,
"rotation": 0,
"scaleX": 1,
"scaleY": 1,
"anchorX": 0,
"anchorY": 0
},
"properties": {
"fillColor": "#ffffff",
"fillEnabled": false,
"strokeColor": "#1d2327",
"strokeWidth": 1.5,
"strokeEnabled": true,
"d": "M 99.00 69.00 L 1301.00 69.00 L 1301.00 891.00 L 99.00 891.00 Z",
"scaleToFit": false
}
},
{
"id": "neat-inner",
"type": "shapes:path",
"name": "Neat Line — Inner Rule",
"visible": true,
"locked": false,
"opacity": 0.8,
"blendMode": "normal",
"transform": {
"x": 0,
"y": 0,
"width": 1400,
"height": 1000,
"rotation": 0,
"scaleX": 1,
"scaleY": 1,
"anchorX": 0,
"anchorY": 0
},
"properties": {
"fillColor": "#ffffff",
"fillEnabled": false,
"strokeColor": "#1d2327",
"strokeWidth": 0.7,
"strokeEnabled": true,
"d": "M 105.00 75.00 L 1295.00 75.00 L 1295.00 885.00 L 105.00 885.00 Z",
"scaleToFit": false
}
},
{
"id": "tone",
"type": "filter:grain",
"name": "Plate Tone",
"visible": true,
"locked": false,
"opacity": 1,
"blendMode": "normal",
"transform": {
"x": 0,
"y": 0,
"width": 1400,
"height": 1000,
"rotation": 0,
"scaleX": 1,
"scaleY": 1,
"anchorX": 0,
"anchorY": 0
},
"properties": {
"intensity": 0.1,
"size": 2,
"seed": 29,
"monochrome": true
}
}
]
}