{
 "genart": "1.2",
 "id": "fall-of-a-ridge",
 "title": "Fall of a Ridge",
 "created": "2026-09-11T00:00:00Z",
 "modified": "2026-09-11T17:05:40.872Z",
 "renderer": {
  "type": "canvas2d",
  "version": "1.x"
 },
 "canvas": {
  "width": 1400,
  "height": 1000
 },
 "parameters": [],
 "colors": [],
 "dataChannels": [
  {
   "name": "hach1",
   "type": "vector",
   "cols": 400,
   "rows": 300
  },
  {
   "name": "hach2",
   "type": "vector",
   "cols": 400,
   "rows": 300
  },
  {
   "name": "hach3",
   "type": "vector",
   "cols": 400,
   "rows": 300
  },
  {
   "name": "hach4",
   "type": "vector",
   "cols": 400,
   "rows": 300
  },
  {
   "name": "coast",
   "type": "vector",
   "cols": 400,
   "rows": 300
  },
  {
   "name": "lining",
   "type": "vector",
   "cols": 400,
   "rows": 300
  }
 ],
 "state": {
  "seed": 4410,
  "params": {},
  "colorPalette": [
   "#1d2327",
   "#3b474f",
   "#6c7a82",
   "#b3bab8",
   "#ebe8e0"
  ]
 },
 "algorithm": "// Fall of a Ridge — field generation only. This code makes NO marks.\n// It publishes tone and direction maps on the ADR 062 data bridge; every line\n// in the picture is drawn by a plugin layer that reads them.\n//\n// A stretch of coast drawn the way an engraved survey map draws ground: in\n// plan, from directly above, with no view and no horizon. Relief is shown by\n// hachures, short strokes that run straight down the fall line, laid in tiers\n// between breaks of height. A stroke's DIRECTION is the way water would run\n// off that slope, and its DENSITY is how steep the slope is and how far it is\n// turned from a light in the north-west. Flat ground carries no strokes at\n// all, so plateaus and valley floors are bare paper. The sea is water-lined:\n// lines laid parallel to the shore, each further out than the last and each\n// gap wider, until the lining gives out into open water.\n//\n// The same two rules as the rest of the series:\n//   the direction of a hatch = the surface it describes\n//   the density of a hatch   = how far that surface is turned from the light\nfunction sketch(ctx, state) {\n  var W = state.canvas.width, H = state.canvas.height;\n  var seed = state.seed || 0;\n\n  ctx.fillStyle = state.colorPalette[4];\n  ctx.fillRect(0, 0, W, H);\n\n  var COLS = Math.round(200 * W / 700), ROWS = Math.round(150 * H / 500);\n  // The field maps onto the inset plate, so this is the plate's aspect.\n  var ASPECT = (W * 0.85) / (H * 0.81);\n  var DX = ASPECT / (COLS - 1), DY = 1 / (ROWS - 1);   // cell size, plate heights\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\n  // --- The lie of the land ------------------------------------------------\n  // The coast runs ACROSS the plate and the land comes in from its edges. An\n  // island set in the middle of the sheet is the map equivalent of a specimen\n  // floating in the middle of the paper: the most generic thing it can do.\n  var th = rand() * Math.PI * 2;\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  // How much of the plate is land: about three fifths. At half, a whole side\n  // of the plate was open water with nothing in it.\n  var off = 0.06 + rand() * 0.12;\n  var ridgeS = 0.36 + rand() * 0.12;                   // the main ridge, inland\n  var ph = [];\n  for (var i = 0; i < 8; i++) ph.push(rand() * Math.PI * 2);\n  // Spurs run off the main ridge toward the sea. Where they reach it they make\n  // headlands, and the valleys between them make the bays.\n  var spurs = [];\n  var nSp = 4 + Math.floor(rand() * 3);\n  for (var k = 0; k < nSp; k++) {\n    spurs.push({\n      t: -0.95 + (k + 0.2 + rand() * 0.6) * (1.9 / nSp),\n      w: 0.045 + rand() * 0.04,\n      hgt: 0.14 + rand() * 0.12,\n      bend: (rand() * 2 - 1) * 0.25,\n      reach: -0.10 + rand() * 0.14,                    // how far into the sea it runs\n    });\n  }\n  var n1 = lattice(7, 5), n2 = lattice(15, 11), n3 = lattice(33, 25);\n\n  function height(x, y) {\n    var dx = x - cx, dy = y - cy;\n    var s = dx * nx0 + dy * ny0 + off;                 // + inland, - out to sea\n    var t = dx * px0 + dy * py0;                       // along the coast\n    var u = x / ASPECT, v = y;\n    // Coastal plain rising to an upland that levels off behind the ridge.\n    var h = 0.26 * Math.tanh(s * 3.4);\n    // The main ridge, only slightly uneven along its length. 🔴 Every crest of\n    // that unevenness is a summit, and a hachured summit is ringed by its own\n    // courses, so a ridge that rose and fell by a third strung a row of\n    // concentric ovals along the plate and it read as a fingerprint. Kept\n    // low, the crest runs as one long ridge and the courses run along it.\n    var rh = 0.30 + 0.045 * Math.sin(t * 3.1 + ph[0]) + 0.025 * Math.sin(t * 7.7 + ph[1]);\n    var prof = Math.exp(-Math.pow((s - ridgeS) / 0.15, 2));\n    h += rh * prof;\n    for (var q = 0; q < spurs.length; q++) {\n      var sp = spurs[q];\n      // A spur bends as it runs down, so the valleys between do not line up\n      // like the teeth of a comb.\n      var tc = sp.t + sp.bend * (ridgeS - s);\n      // Seaward of the crest only. Left at 1 inland, each spur came back on\n      // the far side of the ridge as a ridge of its own and drew horseshoes of\n      // courses along the plate edge.\n      var along = smooth(sp.reach - 0.08, ridgeS, s) * smooth(ridgeS + 0.06, ridgeS, s);\n      // 🔴 A spur is a shoulder coming DOWN off the ridge, so it fades out at\n      // the crest (1 - prof). Added on top of the ridge instead, every junction\n      // stood higher than the crest either side of it: a summit, ringed by its\n      // own courses, and a row of them read as a fingerprint. Because the\n      // ridge outweighs any spur, height now climbs all the way up each spur\n      // to the crest and no junction can become a peak.\n      h += sp.hgt * along * (1 - prof) * Math.exp(-Math.pow((t - tc) / sp.w, 2));\n    }\n    // 🔴 Kept faint. Every lump in the height field is a summit, and a summit\n    // is drawn as a white spot with hachures radiating from it, so the finer\n    // lattices at their first strength strewed the land with starbursts. The\n    // relief is carried by the ridge and the spurs; these only roughen them.\n    h += 0.026 * n1(u, v) + 0.012 * n2(u, v) + 0.004 * n3(u, v);\n    return h;\n  }\n\n  var N = COLS * ROWS;\n  var hmap = new Float32Array(N);\n  for (var r = 0; r < ROWS; r++)\n    for (var c = 0; c < COLS; c++)\n      hmap[r * COLS + c] = height(c * DX, r * DY);\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\n  // --- Hachures -------------------------------------------------------------\n  var slope = new Float32Array(N), fall = 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      // Downhill: the way water runs off it.\n      fall[n] = Math.atan2(-gy, -gx);\n      facing[n] = g > 1e-6 ? (-gx * LX - gy * LY) / g : 0;   // +1 faces the light\n      if (hmap[n] > 0 && (r2 * 7 + c2) % 11 === 0) samples.push(g);\n    }\n  }\n  samples.sort(function (a, b) { return a - b; });\n  // Steepness is read against this coast's own steep ground, so every seed\n  // spends the whole tonal range whatever its relief happens to be.\n  var smax = samples.length ? samples[Math.floor(samples.length * 0.92)] : 1;\n\n  var landTone = new Float32Array(N);\n  // The courses. Without them the strokes piled up as hair: every spur a\n  // feather, every summit an eye with lashes. With them over a LUMPY field\n  // they drew a contour ring round every lump and the land read as a\n  // fingerprint. Over smooth relief they run in long bands along the ridges\n  // and spurs, which is what a survey sheet's hachures actually do. The\n  // interval is set from this coast's own steep ground, so the courses on the\n  // steepest slopes stand about 18px apart whatever the seed.\n  var TIER = 0.022 * smax;\n  for (var m = 0; m < N; m++) {\n    if (hmap[m] <= 0) continue;\n    var sn = Math.min(1.25, slope[m] / smax);\n    var f = facing[m];\n    // Steepness first, as the survey hachure is defined; then the oblique\n    // light, which is what makes the ground read as form rather than as a\n    // chart of gradients. A slope turned away from the light goes darker, one\n    // turned toward it goes lighter, and flat ground stays bare either way.\n    var T = 0.04 + 0.46 * Math.pow(sn, 0.85);\n    T += 0.38 * sn * (f < 0 ? -f : 0);\n    T -= 0.14 * sn * (f > 0 ? f : 0);\n    // The break between courses is narrow. At a quarter of the interval every\n    // course was a band of ink with bare paper either side, and the land read\n    // as zebra stripe or wood grain with the light drowned under it. On a\n    // survey sheet the courses are felt more than seen.\n    var tier = hmap[m] / TIER - Math.floor(hmap[m] / TIER);\n    if (tier < 0.12) T = 0;\n    landTone[m] = T < 0 ? 0 : T > 1 ? 1 : T;\n  }\n\n  // --- The sea ------------------------------------------------------------\n  // Distance from the shore, in cells, by a two-pass chamfer.\n  var dist = new Float32Array(N);\n  var BIG = 1e6;\n  for (var a2 = 0; a2 < N; a2++) dist[a2] = hmap[a2] > 0 ? 0 : BIG;\n  var D1 = 1, D2 = Math.SQRT2;\n  for (var r3 = 0; r3 < ROWS; r3++) for (var c3 = 0; c3 < COLS; c3++) {\n    var i3 = r3 * COLS + c3, best = dist[i3];\n    if (c3 > 0) best = Math.min(best, dist[i3 - 1] + D1);\n    if (r3 > 0) {\n      best = Math.min(best, dist[i3 - COLS] + D1);\n      if (c3 > 0) best = Math.min(best, dist[i3 - COLS - 1] + D2);\n      if (c3 < COLS - 1) best = Math.min(best, dist[i3 - COLS + 1] + D2);\n    }\n    dist[i3] = best;\n  }\n  for (var r4 = ROWS - 1; r4 >= 0; r4--) for (var c4 = COLS - 1; c4 >= 0; c4--) {\n    var i4 = r4 * COLS + c4, b4 = dist[i4];\n    if (c4 < COLS - 1) b4 = Math.min(b4, dist[i4 + 1] + D1);\n    if (r4 < ROWS - 1) {\n      b4 = Math.min(b4, dist[i4 + COLS] + D1);\n      if (c4 < COLS - 1) b4 = Math.min(b4, dist[i4 + COLS + 1] + D2);\n      if (c4 > 0) b4 = Math.min(b4, dist[i4 + COLS - 1] + D2);\n    }\n    dist[i4] = b4;\n  }\n  // The shoreline direction comes from a smoothed copy, because the chamfer's\n  // own gradient steps in eighths of a turn and the lining would follow it.\n  var sm = new Float32Array(dist), tmp = new Float32Array(N);\n  for (var pass = 0; pass < 4; pass++) {\n    for (var r5 = 0; r5 < ROWS; r5++) for (var c5 = 0; c5 < COLS; c5++) {\n      var acc = 0, cnt = 0;\n      for (var oy = -2; oy <= 2; oy++) for (var ox = -2; ox <= 2; ox++) {\n        var cc = c5 + ox, rr = r5 + oy;\n        if (cc < 0 || rr < 0 || cc >= COLS || rr >= ROWS) continue;\n        acc += Math.min(sm[rr * COLS + cc], 60); cnt++;\n      }\n      tmp[r5 * COLS + c5] = acc / cnt;\n    }\n    var sw = sm; sm = tmp; tmp = sw;\n  }\n  var shoreDir = new Float32Array(N), coast = new Float32Array(N), lining = new Float32Array(N);\n  // Each line a little further out than the last and each gap a little wider,\n  // which is how a water-lined chart makes the sea deepen away from the shore.\n  var BANDS = [];\n  for (var k2 = 1; k2 <= 16; k2++) BANDS.push(1.6 + 2.1 * Math.pow(k2, 1.32));\n  for (var r6 = 0; r6 < ROWS; r6++) for (var c6 = 0; c6 < COLS; c6++) {\n    var i6 = r6 * COLS + c6;\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 d = dist[i6];\n    if (d <= 0) continue;\n    if (d <= 1.6) coast[i6] = 1;\n    for (var b = 0; b < BANDS.length; b++) {\n      if (Math.abs(d - BANDS[b]) < 0.62) { lining[i6] = 1; break; }\n    }\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\n  function pack(name, mag, ang, skew) {\n    var f32 = new Float32Array(N * 3);\n    for (var p = 0; p < N; p++) {\n      var an = ang[p] + (skew || 0);\n      var 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  var dith = [lattice(83, 63), lattice(79, 59), lattice(89, 67), lattice(73, 55)];\n  function gate(tone, thr, fray) {\n    var out = new Float32Array(N);\n    for (var p = 0; p < N; p++) {\n      if (tone[p] <= 0) continue;\n      var uu = (p % COLS) / (COLS - 1), vv = Math.floor(p / COLS) / (ROWS - 1);\n      // Small: at 0.06 the edges of flat ground came out moth-eaten.\n      out[p] = tone[p] >= thr + 0.03 * fray(uu, vv) ? 1 : 0;\n    }\n    return out;\n  }\n\n  // Hachures run straight down the fall line and never cross, so the passes\n  // barely diverge. Half are traced UPHILL (skew + PI: the same stroke, the\n  // other way along it). The stepper traces forward only, so a pass traced\n  // one way thins the upstream edge of every tier; two each way cancels it.\n  // The first threshold is high enough that gentle ground stays bare paper:\n  // on a hachured map, flat ground carrying strokes looks like a slope.\n  pack('hach1', gate(landTone, 0.20, dith[0]), fall, 0);\n  pack('hach2', gate(landTone, 0.28, dith[1]), fall, Math.PI + 0.05);\n  pack('hach3', gate(landTone, 0.42, dith[2]), fall, -0.05);\n  pack('hach4', gate(landTone, 0.56, dith[3]), fall, Math.PI);\n  pack('coast', coast, shoreDir, 0);\n  pack('lining', lining, shoreDir, 0);\n}\n",
 "layers": [
  {
   "id": "sheet",
   "type": "textures:paper",
   "name": "Sheet",
   "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": {
    "preset": "rough",
    "roughness": -1,
    "color": "#ebe8e0",
    "seed": 4410
   }
  },
  {
   "id": "lining",
   "type": "painting:flow-lines",
   "name": "Water-Lining — Parallel to the Shore",
   "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:lining",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4421,
    "lineCount": 30000,
    "seedDistribution": "grid-jittered",
    "lineLength": 26,
    "stepSize": 4,
    "lineWeight": 0.92,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.55,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "coast",
   "type": "painting:flow-lines",
   "name": "Shoreline",
   "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:coast",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4433,
    "lineCount": 30000,
    "seedDistribution": "grid-jittered",
    "lineLength": 22,
    "stepSize": 4,
    "lineWeight": 1.6,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.7,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "hach-1",
   "type": "painting:flow-lines",
   "name": "Hachures 1 — All Sloping Ground",
   "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:hach1",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4441,
    "lineCount": 24000,
    "seedDistribution": "grid-jittered",
    "lineLength": 4,
    "stepSize": 4,
    "lineWeight": 1.12,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.52,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "hach-2",
   "type": "painting:flow-lines",
   "name": "Hachures 2 — Steeper, Traced Uphill",
   "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:hach2",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4453,
    "lineCount": 22000,
    "seedDistribution": "grid-jittered",
    "lineLength": 4,
    "stepSize": 4,
    "lineWeight": 1.16,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.54,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "hach-3",
   "type": "painting:flow-lines",
   "name": "Hachures 3 — Steep or Turned from the Light",
   "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:hach3",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4469,
    "lineCount": 21000,
    "seedDistribution": "grid-jittered",
    "lineLength": 4,
    "stepSize": 4,
    "lineWeight": 1.24,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.58,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "hach-4",
   "type": "painting:flow-lines",
   "name": "Hachures 4 — Steepest, Traced Uphill",
   "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:hach4",
    "fieldCols": 400,
    "fieldRows": 300,
    "minMagnitude": 0.5,
    "seed": 4477,
    "lineCount": 19000,
    "seedDistribution": "grid-jittered",
    "lineLength": 4,
    "stepSize": 4,
    "lineWeight": 1.32,
    "lineWeightVariation": 0.12,
    "taper": "none",
    "color": "#1d2327",
    "colorVariation": 0.05,
    "opacity": 0.62,
    "paintMode": "multiply",
    "depthScale": false,
    "horizonY": 0,
    "depthWeightRange": "[1, 1]",
    "depthOpacityRange": "[1, 1]",
    "maskCenterY": -1,
    "maskSpread": 0.25
   }
  },
  {
   "id": "plate-mark",
   "type": "shapes:rect",
   "name": "Plate Mark",
   "visible": true,
   "locked": false,
   "opacity": 0.55,
   "blendMode": "normal",
   "transform": {
    "x": 105,
    "y": 75,
    "width": 1190,
    "height": 810,
    "rotation": 0,
    "scaleX": 1,
    "scaleY": 1,
    "anchorX": 0,
    "anchorY": 0
   },
   "properties": {
    "fillColor": "#ffffff",
    "fillEnabled": false,
    "strokeColor": "#6c7a82",
    "strokeWidth": 2,
    "strokeEnabled": true,
    "cornerRadius": 0
   }
  },
  {
   "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": 4410,
    "monochrome": true
   }
  }
 ]
}