<!--
LUME ✨👉 https://github.com/lume/lume
-->
<script src="https://unpkg.com/[email protected]/dist/global.js"></script>

<!--
And Tween.js: https://github.com/tweenjs/tween.js
-->
<script src="https://unpkg.com/[email protected]"></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%
  );
}
{
  LUME.defineElements();

  const log = console.log.bind(console);

  const { Motor, Scene, Node } = LUME;
  const { Tween, Easing } = TWEEN;

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

  async function rippleFlip() {
    const scene = new Scene();

    document.body.append(scene);

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

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

    scene.append(grid);

    await sleep(500);

    const rippleOptions = {
      // ripple center
      cx: grid.calculatedSize.x / 2,
      cy: grid.calculatedSize.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.calculatedSize.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().set({
          size: [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.append(img);

        grid.append(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
    }
  ) {
    log("start ripple!");
    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) => {
      console.log("ripple task");
      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
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.