A short conclusion.
ALL DAY LONG (7 part series)
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
settings
A coded recreation of
Franziska Volmer's All Day Long
Franziska Volmer's All Day Long
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:
rotateX()CSS functionrotateY()CSS functionrotateZ()CSS functiontranslateX()CSS functiontranslateY()CSS functiontranslateZ()CSS functionbackface-visibilityCSS propertytransform-styleCSS propertybackground-position-yCSS property
JS APIs used in this implementation: