Esta postagem cobre o JS usado para animar os elementos HTML.
Detalhes
O JavaScript é responsável por construir a animação em tempo de execução. Ele realiza três tarefas principais:
- Calcular a geometria do caminho da fita
- Gerar os fragmentos dinamicamente
- Criar as animações que movem e giram cada fragmento
Constantes
A maioria dos valores foi determinada experimentalmente para corresponder às proporções da animação original.
// ====================// 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;Elementos
O elemento container é necessário para que seu transform possa ser definido e para adicionar os fragmentos.
// ====================// elements// ====================const elements = { container: document.querySelector(".container"),};Funções
A função render é o coração da animação.
Calculando valores
A fita se move ao longo de um caminho em forma de estádio.
Um estádio é uma forma geométrica bidimensional construída por um retângulo com semicírculos em um par de lados opostos. A mesma forma também é conhecida como pílula, discorectângulo, obround, pista de corrida ou corpo salsicha. - https://en.wikipedia.org/wiki/Stadium_(geometry)
O perímetro do estádio é igual à altura da imagem (800px). A partir desse valor podemos determinar quanto do caminho é composto de:
- segmentos retos
- curvas semicirculares
Essas proporções são usadas posteriormente para manter a velocidade da animação consistente ao longo de todo o caminho.
const semiCircleLength = Math.PI * RADIUS;const semiCircleProportion = semiCircleLength / PERIMETER;const lineLength = PERIMETER / 2 - semiCircleLength;const lineProportion = lineLength / PERIMETER;Em seguida, calculo a quantidade de fragmentos e o atraso com base no perímetro e na altura do fragmento. Todos os fragmentos têm a mesma animação, mas com um atraso diferente, para que sejam distribuídos ao longo do caminho.
Ao deslocar o tempo de início de cada fragmento, os fragmentos se distribuem uniformemente ao longo do caminho, produzindo a ilusão de uma fita em movimento contínuo.
const fragmentCount = Math.floor(PERIMETER / FRAGMENT_HEIGHT);const delayStep = DURATION / fragmentCount;Definindo as propriedades do container
Com o comprimento da linha calculado, pude definir as propriedades do container:
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)`;Definindo keyframes
Do primeiro ao segundo keyframe
- O fragmento começa no topo do semicírculo esquerdo, depois se move até o fundo dele
- O offset vai de 0% até a proporção de um semicírculo
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, },];Do segundo ao terceiro keyframe
- O fragmento começa na esquerda da linha inferior, depois se move para a direita dela
- O offset vai da proporção de um semicírculo até as proporções de semicírculo + linha
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, },];Do terceiro ao quarto keyframe
- O fragmento começa no fundo do semicírculo direito, depois se move até o topo dele
- O offset vai das proporções de semicírculo + linha até as proporções de semicírculo + linha + semicírculo
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, },];Do quarto ao último keyframe
- O fragmento começa na direita da linha superior, depois se move para a esquerda dela
- O offset vai das proporções de semicírculo + linha + semicírculo até 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, },];Definindo os fragmentos
Em seguida, pude criar todos os fragmentos com um loop.
// cria um fragmento DOM offscreen para adicionar todos os fragmentos depois do loopconst domFragment = document.createDocumentFragment();for (let i = 0; i < fragmentCount; i++) { const fragmentWrapper = document.createElement("div"); const fragment = document.createElement("div"); // define as propriedades de `.fragment-wrapper` fragmentWrapper.classList.add("fragment-wrapper"); fragmentWrapper.style.width = `${FRAGMENT_WIDTH}px`; fragmentWrapper.style.height = `${FRAGMENT_HEIGHT}px`; // define as propriedades de `.fragment` fragment.classList.add("fragment"); fragment.style.width = `${FRAGMENT_WIDTH}px`; fragment.style.height = `${FRAGMENT_HEIGHT}px`; // posiciona o fragmento em 3D fragment.style.transform = `translateZ(${RADIUS}px)`; // posiciona a imagem de fundo para cada fragmento fragment.style.backgroundPositionY = `${i * FRAGMENT_HEIGHT}px`; fragmentWrapper.append(fragment); domFragment.append(fragmentWrapper); // inicia a animação fragmentWrapper.animate(keyframes, { duration: DURATION, // define o atraso para cada fragmento delay: -i * delayStep, iterations: Infinity, });}// concatena todos os fragmentos no container de uma vezelements.container.append(domFragment);Função render completa
// ====================// 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);}Eventos
A função render é executada quando a janela é carregada.
// ====================// events// ====================window.addEventListener("load", render);Resultado
Com isso, o JS está pronto. Confira a implementação completa na próxima postagem.
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);