<figure class="ma0 pa0 vh-100 flex flex-wrap justify-center items-center"
  style="background-color: #A9D7FD;">
  <svg
    viewBox="0 0 400 400"
    class="db w5 h5 center"
    fill="none"
    stroke="#2F3030"
    stroke-linejoin="round"
    stroke-linecap="round"
    stroke-width="8">
    <path
      id="reuleaux-fill"
      transform="scale(1.05) translate(-6 -10)"
      stroke="none"
      fill="#ffffff"
      d=""
    />
    <path
      id="reuleaux-polygon"
      d=""
    />
  </svg>
  <figcaption class="clip">Morphing Reuleaux polygons</figcaption>
</figure>
const { interpolate } = flubber;
const polygonStroke = document.querySelector('#reuleaux-polygon');
const polygonFill = document.querySelector('#reuleaux-fill');

const polygons = [3, 5, 7, 9]
  .map(count => [
    regularPolygon([200, 200], 180, count),
    reuleauxPolygon([200, 200], 180, count),
  ])
  .reduce((acc, v) => acc.concat(v), []);
const colors = ['#fff', '#fff', '#ED705C', '#ED705C', '#337EED', '#337EED', '#ffb700', '#ffb700'];

let idx = 1;
const next = () => {
  idx = idx < polygons.length - 1 ? idx + 1 : 0;
}

const randomColor = (not) => {
  const p = colors[Math.floor(Math.random() * colors.length)];
  return p === not ? randomColor(not) : p;
};

let fromColor = '#fff';
let toColor = randomColor(fromColor);
let interpolator = flubber.interpolate(polygons[0], polygons[idx]);

const time = { t: 0 };

const animate = () => 
  anime({
    targets: [polygonFill, time],
    t: [0, 1],
    fill: colors[idx],
    duration: 900,
    easing: 'easeOutQuint',
    delay: 600,
    update: anim => {
      const d = interpolator(time.t);
      polygonStroke.setAttribute('d', d);
      polygonFill.setAttribute('d', d);
    },
    complete: (anim) => { 
      next();
      interpolator = flubber
        .interpolate(
          polygons[idx === 0 ? polygons.length - 1 : idx - 1], 
          polygons[idx],
        );
      fromColor = toColor;
      toColor = randomColor(fromColor);
      animate();
    }
  });

animate();

/**
 * Polygon Math
 */
function pts(sideCount, radius) {
  const angle = 360 / sideCount;
  const vertexIndices = range(sideCount);
  const offsetDeg = 90 - (180 - angle) / 2;
  const offset = degreesToRadians(offsetDeg);

  return vertexIndices.map(index => {
    return {
      theta: offset + degreesToRadians(angle * index),
      r: radius,
    };
  });
}

function range(count) {
  return Array.from(Array(count).keys());
}

function degreesToRadians(angleInDegrees) {
  return Math.PI * angleInDegrees / 180;
}

function polygon([cx, cy], radius, sideCount) {
  return pts(sideCount, radius).map(({ r, theta }) => [
    cx + r * Math.cos(theta),
    cy + r * Math.sin(theta),
  ]);
}

function dist([x1, y1], [x2, y2]) {
  return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
}

function reuleauxPolygon([cx, cy], radius, sideCount) {
  const pts = polygon([cx, cy], radius, sideCount);
  const l = dist(pts[0], pts[2]);
  const [o, ...rest] = pts;

  return [
    'M',
    o[0],
    o[1],
    ...rest.map(p => `A ${l} ${l} 0 0 1 ${p[0]} ${p[1]}`),
    `A ${l} ${l} 0 0 1 ${o[0]} ${o[1]}`,
    'Z',
  ].join(' ');
}

function regularPolygon([cx, cy], sideCount, radius) {
  const [o, ...rest] = polygon([cx, cy], sideCount, radius);
    return [
    'M',
    o[0],
    o[1],
    ...rest.map(p => `L ${p[0]} ${p[1]}`),
    'Z',
  ].join(' ');
}
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tachyons/4.9.1/tachyons.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.js
  2. https://unpkg.com/flubber@0.3.0