Skip to content

Jiggly Shapes, flubber ​

Nested shapes that jiggle, change colors and morph into another shape every two seconds. flubber interpolates between the d3-shape symbols, d3-ease gives the elastic motion, and Charming draws each frame as paths. After Paul Buffa's Jiggly Map.

js
const size = 640;
const radii = d3.range(150, 4, -15);
const random = d3.randomUniform(-25, 25);
const schemes = [
  "Viridis",
  "Inferno",
  "Magma",
  "Plasma",
  "Warm",
  "Cool",
  "Rainbow",
  "Blues",
  "Greens",
  "Oranges",
  "Purples",
  "Reds",
  "PuRd",
  "YlGnBu",
];
const symbols = [d3.symbolCircle, d3.symbolSquare, d3.symbolDiamond2, d3.symbolCross, d3.symbolStar, d3.symbolWye];
const color = (scheme) => d3.scaleSequential([150, 0], d3[`interpolate${scheme}`]);
const shape = (symbol, r) => d3.symbol(symbol, r * r * 2)();
const clamp = (t) => Math.max(0, Math.min(1, t));
const renderer = cm.canvas({width: size, height: size});
const root = cm.flow().renderer(renderer);
let from = {dx: 0, dy: 0, color: color("Viridis"), symbol: d3.symbolCircle};
let to = from;
let morphs = radii.map((r) => () => shape(from.symbol, r));
let start = 0;

function jiggle(time) {
  from = to;
  to = {
    dx: random(),
    dy: random(),
    color: color(schemes[d3.randomInt(schemes.length)()]),
    symbol: symbols[d3.randomInt(symbols.length)()],
  };
  morphs = radii.map((r) => flubber.interpolate(shape(from.symbol, r), shape(to.symbol, r)));
  start = time;
}

requestAnimationFrame(function frame(time) {
  if (time - start > 2000) jiggle(time);
  const elapsed = time - start;
  root.append(cm.clear, {fill: "white"});
  root.data(radii).append(cm.path, {
    x: (_, i) => size / 2 + i * d3.interpolate(from.dx, to.dx)(d3.easeElastic(clamp((elapsed - i * 20) / 1000))),
    y: (_, i) => size / 2 + i * d3.interpolate(from.dy, to.dy)(d3.easeElastic(clamp((elapsed - i * 20) / 1000))),
    d: (_, i) => morphs[i](clamp((elapsed - 500 - i * 25) / 500)),
    fill: (r, i) => d3.interpolateRgb(from.color(r), to.color(r))(clamp((elapsed - i * 25) / 500)),
    stroke: "black",
    strokeWidth: 0.5,
  });

  root.render();
  requestAnimationFrame(frame);
});

document.body.append(renderer.node());