<div id="container">
  <canvas></canvas>
</div>
html, body { height: 100%; }
body {
  margin: 0;
  display: grid;
  justify-items: center;
  align-items: center;
}
#container {
  width: 100vmin;
  height: 100vmin;
}
/////////////////////////
// Mostly helper stuff //
/////////////////////////

const container = document.querySelector("#container");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

// Set in our resize function
let width, height, minX, maxX, minY, maxY, cellDiff, halfGridItemThickness, numColors, gridItems, parallelograms;

// Init some settings to use
const settings = {
  gridRows: 9,
  gridDotsPerCol: 5,
  clearColor: "#e0e0e0",
  gridItemColor: "#9e9e9e",
  gridItemThickness: 1,
  numParallelograms: 150,
  parallelogramColors: ["#fc705b", "#212f42", "#3f5c86"],
  shadowColor: "#443f3c",
};

// Alternative theme from https://twitter.com/MAKIO135/status/1404488303102005250
// settings.clearColor = "#dfede0";
// settings.parallelogramColors = ["#dfede0", "#201f1e", "#f5ce4e"];

// Alternative theme from https://twitter.com/MAKIO135/status/1404397957810688013
// settings.clearColor = "#7be4e1";
// settings.parallelogramColors = ["#44c0d5", "#162c9b", "#fefac1"];
// settings.shadowColor = "#0f2034";


numColors = settings.parallelogramColors.length;
halfGridItemThickness = settings.gridItemThickness / 2;

// Some helper functions
const wrap01 = gsap.utils.wrap(0, 1);
const progressEase = CustomEase.create("custom", "M0,0,C0,0,0.3,0.5,0.5,0.5,0.7,0.5,1,1,1,1");
const normalizeUpperHalf = gsap.utils.normalize(0.5, 1);

// An animation that drives the progress of all other animations
const overallProgress = { val: 0 };
const progressAnim = gsap.to(overallProgress, {
  val: 1,
  repeat: -1,
  ease: "none",
  duration: 2
});

// An animation that rotates the pluses in the background grid
const rot = { val: 0 };
const rotAnim = gsap.to(rot, {
  val: Math.PI / 2,
  duration: 1,
  repeat: -1,
  repeatDelay: 3,
  ease: "none"
});


// Creates our grid and parallelograms with the proper sizing
const resize = () => {
  gridItems = [];
  parallelograms = [];
  
  width = height = canvas.width = canvas.height = container.offsetWidth;
  cellDiff = width / (settings.gridRows * (settings.gridDotsPerCol - 1));
  
  minX = minY = width * 0.1;
  maxX = maxY = width * 0.9;
  
  createGridItems();
  createParallelograms();
};

// Throttle the resize event
let resizeTimeout;
const handleResize = () => {
  if(resizeTimeout) resizeTimeout.kill();
  
  resizeTimeout = gsap.delayedCall(0.3, resize);
};


////////////////////////////
// The "meat" of the code //
////////////////////////////

// Create the grid background
const createGridItems = () => {
  for(let row = 0; row < settings.gridRows * (settings.gridDotsPerCol - 1); row++) {
    for(let col = 0; col < settings.gridRows * (settings.gridDotsPerCol - 1); col++) {
      // const index = col * settings.gridRows + row;
      const gridItem = {
        x: col * cellDiff + cellDiff / 2,
        y: row * cellDiff + cellDiff / 2,
        type: col % settings.gridDotsPerCol === 0 && row % settings.gridDotsPerCol === 0 ? "plus" : "dot"
      };
      gridItems.push(gridItem);
    }
  }
};

const getRestOfValues = (parallelogram) => {
  parallelogram.distanceX = parallelogram.horizDir * parallelogram.length;
  parallelogram.distanceY = parallelogram.vertDir * parallelogram.length;
  parallelogram.endX = parallelogram.startX + parallelogram.distanceX;
  parallelogram.endY = parallelogram.startY + parallelogram.distanceY;
  parallelogram.mirrorStartX = width - parallelogram.startX - parallelogram.thickness;
  parallelogram.mirrorEndX = parallelogram.mirrorStartX - parallelogram.distanceX;
};

// Create the foreground parallelograms
const createParallelograms = () => {
  for(let i = 0; i < settings.numParallelograms; i++) {
    // Randomly generate some parameters to use
    const parallelogram = {
      color: settings.parallelogramColors[i % numColors],
      startX: gsap.utils.random(0, width, 1),
      startY: gsap.utils.random(0, height, 1),
      horizDir: Math.random() < 0.5 ? 1 : -1, // Determines the angle of the p-gram
      vertDir: Math.random() < 0.5 ? 1 : -1, // Determines the way the p-gram "grows"
      thickness: gsap.utils.random(10, 50, 1),
      length: gsap.utils.random(width / 10, width / 2),
      progress: Math.random(),
      shadowOffset: gsap.utils.random(2, 10, 1)
    };
    
    getRestOfValues(parallelogram);
    
    // Cheap attempt to improve the choices of the horizontal and vertical directions
    let hasChanged = false;
    if(parallelogram.endX > maxX || parallelogram.endX < minX) {
      parallelogram.horizDir *= -1;
      hasChanged = true;
    }
    if(parallelogram.endY > maxY || parallelogram.endY < minY) {
      parallelogram.vertDir *= -1;
      hasChanged = true;
    }
    
    // Recalculate if necessary
    if(hasChanged) getRestOfValues(parallelogram);
    
    
    parallelograms.push(parallelogram);
  }
};

// Draws the given grid item based on its info
const drawGridItem = (item) => {
  if(item.type === "dot") {
    ctx.fillRect(item.x - halfGridItemThickness, item.y - halfGridItemThickness, settings.gridItemThickness, settings.gridItemThickness);
  } else if(item.type === "plus") {
    // Rotate and draw if need be - if nothing to rotate, avoid to save on work
    if(rot.val) {
      ctx.save();
      ctx.translate(item.x, item.y);
      ctx.rotate(rot.val);
      
      ctx.fillRect(-settings.gridItemThickness * 3, -halfGridItemThickness, settings.gridItemThickness * 6, settings.gridItemThickness);
      ctx.fillRect(-halfGridItemThickness, -settings.gridItemThickness * 3, settings.gridItemThickness, settings.gridItemThickness * 6);
      
      ctx.restore();
    } 
    
    // Just draw the plus
    else {
      ctx.fillRect(item.x - settings.gridItemThickness * 2, item.y - halfGridItemThickness, settings.gridItemThickness * 4, settings.gridItemThickness);
      ctx.fillRect(item.x - halfGridItemThickness, item.y - settings.gridItemThickness * 2, settings.gridItemThickness, settings.gridItemThickness * 4);
    }
  }
};

// Draws the given parallelogram based on its info
const drawParallelogram = (parallelogram) => {
  // Get the progress values between 0 and 1
  let currProgress = progressEase(wrap01(overallProgress.val + parallelogram.progress));
  
  let startX, startY, endX, endY, mirrorStartX, mirrorEndX;
  
  // Handle the "first half" of the animation ("growing")
  if(currProgress < 0.5) {
    currProgress *= 2;

    startX = parallelogram.startX;
    startY = parallelogram.startY;
    endX = parallelogram.startX + parallelogram.distanceX * currProgress;
    endY = parallelogram.startY + parallelogram.distanceY * currProgress;
    mirrorStartX = parallelogram.mirrorStartX;
    mirrorEndX = parallelogram.mirrorStartX - parallelogram.distanceX * currProgress;
  } 
  
  // Handle the "second half" of the animation ("shrinking")
  else {
    currProgress = 1 - normalizeUpperHalf(currProgress);
    
    startX = parallelogram.endX;
    startY = parallelogram.endY;
    endX = startX - parallelogram.distanceX * currProgress;
    endY = startY - parallelogram.distanceY * currProgress;
    mirrorStartX = parallelogram.mirrorEndX;
    mirrorEndX = mirrorStartX + parallelogram.distanceX * currProgress;
  }
  
  // Prevent duplicate calculations
  const shadowStartY = startY + parallelogram.shadowOffset;
  const shadowEndY = endY + parallelogram.shadowOffset;
  const startXPlusThickness = startX + parallelogram.thickness;
  const endXPlusThickness = endX + parallelogram.thickness;
  const mirrorStartXPlusThickness = mirrorStartX + parallelogram.thickness;
  const mirrorEndXPlusThickness = mirrorEndX + parallelogram.thickness;
  
  
  // Draw the shadows first
  ctx.fillStyle = settings.shadowColor;
  
  // First shadow
  ctx.beginPath();
  ctx.moveTo(startX, shadowStartY);
  ctx.lineTo(startXPlusThickness, shadowStartY);
  ctx.lineTo(endXPlusThickness, shadowEndY);
  ctx.lineTo(endX, shadowEndY);
  ctx.closePath();
  ctx.fill();

  // Second shadow
  ctx.beginPath();
  ctx.moveTo(mirrorStartX, shadowStartY);
  ctx.lineTo(mirrorStartXPlusThickness, shadowStartY);
  ctx.lineTo(mirrorEndXPlusThickness, shadowEndY);
  ctx.lineTo(mirrorEndX, shadowEndY);
  ctx.closePath();
  ctx.fill();

  
  // Then draw the coloreds
  ctx.fillStyle = parallelogram.color;

  // First colored
  ctx.beginPath();
  ctx.moveTo(startX, startY);
  ctx.lineTo(startXPlusThickness, startY);
  ctx.lineTo(endXPlusThickness, endY);
  ctx.lineTo(endX, endY);
  ctx.closePath();
  ctx.fill();

  // Second colored
  ctx.beginPath();
  ctx.moveTo(mirrorStartX, startY);
  ctx.lineTo(mirrorStartXPlusThickness, startY);
  ctx.lineTo(mirrorEndXPlusThickness, endY);
  ctx.lineTo(mirrorEndX, endY);
  ctx.closePath();
  ctx.fill();
};

// Handle all of our drawing
const draw = () => {
  // Clear
  ctx.fillStyle = settings.clearColor;
  ctx.fillRect(0, 0, width, height);
  
  // Draw background grid
  ctx.fillStyle = settings.gridItemColor; // They're all the same color so we set it here
  gridItems.forEach(drawGridItem);
  
  // Draw the parallelograms
  parallelograms.forEach(drawParallelogram);
};


/////////////////////
// Kick things off //
/////////////////////
const init = () => {
  document.body.style.backgroundColor = settings.clearColor;
  
  resize(); // Also creates our items
  
  // Listen for future resizes
  window.addEventListener("resize", handleResize);
  
  update(); // Start the drawing process
};

// Our update loop
const update = () => {
  draw();
  requestAnimationFrame(update);
};

init();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js
  2. https://assets.codepen.io/16327/CustomEase3.min.js