No final de 2012, não era fácil encontrar projetos de código aberto usando requestAnimationFrame() – esse é o gancho que permite que o código Javascript seja sincronizado com o loop de pintura nativo de um navegador da Web. As animações que usam esse método podem ser executadas a 60 fps e proporcionar interações fantásticas de conteúdo semelhante a um jogo se o senhor tiver o cuidado de seguir as regras.

Por volta dessa época, entrei para os laboratórios do Art.com e aconteceu que eu tinha um caso de uso convincente para um modelo de interação “estilo iOS” em torno da navegação visual do fluxo de conteúdo, que fosse responsivo em vários tamanhos de tela e paradigmas de entrada (toque, ponteiro, trackpad). A partir dessa necessidade, surgiu o TremulaJS, um componente de interface do usuário em Javascript projetado para navegar em grandes conjuntos de resultados de conteúdo visual com um alto grau de fluidez de experiência do usuário.

Este artigo apresenta uma visão de alto nível de como o TremulaJS está organizado com foco na criação de interações animadas responsivas e de longa duração usando Javascript.

*Para os interessados em uma análise aprofundada dos fundamentos de um requestAnimationFrame() Julian Shapiro, criador do velocity.js, fez um resumo sucinto desse tópico para que o senhor possa ler aqui. Para mim, é leitura obrigatória para qualquer pessoa que esteja embarcando em uma aventura de animação JS.




TremulaJS: uma visão geral do componente


O TremulaJS é composto por cinco componentes principais: o eixo de rolagem, o Momentum Loop, a grade de conteúdo, a caixa de conteúdo e a projeção da grade.


fig1. O eixo de rolagem, o deslocamento de rolagem e a grade de conteúdo relacionados ao contêiner de visualização do TremulaJS. Essa figura mostra uma grade de elementos de conteúdo discretos que podem deslizar (em um eixo) pela área visível. O conteúdo fora dessa área não é renderizado.

Eixo de rolagem


O TremulaJS permite todos os tipos de microinterações, no entanto, no final das contas, há apenas uma dimensão de navegação, que é o valor Scroll Offset. Esse valor é encapsulado pelo objeto Scroll Axis que, entre outras coisas, gerencia a orientação horizontal e vertical.

Momentum Loop


O loop de momentum regula o valor do momentum em todo o sistema. Ele é a soma de várias saídas de subcomponentes, incluindo: um relógio de momentum interno, várias funções de amortecimento condicional vinculadas aos estados do eixo de rolagem e um manipulador de eventos de interação com o usuário. Em cada quadro de animação, ele retorna um valor de saída instantâneo do momento usado para calcular a posição de deslocamento da rolagem.

Grade de conteúdo


O Content Grid é um modelo de abstração de caixas de conteúdo dispostas em uma grade XY configurável. Todo o conteúdo adicionado a essa grade é dimensionado proporcionalmente ao longo do eixo transversal para manter as dimensões normalizadas da linha (ou coluna).

Em cada quadro, à medida que o momentum move a Content Grid para uma nova posição ao longo do eixo de rolagem, a Content Grid atualiza suas Content Boxes filhas com novas posições relativas. Isso é a abstração que nos dá oportunidades de estender o processo de pintura e fazer coisas legais acontecerem…

Caixa de conteúdo


Uma Content Box é criada para cada unidade de conteúdo anexada à Content Grid. Uma Content Box tem largura, altura, um modelo HTML opcional e uma imagem principal opcional que (se fornecida) é pré-carregada e transicionada na tela por uma classe CSS. Esse não deve ser um paradigma desconhecido para um desenvolvedor da Web.

A parte interessante começa aqui: Cada bloco de conteúdo também mantém vários valores primitivos de forma de onda correspondentes ao seu próprio progresso de rolagem na tela. Essas formas de onda podem ser mapeadas para animar qualquer aspecto de um elemento DOM da Content Box no tempo e no espaço. Vamos ampliar isso com um diagrama…

fig2. Progressão linear de um bloco de conteúdo na tela com uma forma de onda de “rampa” mostrada abaixo.

Na figura acima, podemos acompanhar um Content Block à medida que ele é movido pela tela e imaginar que a saída da forma de onda da rampa é mapeada para uma função que atualiza uma propriedade CSS translateX().

No entanto, esse não é o comportamento padrão – é um pouco mais complexo do que isso. Aqui está um exemplo simplificado da função padrão chamada em uma Content Box durante um ciclo de renderização…


function updateContentBoxElementProperites(x,y) {
  var ramp = this.waveforms.headRamp,
    xo=x,
    yo=y,
    zo=0;
    
  this.e.style.transform = 'translate3d(' + xo + 'px,' + yo +'px, ' + zo + 'px)';
  //this.e.style.opacity = ramp;
  this.pPos = [x,y];//cache the current position in the Content Box model
}

Essa função é chamada quando chega a hora de reposicionar nosso Content Box e podemos ver aqui que são passadas novas coordenadas. x & y são valores absolutos correspondentes à geometria de nossa visualização do TremulaJS, esses valores são fornecidos à função pelo Content Grid, que tem conhecimento de todos os Content Blocks e é capaz de processar com eficiência todas as posições de todos os Content Boxes na grade. A função acima é então chamada em cada Content Box em cada quadro de animação.

Observe a atribuição de opacidade comentada. Se não comentássemos isso, veríamos nosso bloco de conteúdo esmaecer à medida que se movesse da esquerda para a direita (ou esmaecer à medida que se movesse da direita para a esquerda). Isso funciona porque nosso valor de rampa é um valor derivado (entre 0 e 1) vinculado ao progresso de rolagem de uma Content Box em nossa visualização do TremulaJS. Convenientemente, this.e.style.opacity está esperando um número entre 0 e 1.

News Flash: os caminhos de Bézier são super responsivos

Uma olhada na Projeção de Grade


Há um quinto componente pertencente ao TremulaJS que nos permite pegar elementos de uma grade de conteúdo e projetá-los ao longo de um caminho de Bėzier. Sem surpresa, esse componente é chamado de projeção de grade.

Então, para recapitular: Conforme mostrado no exemplo anterior, estamos analisando uma função Content Box que é executada em cada quadro. A essa função são passados valores instantâneos de x&y correspondentes à orientação da própria Content Box na visualização do TremulaJS em um determinado momento. Também são passados a essa função vários valores primitivos de forma de onda correspondentes ao seu próprio progresso de rolagem na tela. É nesse ponto que podemos remapear um caminho de Bezier arbitrário para praticamente qualquer propriedade CSS. Vamos dar outra olhada no exemplo acima, só que mudaremos a posição vertical da nossa Content Box substituindo a posição absoluta x&y por uma gerada pela nossa função Bézier.

fig3. Progressão linear de um bloco de conteúdo em uma visualização com uma forma de onda de Bézier mostrada abaixo. A saída de Bézier agora é mapeada para a posição x&y da nossa caixa de conteúdo dentro da visualização do TremulaJS.



var bezierArcPath = [
  {x:0,y:0},
  {x:0,y:1},
  {x:1,y:1},
  {x:1,y:0}
];

function updateContentBoxElementProperites(x,y,env) {

  var path = bezierArcPath;

  var 
    areaX = env.viewDims[0],
    areaY = env.viewDims[1],
    ramp = this.waveforms.tailRamp,
    xo=x,
    yo=y,
    zo=0;

  var xyFactor = [
    areaX,
    areaY
  ];

  var scaledPath = env.factorPathBy(path,xyFactor);
  
  var p = jsBezier.pointOnCurve(cubicBezier, ramp);
  var g = jsBezier.gradientAtPoint(cubicBezier, ramp);
  
  xo = p.x - (this.dims[0]*.5);
  yo = areaY - p.y - (this.dims[1]*.5);
  zo = 0;

  this.e.style.transform = 'translate3d(' + xo + 'px,' + yo +'px, ' + zo + 'px)';

  this.pPos = [x,y];
}


Observação: os nomes das variáveis nesses exemplos foram alterados/limpos para melhorar a compreensão de alto nível – o código real não é tão bonito. Faça um fork e melhore!

Neste exemplo, adicionamos alguns métodos para ajudar a implementar nossas transformações de Bėzier. Primeiro, vamos dar uma olhada no env.factorPathBy(path,xyFactor). O poder de resposta dessa função utilitária é grande – ela nos permite definir qualquer área de caixa delimitadora (nesse caso, as dimensões atuais da visualização do TremulaJS) e dimensionar nosso caminho em duas dimensões de modo que o caminho caiba na caixa. O que é retornado são coordenadas de caminho pré-escalonadas e prontas para uso.

O próximo em nossa cadeia é o jsBezier.pointOnCurve(cubicBezier, ramp). Que usa como parâmetros nosso caminho dimensionado e nossa saída de rampa atual. Nossos valores x&y transformados são retornados. Muito obrigado aqui a Simon Porritt por portar a matemática clássica de Bėzier para JS e postar a biblioteca jsBezier no gitHub!

O restante deve parecer bastante familiar. Em seguida, fazemos alguns pequenos ajustes em x&y para que nosso conteúdo seja posicionado a partir de sua origem central.





Mas espere, tem mais! (Mas não neste artigo…)

Além desse exemplo, há muitas animações que podem ser criadas a partir desses blocos de construção básicos. Por exemplo, jsBezier.gradientAtPoint(cubicBezier, ramp) nos dá valores tangentes instantâneos à medida que o conteúdo se move ao longo de nosso caminho, permitindo a rotação coordenada do conteúdo, entre outras possibilidades. Há também o eixo z e uma forma de onda triangular primitiva que permite efeitos de profundidade (fazendo com que o conteúdo pareça mais próximo à medida que se move para o centro de nossa visão).

As curvas podem ser usadas com a mesma facilidade para produzir efeitos de atenuação ou para manter nosso conteúdo em um único eixo posicionado de forma responsiva.

Outro recurso do TremulaJS é o Content Box momentum. Quando ativada, a grade de conteúdo não atualiza imediatamente o DOM de uma Content Box à medida que o Scroll Offset é alterado. Em vez disso, a Content Box gerencia seu próprio valor de momentum em relação ao seu relacionamento com o local da força motriz (por exemplo, o dedo ou o ponteiro do mouse sobre a grade) – isso pode produzir efeitos interessantes de momentum no nível do conteúdo.




Para os interessados, há uma ótima ferramenta de edição de caminhos aqui…

https://www.desmos.com/calculator/d1ofwre0fr