block by HarryStevens 6c4ee7e57df79fbd3137aad5de289cf6

Voronoi Jiggle

Full Screen

D3’s general update pattern with a Voronoi diagram & rapidly updating data.

index.html

<html>
  <head>
    <style>
    body {
      margin: 0;
    }
    .control {
      position: absolute;
      right: 5px;
      top: 5px;
    }
    .control .label {
      font-family: sans-serif;
      text-align: center;
    }
    .control input {
      display: inline-block;
      vertical-align: middle;
    }
    .control .value {
      display: inline-block;
      font-family: monospace;
      vertical-align: middle;
    }
    .voronoi {
      stroke: black;
    }
    </style>
  </head>
  <body>
    
    <div class="control">
      <div class="label">Jiggle</div>
      <div class="range">
        <input type="range" min="0.1" step="0.1" max="9.9" value="1.5">
        <div class="value"></div>
      </div>
    </div>

    <div class="jiggle"></div>

    <script src="https://unpkg.com/flubber@0.3.0"></script>
    <script type="module">
    import { Delaunay } from "https://cdn.skypack.dev/d3-delaunay@6";
    import { interpolateLab } from "https://cdn.skypack.dev/d3-interpolate@3";
    import { polygonArea } from "https://cdn.skypack.dev/d3-polygon@3";
    import { randomUniform } from "https://cdn.skypack.dev/d3-random@3";
    import { scaleLinear, scaleSequential } from "https://cdn.skypack.dev/d3-scale@4";
    import { select } from "https://cdn.skypack.dev/d3-selection@3";
    import { interval } from "https://cdn.skypack.dev/d3-timer@3";
    import { transition } from "https://cdn.skypack.dev/d3-transition@3";

    const { abs, max, min } = Math;

    const width = window.innerWidth;
    const height = window.innerHeight;
    const duration = 12; // milliseconds of transition duration
    const radius = 5; // circle radius
    const x = randomUniform(radius, width - radius);
    const y = randomUniform(radius, height - radius);
    
    const data = Array.from({ length: 64 })
        .map((_, id) => ({ id, x: x(), y: y() }));

    const color = scaleSequential()
        .domain([0, width * height / data.length * 2])
        .interpolator(interpolatePalette(["#fc8d62", "#ffd92f", "#66c2a5", "#8da0cb"]));

    const input = document.querySelector(".control input");
    const value = document.querySelector(".control .value");

    const svg = select(".jiggle").append("svg")
        .attr("width", width)
        .attr("height", height);

    function redraw(data){
      const voronoi = Array.from(
        Delaunay
          .from(data.map(({x, y}) => [x, y]))
          .voronoi([0, 0, width, height])
          .cellPolygons(),
        (p, i) => Object.assign(p, { data: data[i] })
      );

      // transition
      const t = transition()
          .duration(duration);

      // JOIN
      const polygon = svg.selectAll(".voronoi")
          .data(voronoi, d => d.data.id);

      const circle = svg.selectAll(".dot")
          .data(data, d => d.id);

      // UPDATE
      polygon
        .transition(t)
          .attrTween("d", (d, i, e) => {
            const prev = select(e[i]).attr("d");
            const curr = `M${d.join("L")}Z`;
            return flubber.interpolate(prev, curr);
          })
          .style("fill", d => color(abs(polygonArea(d))));

      circle
        .transition(t)
          .attr("cx", d => d.x)
          .attr("cy", d => d.y);

      // ENTER
      polygon.enter().append("path")
          .attr("class", "voronoi")
          .attr("d", d => `M${d.join("L")}Z`)
          .style("fill", d => color(abs(polygonArea(d))));

      circle.enter().append("circle")
          .attr("class", "dot")
          .attr("r", 5)
          .attr("cx", d => d.x)
          .attr("cy", d => d.y);
    }

    redraw(data);

    interval(() => {
      const jiggle = +input.value;
      const random = randomUniform(-jiggle, jiggle);

      value.innerHTML = jiggle.toFixed(1);

      data.forEach(d => {
        d.x = min(width - radius, max(radius, d.x + random()));
        d.y = min(height - radius, max(radius, d.y + random()));

        return d;
      });

      redraw(data);
    }, duration * 2);

    // https://observablehq.com/@harrystevens/roll-your-own-color-palette-interpolator
    function interpolatePalette(palette){
      const domain = [0];
      for (let i = 1, l = palette.length - 1; i <= l; i++){
        domain[i] = i / l;
      }
      
      const scale = scaleLinear(domain, palette)
          .interpolate(interpolateLab)
          .clamp(true);
      
      return t => scale(t);
    }
    </script>
  </body>
</html>