Skip to main content

ALL DAY LONG part 5: JS

· 7 min read

This post covers the JS used to animate the HTML elements.


ALL DAY LONG (7 part series)
  1. Introduction
  2. Concept
  3. Images
  4. HTML
  5. CSS
  6. JS
  7. Conclusion

Details

The JavaScript is responsible for constructing the animation at runtime. It performs three main tasks:

  • Calculating the geometry of the ribbon path
  • Generating the fragments dynamically
  • Creating the animations that move and flip each fragment

Constants

Most values were determined experimentally to match the proportions of the original animation.

script.js
// ====================// constants// ====================const IMAGE_WIDTH = 200;const IMAGE_HEIGHT = 800;const FRAGMENT_WIDTH = IMAGE_WIDTH;const FRAGMENT_HEIGHT = 10;const PERIMETER = IMAGE_HEIGHT;const RADIUS = 36;const ROTATE_X = 56;const ROTATE_Y = 20;const ROTATE_Z = 310;const DURATION = 6_000;

Elements

The container element is required so its transform can be set, and to append the fragments.

script.js
// ====================// elements// ====================const elements = {  container: document.querySelector(".container"),};

Functions

The render function is the heart of the animation.

Calculating values

The ribbon moves along a stadium-shaped path.

A stadium is a two-dimensional geometric shape constructed of a rectangle with semicircles at a pair of opposite sides. The same shape is known also as a pill shape, discorectangle, obround, racetrack or sausage body. - https://en.wikipedia.org/wiki/Stadium_(geometry)

The stadium perimeter is equal to the image height (800px). From that value we can determine how much of the path is composed of:

  • straight segments
  • semicircular curves

These proportions are later used to keep the animation speed consistent across the entire path.

script.js
const semiCircleLength = Math.PI * RADIUS;const semiCircleProportion = semiCircleLength / PERIMETER;const lineLength = PERIMETER / 2 - semiCircleLength;const lineProportion = lineLength / PERIMETER;

Then, I calculate the fragment count and delay step based on the perimeter and fragment height. All fragments have the same animation, but with a different delay, so they are distributed across the path.

By offsetting each fragment's start time, the fragments distribute themselves evenly along the path, producing the illusion of a continuous moving ribbon.

script.js
const fragmentCount = Math.floor(PERIMETER / FRAGMENT_HEIGHT);const delayStep = DURATION / fragmentCount;

Setting the container's properties

With the line length calculated, I could set the container's properties:

script.js
elements.container.style.width = `${IMAGE_WIDTH}px`;elements.container.style.height = `${lineLength}px`;elements.container.style.transform =  `rotateX(${ROTATE_X}deg) ` +  `rotateY(${ROTATE_Y}deg) ` +  `rotateZ(${ROTATE_Z}deg)`;

Setting keyframes

First to second keyframe

  • The fragment starts at the top of the left semicircle, then moves to the bottom of it
  • The offset goes from 0% to one semicircle proportion

script.js
const keyframes = [  {    transform: `translateY(0px) rotateX(0deg)`,    offset: 0,  },  {    transform: `translateY(0px) rotateX(180deg)`,    offset: semiCircleProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(180deg)`,    offset: semiCircleProportion + lineProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(360deg)`,    offset: semiCircleProportion + lineProportion + semiCircleProportion,  },  {    transform: `translateY(0px) rotateX(360deg)`,    offset: 1,  },];

Second to third keyframe

  • The fragment starts at the left of the bottom line, then moves to the right of it
  • The offset goes from one semicircle proportion to semicircle + line proportions

script.js
const keyframes = [  {    transform: `translateY(0px) rotateX(0deg)`,    offset: 0,  },  {    transform: `translateY(0px) rotateX(180deg)`,    offset: semiCircleProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(180deg)`,    offset: semiCircleProportion + lineProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(360deg)`,    offset: semiCircleProportion + lineProportion + semiCircleProportion,  },  {    transform: `translateY(0px) rotateX(360deg)`,    offset: 1,  },];

Third to fourth keyframe

  • The fragment starts at the bottom of the right semicircle, then moves to the top of it
  • The offset goes from semicircle + line proportions to semicircle + line + semicircle proportions

script.js
const keyframes = [  {    transform: `translateY(0px) rotateX(0deg)`,    offset: 0,  },  {    transform: `translateY(0px) rotateX(180deg)`,    offset: semiCircleProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(180deg)`,    offset: semiCircleProportion + lineProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(360deg)`,    offset: semiCircleProportion + lineProportion + semiCircleProportion,  },  {    transform: `translateY(0px) rotateX(360deg)`,    offset: 1,  },];

Fourth to last keyframe

  • The fragment starts at the right of the top line, then moves to the left of it
  • The offset goes from semicircle + line + semicircle proportions to 100%

script.js
const keyframes = [  {    transform: `translateY(0px) rotateX(0deg)`,    offset: 0,  },  {    transform: `translateY(0px) rotateX(180deg)`,    offset: semiCircleProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(180deg)`,    offset: semiCircleProportion + lineProportion,  },  {    transform: `translateY(${lineLength}px) rotateX(360deg)`,    offset: semiCircleProportion + lineProportion + semiCircleProportion,  },  {    transform: `translateY(0px) rotateX(360deg)`,    offset: 1,  },];

Setting fragments

Then I could create all the fragments with a loop.

script.js
// creates an offscreen DOM fragment to append all the fragments after the loopconst domFragment = document.createDocumentFragment();for (let i = 0; i < fragmentCount; i++) {  const fragmentWrapper = document.createElement("div");  const fragment = document.createElement("div");  // set the `.fragment-wrapper` properties  fragmentWrapper.classList.add("fragment-wrapper");  fragmentWrapper.style.width = `${FRAGMENT_WIDTH}px`;  fragmentWrapper.style.height = `${FRAGMENT_HEIGHT}px`;  // set the `.fragment` properties  fragment.classList.add("fragment");  fragment.style.width = `${FRAGMENT_WIDTH}px`;  fragment.style.height = `${FRAGMENT_HEIGHT}px`;  // positions the fragment in 3D  fragment.style.transform = `translateZ(${RADIUS}px)`;  // positions the background image for each fragment  fragment.style.backgroundPositionY = `${i * FRAGMENT_HEIGHT}px`;  fragmentWrapper.append(fragment);  domFragment.append(fragmentWrapper);  // starts the animation  fragmentWrapper.animate(keyframes, {    duration: DURATION,    // set the delay for each fragment    delay: -i * delayStep,    iterations: Infinity,  });}// append all fragments to the container at onceelements.container.append(domFragment);

Full render function

script.js
// ====================// functions// ====================function render() {  const semiCircleLength = Math.PI * RADIUS;  const semiCircleProportion = semiCircleLength / PERIMETER;  const lineLength = PERIMETER / 2 - semiCircleLength;  const lineProportion = lineLength / PERIMETER;  const fragmentCount = Math.floor(PERIMETER / FRAGMENT_HEIGHT);  const delayStep = DURATION / fragmentCount;  const keyframes = [    {      transform: `translateY(0px) rotateX(0deg)`,      offset: 0,    },    {      transform: `translateY(0px) rotateX(180deg)`,      offset: semiCircleProportion,    },    {      transform: `translateY(${lineLength}px) rotateX(180deg)`,      offset: semiCircleProportion + lineProportion,    },    {      transform: `translateY(${lineLength}px) rotateX(360deg)`,      offset: semiCircleProportion + lineProportion + semiCircleProportion,    },    {      transform: `translateY(0px) rotateX(360deg)`,      offset: 1,    },  ];  elements.container.style.width = `${IMAGE_WIDTH}px`;  elements.container.style.height = `${lineLength}px`;  elements.container.style.transform =    `rotateX(${ROTATE_X}deg) ` +    `rotateY(${ROTATE_Y}deg) ` +    `rotateZ(${ROTATE_Z}deg)`;  const domFragment = document.createDocumentFragment();  for (let i = 0; i < fragmentCount; i++) {    const fragmentWrapper = document.createElement("div");    const fragment = document.createElement("div");    fragmentWrapper.classList.add("fragment-wrapper");    fragmentWrapper.style.width = `${FRAGMENT_WIDTH}px`;    fragmentWrapper.style.height = `${FRAGMENT_HEIGHT}px`;    fragment.classList.add("fragment");    fragment.style.width = `${FRAGMENT_WIDTH}px`;    fragment.style.height = `${FRAGMENT_HEIGHT}px`;    fragment.style.transform = `translateZ(${RADIUS}px)`;    fragment.style.backgroundPositionY = `${i * FRAGMENT_HEIGHT}px`;    fragmentWrapper.append(fragment);    domFragment.append(fragmentWrapper);    fragmentWrapper.animate(keyframes, {      duration: DURATION,      delay: -i * delayStep,      iterations: Infinity,    });  }  elements.container.append(domFragment);}

Events

The render function is run when the window is loaded.

script.js
// ====================// events// ====================window.addEventListener("load", render);

Result

With that, the JS is done. Check the complete implementation in the next post.


script.js
// ====================// constants// ====================const IMAGE_WIDTH = 200;const IMAGE_HEIGHT = 800;const FRAGMENT_WIDTH = IMAGE_WIDTH;const FRAGMENT_HEIGHT = 10;const PERIMETER = IMAGE_HEIGHT;const RADIUS = 36;const ROTATE_X = 56;const ROTATE_Y = 20;const ROTATE_Z = 310;const DURATION = 6_000;// ====================// elements// ====================const elements = {  container: document.querySelector(".container"),};// ====================// functions// ====================function render() {  const semiCircleLength = Math.PI * RADIUS;  const semiCircleProportion = semiCircleLength / PERIMETER;  const lineLength = PERIMETER / 2 - semiCircleLength;  const lineProportion = lineLength / PERIMETER;  const fragmentCount = Math.floor(PERIMETER / FRAGMENT_HEIGHT);  const delayStep = DURATION / fragmentCount;  const keyframes = [    {      transform: `translateY(0px) rotateX(0deg)`,      offset: 0,    },    {      transform: `translateY(0px) rotateX(180deg)`,      offset: semiCircleProportion,    },    {      transform: `translateY(${lineLength}px) rotateX(180deg)`,      offset: semiCircleProportion + lineProportion,    },    {      transform: `translateY(${lineLength}px) rotateX(360deg)`,      offset: semiCircleProportion + lineProportion + semiCircleProportion,    },    {      transform: `translateY(0px) rotateX(360deg)`,      offset: 1,    },  ];  elements.container.style.width = `${IMAGE_WIDTH}px`;  elements.container.style.height = `${lineLength}px`;  elements.container.style.transform =    `rotateX(${ROTATE_X}deg) ` +    `rotateY(${ROTATE_Y}deg) ` +    `rotateZ(${ROTATE_Z}deg)`;  const domFragment = document.createDocumentFragment();  for (let i = 0; i < fragmentCount; i++) {    const fragmentWrapper = document.createElement("div");    const fragment = document.createElement("div");    fragmentWrapper.classList.add("fragment-wrapper");    fragmentWrapper.style.width = `${FRAGMENT_WIDTH}px`;    fragmentWrapper.style.height = `${FRAGMENT_HEIGHT}px`;    fragment.classList.add("fragment");    fragment.style.width = `${FRAGMENT_WIDTH}px`;    fragment.style.height = `${FRAGMENT_HEIGHT}px`;    fragment.style.transform = `translateZ(${RADIUS}px)`;    fragment.style.backgroundPositionY = `${i * FRAGMENT_HEIGHT}px`;    fragmentWrapper.append(fragment);    domFragment.append(fragmentWrapper);    fragmentWrapper.animate(keyframes, {      duration: DURATION,      delay: -i * delayStep,      iterations: Infinity,    });  }  elements.container.append(domFragment);}// ====================// events// ====================window.addEventListener("load", render);

Recommended reading