<!--
Made with LUME: https://github.com/lume/lume
(when it was called Infamous)
-->
<script src="https://unpkg.com/infamous@17.0.5/global.js"></script>

<!--
And Tween.js: https://github.com/tweenjs/tween.js
-->
<script src="https://unpkg.com/tween.js@16.6.0"></script>
html,
body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;

  background: radial-gradient(
    circle,
    rgb(23, 132, 252) 0%,
    rgb(0, 0, 198) 43.67%,
    rgb(13, 9, 98) 100%
  );
}
const { Motor, Scene, Node } = infamous.core;
const { Tween, Easing } = TWEEN;

const sleep = (duration) => new Promise((r) => setTimeout(r, duration));

async function rippleFlip() {
  const scene = new Scene();
  scene.mount("body");

  const gridSizeX = 16;
  const gridSizeY = 16;
  const gridCellSize = 100;

  const grid = new Node({
    absoluteSize: [gridSizeX * gridCellSize, gridSizeY * gridCellSize],
    align: [0.5, 0.5],
    mountPoint: [0.5, 0.5],
    rotation: [30],
    position: { z: -600 }
  });

  scene.addChild(grid);

  await grid.mountPromise;
  await sleep(500);

  console.log("grid size", grid.actualSize);

  const rippleOptions = {
    // ripple center
    cx: grid.actualSize.x / 2,
    cy: grid.actualSize.y / 2,

    amountToRotate: 180,
    rotationDuration: 1600,
    rotationCurve: Easing.Bounce.Out,

    amountToDisplace: 200,
    displaceDuration: 1600,
    displaceCurve: Easing.Exponential.Out,

    amountToOpacify: 1,
    opacifyDuration: 2400,
    opacifyCurve: Easing.Exponential.Out,

    rippleDistance: grid.actualSize.x / 2,
    rippleDuration: 1000,
    rippleCurve: Easing.Linear.None,

    rotation: true,
    displacement: true,
    opacification: true
  };

  // make a grid of rectangles
  for (let i = 0; i < gridSizeX; i++) {
    for (let j = 0; j < gridSizeY; j++) {
      const node = new Node({
        absoluteSize: [gridCellSize, gridCellSize],
        position: [i * gridCellSize, j * gridCellSize],
        opacity: 0
      });

      node.opacity = 0;

      const img = document.createElement("img");
      img.src = "https://assets.codepen.io/191583/blue-gradient-square.svg";
      img.style.display = "block";
      img.style.width = "100%";
      img.style.height = "100%";
      node.element.append(img);

      grid.addChild(node);
    }
  }

  await sleep(500);

  while (true) {
    await ripple(grid, rippleOptions);
    await sleep(1000);
  }
}

function ripple(
  grid,
  {
    cx,
    cy,
    amountToRotate,
    rotationDuration,
    rotationCurve,
    amountToDisplace,
    displaceDuration,
    displaceCurve,
    amountToOpacify,
    opacifyDuration,
    opacifyCurve,
    rippleDistance,
    rippleDuration,
    rippleCurve,
    rotation,
    displacement,
    opacification
  }
) {
  let resolve = null;
  const promise = new Promise((r) => (resolve = r));

  let radiusTweenComplete = false;
  const radius = { value: 0 };
  const radiusTween = new Tween(radius)
    .to({ value: rippleDistance }, rippleDuration)
    .easing(rippleCurve)
    .onComplete(() => (radiusTweenComplete = true))
    .start();

  Motor.addRenderTask((time) => {
    radiusTween.update(time);

    for (let i = 0, l = grid.children.length; i < l; i += 1) {
      const node = grid.children[i];

      if (node.animating) continue;

      if (!node.distanceFromCircle) {
        const dx = cx - (node.position.x + 50);
        const dy = cy - (node.position.y + 50);
        const distanceToCircleCenter = Math.sqrt(dx ** 2 + dy ** 2);
        node.initialDistanceFromCircle = distanceToCircleCenter - radius.value;
        node.distanceFromCircle = node.initialDistanceFromCircle;
      } else {
        node.distanceFromCircle = node.initialDistanceFromCircle - radius.value;
      }

      if (node.distanceFromCircle <= 0) {
        node.animating = true;

        if (rotation)
          rotateNode(node, amountToRotate, rotationDuration, rotationCurve);
        if (displacement)
          displaceNode(node, amountToDisplace, displaceDuration, displaceCurve);
        if (opacification)
          opacifyNode(node, amountToOpacify, opacifyDuration, opacifyCurve);
      }
    }

    if (radiusTweenComplete) {
      const children = grid.children;
      for (let i = 0, l = children.length; i < l; i += 1) {
        children[i].animating = false;
      }
      resolve();
      return false;
    }
  });

  return promise;
}

function rotateNode(node, finalValue, duration, curve) {
  let resolve = null;
  const promise = new Promise((r) => (resolve = r));

  let tweenDone = false;

  const rotationTween = new Tween(node.rotation)
    .to({ y: "+180" }, duration)
    .easing(curve)
    .onComplete(() => (tweenDone = true))
    .start();

  Motor.addRenderTask((time) => {
    rotationTween.update(time);
    if (tweenDone) {
      resolve();
      return false;
    }
  });

  return promise;
}

function displaceNode(node, amount, duration, curve) {
  let resolve = null;
  const promise = new Promise((r) => (resolve = r));

  const displace = { value: 0 };
  let tweenDone = false;

  const displacementTween = new Tween(displace)
    .to({ value: Math.PI }, duration)
    .easing(curve)
    .onComplete(() => (tweenDone = true))
    .start();

  Motor.addRenderTask((time) => {
    displacementTween.update(time);

    node.position.z = amount * Math.sin(displace.value);

    if (tweenDone) {
      resolve();
      return false;
    }
  });

  return promise;
}

function opacifyNode(node, amount, duration, curve) {
  let resolve = null;
  const promise = new Promise((r) => (resolve = r));

  const opacify = { value: 0 };
  let tweenDone = false;

  const opacifyTween = new Tween(opacify)
    .to({ value: Math.PI }, duration)
    .easing(curve)
    .onComplete(() => (tweenDone = true))
    .start();

  Motor.addRenderTask((time) => {
    opacifyTween.update(time);

    node.opacity = amount * Math.sin(opacify.value);

    if (tweenDone) {
      resolve();
      return false;
    }
  });

  return promise;
}

rippleFlip();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.