Usando animações orientadas por rolagem para direções de rolagem opostas ✣ S.Bistrović


Às vezes, os designers têm ideias bobas que eventualmente crescem em você. Isso aconteceu comigo com esse conceito em que tive que construir colunas de itens movendo-se em direções opostas quando um usuário rola a página.

Esta demonstração respeita as configurações de movimento reduzido, então você precisará ativar o movimento para ver o efeito. E estamos analisando o suporte para Chrome e Safari enquanto escrevo isto.

Na verdade, não é tão difícil quanto você imagina, graças aos recursos CSS modernos, especificamente animações orientadas por rolagem. Não só isso, mas também é divertido de fazer! Deixe-me mostrar como abordei isso – e talvez você queira compartilhar como faria isso de forma diferente.

O HTML

O HTML consiste em um elemento pai (.opposing-columns), seus filhos (.opposing-column), e os filhos de seus filhos (.opposing-item):

div class="opposing-columns">
  
  div class="opposing-column">
    div class="opposing-item">...div>
    div class="opposing-item">...div>
    div class="opposing-item">...div>
  div>
  
  div class="opposing-column">
    div class="opposing-item">...div>
    div class="opposing-item">...div>
    div class="opposing-item">...div>
  div>
  
  div class="opposing-column">
    div class="opposing-item">...div>
    div class="opposing-item">...div>
    div class="opposing-item">...div>
  div>
div>

Isso é tudo que precisamos na marcação. CSS fará o resto!

Estilizando o contêiner pai

Primeiro, vamos configurar as coisas para que esse efeito se aplique apenas a telas maiores — não faz sentido suportar algo assim em telas menores porque precisamos de espaço adicional para o efeito.


@media screen and (width >= 40rem) {
  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 40rem);
    margin-inline: auto;
  }
}

Configurando um efeito de “máscara”

Precisamos fazer mais algumas coisas com o contêiner pai para ter a ilusão de que os itens em cada .opposing-column estão desaparecendo à medida que passam por ele. Os itens nas colunas externas movem-se para cima na rolagem e os itens na coluna central movem-se para baixo. À medida que ultrapassam os limites dos pais, queremos que desapareçam.

Então, vamos fazer algumas coisas. Primeiro, definiremos uma variável de cor de fundo no documento como um todo:

@media screen and (width >= 40rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    
  }
}

Segundo, aplicaremos a mesma cor de fundo no pai :before e :after pseudo-elementos:

@media screen and (width >= 40rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    

    &:before,
    &:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}

Observe que estabelecemos um contexto de empilhamento nos pseudos e os colocamos uma camada acima do pai e de seus descendentes. Esta é a chave para mascarar os itens em cada coluna à medida que eles entram e saem do contêiner. Os itens estão tecnicamente deslizantes sob as pseudomáscaras.

Falando nisso, vamos criar outra variável chamada --opposing-mask que adiciona espaço vertical entre o elemento pai e as três colunas:

@media screen and (width >= 40rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 40rem);
    margin-inline: auto;
    margin-block: var(--opposing-mask, 3rem);
    position: relative;
  }
}

Destacando o espaço vertical entre o contêiner pai e seus elementos filhos.

Vamos fazer o mesmo com os pseudos dos pais, aplicando apenas --opposing-mask para o seu block-size por um múltiplo de três. Dessa forma, há espaço vertical adicional entre eles e o pai.

@media screen and (width >= 40rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    

    &:before,
    &:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}

Destacando o espaço vertical entre o contêiner pai e seu pseudoelemento anterior.

Você pode ver onde isso vai dar. Temos uma boa quantidade de espaço entre o contêiner pai e seus pseudos. Queremos que os itens da coluna apareçam como se estivessem desaparecendo à medida que rolam para fora do contêiner pai. Não precisamos mexer com a opacidade deles ou algo parecido. Em vez disso, podemos adicionar gradientes de fundo nos pseudos.

O :before pseudo está na parte superior do contêiner, então daremos a ele um gradiente que vai de uma cor sólida que corresponde à cor de fundo subjacente do documento até transparente, de cima para baixo. E desde o :after pseudo fica na parte inferior do contêiner pai, inverteremos o gradiente para que fique transparente para a cor de fundo do documento, de baixo para cima.

@media screen and (width >= 40rem) {
  :root {
    
  }

  .opposing-columns {
    

    &:before,
    &:after {
      
    }

    &:before {
      background-image: linear-gradient(
        to bottom,
        var(--opposing-bg) var(--opposing-mask),
        transparent
      );
      inset-block-start: calc(var(--opposing-mask) * -1);
    }

    &:after {
      background-image: linear-gradient(
        to top,
        var(--opposing-bg) var(--opposing-mask),
        transparent
      );
      inset-block-end: calc(var(--opposing-mask) * -1);
    }
  }
}

Os layouts das colunas

Antes de chegarmos à mágica, devemos organizar os itens em cada coluna. Cada coluna é um item flexível dentro do pai, que é um contêiner flexível. Vamos deixá-los encolher (flex-shrink: 1) e crescer (flex-grow: 1), limitando o tamanho em um determinado ponto (flex-basis: 10rem).

Podemos definir tudo isso com o flex propriedade abreviada:

@media screen and (width >= 40rem) {
  

  .opposing-column {
    flex: 1 1 10rem;
  }
}

Agora quero que essas colunas sejam contêineres de grade para que eu possa usar o gap propriedade para inserir espaço entre os itens:

@media screen and (width >= 40rem) {
  

  .opposing-column {
    flex: 1 1 10rem;
    display: grid;
    gap: 2rem;
  }
}

Poderíamos ter usado o Flexbox aqui também para ter acesso a gapmas o layout padrão está definido como row e teríamos que substituir isso para column. Grid é um pouco mais conciso nesta situação.

A animação!

Foi para isso que você veio, certo? Configuramos tudo para que os itens da coluna possam entrar e sair do contêiner pai na rolagem. Agora precisamos adicionar esse comportamento de rolagem.

É aqui que animation-timeline propriedade é muito útil. Normalmente, uma animação CSS roda sozinha. Ele começa quando a página carrega (ou após um atraso específico definido) e termina após o tempo que você define a duração. Com animation-timelinedizemos que a animação deve ser executada com base na posição de rolagem… daí o termo animação “orientada por rolagem”.

Temos duas funções suportadas aqui, scroll() e view(). Eles estão relacionados, mas são super diferentes nisso scroll() executa a animação com base na rolagem de um elemento posição. O view() A função é semelhante, mas rastreia o progresso do elemento conforme ele entra e sai do scrollport (ou seja, a área rolável do contêiner em que ele está).

Nós vamos com o view() função porque configuramos isso onde há uma área de rolagem clara dentro do contêiner pai. Precisamos executar a animação com base em onde ela entra e sai daquela área, e não na posição de rolagem dos itens da coluna.

Isso é muito interessante porque podemos dizer view() exatamente onde queremos que a animação comece, uma vez entra a área de rolagem e onde parar quando saídas essa mesma área. Assim:


animation-timeline: view((  || 'view-timeline-inset'> )?);

Vamos começar definindo os eixos:

@media screen and (width >= 40rem) {
  

  .opposing-column {
    
    animation-timeline: view();
    animation-range: entry cover;
  }
}

Isso é apenas parcialmente o que queremos, mas o que estamos dizendo é que queremos que a animação (1) comece no exato momento em que entra no scrollport (entry), e (2) termina quando sai completamente da área (cover). Precisamos ser explicitamente sobre as inserções porque é isso que estabelece o alcance da animação em relação ao local onde ela entra e sai. Queremos a gama completa, por isso o entry começa em 0% e a saída é quando um item é coberto em 100%.

@media screen and (width >= 40rem) {
  

  .opposing-column {
    
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}

Por último, definiremos a animação para ser executada linearmente – sem necessidade de os itens ficarem mais lentos ou mais lentos à medida que rolam.

@media screen and (width >= 40rem) {
  

  .opposing-column {
    
    animation-timing-function: linear;
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}

OK, ótimo. Mas o que não fizemos foi criar uma animação. Configuramos o que queremos que ele faça quando for executado, mas precisamos definir o movimento real.

Quero configurar três animações CSS separadas:

  1. Aquele que traduz (move) os itens para cima na primeira coluna.
  2. Aquele que é o inverso da primeira animação para os itens da outra coluna.

Poderíamos tecnicamente definir a primeira animação em ambas as colunas externas, mas quero uma terceira que seja um pouco deslocada da primeira para que essas colunas pareçam escalonadas.

@keyframes scroll1 {
  from { transform: translateY(var(--opposing-mask)); }
  to { transform: translateY(calc(var(--opposing-mask) * -1)); }
}

@keyframes scroll2 {
  from { transform: translateY(calc(var(--opposing-mask) * -1)); }
  to { transform: translateY(var(--opposing-mask)); }
}

@keyframes scroll3 {
  from { transform: translateY(calc(var(--opposing-mask) * .66)); }
  to { transform: translateY(calc(var(--opposing-mask) * -.33)); }
}

Podemos criar variáveis ​​para estes, é claro, caso precisemos atualizá-los:

@media screen and (width >= 40rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    --animation-1: scroll1;
    --animation-2: scroll2;
    --animation-3: scroll3;

    
  }
}

…e aplique-os a cada coluna:

@media screen and (width >= 40rem) {
  

  .opposing-column {
    
  }

  :where(.opposing-column:nth-of-type(1)) {
    animation-name: var(--animation-1);
  }

  :where(.opposing-column:nth-of-type(2)) {
    animation-name: var(--animation-2);
  }

  :where(.opposing-column:nth-of-type(3)) {
    animation-name: var(--animation-3);
  }
}

Enquanto estamos nisso, devemos desabilitar as animações para respeitar as configurações do usuário para movimento reduzido (e remover a máscara, caso contrário pode parecer estranho):

@media (prefers-reduced-motion: reduce) {
  .opposing-column {
    animation: unset;

    &:before,
    &:after {
      content: unset;
    }
  }
}

Concluindo

Então, sim, animações baseadas em rolagem são muito, muito legais. Ainda estamos aguardando o suporte do Firefox enquanto escrevo isto, mas você certamente pode encerrar isso @supports para fornecer uma experiência padrão que usa as anotações de rolagem e, em seguida, definir uma experiência alternativa para navegadores sem suporte, como executar em uma linha do tempo de animação normal:

@supports (animation-timeline: view()) {
  
}

Isso é apenas uma visão geral do que as animações baseadas em rolagem podem fazer, é claro. Que tipo de coisas você fez ou experimentou? Ou você abordaria este de forma diferente? Avise!



Source link

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *

Botão Voltar ao Topo