Skip to main content

ALL DAY LONG conclusion: 3D animation with HTML, CSS, and JS

· 4 min read

A short conclusion.


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

Conclusion

This implementation demonstrates that complex motion paths and flipping effects can be achieved with standard DOM elements and CSS transforms, without relying on canvas or WebGL.

Key ideas behind the animation:

  • Decompose the ribbon into small fragments
  • Arrange fragments along a stadium-shaped path
  • Animate the fragment wrappers along that path
  • Use pseudo-elements to represent the ribbon's back face

Check an interactive version in CodePen.

Result



HTML

index.html
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <link rel="stylesheet" href="./style.css" />    <title>All Day Long</title>  </head>  <body>    <div class="wrapper">      <div class="container">        <!-- fragments are added by the script -->      </div>    </div>    <script src="./script.js"></script>  </body></html>

CSS

style.css
*,*::before,*::after {  margin: 0;  padding: 0;  box-sizing: border-box;}html {  background-color: #3b7cf3;}.wrapper {  min-height: 100dvh;  display: grid;  place-content: center;}.container {  transform-style: preserve-3d;  position: relative;  /* width, height, and transform are applied by the script */}.fragment-wrapper {  position: absolute;  transform-style: preserve-3d;  /* width, height, and animation are applied by the script */  /* animation (and its duration, delay, and iterations) is also applied by the script */}.fragment {  position: absolute;  transform-style: preserve-3d;  background-image: url("/img/all-day-long/front.png");  background-size: 100% auto;  backface-visibility: hidden;  /* width, height, transform, and background position are applied by the script */}.fragment::after {  content: "";  position: absolute;  top: 0;  left: 0;  transform: rotateY(180deg);  background-image: url("/img/all-day-long/back.png");  background-size: 100% auto;  backface-visibility: hidden;  /* these properties are inherited from `.fragment` */  width: inherit;  height: inherit;  background-position-y: inherit;}

JS

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);

Images

front.png



back.png

Recommended reading

CSS functions and properties used in this implementation:

JS APIs used in this implementation: