<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ivan Pribec</title><link>https://ivan-pi.github.io/tags/node-generation/</link><description>Recent content on Ivan Pribec</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>© 2025 — Ivan Pribec — All rights reserved.</copyright><lastBuildDate>Fri, 04 Sep 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://ivan-pi.github.io/tags/node-generation/index.xml" rel="self" type="application/rss+xml"/><item><title>Circle-Fitting Relaxation</title><link>https://ivan-pi.github.io/gallery/circle-fit-relaxation/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0200</pubDate><guid>https://ivan-pi.github.io/gallery/circle-fit-relaxation/</guid><description>&lt;!-- Minimal working example: Poisson-disc sampling (Bridson) followed
by circle-fitting relaxation: each node is attracted toward the
centre of the least-squares (Kasa) circle through its k=6 nearest
neighbours, blended with short-range repulsion. The residual of
the same fit is the convergence monitor, shown as the colour of
each point and as the mean readout. Hover to see one node's
neighbours, fitted circle, and attraction; click to resample.
Style after Jason Davies' Lloyd's Relaxation / Poisson-Disc pages.
This is a Hugo content file: the front matter above is parsed, the
rest is inserted verbatim by layouts/gallery/single.html. --&gt;
&lt;div class="gallery-chart" id="chart"&gt;&lt;/div&gt;
&lt;p class="gallery-readout"&gt;
&lt;span&gt;Iteration: &lt;span id="iter"&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;mean &amp;sigma;/h: &lt;span id="sigma"&gt;&amp;ndash;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;mean &amp;psi;&lt;sub&gt;6&lt;/sub&gt;: &lt;span id="psi6"&gt;&amp;ndash;&lt;/span&gt;&lt;/span&gt;
&lt;span id="status"&gt;&lt;/span&gt;
&lt;/p&gt;</description><content:encoded><![CDATA[<!-- Minimal working example: Poisson-disc sampling (Bridson) followed
     by circle-fitting relaxation: each node is attracted toward the
     centre of the least-squares (Kasa) circle through its k=6 nearest
     neighbours, blended with short-range repulsion.  The residual of
     the same fit is the convergence monitor, shown as the colour of
     each point and as the mean readout.  Hover to see one node's
     neighbours, fitted circle, and attraction; click to resample.
     Style after Jason Davies' Lloyd's Relaxation / Poisson-Disc pages.
     This is a Hugo content file: the front matter above is parsed, the
     rest is inserted verbatim by layouts/gallery/single.html. -->

<div class="gallery-chart" id="chart"></div>

<p class="gallery-readout">
<span>Iteration: <span id="iter">0</span></span>
<span>mean &sigma;/h: <span id="sigma">&ndash;</span></span>
<span>mean &psi;<sub>6</sub>: <span id="psi6">&ndash;</span></span>
<span id="status"></span>
</p>

<div class="gallery-controls">
  <label for="omega">&omega;</label>
  <input type="range" id="omega" min="0" max="1" step="0.05" value="0.4">
  <span id="omegaval">0.40</span>
  <span class="note">0 = circle fit, 1 = repulsion</span>

  <label for="mexp">m</label>
  <input type="range" id="mexp" min="2" max="16" step="1" value="8">
  <span id="mval">8</span>
  <span class="note">repulsion exponent</span>

  <label for="knb">k</label>
  <input type="range" id="knb" min="3" max="12" step="1" value="6">
  <span id="kval">6</span>
  <span class="note">neighbours, force and fit</span>
</div>

<p>Poisson-disc samples, relaxed by two forces.
Click (or tap) the canvas to restart; hover to inspect one node.</p>

<p>The repulsion from the k nearest neighbours y<sub>j</sub> is
$$f(x) = \sum_{j} \left( \frac{h}{|x - y_j|} \right)^{m} (x - y_j),$$
and each step blends it with the pull toward &#265;(x), the centre of
the least-squares circle through the neighbours (K&aring;sa, 1976):
$$\Delta x = \omega\, f + (1 - \omega)\, \big(\hat{c}(x) - x\big).$$
</p>

<p>&sigma; is the root-mean-square deviation of the neighbours
from their fitted circle, in units of the spacing h: zero for a
perfect ring.
Each point is coloured by its own &sigma;.
&psi;<sub>6</sub> compares the directions to the six nearest
neighbours with a perfect hexagon: 1 is hexagonal, near 0 is random.
(At k = 3 a circle fits three points exactly, and &sigma; stops
meaning anything.)</p>

<p>The black squares are fixed.  The first ten iterations run pure
repulsion with a ramped-in step clamp; after that the sliders take
over.  A hard floor keeps every pair at least 0.7&thinsp;h apart;
the repulsive shift and the total step are clamped to
0.2&thinsp;h, the step is damped by &frac12;, and neighbour lists
refresh every five iterations.</p>

<p>Inspired by the beautiful visualizations of
<a href="https://www.jasondavies.com/">Jason Davies</a>.</p>

<script>
(function() {
"use strict";

var chart = document.getElementById("chart"),
    canvas = document.createElement("canvas"),
    ctx;
chart.appendChild(canvas);

// The canvas fills its container (the full text column on a phone, up to
// 960 CSS px on a large screen) and is redrawn at the device pixel ratio.
// The target spacing h is fixed in CSS pixels, so a narrower canvas simply
// holds fewer points instead of shrinking them.
var ratio, width, height, h,
    clampFrac = 0.2,          // max step per iteration, in units of h
    damping = 0.5,            // under-relaxation of the total step:
                              // deadbeat for period-2 oscillation
    precond = 10,             // pure-repulsion iterations
    refreshEvery = 5,          // neighbour-list refresh interval
    floorRatio = 0.7;         // hard pairwise floor, in units of h

function resize() {
  ratio = window.devicePixelRatio || 1;
  var cssWidth = Math.max(240, chart.clientWidth || 960);
  width = Math.round(cssWidth * ratio);
  height = Math.round(width * 500 / 960);
  h = 18 * ratio;             // target spacing
  canvas.width = width; canvas.height = height;
  ctx = canvas.getContext("2d");
}

var pts, nfixed, iter, running, hover = null, nbCache = null,
    k = 6,
    omegaSlider = document.getElementById("omega"),
    mSlider = document.getElementById("mexp"),
    kSlider = document.getElementById("knb");
function rearm() {
  running = true;
  document.getElementById("omegaval").textContent =
      (+omegaSlider.value).toFixed(2);
  document.getElementById("mval").textContent = mSlider.value;
  document.getElementById("kval").textContent = kSlider.value;
  if (+kSlider.value !== k) { k = +kSlider.value; nbCache = null; }
  mSlider.disabled = +omegaSlider.value === 0;
}
omegaSlider.addEventListener("input", rearm);
mSlider.addEventListener("input", rearm);
kSlider.addEventListener("input", rearm);

function dist2(a, b) {
  var dx = b[0] - a[0], dy = b[1] - a[1];
  return dx * dx + dy * dy;
}

// ---------- fixed boundary ring along the canvas rectangle ----------
function boundaryRing() {
  var inset = 3 * ratio,
      w = width - 2 * inset, hh = height - 2 * inset,
      out = [], n, i;
  n = Math.round(w / h);
  for (i = 0; i < n; ++i) {
    out.push([inset + w * i / n, inset]);
    out.push([inset + w - w * i / n, inset + hh]);
  }
  n = Math.round(hh / h);
  for (i = 0; i < n; ++i) {
    out.push([inset + w, inset + hh * i / n]);
    out.push([inset, inset + hh - hh * i / n]);
  }
  return out;
}

// ---- Poisson-disc sampling (Bridson), seeded by the fixed ring ----
function poisson(r, seeds) {
  var kk = 30,
      r2 = r * r, A = 3 * r2,
      cell = r * Math.SQRT1_2,
      gw = Math.ceil(width / cell), gh = Math.ceil(height / cell),
      grid = new Array(gw * gh),
      queue = [], out = [];
  function put(p) {
    grid[gw * (p[1] / cell | 0) + (p[0] / cell | 0)] = p;
  }
  function far(p) {
    var x = p[0] / cell | 0, y = p[1] / cell | 0,
        x0 = Math.max(x - 2, 0), y0 = Math.max(y - 2, 0),
        x1 = Math.min(x + 3, gw), y1 = Math.min(y + 3, gh);
    for (var j = y0; j < y1; ++j)
      for (var i = x0; i < x1; ++i) {
        var g = grid[gw * j + i];
        if (g && dist2(g, p) < 0.72 * r2) return false;
      }
    return true;
  }
  seeds.forEach(function(p) { put(p); queue.push(p); });
  while (queue.length) {
    var qi = Math.random() * queue.length | 0, p = queue[qi], j;
    for (j = 0; j < kk; ++j) {
      var t = 2 * Math.PI * Math.random(),
          rr = Math.sqrt(r2 + A * Math.random()),
          q = [p[0] + rr * Math.cos(t), p[1] + rr * Math.sin(t)];
      if (q[0] >= 0 && q[0] < width && q[1] >= 0 && q[1] < height
          && far(q)) { put(q); queue.push(q); out.push(q); break; }
    }
    if (j === kk) queue[qi] = queue[queue.length - 1], queue.pop();
  }
  return out;
}

// ---------- k nearest neighbours via a cell grid ----------
function buildGrid() {
  var cell = h,
      gw = Math.ceil(width / cell), gh = Math.ceil(height / cell),
      grid = new Array(gw * gh);
  pts.forEach(function(p, i) {
    var c = gw * (p[1] / cell | 0) + (p[0] / cell | 0);
    (grid[c] || (grid[c] = [])).push(i);
  });
  return function(i, kk) {
    kk = kk || k;
    var p = pts[i],
        x = p[0] / cell | 0, y = p[1] / cell | 0,
        cand = [];
    for (var ring = 1; ring <= 3; ++ring) {
      cand.length = 0;
      var x0 = Math.max(x - ring, 0), y0 = Math.max(y - ring, 0),
          x1 = Math.min(x + ring + 1, gw),
          y1 = Math.min(y + ring + 1, gh);
      for (var j = y0; j < y1; ++j)
        for (var ii = x0; ii < x1; ++ii) {
          var b = grid[gw * j + ii];
          if (b) for (var t = 0; t < b.length; ++t)
            if (b[t] !== i) cand.push(b[t]);
        }
      if (cand.length >= Math.max(kk, 6) + 2) break;
    }
    cand.sort(function(a, b) {
      return dist2(pts[a], p) - dist2(pts[b], p);
    });
    return cand.slice(0, kk);
  };
}

// ------ Kasa circle fit (centred, so the system is 2x2) ------
// Least squares on 2*a*u + 2*b*v + c = u^2 + v^2 in coordinates centred
// on the centroid: the first-moment sums vanish, leaving a 2x2 system
// for the centre and c = mean(z); also better conditioned than the raw
// 3x3 normal equations.
function kasa(q) {
  var n = q.length, mx = 0, my = 0, i;
  for (i = 0; i < n; ++i) { mx += q[i][0]; my += q[i][1]; }
  mx /= n; my /= n;
  var Suu = 0, Suv = 0, Svv = 0, Suz = 0, Svz = 0, Sz = 0;
  for (i = 0; i < n; ++i) {
    var u = q[i][0] - mx, v = q[i][1] - my, z = u * u + v * v;
    Suu += u * u; Suv += u * v; Svv += v * v;
    Suz += u * z; Svz += v * z; Sz += z;
  }
  var d = 2 * (Suu * Svv - Suv * Suv);
  if (Math.abs(d) < 1e-9) return null;   // collinear neighbours
  var a = (Svv * Suz - Suv * Svz) / d,   // centre, relative to the centroid
      b = (Suu * Svz - Suv * Suz) / d,
      R = Math.sqrt(Math.max(a * a + b * b + Sz / n, 0)),
      st = 0;
  for (i = 0; i < n; ++i)
    st += Math.pow(Math.hypot(q[i][0] - mx - a, q[i][1] - my - b) - R, 2);
  return { cx: mx + a, cy: my + b, r: R, sigma: Math.sqrt(st / n) };
}

// ------- hard floor: pairwise Gauss-Seidel projection -------
function floorSweep() {
  floorSweep.maxCorr = 0;
  var dmin = floorRatio * h, dmin2 = dmin * dmin,
      cell = h,
      gw = Math.ceil(width / cell), gh = Math.ceil(height / cell),
      grid = new Array(gw * gh), i, moved = false;
  for (i = 0; i < pts.length; ++i) {
    var c = gw * (pts[i][1] / cell | 0) + (pts[i][0] / cell | 0);
    (grid[c] || (grid[c] = [])).push(i);
  }
  for (i = 0; i < pts.length; ++i) {
    var p = pts[i],
        x = p[0] / cell | 0, y = p[1] / cell | 0,
        x0 = Math.max(x - 1, 0), y0 = Math.max(y - 1, 0),
        x1 = Math.min(x + 2, gw), y1 = Math.min(y + 2, gh);
    for (var jj = y0; jj < y1; ++jj)
      for (var ii = x0; ii < x1; ++ii) {
        var b = grid[gw * jj + ii];
        if (!b) continue;
        for (var tt = 0; tt < b.length; ++tt) {
          var j = b[tt];
          if (j <= i) continue;
          if (i < nfixed && j < nfixed) continue;
          var q = pts[j],
              dx = p[0] - q[0], dy = p[1] - q[1],
              d2 = dx * dx + dy * dy;
          if (d2 >= dmin2) continue;
          var d = Math.sqrt(d2), ux, uy;
          if (d < 1e-9) {           // coincident: random direction
            var th = 2 * Math.PI * Math.random();
            ux = Math.cos(th); uy = Math.sin(th); d = 0;
          } else { ux = dx / d; uy = dy / d; }
          var gap = dmin - d;
          floorSweep.maxCorr = Math.max(floorSweep.maxCorr, gap);
          if (i >= nfixed && j >= nfixed) {
            p[0] += 0.5 * gap * ux; p[1] += 0.5 * gap * uy;
            q[0] -= 0.5 * gap * ux; q[1] -= 0.5 * gap * uy;
          } else if (i >= nfixed) {
            p[0] += gap * ux; p[1] += gap * uy;
          } else {
            q[0] -= gap * ux; q[1] -= gap * uy;
          }
          moved = true;
        }
      }
  }
  return moved;
}

// ---------- one relaxation iteration (fixed ring clamped) ----------
function step() {
  if (nbCache === null || iter % refreshEvery === 0) {
    var knnf = buildGrid();
    nbCache = new Array(pts.length);
    var psum = 0, pn = 0;
    for (var c = nfixed; c < pts.length; ++c) {
      nbCache[c] = knnf(c);
      var six = k === 6 ? nbCache[c] : knnf(c, 6),
          re = 0, im = 0;
      for (var jj6 = 0; jj6 < six.length; ++jj6) {
        var th6 = Math.atan2(pts[six[jj6]][1] - pts[c][1],
                             pts[six[jj6]][0] - pts[c][0]);
        re += Math.cos(6 * th6); im += Math.sin(6 * th6);
      }
      if (six.length === 6) {
        psum += Math.hypot(re, im) / 6; ++pn;
      }
    }
    document.getElementById("psi6").textContent =
        pn ? (psum / pn).toFixed(3) : "-";
  }
  var omega = iter < precond ? 1.0 : +omegaSlider.value,
      m = +mSlider.value,
      clamp = clampFrac * h *
          (0.3 + 0.7 * Math.min(1, (iter + 1) / precond)),
      shifts = new Array(pts.length),
      sig = 0, nsig = 0, maxShift = 0, i, j;
  for (i = 0; i < pts.length; ++i) {
    if (i < nfixed) { shifts[i] = [0, 0]; continue; }
    var p = pts[i], nb = nbCache[i];
    if (!nb || nb.length < 4) { shifts[i] = [0, 0]; continue; }
    // repulsion with LENGTH units: f = sum (h/d)^m (p - q)
    var fx = 0, fy = 0;
    for (j = 0; j < nb.length; ++j) {
      var q = pts[nb[j]],
          dx = p[0]-q[0], dy = p[1]-q[1],
          d = Math.hypot(dx, dy) || 1e-9,
          w = Math.pow(h / d, m);
      fx += w * dx; fy += w * dy;
    }
    var fn = Math.hypot(fx, fy);
    if (fn > clamp) {               // clamp the repulsive shift
      fx *= clamp / fn; fy *= clamp / fn;
    }
    // proportional attraction toward the fitted centre
    var fit = kasa(nb.map(function(t) { return pts[t]; })),
        ax = 0, ay = 0;
    if (fit) {
      ax = fit.cx - p[0]; ay = fit.cy - p[1];
      sig += fit.sigma / h; ++nsig;
      pts[i].sigma = fit.sigma / h;
    }
    var sx = omega * fx + (1 - omega) * ax,
        sy = omega * fy + (1 - omega) * ay,
        sn = Math.hypot(sx, sy);
    if (sn > clamp) {               // clamp the total step
      sx *= clamp / sn; sy *= clamp / sn;
    }
    shifts[i] = [sx, sy];
  }
  for (i = nfixed; i < pts.length; ++i) {
    var s = shifts[i];
    pts[i][0] += damping * s[0]; pts[i][1] += damping * s[1];
    maxShift = Math.max(maxShift,
        damping * Math.hypot(s[0], s[1]));
  }
  for (var sweep = 0; sweep < 3 && floorSweep(); ++sweep) {
    maxShift = Math.max(maxShift, floorSweep.maxCorr);
  }
  ++iter;
  document.getElementById("iter").textContent = iter;
  var sigBar = nsig ? sig / nsig : Infinity;
  document.getElementById("sigma").textContent =
      nsig ? sigBar.toFixed(4) : "-";
  if (iter > precond + 5 && maxShift < 5e-3 * h)
    running = false;
}

// ---------- drawing ----------
function colour(s) {
  var t = Math.max(0, Math.min(1, (s || 0) / 0.25));
  var r = Math.round( 40 + 215 * t),
      g = Math.round( 60 + 160 * t),
      b = Math.round(110 - 80 * t);
  return "rgb(" + r + "," + g + "," + b + ")";
}
function draw() {
  ctx.clearRect(0, 0, width, height);
  var s = 4.5 * ratio;
  for (var i = 0; i < pts.length; ++i) {
    if (i < nfixed) {
      ctx.fillStyle = "#000";
      ctx.fillRect(pts[i][0] - s / 2, pts[i][1] - s / 2, s, s);
    } else {
      ctx.beginPath();
      ctx.arc(pts[i][0], pts[i][1], 2 * ratio, 0, 2 * Math.PI);
      ctx.fillStyle = colour(pts[i].sigma);
      ctx.fill();
    }
  }
  if (hover !== null && hover >= nfixed && pts[hover]) {
    var nb = nbCache && nbCache[hover]
        ? nbCache[hover] : buildGrid()(hover),
        p = pts[hover],
        fit = kasa(nb.map(function(t) { return pts[t]; }));
    ctx.lineWidth = 1.2 * ratio;
    for (var j = 0; j < nb.length; ++j) {
      var q = pts[nb[j]];
      ctx.beginPath();
      ctx.arc(q[0], q[1], 3.2 * ratio, 0, 2 * Math.PI);
      ctx.strokeStyle = "#1f77b4"; ctx.stroke();
    }
    if (fit) {
      ctx.beginPath();
      ctx.arc(fit.cx, fit.cy, fit.r, 0, 2 * Math.PI);
      ctx.strokeStyle = "#2ca02c"; ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(p[0], p[1]); ctx.lineTo(fit.cx, fit.cy);
      ctx.strokeStyle = "#d62728"; ctx.stroke();
      ctx.beginPath();
      ctx.arc(fit.cx, fit.cy, 2.5 * ratio, 0, 2 * Math.PI);
      ctx.fillStyle = "#2ca02c"; ctx.fill();
    }
    ctx.beginPath();
    ctx.arc(p[0], p[1], 3.5 * ratio, 0, 2 * Math.PI);
    ctx.fillStyle = "#d62728"; ctx.fill();
  }
}

function statusText() {
  var t = "(" + pts.length + " points, " + nfixed + " fixed)";
  if (iter < precond) t += " (preconditioning: ω=1)";
  else if (!running) t += " (converged)";
  return t;
}

function frame() {
  if (running) step();
  draw();
  document.getElementById("status").textContent = statusText();
  requestAnimationFrame(frame);
}

function reset() {
  var ring = boundaryRing();
  nfixed = ring.length;
  var inner = poisson(h, ring);
  pts = ring.concat(inner);
  nbCache = null;
  hover = null;
  iter = 0;
  running = true;
}

canvas.addEventListener("click", reset);
canvas.addEventListener("mousemove", function(ev) {
  // map CSS pixels to canvas pixels (the canvas is scaled by CSS)
  var r = canvas.getBoundingClientRect(),
      mx = (ev.clientX - r.left) * width / r.width,
      my = (ev.clientY - r.top) * height / r.height,
      best = -1, bd = 1e18;
  for (var i = nfixed; i < pts.length; ++i) {
    var d = dist2(pts[i], [mx, my]);
    if (d < bd) { bd = d; best = i; }
  }
  hover = bd < h * h * 4 ? best : null;
});
canvas.addEventListener("mouseleave", function() { hover = null; });
window.addEventListener("resize", function() {
  // resample when the layout width changes (rotation, window resize)
  var cssWidth = Math.max(240, chart.clientWidth || 960);
  if (Math.abs(cssWidth * (window.devicePixelRatio || 1) - width) > 2) {
    resize(); reset();
  }
});

resize();
reset();
frame();

})();
</script>
]]></content:encoded></item><item><title>Advancing-Front Node Generators</title><link>https://ivan-pi.github.io/gallery/advancing-front-node-generators/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0200</pubDate><guid>https://ivan-pi.github.io/gallery/advancing-front-node-generators/</guid><description>&lt;!-- Faithful JavaScript ports of the two bulk node generators in the
nodeplace library (Fortran/C):
node_placing -- Fornberg &amp; Flyer (2015)
node_placing_heightfield -- van der Sande &amp; Fornberg (2021),
2-D specialisation
The ports preserve the seeding (cell-centred with jitter), the
outward walk for the nearest outside-PDPs, the five cell-centred
arc fractions, the splice semantics, the first-argmin tie rule,
and the height-field direction-dependence correction (their
section 2.3; inactive for constant radius).
This is a Hugo content file: the front matter above is parsed, the
rest is inserted verbatim by layouts/gallery/single.html. --&gt;
&lt;div class="gallery-chart" id="chart"&gt;&lt;/div&gt;
&lt;p class="gallery-readout"&gt;
&lt;span&gt;nodes: &lt;span id="count"&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;front size: &lt;span id="fsize"&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;</description><content:encoded><![CDATA[<!-- Faithful JavaScript ports of the two bulk node generators in the
     nodeplace library (Fortran/C):
       node_placing            -- Fornberg & Flyer (2015)
       node_placing_heightfield -- van der Sande & Fornberg (2021),
                                   2-D specialisation
     The ports preserve the seeding (cell-centred with jitter), the
     outward walk for the nearest outside-PDPs, the five cell-centred
     arc fractions, the splice semantics, the first-argmin tie rule,
     and the height-field direction-dependence correction (their
     section 2.3; inactive for constant radius).
     This is a Hugo content file: the front matter above is parsed, the
     rest is inserted verbatim by layouts/gallery/single.html. -->

<div class="gallery-chart" id="chart"></div>

<p class="gallery-readout">
<span>nodes: <span id="count">0</span></span>
<span>front size: <span id="fsize">0</span></span>
</p>

<div class="gallery-controls">
  <label for="method">method</label>
  <select id="method">
    <option value="ff">Fornberg&ndash;Flyer 2015 (advancing front)</option>
    <option value="hf">van der Sande&ndash;Fornberg 2021 (height field, 2-D)</option>
  </select>

  <label for="ninit">ninit</label>
  <input type="range" id="ninit" min="20" max="120" step="5" value="60">
  <span id="ninitval">60</span>
  <span class="note">both algorithms</span>

  <label for="rfun">radius</label>
  <select id="rfun">
    <option value="const">constant</option>
    <option value="grade">graded in y</option>
    <option value="wave">wave in x</option>
  </select>

  <label for="grad">strength</label>
  <input type="range" id="grad" min="0" max="2" step="0.1" value="1">
  <span id="gradval">1.0</span>
  <span class="note">both algorithms</span>

  <label for="nang">arc points</label>
  <input type="range" id="nang" min="2" max="12" step="1" value="5">
  <span id="nangval">5</span>
  <span class="note">Fornberg&ndash;Flyer</span>

  <label for="gfac">grid factor</label>
  <input type="range" id="gfac" min="3" max="20" step="1" value="10">
  <span id="gfacval">10</span>
  <span class="note">van der Sande&ndash;Fornberg</span>

  <label for="speed">dots/frame</label>
  <input type="range" id="speed" min="1" max="50" step="1" value="8">
  <span id="speedval">8</span>
  <span class="note">animation</span>
</div>

<p>Two bulk generators, ported line for line from the reference
Fortran/C.  Both sweep the box bottom to top, placing each node at
the lowest admissible point and pushing an exclusion disc of radius
r(x,&thinsp;y) into the front; the red line is the front.  Click (or
tap) the canvas to restart.</p>

<p>Fornberg&ndash;Flyer keep the front as an x-sorted list of
potential dot positions (PDPs).  The next node is the lowest PDP; it is
accepted, and the PDPs inside its exclusion disc are replaced by a
configurable number of cell-centred points on the exclusion arc
(five by default), spliced between the
nearest outside PDPs found by walking outward from the dot.</p>

<p>Van der Sande&ndash;Fornberg keep a height field on a background
grid of grid-factor&thinsp;&times;&thinsp;ninit columns.  The next
node goes to the lowest column; each accepted dot raises the
heights under its exclusion disc to the disc&rsquo;s far
hemisphere.  When the radius varies, the node is first pushed off
the discs of earlier nearby nodes &mdash; the direction-dependence
correction of their section&nbsp;2.3 (inactive for constant
radius).</p>

<div class="gallery-figures">
<figure>
<svg width="460" height="250" viewBox="0 0 460 250">
  <g fill="#345">
    <circle cx="230.0" cy="8.7" r="2.5"/>
    <circle cx="361.4" cy="19.9" r="2.5"/>
    <circle cx="164.3" cy="35.3" r="2.5"/>
    <circle cx="32.9" cy="36.4" r="2.5"/>
    <circle cx="428.9" cy="38.5" r="2.5"/>
    <circle cx="274.2" cy="63.0" r="2.5"/>
    <circle cx="115.3" cy="85.3" r="2.5"/>
    <circle cx="340.3" cy="86.6" r="2.5"/>
  </g>
  <polyline points="14.3,103.9 52.3,115.9 77.1,144.0 112.9,155.3 149.3,146.5 175.9,120.2 183.2,102.7 218.5,105.4 244.2,126.2 276.6,132.9 294.3,139.4 321.9,154.2 353.2,155.4 381.9,142.9 402.3,119.1 422.5,108.2 455.1,103.4"
      fill="none" stroke="#bbb" stroke-width="1.2"
      stroke-dasharray="5 4"/>
  <circle cx="183.2" cy="102.7" r="70" fill="none" stroke="#999"
      stroke-dasharray="4 3"/>
  <polyline points="14.3,103.9 52.3,115.9 77.1,144.0 112.9,155.3 137.5,155.8 164.3,170.1 194.6,171.8 222.8,160.4 243.5,138.2 276.6,132.9 294.3,139.4 321.9,154.2 353.2,155.4 381.9,142.9 402.3,119.1 422.5,108.2 455.1,103.4"
      fill="none" stroke="#d62728" stroke-width="1.6"/>
  <g stroke="#9c9" stroke-width="1">
    <line x1="130.3" y1="142.2" x2="123.9" y2="147.0"/>
    <line x1="152.0" y1="160.9" x2="148.2" y2="167.9"/>
    <line x1="179.6" y1="168.6" x2="179.2" y2="176.6"/>
    <line x1="207.9" y1="163.9" x2="210.8" y2="171.4"/>
    <line x1="231.5" y1="147.7" x2="237.3" y2="153.2"/>
    <line x1="246.0" y1="123.0" x2="253.6" y2="125.5"/>
  </g>
  <g stroke="#1f77b4" stroke-width="0.8" stroke-dasharray="2 3">
    <line x1="183.2" y1="102.7" x2="112.9" y2="155.3"/>
    <line x1="183.2" y1="102.7" x2="276.6" y2="132.9"/>
  </g>
  <g stroke="#999" stroke-width="1.6">
    <line x1="144.3" y1="141.5" x2="154.3" y2="151.5"/>
    <line x1="154.3" y1="141.5" x2="144.3" y2="151.5"/>
    <line x1="170.9" y1="115.2" x2="180.9" y2="125.2"/>
    <line x1="180.9" y1="115.2" x2="170.9" y2="125.2"/>
    <line x1="213.5" y1="100.4" x2="223.5" y2="110.4"/>
    <line x1="223.5" y1="100.4" x2="213.5" y2="110.4"/>
    <line x1="239.2" y1="121.2" x2="249.2" y2="131.2"/>
    <line x1="249.2" y1="121.2" x2="239.2" y2="131.2"/>
  </g>
  <circle cx="112.9" cy="155.3" r="4" fill="#1f77b4"/>
  <circle cx="276.6" cy="132.9" r="4" fill="#1f77b4"/>
  <circle cx="183.2" cy="102.7" r="5.5" fill="none" stroke="#d62728"
      stroke-width="1.5"/>
  <circle cx="183.2" cy="102.7" r="3.2" fill="#345"/>
  <g fill="#2ca02c">
    <circle cx="137.5" cy="155.8" r="3.5"/>
    <circle cx="164.3" cy="170.1" r="3.5"/>
    <circle cx="194.6" cy="171.8" r="3.5"/>
    <circle cx="222.8" cy="160.4" r="3.5"/>
    <circle cx="243.5" cy="138.2" r="3.5"/>
  </g>
  <g font-size="13" font-family="sans-serif">
    <text x="157.2" y="94.7" fill="#345">&#9312;</text>
    <text x="127.3" y="134.5" fill="#777">&#9313;</text>
    <text x="175.2" y="194.7" fill="#2ca02c">&#9314;</text>
  </g>
  <g font-size="10.5" font-family="sans-serif">
    <text x="8" y="16" fill="#666">placed nodes</text>
    <text x="6.0" y="173.3" fill="#1f77b4">nearest outside PDP</text>
    <text x="195.2" y="94.7" fill="#d62728">current lowest point</text>
    <text x="238.2" y="157.7" fill="#d62728">new front</text>
  </g>
</svg>
<figcaption>
Fornberg&ndash;Flyer, a snapshot from an actual run of the port.
Dark dots are placed nodes; the front advances downward.  &#9312; the lowest PDP of the old front (grey
dashed) is accepted as a node.  &#9313; the PDPs inside its
exclusion disc are removed.  &#9314; the angle between the
directions to the nearest outside PDPs (dotted rays) is divided
into five equal cells (ticks), and one point is placed at each
cell&rsquo;s centre &mdash; so the end gaps to the rays are half
the dot-to-dot gap, keeping new PDPs clear of the surviving
outside ones.  The spliced result is the new front (red).
</figcaption>
</figure>
<figure>
<svg width="460" height="250" viewBox="0 0 460 250">
  <g stroke="#f4f4f4" stroke-width="1">
    <line x1="0.0" y1="20" x2="0.0" y2="235"/>
    <line x1="10.0" y1="20" x2="10.0" y2="235"/>
    <line x1="20.0" y1="20" x2="20.0" y2="235"/>
    <line x1="30.0" y1="20" x2="30.0" y2="235"/>
    <line x1="40.0" y1="20" x2="40.0" y2="235"/>
    <line x1="50.0" y1="20" x2="50.0" y2="235"/>
    <line x1="60.0" y1="20" x2="60.0" y2="235"/>
    <line x1="70.0" y1="20" x2="70.0" y2="235"/>
    <line x1="80.0" y1="20" x2="80.0" y2="235"/>
    <line x1="90.0" y1="20" x2="90.0" y2="235"/>
    <line x1="100.0" y1="20" x2="100.0" y2="235"/>
    <line x1="110.0" y1="20" x2="110.0" y2="235"/>
    <line x1="120.0" y1="20" x2="120.0" y2="235"/>
    <line x1="130.0" y1="20" x2="130.0" y2="235"/>
    <line x1="140.0" y1="20" x2="140.0" y2="235"/>
    <line x1="150.0" y1="20" x2="150.0" y2="235"/>
    <line x1="160.0" y1="20" x2="160.0" y2="235"/>
    <line x1="170.0" y1="20" x2="170.0" y2="235"/>
    <line x1="180.0" y1="20" x2="180.0" y2="235"/>
    <line x1="190.0" y1="20" x2="190.0" y2="235"/>
    <line x1="200.0" y1="20" x2="200.0" y2="235"/>
    <line x1="210.0" y1="20" x2="210.0" y2="235"/>
    <line x1="220.0" y1="20" x2="220.0" y2="235"/>
    <line x1="230.0" y1="20" x2="230.0" y2="235"/>
    <line x1="240.0" y1="20" x2="240.0" y2="235"/>
    <line x1="250.0" y1="20" x2="250.0" y2="235"/>
    <line x1="260.0" y1="20" x2="260.0" y2="235"/>
    <line x1="270.0" y1="20" x2="270.0" y2="235"/>
    <line x1="280.0" y1="20" x2="280.0" y2="235"/>
    <line x1="290.0" y1="20" x2="290.0" y2="235"/>
    <line x1="300.0" y1="20" x2="300.0" y2="235"/>
    <line x1="310.0" y1="20" x2="310.0" y2="235"/>
    <line x1="320.0" y1="20" x2="320.0" y2="235"/>
    <line x1="330.0" y1="20" x2="330.0" y2="235"/>
    <line x1="340.0" y1="20" x2="340.0" y2="235"/>
    <line x1="350.0" y1="20" x2="350.0" y2="235"/>
    <line x1="360.0" y1="20" x2="360.0" y2="235"/>
    <line x1="370.0" y1="20" x2="370.0" y2="235"/>
    <line x1="380.0" y1="20" x2="380.0" y2="235"/>
    <line x1="390.0" y1="20" x2="390.0" y2="235"/>
    <line x1="400.0" y1="20" x2="400.0" y2="235"/>
    <line x1="410.0" y1="20" x2="410.0" y2="235"/>
    <line x1="420.0" y1="20" x2="420.0" y2="235"/>
    <line x1="430.0" y1="20" x2="430.0" y2="235"/>
    <line x1="440.0" y1="20" x2="440.0" y2="235"/>
    <line x1="450.0" y1="20" x2="450.0" y2="235"/>
    <line x1="460.0" y1="20" x2="460.0" y2="235"/>
  </g>
  <g fill="#345">
    <circle cx="225.0" cy="16.2" r="2.5"/>
    <circle cx="345.0" cy="16.9" r="2.5"/>
    <circle cx="425.0" cy="20.1" r="2.5"/>
    <circle cx="155.0" cy="20.2" r="2.5"/>
    <circle cx="25.0" cy="20.8" r="2.5"/>
    <circle cx="285.0" cy="52.9" r="2.5"/>
    <circle cx="95.0" cy="56.3" r="2.5"/>
    <circle cx="385.0" cy="77.6" r="2.5"/>
    <circle cx="195.0" cy="79.4" r="2.5"/>
    <circle cx="455.0" cy="83.4" r="2.5"/>
    <circle cx="5.0" cy="87.9" r="2.5"/>
    <circle cx="325.0" cy="113.6" r="2.5"/>
    <circle cx="135.0" cy="115.5" r="2.5"/>
  </g>
  <polyline points="0.0,157.9 10.0,157.9 10.0,157.2 20.0,157.2 20.0,155.0 30.0,155.0 30.0,151.1 40.0,151.1 40.0,145.3 50.0,145.3 50.0,136.9 60.0,136.9 60.0,123.9 70.0,123.9 70.0,151.5 80.0,151.5 80.0,164.4 90.0,164.4 90.0,172.9 100.0,172.9 100.0,178.7 110.0,178.7 110.0,182.5 120.0,182.5 120.0,184.7 130.0,184.7 130.0,185.5 140.0,185.5 140.0,184.7 150.0,184.7 150.0,182.5 160.0,182.5 160.0,178.7 170.0,178.7 170.0,172.9 180.0,172.9 180.0,164.4 190.0,164.4 190.0,151.5 200.0,151.5 200.0,148.7 210.0,148.7 210.0,146.5 220.0,146.5 220.0,142.6 230.0,142.6 230.0,136.8 240.0,136.8 240.0,128.4 250.0,128.4 250.0,116.2 260.0,116.2 260.0,149.7 270.0,149.7 270.0,162.6 280.0,162.6 280.0,171.1 290.0,171.1 290.0,176.9 300.0,176.9 300.0,180.7 310.0,180.7 310.0,182.9 320.0,182.9 320.0,183.6 330.0,183.6 330.0,182.9 340.0,182.9 340.0,180.7 350.0,180.7 350.0,176.9 360.0,176.9 360.0,171.1 370.0,171.1 370.0,162.6 380.0,162.6 380.0,149.7 390.0,149.7 390.0,146.8 400.0,146.8 400.0,144.6 410.0,144.6 410.0,140.8 420.0,140.8 420.0,146.6 430.0,146.6 430.0,150.4 440.0,150.4 440.0,152.6 450.0,152.6 450.0,153.4 460.0,153.4"
      fill="none" stroke="#bbb" stroke-width="1.2"
      stroke-dasharray="5 4"/>
  <circle cx="255.0" cy="116.2" r="70" fill="none" stroke="#999"
      stroke-dasharray="4 3"/>
  <polyline points="0.0,157.9 10.0,157.9 10.0,157.2 20.0,157.2 20.0,155.0 30.0,155.0 30.0,151.1 40.0,151.1 40.0,145.3 50.0,145.3 50.0,136.9 60.0,136.9 60.0,123.9 70.0,123.9 70.0,151.5 80.0,151.5 80.0,164.4 90.0,164.4 90.0,172.9 100.0,172.9 100.0,178.7 110.0,178.7 110.0,182.5 120.0,182.5 120.0,184.7 130.0,184.7 130.0,185.5 140.0,185.5 140.0,184.7 150.0,184.7 150.0,182.5 160.0,182.5 160.0,178.7 170.0,178.7 170.0,172.9 180.0,172.9 180.0,164.4 190.0,164.4 190.0,152.2 200.0,152.2 200.0,165.2 210.0,165.2 210.0,173.6 220.0,173.6 220.0,179.4 230.0,179.4 230.0,183.3 240.0,183.3 240.0,185.5 250.0,185.5 250.0,186.2 260.0,186.2 260.0,185.5 270.0,185.5 270.0,183.3 280.0,183.3 280.0,179.4 290.0,179.4 290.0,176.9 300.0,176.9 300.0,180.7 310.0,180.7 310.0,182.9 320.0,182.9 320.0,183.6 330.0,183.6 330.0,182.9 340.0,182.9 340.0,180.7 350.0,180.7 350.0,176.9 360.0,176.9 360.0,171.1 370.0,171.1 370.0,162.6 380.0,162.6 380.0,149.7 390.0,149.7 390.0,146.8 400.0,146.8 400.0,144.6 410.0,144.6 410.0,140.8 420.0,140.8 420.0,146.6 430.0,146.6 430.0,150.4 440.0,150.4 440.0,152.6 450.0,152.6 450.0,153.4 460.0,153.4"
      fill="none" stroke="#d62728" stroke-width="1.6"/>
  <circle cx="255.0" cy="116.2" r="5.5" fill="none" stroke="#d62728"
      stroke-width="1.5"/>
  <circle cx="255.0" cy="116.2" r="3.2" fill="#345"/>
  <g font-size="13" font-family="sans-serif">
    <text x="229.0" y="108.2" fill="#345">&#9312;</text>
    <text x="247.0" y="208.2" fill="#d62728">&#9313;</text>
  </g>
  <g font-size="10.5" font-family="sans-serif">
    <text x="158" y="143" fill="#666">old heights</text>
    <text x="296.0" y="224.2" fill="#d62728">new heights follow</text>
    <text x="296.0" y="237.2" fill="#d62728">the disc boundary</text>
    <text x="8" y="229" fill="#999">background columns</text>
  </g>
  <text x="267.0" y="108.2" fill="#d62728" font-size="10.5" font-family="sans-serif">lowest column</text>
</svg>
<figcaption>
Van der Sande&ndash;Fornberg.  The height field (one value per
background column) is the front.  &#9312; the node is placed at the
lowest column of the old heights (grey dashed).  &#9313; the
heights under its exclusion disc are raised to the disc&rsquo;s far
hemisphere, so the new heights (red) sample the disc boundary on
the columns; elsewhere the old heights already lie outside the disc
and are unchanged.  The data is a snapshot from an actual run of
the height-field port.  Later nodes sit on the height curve, hence at least
r away &mdash; for constant radius no node ever lands inside
another&rsquo;s disc.
</figcaption>
</figure>
</div>

<div class="gallery-references">
<p>References:</p>
<p>
Fornberg, B., &amp; Flyer, N. (2015).  Fast generation of 2-D node
distributions for mesh-free PDE discretizations.
<i>Computers &amp; Mathematics with Applications</i>, 69(7),
531&ndash;544.
<a href="https://doi.org/10.1016/j.camwa.2015.01.009">doi:10.1016/j.camwa.2015.01.009</a>
</p>
<p>
van der Sande, K., &amp; Fornberg, B. (2021).  Fast variable density
3-D node generation.  <i>SIAM Journal on Scientific Computing</i>,
43(1), A242&ndash;A257.
<a href="https://doi.org/10.1137/20M1337016">doi:10.1137/20M1337016</a>
</p>
</div>

<p>Inspired by the visualisations of
<a href="https://www.jasondavies.com/">Jason Davies</a>.</p>

<script>
(function() {
"use strict";

var chart = document.getElementById("chart"),
    canvas = document.createElement("canvas"),
    ctx;
chart.appendChild(canvas);

// The canvas fills its container (the full text column on a phone, up to
// 960 CSS px on a large screen) and is drawn at the device pixel ratio.
// Both generators scale with the box, so a narrower canvas gives the same
// picture with fewer pixels, not a cropped one.
var ratio, width, height,
    bbox;                           // xmin, xmax, ymin, ymax

function resize() {
  ratio = window.devicePixelRatio || 1;
  var cssWidth = Math.max(240, chart.clientWidth || 960);
  width = Math.round(cssWidth * ratio);
  height = Math.round(width * 500 / 960);
  canvas.width = width; canvas.height = height;
  ctx = canvas.getContext("2d");
  bbox = [0, width, 0, height];
}

var mSel = document.getElementById("method"),
    ninitSl = document.getElementById("ninit"),
    nangSl = document.getElementById("nang"),
    rfunSel = document.getElementById("rfun"),
    gradSl = document.getElementById("grad"),
    gfacSl = document.getElementById("gfac"),
    speedSl = document.getElementById("speed");

var gen = null;                     // active generator state

function radiusFactory() {
  var ninit = +ninitSl.value,
      r0 = (bbox[1] - bbox[0]) / ninit,
      g = +gradSl.value,
      kind = rfunSel.value;
  if (kind === "const")
    return function(x, y) { return r0; };
  if (kind === "grade")
    return function(x, y) {
      return r0 * (1 + g * (y - bbox[2]) / (bbox[3] - bbox[2]));
    };
  return function(x, y) {
    return r0 * (1 + 0.5 * g *
        Math.sin(6 * Math.PI * (x - bbox[0]) / (bbox[1] - bbox[0])));
  };
}

// ---------- Fornberg-Flyer 2015: node_placing ----------
function makeFF() {
  var ninit = +ninitSl.value,
      radius = radiusFactory(),
      nang = +nangSl.value,
      dx = (bbox[1] - bbox[0]) / ninit,
      fx = [], fy = [], dots = [];
  for (var i = 0; i < ninit; ++i) {
    fx.push(bbox[0] + dx * (i + 0.5));
    fy.push(bbox[2] + 0.5 * dx * Math.random());
  }
  function argminY() {              // first index of the minimum
    var im = 0;
    for (var i = 1; i < fy.length; ++i)
      if (fy[i] < fy[im]) im = i;
    return im;
  }
  return {
    dots: dots,
    frontSize: function() { return fx.length; },
    step: function() {              // place one dot; false when done
      if (fx.length === 0) return false;
      var im = argminY();
      var y0 = fy[im];
      if (y0 > bbox[3]) return false;
      var x0 = fx[im];
      dots.push([x0, y0]);
      var r = radius(x0, y0), r2 = r * r;

      var il = im - 1;              // walk outward: nearest outside
      while (il >= 0 &&
             (fx[il]-x0)*(fx[il]-x0) + (fy[il]-y0)*(fy[il]-y0) <= r2)
        --il;
      var angL = il < 0 ? Math.PI
                        : Math.atan2(fy[il]-y0, fx[il]-x0);
      var ir = im + 1;
      while (ir < fx.length &&
             (fx[ir]-x0)*(fx[ir]-x0) + (fy[ir]-y0)*(fy[ir]-y0) <= r2)
        ++ir;
      var angR = ir >= fx.length ? 0
                                 : Math.atan2(fy[ir]-y0, fx[ir]-x0);

      var nx = [], ny = [];
      for (var k = 1; k <= nang; ++k) {
        var frac = (k - 0.5) / nang,
            a = angL - frac * (angL - angR),
            px = x0 + r * Math.cos(a);
        if (px < bbox[0] || px > bbox[1]) continue;
        nx.push(px); ny.push(y0 + r * Math.sin(a));
      }
      // splice: [0..il] ++ arc ++ [ir..end]
      var args = [il + 1, ir - il - 1];
      fx.splice.apply(fx, args.concat(nx));
      fy.splice.apply(fy, args.concat(ny));
      return true;
    },
    drawFront: function() {
      if (fx.length === 0) return;
      ctx.strokeStyle = "#d62728";
      ctx.lineWidth = 1.0 * ratio;
      ctx.beginPath();
      ctx.moveTo(fx[0], fy[0]);
      for (var i = 1; i < fx.length; ++i) ctx.lineTo(fx[i], fy[i]);
      ctx.stroke();
      ctx.fillStyle = "#d62728";
      for (i = 0; i < fx.length; ++i) {
        ctx.beginPath();
        ctx.arc(fx[i], fy[i], 1.4 * ratio, 0, 2 * Math.PI);
        ctx.fill();
      }
    }
  };
}

// -- van der Sande-Fornberg 2021 (2-D): node_placing_heightfield --
function makeHF() {
  var ninit = +ninitSl.value,
      gf = +gfacSl.value,
      radius = radiusFactory(),
      ncol = gf * ninit,
      dx = (bbox[1] - bbox[0]) / ncol,
      gx = [], gy = [], lasty = [], lset = [], dots = [];
  for (var i = 0; i < ncol; ++i) {
    gx.push(bbox[0] + dx * (i + 0.5));
    gy.push(bbox[2] + radius(gx[i], bbox[2]) * Math.random());
    lasty.push(0); lset.push(false);
  }
  function argminY() {
    var im = 0;
    for (var i = 1; i < ncol; ++i)
      if (gy[i] < gy[im]) im = i;
    return im;
  }
  return {
    dots: dots,
    frontSize: function() { return ncol; },
    step: function() {
      var im = argminY();
      var ym = gy[im];
      if (ym > bbox[3]) return false;
      var x0 = gx[im], y0 = ym;

      // direction-dependence correction (section 2.3, first order)
      var r = radius(x0, y0), r2 = r * r,
          kw = Math.floor(r / dx),
          k0 = Math.max(0, im - kw),
          k1 = Math.min(ncol - 1, im + kw), k, rx;
      for (k = k0; k <= k1; ++k) {
        if (!lset[k]) continue;
        rx = (im - k) * dx;
        if (rx * rx + (y0 - lasty[k]) * (y0 - lasty[k]) < r2)
          y0 = Math.max(y0, lasty[k] + Math.sqrt(r2 - rx * rx));
      }
      dots.push([x0, y0]);
      lasty[im] = y0; lset[im] = true;

      // raise heights to the upper hemisphere of the exclusion disc
      r = radius(x0, y0); r2 = r * r;
      kw = Math.floor(r / dx);
      k0 = Math.max(0, im - kw);
      k1 = Math.min(ncol - 1, im + kw);
      for (k = k0; k <= k1; ++k) {
        rx = (im - k) * dx;
        gy[k] = Math.max(gy[k], y0 + Math.sqrt(r2 - rx * rx));
      }
      return true;
    },
    drawFront: function() {
      ctx.strokeStyle = "#d62728";
      ctx.lineWidth = 1.0 * ratio;
      ctx.beginPath();
      var y = Math.min(gy[0], height);
      ctx.moveTo(bbox[0], y);
      for (var i = 0; i < ncol; ++i) {
        y = Math.min(gy[i], height);
        ctx.lineTo(bbox[0] + dx * i, y);
        ctx.lineTo(bbox[0] + dx * (i + 1), y);
      }
      ctx.stroke();
    }
  };
}

function reset() {
  gen = mSel.value === "ff" ? makeFF() : makeHF();
  document.getElementById("ninitval").textContent = ninitSl.value;
  document.getElementById("gradval").textContent =
      (+gradSl.value).toFixed(1);
  document.getElementById("gfacval").textContent = gfacSl.value;
  document.getElementById("nangval").textContent = nangSl.value;
  gfacSl.disabled = mSel.value !== "hf";
  nangSl.disabled = mSel.value !== "ff";
  gradSl.disabled = rfunSel.value === "const";
}

function draw() {
  ctx.clearRect(0, 0, width, height);
  ctx.fillStyle = "#345";
  for (var i = 0; i < gen.dots.length; ++i) {
    ctx.beginPath();
    ctx.arc(gen.dots[i][0], gen.dots[i][1], 1.8 * ratio,
            0, 2 * Math.PI);
    ctx.fill();
  }
  gen.drawFront();
  document.getElementById("count").textContent = gen.dots.length;
  document.getElementById("fsize").textContent = gen.frontSize();
}

function frame() {
  var n = +speedSl.value;
  document.getElementById("speedval").textContent = n;
  for (var i = 0; i < n; ++i)
    if (!gen.step()) break;
  draw();
  requestAnimationFrame(frame);
}

[mSel, ninitSl, rfunSel, gradSl, gfacSl, nangSl].forEach(function(el) {
  el.addEventListener("change", reset);
  el.addEventListener("input", reset);
});
canvas.addEventListener("click", reset);
window.addEventListener("resize", function() {
  // restart when the layout width changes (rotation, window resize)
  var cssWidth = Math.max(240, chart.clientWidth || 960);
  if (Math.abs(cssWidth * (window.devicePixelRatio || 1) - width) > 2) {
    resize(); reset();
  }
});

resize();
reset();
frame();

})();
</script>
]]></content:encoded></item></channel></rss>