Conteúdo deste artigo
- 1. Modelo
- HTML
- Estilo
- 2. O controle deslizante
- Rolagem infinita
- Tornando-o interminável
- Proxy de limpeza
- Entrada via observador
- Paralaxe sem costuras
- 3. Revelar
- 4. Transição
- Abertura
- O texto
- Fechamento
- Quatro estados, um guarda
- Como funciona
- Refinamentos
- Acessibilidade
- Conclusão
Existem muitos layouts de galeria que continuam aparecendo em sites, desde portfólios até sites de agências. Neste tutorial, construirei uma galeria de imagens dispersas que rola infinitamente, com cada imagem se movendo em sua própria velocidade para criar um efeito flutuante sutil. Quando você clica em um item, ele sai da grade para uma visualização detalhada completa.
Dividirei este tutorial em três seções:
- Slider (Scroller): Rolagem infinita acionada pela roda do mouse e entrada por toque, com paralaxe por item.
- Revelar: Quando um item entra na janela de visualização, ele aparece gradualmente para evitar qualquer ajuste conforme as imagens aparecem.
- Transição: Um efeito de transformação que expande um item clicado para a visualização de detalhes.
Neste tutorial, uso apenas GSAP e JavaScript simples. Usamos Observer para entrada, GSAP Flip para a transformação de transição e SplitText para o texto revela.
1. Modelo
HTML
Para começar precisamos criar a galeria:
###PRE_85b21332ada22730ed628525248c0956###
Em seguida, a visualização detalhada fica em uma sobreposição separada, oculta até que algo seja clicado:
###PRE_8134adac899593df2e3034af33c8c4f9###
Conforme mostrado acima, há um grupo de conteúdo por slide, correspondente ao seu data-index. Quando clicamos em um item da galeria, ele se torna o item ativo. A transição nunca cria nenhuma marcação; ele simplesmente troca qual grupo de conteúdo está visível e qual fonte de imagem a visualização exibe. O <figure class="content__preview-img"> é o único elemento no qual cada miniatura eventualmente se transforma.
Estilo
Agora o CSS, como estamos sequestrando o scroll temos que matar o scroll nativo e com certeza temos que configurar o scatter de cada item
###PRE_1ffab94df84db19d4df5c3119824d4a4###
Cada gallery__slide terá duas propriedades personalizadas: --stagger para a qual empurra o item do centro e --img-w apenas para a largura. Eu o mantenho em CSS e não em JavaScript porque mais tarde reconstruímos o loop no redimensionamento, e isso significa limpar todas as transformações de JavaScript desses elementos.
2. O controle deslizante
Como mencionado, este é o mecanismo que possui a posição de rolagem e os outros dois módulos reagem a ele.
Rolagem infinita
O truque é não usar a rolagem real do navegador para gerenciar a rolagem. Em vez disso, uso uma linha do tempo pausada na qual cada slide percorre uma volta completa e, em seguida, trato seu indicador de reprodução como a posição de rolagem.
No código temos verticalLoop, um auxiliar que constrói essa linha do tempo. É baseado no seamlessLoop helper do GSAP, adaptado aqui para um layout vertical.
Para cada item ele cria duas interpolações: uma que carrega o item até que ele passe pela borda superior e outra que entra imediatamente novamente pela parte inferior da galeria.
###PRE_dcf500425bedbd3854dec64b7b00eeb4###
Alguns de vocês provavelmente entenderiam que as posições são animadas com yPercent e não y porque é relativo à própria altura do elemento, dessa forma os valores ainda se mantêm mesmo quando os elementos não são todos do mesmo tamanho. Mas usamos a propriedade y para o efeito de paralaxe.
Há também immediateRender: false no fromTo para impedir que o GSAP aplique esse valor “iniciar na parte inferior” no momento em que a interpolação é criada. Sem ele, cada item iria para o fundo da galeria no primeiro quadro.
A linha final do auxiliar faz o mesmo trabalho para a linha do tempo como um todo:
###PRE_793336bed0ad322e23682fb4698c8ed1###
Esfregar até o final e voltar força cada interpolação a renderizar uma vez, de modo que nenhuma interpolação seja capturada inicializando no primeiro quadro real.
A duração é derivada da distância, então a velocidade permanece constante em itens de diferentes alturas:
###PRE_df8ab20e5316a990769bf3b559cecb50###
Então duration: distanceToLoop / pixelsPerSecond, confuso sobre esse cálculo? Nossa linha do tempo é paused: true e nunca a reproduzimos, apenas configuramos o playhead manualmente. Esta linha nos dá uma taxa de câmbio: um segundo de tempo de reprodução é igual a 100px de movimento. Segundos são apenas a unidade que o GSAP usa para medir cronogramas.
A divisão em si é simples time = distance / speed. Um item que precisa viajar 400px obtém uma duração de 4, um item que precisa viajar 800px obtém 8. Esse é o ponto: cada slide se move na mesma velocidade, não importa quão alto seja. Se escolhêssemos as durações manualmente, slides de tamanhos diferentes se separariam imediatamente.
Em doze slides, nossa galeria tem cerca de 5.200 px, então loop.duration() termina perto de 52. Uma volta de cinquenta e dois segundos que ninguém espera.
Por que 100 e não outro número? Porque faz com que a galeria siga a roda um a um. Veremos a outra metade disso no método scroll() do controle deslizante mais tarde, onde dividimos os pixels rolados pelo mesmo 100. Role 300px, o cursor de reprodução se move 3, a galeria se move 300px.
Tenha em mente que esses dois 100s são na verdade um número que vive em dois arquivos. config.speed parece um botão que você pode girar, mas se você passar speed: 2 a galeria se torna 200px por segundo de reprodução enquanto scroll() ainda divide por 100, então uma rolagem de 300px moveria a galeria em 600px. Você teria dobrado a sensibilidade, não a velocidade.
Tornando-o interminável
A linha do tempo faz um loop, mas o playhead ainda pode ficar fora de 0 ao máximo. gsap.utils.wrap dobra qualquer valor fora do intervalo de volta:
###PRE_e31eb51903cc3b0c8085ac9e2885e7d7###
this.wrap(-3) em um loop de 52 segundos retorna 49. Portanto, nosso valor de reprodução pode ir a qualquer lugar, positivo ou negativo ou milhares, e sempre entregamos à linha do tempo um horário legal.
Sobre o paddingBottom: gap, passamos rowGap para que leia 12vh do CSS. Mas dentro do auxiliar, totalHeight é construído a partir de offsetTop, que é medido a partir da página e não da caixa de conteúdo da galeria. Isso significa que o padding-top: 14vh da galeria já está dentro do número antes de paddingBottom ser adicionado.
Então o espaço vazio na costura, onde o último slide sai e o primeiro volta, é esse 14vh + 12vh = 26vh. Em todos os outros lugares, os slides ficam separados por 12vh, de modo que uma junção é duas vezes mais espaçosa.
Deixei assim. Role uma volta completa e você poderá sentir isso, uma pausa um pouco mais longa logo antes de a sequência recomeçar, que é exatamente o momento em que você a veria se repetindo.
Proxy de limpeza
Agora a parte que dá um toque à galeria. Em vez de definir loop.time() diretamente do delta da roda, animamos um objeto simples em direção ao alvo e deixamos a suavização do GSAP suavizá-lo:
###PRE_e1c3432286f6cdf6195145db0365ab10###
Neste código obtemos a inércia, o tratamento de interrupções e um único retorno de chamada de renderização sem escrever um loop lerp ou executar nosso próprio requestAnimationFrame.
Dirigir é igualmente pequeno:
###PRE_211a1de6fbd3f00786338e51eb99d35d###
Uma interpolação GSAP registra seus valores iniciais na primeira vez que é renderizada e invalidate() os descarta para que a interpolação leia playhead.time novamente como seu novo começo. Então restart() reproduz a facilidade de 0,75s de onde quer que o indicador de reprodução esteja agora em direção ao novo alvo. Role rapidamente e cada gesto redireciona uma interpolação que já está em movimento, que é exatamente o impulso que desejamos.
Sobre o delta/100, a galeria cobre 100px por segundo de reprodução, então dividir os pixels rolados por 100 os transforma em tempo de reprodução, e a galeria se move tanto quanto você rolou.
Entrada via observador
Eu uso Observer porque normaliza eventos de roda, toque e ponteiro em uma API:
###PRE_0df5d231fab86e266b92d1620a9239d9###
preventDefault: true é o que faz o CSS da Parte 0 aderir. overflow: hidden interrompe a rolagem do documento, mas o navegador ainda aciona eventos de roda e toque com seu próprio comportamento padrão anexado, como puxar para atualizar, elástico ou deslizar para trás. Cancelá-los aqui significa que o gesto chega ao nosso matagal e nada mais.
Paralaxe sem costuras
Cada slide recebe um multiplicador de velocidade, alternado a partir de uma pequena matriz:
###PRE_67c1870c0e926c7cb9ad57809dbcc7c6###
Eu armazeno speed - 1 em vez de speed de propósito, dessa forma o fator se torna quanto extra para mover em cima do que o loop já faz. Um fator de 0 significa apenas que ele se move exatamente com o loop, para que os deslocamentos permaneçam pequenos.
Os valores alternam rápido e lento em vez de aumentar, para que dois slides próximos um do outro não fiquem sincronizados. Uma coisa que vale a pena saber: i % speeds.length se repete a cada seis slides, então o slide 1 e o slide 7 acabam se movendo de forma idêntica. Tudo bem aqui, já que eles ficam separados por cerca de 2.500 px e nunca aparecem na tela ao mesmo tempo, mas apareceriam se seus slides ficassem menores ou sua janela de visualização ficasse mais alta. Se você quiser zero repetições em uma volta, basta fazer a matriz tão longa quanto o seu slide contar.
Agora a parte que me custou algumas tentativas. A abordagem óbvia, compensando cada slide em progress * factor, fornece um salto visível sempre que um slide é quebrado, porque o deslocamento é maior quando o item se teletransporta de cima para baixo. Então, faça o que fizermos, o deslocamento deve ser zero no exato momento em que um slide termina:
###PRE_8eca1d400382b8bca42eae3694fa66fc###
loopTop + rect.height é a distância do topo da janela de visualização até a borda inferior do slide. Esse valor chega a zero exatamente quando o slide ultrapassa a parte superior da tela, que é onde o loop o envolve. Multiplique pelo fator e o deslocamento extra desaparece na costura, de modo que o envoltório fica invisível.
Subtrair o item.offset do quadro anterior de rect.top é o que mantém isso estável. getBoundingClientRect() relata a posição incluindo o deslocamento que definimos no último quadro, então desfazemos para voltar à posição pura do loop antes de trabalhar no novo. Sem isso, os compostos offset e os slides saem da tela em cerca de um segundo. Em seguida, o loop escreve yPercent, a paralaxe escreve y e o GSAP compõe ambos na mesma transformação.
A segunda metade do método é gratuita. Já estamos medindo cada retângulo de cada quadro, portanto, detectar entradas e saídas da janela de visualização custa duas comparações. Observe que ele testa top, a posição após o deslocamento de paralaxe, que é onde o slide realmente está na tela.
Em vez de agir sobre isso, o controle deslizante apenas informa: this.onToggle?.(changes, immediate) é toda a interface entre o controle deslizante e a revelação. Um retorno de chamada, uma matriz de { el, visible, top }. O encadeamento opcional significa que o controle deslizante ainda funciona bem sem nenhuma revelação anexada.
3. Revelar
A revelação é bem simples. Quando um slide entrar na janela de visualização, faça fade in. Quando ele sair, redefina-o para que a animação possa ser reproduzida novamente na próxima vez. Em um loop infinito, a “próxima vez” acontece muito, então redefinir corretamente não é opcional.
###PRE_61412bd5f471ccf737145de40e8406e0###
Um Map digitado pelo elemento slide nos fornece uma pesquisa O(1) posteriormente sem armazenar o estado no nó DOM.
Oh, autoAlpha é uma abreviação GSAP para opacidade mais visibilidade. Ele interpola a opacidade e define visibility: hidden no momento em que a opacidade atinge zero e, em seguida, volta para inherit assim que sai. Uma propriedade faz o trabalho de duas, e o navegador deixa de pintar qualquer coisa totalmente transparente, que em um loop infinito ocupa a maior parte da galeria na maior parte do tempo.
Lidar com um lote de mudanças é onde mora a coreografia:
###PRE_ed2084699c25cedbe10fd35e76950131###
Classificar por top antes de escalonar é o pequeno detalhe que faz com que isso pareça deliberado. O controle deslizante relata alterações na ordem do DOM, mas em um loop infinito, a ordem do DOM não tem nada a ver com a ordem da tela. O slide na parte superior da janela de visualização pode ser o nono na marcação. Classificar pela posição real da tela significa que o escalonamento sempre cai em cascata, independentemente da maneira como você rola.
A revelação em si é composta por duas interpolações, a imagem e a legenda um pouco atrás:
###PRE_42d7244ffcff06dd4d4f9da69aea32e2###
Role rapidamente e um slide pode sair e voltar antes que seu fade-in termine, empilhando interpolações concorrentes no mesmo alvo e causando oscilação. overwrite: true mata todas as outras interpolações nos mesmos alvos no momento em que uma nova é iniciada, independentemente de quais propriedades elas tocam. Isso é mais forte do que o normal "auto", que apenas substitui interpolações que tocam nas mesmas propriedades. Está tudo bem aqui, já que esses são os únicos interpolações que tocam nesses elementos.
A ocultação é instantânea, pelo mesmo motivo:
###PRE_5d05ef50f5e2a0b726894a15d92e514e###
Elementos fora da tela não precisam de animação de saída, porque ninguém pode vê-los de qualquer maneira. Eles só precisam ser redefinidos de forma limpa e imediata para que a próxima entrada comece a partir de um estado conhecido.
4. Transição
A ideia por trás da transição é fazer com que ela se transforme na posição e no tamanho do pop-up sem parecer uma transição. Ele registra o estado atual, e mesmo quando alteramos o DOM ou CSS, o Flip do GSAP cuida disso, animando do estado gravado para o novo, comparando os dois instantâneos e gerando as transformações automaticamente.
Mas há uma parte complicada aqui. O que está sendo animado não é o que gravamos. A miniatura permanece na galeria e uma tela cheia separada figure é o que fica visível. Flip lida com isso com IDs correspondentes:
###PRE_78a6d8731dbe6433d2c26ae4f06e6806### ###PRE_8483c17d3187b5687be56a5f6a6d78ec###
Dois elementos diferentes compartilhando um data-flip-id. Flip vê o ID se mover de um elemento para outro e anima o segundo fora dos limites registrados do primeiro.
Isso só funciona porque exatamente uma miniatura reivindica o ID por vez. É por isso que o atribuímos ao abrir e o excluímos ao redefinir, em vez de colocá-lo na marcação. Doze miniaturas, todas contendo flipId: "preview" não deixariam a Flip sem saber a qual delas nos referíamos.
Abertura
###PRE_cddb60c2d2b21d1b5e3b9abe5259e7e9###
Existem duas regras de ordenação escondidas nessas linhas.
O primeiro é estrito: wrapper.dataset.flipId = "preview" deve ser definido antes de Flip.getState(wrapper). O objeto de estado registra o Flip ID de cada elemento no momento da captura e o Flip gera automaticamente um para qualquer elemento que ainda não o possui. Defina o ID depois e seu snapshot carrega um ID gerado que não corresponde a nada, Flip.from não encontra nenhum parceiro e você obtém uma operação autônoma silenciosa. Nada de erros, a animação simplesmente não acontece, que é o tipo de bug do Flip mais difícil de encontrar.
A segunda é a regra geral do Flip: instantâneo primeiro, mutação em segundo, animação em terceiro. Nesta demonstração, as duas chamadas gsap.set após serem inofensivas porque .content é position: fixed, portanto, mostrá-lo não pode mover a galeria. Mas manter a captura no topo ainda é o hábito certo.
gsap.killTweensOf(wrapper) lida com casos extremos reais. Você pode clicar em um slide enquanto seu próprio fade de revelação ainda está em execução. Sem matar a interpolação primeiro, ele continua animando autoAlpha de volta para 1 em um elemento que acabamos de ocultar, e a miniatura passa como um fantasma na visualização.
Em seguida, a linha do tempo:
###PRE_b31fa258183e53971ae278a730a962a4###
Cada interpolação fica na posição 0 então tudo parece um gesto em vez de uma sequência de etapas. power4.inOut dá à transformação uma curva pesada que se adapta a uma imagem do tamanho da janela de visualização, e os fades são mais suaves power2.out para que não desviem a atenção disso.
absolute: true diz ao Flip para posicionar o elemento de forma absoluta enquanto ele é animado. Sem ele, uma metamorfose que altera a posição do layout de um elemento pode empurrar seus irmãos no meio do voo.
Essa última interpolação faz mais do que parece. Tanto a imagem em miniatura quanto a imagem de visualização ficam em transform: scale(1.2) dentro de um overflow: hidden invólucro, portanto, um sexto de cada dimensão é sempre cortado. Animar a imagem interna para scale: 1 enquanto o wrapper cresce significa que o quadro e seu conteúdo se movem em taxas diferentes, e a imagem parece se estabilizar conforme o quadro se expande. É a mesma ideia da paralaxe, apenas dentro de um elemento.
O texto
O texto detalhado aparece gradualmente por caractere, por linha:
###PRE_3238f86010bd1bf12bfc2f6bd22abce5###
Dividir em lines,chars oferece dois níveis para trabalhar. Os caracteres ficam escalonados em 0,01s, de modo que uma linha aparece quase como uma peça única, e as linhas são deslocadas em 0,06s, de modo que o parágrafo se desenvolve para baixo.
O início 0.8 coloca o texto cerca de dois terços da metamorfose de 1,2s. Comece do zero e a tela ficará ocupada, espere a transformação terminar e tudo parecerá lento. A sobreposição é o que faz com que seja lido como um movimento contínuo.
Criar a divisão após display: block é importante, SplitText precisa do elemento renderizado para saber onde as linhas realmente quebram.
Fechamento
O encerramento não é apenas a linha do tempo de abertura invertida, porque o que precisamos é diferente. A visualização já é um elemento posicionado com limites conhecidos, portanto não há nada para registrar, e Flip.fit() pega o elemento A e o anima na posição atual do elemento B:
###PRE_6c88528ca98d146b93f58e3a5b3f993e###
O fechamento é mais rápido que a abertura, 1s contra 1,2s, e esmaece linhas inteiras em vez de caracteres individuais. Sair do caminho deve ser sempre mais rápido do que chegar. Os outros slides voltam em 0.5, no meio da transformação, porque trazê-los em 0 torna a tela lotada enquanto a visualização ainda é grande.
Quatro estados, um guarda
As pessoas clicam duas vezes nas coisas. Eles acertaram Escape no meio da animação. Eles clicam em um segundo slide enquanto o primeiro ainda está abrindo. Um booleano não pode descrever nada disso, então a transição rastreia quatro estados:
###PRE_b004fd4d988d5b7209d3072333bcffa2###
open() se recusa a iniciar, a menos que o estado seja closed, e close() lê o estado e escolhe uma estratégia:
###PRE_d1440314379670d9bd326784c1a34726###
Pressionar Escape no meio da abertura retrocede a linha do tempo de abertura em vez de construir uma linha do tempo fechada contra limites que ainda estão em movimento. É por isso que a linha do tempo aberta carrega onReverseComplete: () => this.reset(), uma abertura cancelada deve ser limpa exatamente como um fechamento concluído.
reset() coloca o DOM de volta:
###PRE_8e304bfcc9b213930b2912ecfa6c8fb6###
split.revert() restaura os nós de texto originais, o que é importante tanto para a próxima abertura quanto para leitores de tela e seleção de texto, nenhum dos quais desfruta de um parágrafo fragmentado em um <div> por personagem. clearProps: "all" remove todos os estilos embutidos que as animações escreveram, para que a próxima abertura comece com CSS e não com sobras.
Como funciona
Os três módulos se comunicam por meio de dois pequenos contratos em index.js:
###PRE_c1e59858fa65a94bbb33ec051bad0912###
enabled é uma função e não um booleano, portanto, é avaliado em cada evento de rolagem e sempre reflete o estado atual. Rolar enquanto a visualização está aberta não faz nada.
slider.stop() faz duas coisas, e vale a pena vê-las separadas:
###PRE_ff49c56dad74f0d335b5c5a54521df83###
Pausar sozinho deixaria o alvo do esfregaço em algum lugar à frente de onde a galeria realmente parou, e a próxima rolagem após fechar a visualização avançaria para alcançá-lo. Sincronizar o alvo com a posição real significa que a rolagem continua exatamente de onde parou.
Uma observação sobre o sinalizador immediate passando por applyParallax, toggle e show. Em um redimensionamento, o loop é reconstruído e cada slide é medido novamente, de modo que os slides que já estavam na tela voltam relatados como novas entradas e a revelação reproduz todo o seu fade-in. A bandeira diz: nada entrou na viewport, a viewport se moveu ao redor dela, então vá para o estado final em vez de animar.
Mesma história com freeze() e stop(). Ambos pausam a limpeza, mas stop() também desativa o Observer e start() o liga novamente quando a transição é reiniciada. Eu só descobri isso quebrando-o: com o Observer ainda ouvindo durante a visualização, preventDefault: true continua engolindo eventos da roda mesmo que enabled() faça scroll() retornou mais cedo, então a visualização detalhada também não pôde rolar.
O guarda no manipulador de cliques saiu da mesma caçada. slider.stop() costumava ser executado antes de transition.open() ter decidido se aceitaria, e open() fiança se o estado não estiver fechado. Clique em um segundo slide no meio da animação e o Observer saiu sem mais nada para ligá-lo novamente, então a galeria simplesmente morreu. Verificar primeiro o estado significa que só entregaremos quando houver algo para devolver.
E open() é assíncrono agora porque a imagem de visualização nunca manteve esse src, portanto, não possui quadro decodificado e fica em branco para os primeiros quadros da transformação. Awaiting previewImg.decode() corrige isso, mas abre uma janela onde um fechamento pode ocorrer antes que a linha do tempo exista, que é o que os guardas extras em open() e close() são para.
Refinamentos
Duas coisas que se aplicam a todos os três módulos e não a qualquer um deles.
Desempenho. O trabalho por quadro é um getBoundingClientRect() e um gsap.set() por slide. applyParallax intercala essas leituras e gravações, que é o padrão clássico de thrashing de layout, então doze slides significam doze refluxos forçados por quadro. Ótimo para esse tamanho, não ótimo para oitenta. Se você aumentar a escala, primeiro meça tudo em uma matriz e depois escreva a partir dessa matriz.
Caso contrário a galeria apenas anima transform e opacity. A única exceção é a metamorfose Flip, que anima largura e altura porque scale: false é o padrão. Isso aciona o layout, mas mantém a imagem nítida em vez de esticar um bitmap e é executado por 1,2 segundos em um único elemento. Vale a pena.
Estrutura. Cada módulo possui uma coisa. O controle deslizante expõe stop(), start() e freeze(), a revelação expõe toggle(), a transição expõe open(), close() e um legível state. Nenhum deles importa um ao outro, toda a fiação está em index.js. Exclua a transição e você ainda terá uma galeria infinita de paralaxe. Exclua a revelação e o controle deslizante nunca perceberá.
Acessibilidade
Uma galeria que sequestra a roda precisa de cuidados aqui, então deixe-me ser direto sobre o que a demonstração faz e onde ela para.
Cada slide recebe tabindex="0" e role="button" in index.js e escuta Enter e Espaço, bem como clique, porque os botões nativos respondem a ambos. Escape fecha a visualização e o botão é rotulado como “Voltar (Esc)” para que o atalho possa ser descoberto. Cada imagem da galeria tem um alt real e fillContent() copia-o para a visualização para que a descrição siga a foto na visualização de detalhes.
Duas coisas que ele não faz. Não há rolagem do teclado, porque o Observer apenas escuta a roda e o toque e não há rolagem nativa para voltar, então as teclas de seta não fazem nada e a guia para um slide mais abaixo foca algo que você não pode ver. E não há manipulação de movimento reduzida, que uma transformação em tela cheia de 1,2 segundos e uma galeria com impulso merecem.
Nenhum dos dois é difícil. Roteie os principais eventos para o mesmo método scroll() que o Observer alimenta e use gsap.matchMedia() para encurtar a limpeza, nivelar os fatores de paralaxe e compactar a linha do tempo com timeScale. Eu os deixei de fora, então cada linha aqui é um código que você pode ler no repositório, mas eu adicionaria os dois antes de enviá-lo.
Conclusão
Três efeitos, três módulos e a maior parte do trabalho interessante entre eles. O controle deslizante transforma uma linha do tempo pausada em uma posição de rolagem e fixa sua paralaxe ao ponto de quebra para que a costura permaneça invisível. A revelação ordena as entradas por posição na tela e, como o controle deslizante relata seu primeiro lote durante a configuração, a introdução e a revelação da rolagem têm o mesmo código. A transição move uma foto entre uma miniatura e uma visualização em tela cheia com Flip.from e Flip.fit, guardado por quatro estados para que um clique impaciente não possa quebrá-lo.
Os botões menores são os mais divertidos de girar. Altere a matriz speeds e toda a sensação de profundidade muda. Troque power4.inOut por expo.inOut e a transformação passa de deliberada a repentina. Mova o deslocamento do texto 0.8 em um décimo de segundo e observe como a visualização de detalhes é diferente.
