Uma breve conclusão.
Conclusão
Esta implementação demonstra que animações 3D podem ser alcançadas com elementos HTML e estilos CSS, sem usar de canvas ou WebGL.
Ideias principais por trás da animação:
- Decompor a imagem em pequenos fragmentos
- Posicionar os fragmentos ao longo de um caminho em forma de estádio
- Animar os fragmentos ao longo desse caminho
- Usar pseudo-elementos para representar a face oposta do fragmento
Confira uma versão interativa no CodePen.
Resultado
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"> <!-- fragmentos são adicionados pelo o 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, e transform são aplicados pelo o script */}.fragment-wrapper { position: absolute; transform-style: preserve-3d; /* width, height, e animação são aplicados pelo o script */ /* a animação (e sua duração, atraso, e iteração) também é aplicada pelo o 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, e background-position-y são aplicados pelo o 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; /* essas propriedades são herdadas do `.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);Imagens
front.png
back.png
Leitura recomendada
Funções e propriedades CSS usadas nesta implementação:
rotateX()CSS functionrotateY()CSS functionrotateZ()CSS functiontranslateX()CSS functiontranslateY()CSS functiontranslateZ()CSS functionbackface-visibilityCSS propertytransform-styleCSS propertybackground-position-yCSS property
APIs JS usadas nesta implementação: