Pular para o conteúdo principal

ALL DAY LONG parte 5: JS

· Leitura de 8 minutos

Esta postagem cobre o JS usado para animar os elementos HTML.


ALL DAY LONG (série de 7 partes)
  1. Introdução
  2. Conceito
  3. Imagens
  4. HTML
  5. CSS
  6. JS
  7. Conclusão

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.

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;

Elementos

O elemento container é necessário para que seu transform possa ser definido e para adicionar os fragmentos.

script.js
// ====================// 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.

script.js
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.

script.js
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:

script.js
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

script.js
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

script.js
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

script.js
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%

script.js
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.

script.js
// 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

script.js
// ====================// 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.

script.js
// ====================// events// ====================window.addEventListener("load", render);

Resultado

Com isso, o JS está pronto. Confira a implementação completa na próxima postagem.


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

Leitura recomendada {leitura-recomendada}