Análise detalhada de CSS: matrix3d() para ter uma barra de rolagem personalizada com perfeição

As barras de rolagem personalizadas são extremamente raras. Isso se deve principalmente ao fato de que as barras de rolagem são um dos bits restantes na Web que são praticamente não estilizável (Estou olhando para você, seletor de datas). Você pode usar o JavaScript para criar o seu próprio, mas isso é caro, barato a fidelidade e podem gerar lentidão. Neste artigo, vamos usar matrizes CSS não convencionais para criar um botão de rolagem personalizado que não JavaScript durante a rolagem, apenas um pouco de código de configuração.

Texto longo, leia o resumo

Você não se importa com as pequenas coisas? Você só quer conferir Demonstração do gato Nyan e acessar a biblioteca? Você pode encontrar o código de demonstração na nossa Repositório do GitHub.

LAM;WRA (longo e matemático; vai ser lido mesmo assim)

Há algum tempo, construímos um botão de rolagem de paralaxe (você leu aquele artigo? É muito bom e vale a pena! Movendo os elementos de volta usando o CSS 3D transformações, os elementos se moveram mais lentamente do que a velocidade real de rolagem.

Recapitulação

Vamos começar recapitulando como o botão de rolagem de paralaxe funcionava.

Como mostrado na animação, alcançamos o efeito paralaxe ao empurrar os elementos "para trás" no espaço 3D, ao longo do eixo Z. Rolar um documento é efetivamente uma tradução ao longo do eixo Y. Então, se rolarmos para baixo em 100 px, cada será traduzido para cima em 100 px. Isso se aplica a todos os elementos, mesmo os que estão “mais atrás”. Mas porque eles estão mais longe câmera, o movimento observado na tela será inferior a 100 px, produzindo o efeito de paralaxe desejado.

É claro que mover um elemento de volta para o espaço também o fará parecer menor, corrigindo, redimensionando o elemento novamente. Descobrimos os cálculos exatos quando criamos rolador de paralaxe, para não repetir todos os detalhes.

Etapa 0: o que queremos fazer?

Barras de rolagem. É isso que vamos criar. Mas você já pensou sobre o que ela faz? Eu com certeza não fiz isso. As barras de rolagem indicam quanto do conteúdo disponível está visível no momento e quanto progresso você, como o leitor, fez. Se você rolar para baixo, o mesmo acontece com a barra de rolagem indicam que você está progredindo para o final. Se todo o conteúdo se encaixar na janela de visualização, a barra de rolagem geralmente fica escondida. Se o conteúdo tiver o dobro da altura da janela de visualização, o a barra de rolagem preenche 1⁄2 da altura da janela de visualização. Conteúdo que vale três vezes a altura a janela de visualização dimensiona a barra de rolagem para 1⁄3 da janela etc. Você vê o padrão. Em vez de rolar, você também pode clicar e arrastar a barra de rolagem para passar o site com mais rapidez. É uma quantidade surpreendente de comportamento para uma um elemento desse tipo. Vamos lutar uma batalha de cada vez.

Etapa 1: ao contrário

Certo, podemos fazer com que os elementos se movam mais lentamente do que a velocidade de rolagem com CSS 3D. conforme descrito no artigo sobre rolagem de paralaxe. Também podemos reverter a direção? Acontece que sim, e essa é a nossa maneira de construir uma uma barra de rolagem personalizada perfeita. Para entender como isso funciona, precisamos abordar um alguns fundamentos de CSS 3D primeiro.

Para ter qualquer tipo de projeção de perspectiva no sentido matemático, você provavelmente acabar usando coordenadas homogêneas. Não vou entrar em detalhes sobre o que são e por que funcionam, mas você pode pensar com base em coordenadas 3D com uma quarta coordenada adicional, chamada w. Isso coordenada deve ser 1, exceto se você quiser distorção de perspectiva. Qa não precisa se preocupar com os detalhes de w, pois não usaremos nenhum diferente de 1. Portanto, a partir de agora, todos os pontos são vetores de quatro dimensões. [x, y, z, w=1] e, consequentemente, as matrizes precisam também seja 4x4.

Uma ocasião em que você pode ver que o CSS usa coordenadas homogêneas sob o elemento Capô é quando você define suas próprias matrizes 4x4 em uma propriedade de transformação usando o função matrix3d(). matrix3d usa 16 argumentos (porque a matriz é 4 x 4), especificando uma coluna após a outra. Então, podemos usar essa função para especificar manualmente rotações, conversões etc. Mas isso também nos permite fazer está mexendo na coordenada w!

Antes de usarmos matrix3d(), precisamos de um contexto 3D, porque sem uma no contexto 3D, não haveria distorção de perspectiva e não seria necessário coordenadas homogêneas. Para criar um contexto 3D, precisamos de um contêiner com perspective e alguns elementos que podemos transformar na nova criou espaço 3D. Para exemplo:

Um código CSS que distorce um div usando o
    atributo de perspectiva.

Os elementos dentro de um contêiner de perspectiva são processados pelo mecanismo CSS da seguinte forma:

  • Transformar cada canto (vértice) de um elemento em coordenadas homogêneas [x,y,z,w], em relação ao contêiner de perspectiva.
  • Aplique todas as transformações do elemento como matrizes da direita para a esquerda.
  • Se o elemento de perspectiva for rolável, aplique uma matriz de rolagem.
  • Aplique a matriz de perspectiva.

A matriz de rolagem é uma translação ao longo do eixo Y. Se rolarmos para baixo 400 px, todos os elementos precisam ser movidos por 400 px. A matriz de perspectiva é uma matriz que "puxa" os pontos mais próximos do ponto de fuga em 3D espaço em que estão. Isso alcança os dois efeitos de fazer as coisas parecerem menores quando estão mais distantes e também faz com que “avancem mais devagar” durante a tradução. Portanto, se um elemento for empurrado para trás, uma translação de 400px fará com que o elemento para mover somente 300px na tela.

Se quiser saber todos os detalhes, leia a spec sobre a política mas, para este artigo, simplificamos a algoritmo acima.

Nossa caixa está dentro de um contêiner de perspectiva com valor p para perspective. e vamos supor que o contêiner é rolável e é rolado para baixo n pixels.

Matriz de perspectiva vezes a matriz de rolagem vezes a matriz de transformação do elemento
  é igual à matriz de identidade de quatro por quatro com menos um sobre p na quarta linha
  terceira coluna vezes matriz de identidade de quatro por quatro com menos n na segunda
  linha quarta coluna vezes a matriz de transformação do elemento.

A primeira matriz é a matriz de perspectiva, a segunda é a matriz de rolagem, matricial. Recapitulando: a função da matriz de rolagem é fazer com que um elemento se mova para cima quando rolam para baixo, daí o sinal negativo.

No entanto, para a barra de rolagem, queremos o oposto, ou seja, o elemento que mover para baixo quando estivermos rolando para baixo. Aqui está onde podemos usar um truque: Invertendo a coordenada w dos cantos do box. Se a coordenada w for -1, todas as traduções entrarão em vigor na direção oposta. Então, como fazemos isso? O mecanismo CSS cuida de converter os cantos da caixa em coordenadas homogêneas e define w para 1. É hora de matrix3d() brilhar!

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Essa matriz não fará nada mais que negar w. Portanto, quando o mecanismo CSS tem transformou cada canto em um vetor no formato [x,y,z,1], a matriz e converteremos em [x,y,z,-1].

Matriz de identidade quatro por quatro com menos um sobre p na quarta linha
  terceira coluna vezes matriz de identidade de quatro por quatro com menos n na segunda
  linha quarta coluna vezes a matriz de identidade de quatro por quatro com menos um
  quarta linha quarta coluna vezes o vetor de quatro dimensões x, y, z, 1 é igual a quatro
  por quatro matrizes de identidade com menos um sobre p na terceira coluna da quarta linha,
  menos n na quarta coluna da segunda linha e menos um na quarta linha
  a quarta coluna é igual ao vetor de quatro dimensões x, y mais n, z, menos z sobre
  p menos 1.

Listei uma etapa intermediária para mostrar o efeito da transformação do elemento matricial. Se não estiver confortável com matemática matricial, tudo bem. Eureka é que, na última linha, acabamos adicionando o deslocamento de rolagem n ao nosso valor y coordenada em vez de subtraí-la. O elemento será convertido para baixo se rolarmos para baixo.

No entanto, se colocarmos essa matriz em nosso exemplo, o elemento não será exibido. Isso ocorre porque a especificação do CSS exige que vértice com w < O valor 0 impede que o elemento seja renderizado. E como nosso z coordenada é 0 no momento e p é 1, w será -1.

Felizmente, podemos escolher o valor de z. Para garantir que o resultado seja w=1, precisamos para definir z = -2.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Ei, nossos a caixa está de volta!

Etapa 2: faça a mudança

Agora nossa caixa está lá e tem a mesma aparência que teria sem nenhuma de dados. No momento, não é possível rolar o contêiner da perspectiva. Portanto, não podemos mas sabemos que nosso elemento seguirá em outra direção quando rolou. Então, vamos fazer o contêiner rolar, certo? Basta adicionar um elemento espaçador que ocupa espaço:

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

E agora role a caixa para baixo. A caixa vermelha se move para baixo.

Etapa 3: dar um tamanho

Temos um elemento que se move para baixo quando a página rola para baixo. Essa é a parte difícil um pouco fora do caminho, na verdade. Agora precisamos estilizá-la para que se pareça com uma barra de rolagem e torná-lo um pouco mais interativo.

Uma barra de rolagem geralmente consiste em um "dedo" e uma "faixa", mas a faixa não é está sempre visível. A altura do polegar é diretamente proporcional a quanto o conteúdo fica visível.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight é a altura do elemento rolável, enquanto scroller.scrollHeight é a altura total do conteúdo rolável. scrollerHeight/scroller.scrollHeight é a fração do conteúdo que é visíveis. A proporção do espaço vertical das capas de polegar deve ser igual ao proporção de conteúdo visível:

estilo de ponto do polegar para altura do ponto sobre o roleerHeight é igual à altura do botão de rolagem
  acima da altura de rolagem ponto de rolagem apenas se, estilo de ponto de polegar, altura do ponto
  é igual à altura do botão de rolagem vezes a altura do botão sobre a rolagem do ponto
  a altura.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

O tamanho do polegar é que está bonito, mas está se movendo muito rápido. É aqui que podemos pegar nossa técnica botão de rolagem de paralaxe. Se movermos o elemento para trás, ele se moverá mais devagar enquanto rolagem. Podemos corrigir o tamanho aumentando-o. Mas quanto devemos empurrar exatamente de volta? Vamos fazer uma parte, você adivinhou, matemática! Esta é a última vez, eu promessa.

A informação essencial é que queremos que a borda inferior do polegar alinhada com a borda inferior do elemento rolável quando ele é totalmente rolado para baixo. Em outras palavras: se rolarmos a tela scroller.scrollHeight - scroller.height pixels, queremos que o polegar fique traduzido por scroller.height - thumb.height. Para cada pixel de rolagem, queremos que o polegar mova uma fração do pixel:

O fator é igual à altura do ponto do botão de rolagem menos a altura do ponto do polegar sobre o botão de rolagem
  altura de rolagem do ponto menos altura do ponto de rolagem.

Esse é o nosso fator de escalonamento. Agora precisamos converter o fator de escalonamento translação ao longo do eixo z, o que já fizemos na rolagem de paralaxe. artigo. De acordo com as seção relevante na especificação: O fator de escalonamento é igual a p/(p - z). Podemos resolver esta equação de z a para descobrir o quanto precisamos para traduzir nosso polegar ao longo do eixo z. Mas mantenha em mente que, devido a nossos manobras das coordenadas w, precisamos traduzir uma mais -2px ao longo de z. As transformações de um elemento são aplicadas da direita para a esquerda, o que significa que todas as traduções antes de nossa matriz especial não ser invertidas, todas as traduções depois da nossa matriz especial serão! Vamos codifique isso!

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

Temos um scrollbar. E é apenas um elemento DOM que podemos estilizar como quiser. Algo que em termos de acessibilidade é fazer o polegar responder clicar e arrastar, já que muitos usuários estão acostumados a interagir com uma barra de rolagem dessa forma. Para não tornar esta postagem do blog ainda mais longa, não vou explicar os detalhes daquela parte. Dê uma olhada no código da biblioteca para saber como isso é feito.

E o iOS?

Ah, meu velho amigo iOS Safari. Assim como na rolagem paralaxe, nos deparamos com uma problema aqui. Como estamos rolando em um elemento, precisamos especificar -webkit-overflow-scrolling: touch, mas isso causa o achatamento 3D e toda a o efeito de rolagem para de funcionar. Resolvemos esse problema com o botão de rolagem de paralaxe detectando o Safari do iOS e contando com o position: sticky como solução alternativa, e vamos fazer exatamente o mesmo aqui. Dê uma olhada no artigo sobre paralaxe para refrescar sua memória.

E a barra de rolagem do navegador?

Em alguns sistemas, será necessário usar uma barra de rolagem nativa e permanente. Historicamente, a barra de rolagem não pode ser ocultada (exceto com uma pseudoseletor não padrão). Então, para ocultá-lo, temos que recorrer a alguns hackers (sem matemática). Concluímos elemento de rolagem em um contêiner com overflow-x: hidden e tornar o elemento de rolagem mais largo que o contêiner. A barra de rolagem nativa do navegador é agora fora de vista.

Fin

Juntando tudo, podemos criar um frame personalizado perfeito como a de nosso Demonstração do gato Nyan (em inglês).

Se você não consegue ver o gato Nyan, isso significa que um bug que encontramos e registramos ao criar esta demonstração (clique no ícone de polegar para que o gato Nyan apareça). O Chrome é muito bom em evitar trabalhos desnecessários como pintar ou animar coisas que estão fora da tela. A má notícia é que nossos manobras matriciais fazem o Chrome pensar que o gif do gato Nyan está, na verdade, fora da tela. Esperamos que o problema seja corrigido em breve.

Aí está. Foi muito trabalho. Parabéns por ler o livro inteiro coisa. Estes são alguns para fazer isso funcionar e provavelmente raramente vai valer o esforço, exceto quando uma barra de rolagem personalizada é uma parte essencial da experiência. Mas é bom saber que isso é possível, né? O fato de ser tão difícil fazer barra de rolagem personalizada mostra que há trabalho a ser feito no lado do CSS. Mas não se preocupe! No futuro, de Houdini AnimationWorklet vai facilitar muito mais fáceis de aplicar efeitos de rolagem perfeitos, como esse.