Worklet de animação da Houdini's

Turbine as animações do seu app da Web

O Worklet de animação está no Chrome Canary (por trás da flag "Recursos experimentais da Plataforma Web") e estamos planejando um teste de origem para o Chrome 71. Você pode começar a usá-lo como um aprimoramento progressivo hoje mesmo.

Outra API Animation?

Na verdade, não, é uma extensão do que já temos, e por um bom motivo. Vamos começar do início. Se você quiser animar qualquer elemento do DOM na Web atualmente, terá duas opções: Transições CSS para transições simples de A para B, Animações CSS para animações potencialmente cíclicas e mais complexas com base em tempo e a API Web Animations (WAAPI) para animações quase arbitrariamente complexas. A matriz de suporte de WAAPI é um pouco sombria, mas está evoluindo em breve. Até lá, há um polyfill.

O que todos esses métodos têm em comum é que são sem estado e baseados no tempo. No entanto, alguns dos efeitos que os desenvolvedores estão tentando tentar não são orientados por tempo nem sem estado. Por exemplo, o infame botão de paralaxe é, como o nome indica, orientado a rolagem. Atualmente, implementar um controle de paralaxe de alto desempenho na Web é surpreendentemente difícil.

E quando não há estado? Pense na barra de endereço do Chrome no Android, por exemplo. Se você rolar para baixo, ele sairá da visualização. Porém, assim que você rola para cima, ele retorna, mesmo que você esteja na metade da página. A animação depende não apenas da posição de rolagem, mas também da direção de rolagem anterior. Ele tem estado.

Outro problema é o estilo das barras de rolagem. Eles são notoriamente não estilizados ou, pelo menos, não têm estilo suficiente. E se eu quiser um gato nyan como barra de rolagem? Seja qual for a técnica escolhida, criar uma barra de rolagem personalizada não é eficiente nem é fácil.

A questão é que todas essas coisas são estranhas e difíceis de implementar com eficiência. A maioria deles depende de eventos e/ou requestAnimationFrame, que podem manter você em 60 QPS, mesmo quando a tela puder ser executada a 90 QPS, 120 QPS ou mais e use uma fração do orçamento de frames da linha de execução principal.

O Worklet de animação estende os recursos da pilha de animações da Web para facilitar esses tipos de efeitos. Antes de começar, vamos garantir que temos os conceitos básicos das animações.

Uma introdução sobre animações e linhas do tempo

WAAPI e Worklet de animação fazem uso extensivo de linhas do tempo para permitir que você orquestre animações e efeitos da maneira que quiser. Esta seção é um resumo rápido ou introdução às linhas do tempo e como elas funcionam com animações.

Cada documento tem document.timeline. Ele começa em 0 quando o documento é criado e conta os milissegundos desde que o documento começou a ser atual. Todas as animações de um documento funcionam em relação a essa linha do tempo.

Para entender melhor as coisas, vamos analisar este snippet da WAAPI

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Quando chamamos animation.play(), a animação usa o currentTime da linha do tempo como o horário de início. Nossa animação tem um atraso de 3.000 ms, o que significa que a animação será iniciada (ou ficará "ativa") quando a linha do tempo chegar a "startTime"

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by theduraçãooptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. A questão é que os controles da linha do tempo em que estamos na animação!

Quando a animação alcançar o último frame-chave, ela vai voltar ao primeiro frame-chave e iniciar a próxima iteração da animação. Esse processo é repetido um total de três vezes desde que definimos iterations: 3. Se quiséssemos que a animação nunca terminasse, criaríamos iterations: Number.POSITIVE_INFINITY. Veja o resultado do código acima.

A WAAPI é incrivelmente eficiente e há muitos outros recursos nessa API, como easing, deslocamentos iniciais, ponderações de frames-chave e comportamento de preenchimento que seriam muito úteis neste artigo. Para saber mais, recomendamos a leitura deste artigo sobre Animações CSS sobre truques de CSS.

Como escrever uma Worklet de animação

Agora que já falamos sobre o conceito de linhas do tempo, podemos começar a analisar o Worklet de animação e como ele permite mexer nas linhas do tempo. A API Animation Worklet não é baseada apenas na WAAPI, mas é (no sentido da Web extensível) um primitivo de nível inferior que explica como a WAAPI funciona. Em termos de sintaxe, elas são muito parecidas:

Objeto de animação WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

A diferença está no primeiro parâmetro, que é o nome do worklet que impulsiona essa animação.

Detecção de recursos

O Chrome é o primeiro navegador a lançar esse recurso, então é necessário garantir que seu código não espere que AnimationWorklet esteja lá. Portanto, antes de carregar o worklet, precisamos detectar se o navegador do usuário oferece suporte a AnimationWorklet com uma simples verificação:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Como carregar uma worklet

Worklets são um novo conceito introduzido pela força-tarefa Houdini para facilitar a criação e o escalonamento de muitas das novas APIs. Abordaremos os detalhes dos worklets um pouco mais tarde, mas, para simplificar, você pode pensar neles como linhas de execução baratas e leves (como workers) por enquanto.

Precisamos ter certeza de que carregamos um worklet com o nome "passthrough", antes de declarar a animação:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

O que está acontecendo aqui? Estamos registrando uma classe como animador usando a chamada registerAnimator() do AnimationWorklet, que recebe o nome "passthrough". É o mesmo nome que usamos no construtor WorkletAnimation() acima. Quando o registro for concluído, a promessa retornada por addModule() vai ser resolvida e poderemos começar a criar animações usando essa worklet.

O método animate() da nossa instância será chamado para cada frame que o navegador quiser renderizar, transmitindo o currentTime da linha do tempo da animação, bem como o efeito que está sendo processado no momento. Temos apenas um efeito, o KeyframeEffect. Estamos usando currentTime para definir o localTime do efeito. É por isso que esse animador é chamado de "passthrough". Com esse código para a worklet, a WAAPI e o AnimationWorklet acima se comportam exatamente da mesma forma, como mostrado na demonstração.

Tempo

O parâmetro currentTime do nosso método animate() é o currentTime da linha do tempo transmitida ao construtor WorkletAnimation(). No exemplo anterior, acabamos de transmitir esse tempo para o efeito. Mas, como esse é um código JavaScript, podemos distorcer o tempo 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Vamos pegar o Math.sin() do currentTime e remapear esse valor para o intervalo [0; 2000], que é o período para o qual nosso efeito é definido. Agora, a animação é muito diferente, sem ter mudado os frames-chave ou as opções da animação. O código do worklet pode ser arbitrariamente complexo e permite definir programaticamente quais efeitos são executados em que ordem e até que ponto.

Opções em vez de opções

Talvez você queira reutilizar uma worklet e alterar os números dela. Por esse motivo, o construtor WorkletAnimation permite transmitir um objeto de opções para o worklet:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

Neste exemplo, as duas animações são conduzidas com o mesmo código, mas com opções diferentes.

Mostre seu estado.

Como mencionei antes, um dos principais problemas que o worklet de animação quer resolver são animações com estado. Os worklets de animação podem manter o estado. No entanto, um dos principais recursos dos worklets é que eles podem ser migrados para uma linha de execução diferente ou até mesmo destruídos para economizar recursos, o que também destruiria o estado deles. Para evitar a perda de estado, a worklet de animação oferece um hook que é chamado antes de ser destruído. Ele pode ser usado para retornar um objeto de estado. Esse objeto será passado para o construtor quando o worklet for recriado. Na criação inicial, esse parâmetro será undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Sempre que você atualizar esta demonstração, terá uma chance de 50/50 em que direção o quadrado vai girar. Se o navegador desmontasse o worklet e o migrasse para uma linha de execução diferente, haveria outra chamada Math.random() na criação, o que poderia causar uma mudança repentina de direção. Para garantir que isso não aconteça, retornamos as animações escolhidas aleatoriamente como state e a usamos no construtor, se fornecida.

Conexão com a continuidade espaço-tempo: ScrollTimeline

Como mostrado na seção anterior, o AnimationWorklet permite definir de maneira programática como o avanço da linha do tempo afeta os efeitos da animação. Mas, até agora, nossa linha do tempo sempre foi document.timeline, que monitora o tempo.

O ScrollTimeline abre novas possibilidades e permite gerar animações com rolagem em vez de tempo. Vamos reutilizar nosso primeiro worklet "passthrough" para esta demonstração:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Em vez de transmitir document.timeline, estamos criando um ScrollTimeline. Talvez você tenha adivinhado que ScrollTimeline não usa tempo, mas a posição de rolagem do scrollSource para definir currentTime na worklet. Rolar totalmente para a parte de cima (ou para a esquerda) significa currentTime = 0, enquanto uma rolagem até a parte de baixo (ou à direita) define currentTime como timeRange. Se você rolar a caixa nesta demonstração, poderá controlar a posição da caixa vermelha.

Se você criar uma ScrollTimeline com um elemento que não rola, a currentTime da linha do tempo será NaN. Portanto, especialmente com o design responsivo em mente, você precisa estar sempre preparado para NaN como sua currentTime. Muitas vezes, é conveniente definir o valor 0 como padrão.

Vincular animações com posição de rolagem é algo que tem sido buscado há muito tempo, mas que nunca foi realmente alcançado nesse nível de fidelidade (além das soluções alternativas interessantes com CSS3D). O Worklet de animação permite que esses efeitos sejam implementados de maneira direta e tenham alta performance. Por exemplo: um efeito de rolagem paralaxe como esta demonstração mostra que agora são necessárias apenas algumas linhas para definir uma animação de rolagem.

Configurações avançadas

Worklets

Worklets são contextos JavaScript com um escopo isolado e uma superfície de API muito pequena. A pequena plataforma da API permite uma otimização mais agressiva do navegador, especialmente em dispositivos mais simples. Além disso, os worklets não estão vinculados a um loop de eventos específico, mas podem ser movidos entre linhas de execução conforme necessário. Isso é especialmente importante para o AnimationWorklet.

NSync do composto

Você já deve saber que algumas propriedades de CSS são rápidas para animar, enquanto outras não. Algumas propriedades só precisam de algum trabalho na GPU para serem animadas, enquanto outras forçam o navegador a refazer o layout do documento inteiro.

No Chrome (como em muitos outros navegadores), temos um processo chamado compositor, que é a função dele (e estou simplificando muito aqui) para organizar camadas e texturas e depois usar a GPU para atualizar a tela o mais regularmente possível, de preferência o mais rápido possível na tela (normalmente 60 Hz). Dependendo de quais propriedades CSS estão sendo animadas, o navegador pode precisar que o compositor faça o trabalho, enquanto outras propriedades precisam executar o layout, que é uma operação que somente a linha de execução principal pode fazer. Dependendo das propriedades que você planeja animar, o worklet de animação será vinculado à linha de execução principal ou será executado em uma linha de execução separada em sincronia com o compositor.

Tap no pulso

Geralmente, há apenas um processo do compositor possivelmente compartilhado entre várias guias, já que a GPU é um recurso altamente utilizado. Se o compositor for bloqueado, todo o navegador será interrompido até parar e não responderá à entrada do usuário. Isso precisa ser evitado a todo custo. O que vai acontecer se o worklet não conseguir fornecer os dados necessários para o compositor a tempo para que o frame seja renderizado?

Se isso acontecer, o worklet será permitido — de acordo com a especificação — para "slip". Ele fica atrás do compositor, e ele pode reutilizar os dados do último frame para manter o frame rate ativo. Visualmente, isso parece instabilidade, mas a grande diferença é que o navegador ainda é responsivo à entrada do usuário.

Conclusão

Há muitas facetas para o AnimationWorklet e os benefícios que ele traz para a Web. Os benefícios óbvios são mais controle sobre as animações e novas maneiras de impulsionar as animações para trazer um novo nível de fidelidade visual à Web. Mas o design das APIs também permite tornar seu app mais resiliente à instabilidade e, ao mesmo tempo, ter acesso a todas as vantagens novas.

O Worklet de animação está na versão Canary, e queremos um teste de origem com o Chrome 71. Estamos ansiosos por suas novas experiências na Web e o que podemos melhorar. Há também um polyfill (link em inglês) que oferece a mesma API, mas sem isolamento de desempenho.

As transições CSS e as animações CSS ainda são opções válidas e podem ser muito mais simples para animações básicas. Mas se precisar de um truque, o AnimationWorklet pode ajudar!