This post covers the JS used to animate the HTML elements.
ALL DAY LONG (7 part series)
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.
// ====================// 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.
// ====================// 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.
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.
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:
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
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
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
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%
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.
// 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
// ====================// 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.
// ====================// events// ====================window.addEventListener("load", render);Result
With that, the JS is done. Check the complete implementation in the next post.
settings
Franziska Volmer's All Day Long
// ====================// 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);