Turbine as animações da sua app da Web
Texto longo, leia o resumo:o Animation Worklet permite criar animações imperativas que são executadas com o frame rate nativo do dispositivo para ter uma suavidade extra sem instabilidade, tornando as animações mais resilientes contra instabilidade da linha de execução principal, além de permitir links para rolagem em vez de tempo. O Animation Worklet está no Chrome Canary (por trás da flag "Experimental Web Platform features") e estamos planejando um teste de origem para o Chrome 71. Você pode começar a usá-lo como um aprimoramento progressivo hoje.
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 quiser animar qualquer elemento DOM na Web hoje, você tem duas opções: Transições CSS para transições A para B simples, Animações CSS para animações possivelmente cíclicas e mais complexas baseadas em tempo e API Web Animations (WAAPI) para animações quase arbitrariamente complexas. A matriz de suporte da WAAPI está parecendo bem ruim, mas está melhorando. Até lá, há um polyfill.
O que todos esses métodos têm em comum é que eles são sem estado e orientados por tempo. No entanto, alguns dos efeitos que os desenvolvedores estão tentando não são orientados por tempo nem sem estado. Por exemplo, o famoso scroller de paralaxe é, como o nome indica, movido por rolagem. Atualmente, é surpreendente implementar um botão de rolagem de paralaxe com alto desempenho na Web.
E sem estado? Pense na barra de endereço do Chrome no Android, por exemplo. Se você rolar a tela para baixo, o conteúdo vai sair da visualização. Mas, assim que você rola para cima, ele volta, 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 é stateful.
Outro problema é o estilo das barras de rolagem. Eles são notoriamente não estilosos ou, pelo menos, não têm estilo suficiente. E se eu quiser um gato Nyanyan como barra de rolagem? Seja qual for a técnica escolhida, criar uma barra de rolagem personalizada não é eficiente nem fácil.
O ponto é que todas essas coisas são estranhas e difíceis de serem implementadas
com eficiência. A maioria deles depende de eventos e/ou
requestAnimationFrame
, que podem manter você a 60 fps, mesmo quando a tela é
capaz de executar a 90 fps, 120 fps ou mais e usar uma fração do
precioso orçamento de frames da linha de execução principal.
A worklet de animação estende os recursos da pilha de animações da Web para facilitar esse tipo de efeito. Antes de começarmos, vamos conferir se você está por dentro dos conceitos básicos de animações.
Uma introdução sobre animações e linhas do tempo
A WAAPI e o Animation Worklet fazem uso extensivo de linhas do tempo para permitir orquestrar animações e efeitos da maneira que você quiser. Esta seção é uma revisão rápida 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 existir. Todas
as animações de um documento funcionam em relação a essa linha do tempo.
Para ir mais além, vamos conferir 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 3000ms, o que significa que ela
vai começar (ou se tornar "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 the
duraçãooptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000`. O ponto é que o cronograma controla onde estamos na animação.
Quando a animação atinge o último frame-chave, ela retorna ao primeiro
frame e inicia a próxima iteração da animação. Esse processo é repetido um
total de três vezes desde que definimos iterations: 3
. Se você quiser que a animação
nunca pare, escreva iterations: Number.POSITIVE_INFINITY
. Veja o
resultado do código
acima.
A WAAPI é incrivelmente eficiente, e há muitos outros recursos nessa API, como easing, deslocamento inicial, ponderação de frame-chave e comportamento de preenchimento, o que vai contrariar o escopo deste artigo. Para saber mais, recomendamos a leitura deste artigo sobre animações CSS em truques do CSS (em inglês).
Como gravar um worklet de animação
Agora que entendemos o conceito de linhas do tempo, podemos começar a analisar o Animation Worklet e como ele permite que você mexa nas linhas do tempo. A API AnimationWorklet não é apenas baseada na WAAPI, mas é, no sentido da Web extensível, uma primitiva de nível inferior que explica como a WAAPI funciona. Em termos de sintaxe, elas são muito semelhantes:
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 gera essa animação.
Detecção de recursos
O Chrome é o primeiro navegador a oferecer esse recurso. Portanto, verifique se o
código não espera que AnimationWorklet
esteja presente. Portanto, antes de carregar o
worklet, precisamos detectar se o navegador do usuário oferece suporte a
AnimationWorklet
com uma verificação simples:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Como carregar um worklet
Os worklets são um novo conceito introduzido pelo grupo de trabalho do Houdini para facilitar a criação e a escalabilidade de muitas das novas APIs. Vamos abordar os detalhes dos worklets um pouco mais tarde, mas, para simplificar, pense neles como threads baratas e leves (como workers) por enquanto.
Precisamos garantir 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 um animador usando a
chamada registerAnimator()
da AnimationWorklet, a ela o nome "passthrough".
É o mesmo nome que usamos no construtor WorkletAnimation()
acima. Quando o
registro for concluído, a promessa retornada por addModule()
será resolvida, e
poderemos começar a criar animações usando esse 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
e o efeito que está sendo processado. Temos apenas um
efeito, o KeyframeEffect
, e estamos usando currentTime
para definir o
localTime
do efeito. Por isso, esse animador é chamado de "passthrough". Com esse código para
o worklet, a WAAPI e a AnimationWorklet acima se comportam exatamente da
mesma forma, como você pode conferir na
demonstração.
Tempo
O parâmetro currentTime
do método animate()
é o currentTime
da
linha do tempo transmitida ao construtor WorkletAnimation()
. No exemplo anterior, acabamos de passar esse tempo para o efeito. Mas como esse é
código JavaScript, e 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)
);
}
}
);
Estamos usando o Math.sin()
do currentTime
e remapeando esse valor para
o intervalo [0; 2000], que é o período em que o efeito é definido. Agora
a animação está 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 que você defina de forma programática quais efeitos são
reproduzidos em qual ordem e em que extensão.
Opções sobre opções
Talvez você queira reutilizar um worklet e alterar seus números. 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, ambas as animações são controladas pelo mesmo código, mas com opções diferentes.
Me diga seu estado local.
Como mencionei antes, um dos principais problemas que o worklet de animação pretende 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, o worklet de animação oferece um hook que
é chamado antes de um worklet ser destruído e que pode ser usado para retornar um objeto
de estado. Esse objeto será transmitido ao construtor quando o worklet for
criado novamente. 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 girará. Se o navegador desmontassem
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 a direção
escolhida aleatoriamente como estado e a usamos no construtor, se fornecido.
Como conectar ao continuum espaço-tempo: ScrollTimeline
Como mostrado na seção anterior, o AnimationWorklet permite que
definamos de forma programática como o avanço da linha do tempo afeta os efeitos da
animação. No entanto, até agora, nossa linha do tempo sempre foi document.timeline
, que
monitora o tempo.
ScrollTimeline
abre novas possibilidades e permite gerar animações
com rolagem em vez de tempo. Vamos reutilizar nosso primeiro
worklet de "passagem" 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 uma nova ScrollTimeline
.
Talvez você tenha adivinhado, ScrollTimeline
não usa tempo, mas a
posição de rolagem do scrollSource
para definir o currentTime
no worklet. O
rolagem até o topo (ou à esquerda) significa currentTime = 0
, enquanto
a 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 um ScrollTimeline
com um elemento que não pode ser rolado, o
currentTime
da linha do tempo será NaN
. Portanto, especialmente com o design responsivo em
mente, você deve estar sempre preparado para NaN
como seu currentTime
. Muitas vezes, é sensato definir como padrão um valor de 0.
A vinculação de animações à posição de rolagem é algo que há muito tempo é buscado, mas nunca foi alcançado neste nível de fidelidade (exceto soluções alternativas com CSS3D). O Animation Worklet permite que esses efeitos sejam implementados de maneira simples e com alta performance. Por exemplo, um efeito de rolagem de paralaxe como este demonstração mostra que agora são necessárias apenas algumas linhas para definir uma animação orientada por rolagem.
Configurações avançadas
Worklets
Os worklets são contextos JavaScript com um escopo isolado e uma superfície de API muito pequena. A pequena superfície 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 as linhas de execução conforme necessário. Isso é especialmente importante para AnimationWorklet.
Compositor NSync
Você deve saber que algumas propriedades 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 reorganizar o layout do documento inteiro.
No Chrome (como em muitos outros navegadores), temos um processo chamado compositor, cujo trabalho é (e estou simplificando muito aqui) para organizar camadas e texturas e, em seguida, utilizar a GPU para atualizar a tela com a maior frequência possível, de preferência o mais rápido possível (geralmente 60 Hz). Dependendo de quais propriedades do CSS estão sendo animadas, o navegador pode precisar apenas do compositor para fazer o trabalho, enquanto outras propriedades precisam executar o layout, que é uma operação que só 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 executado em uma linha de execução separada em sincronia com o compositor.
Toque no pulso
Geralmente, há apenas um processo de compositor que pode ser compartilhado entre várias guias, já que a GPU é um recurso de alta concorrência. Se o compositor for bloqueado de alguma forma, todo o navegador vai parar e não vai mais responder à entrada do usuário. Isso precisa ser evitado a todo custo. O que acontece se o worklet não conseguir entregar os dados necessários ao compositor a tempo para que o frame seja renderizado?
Se isso acontecer, o worklet poderá "escorregar", de acordo com a especificação. Ele fica atrás do compositor, e ele tem permissão para reutilizar os dados do último frame para manter o frame rate alto. Visualmente, isso vai parecer instável, mas a grande diferença é que o navegador ainda responde à entrada do usuário.
Conclusão
O AnimationWorklet e os benefícios que ele traz para a Web têm muitas facetas. Os benefícios óbvios são mais controle sobre as animações e novas maneiras de gerar animações para trazer um novo nível de fidelidade visual para a Web. No entanto, o design das APIs também permite que você torne seu app mais resistente a saltos, além de acessar todas as novas funcionalidades ao mesmo tempo.
O Animation Worklet está no Canary, e nosso objetivo é um teste de origem com o Chrome 71. Estamos ansiosos para saber mais sobre suas novas experiências na Web e o que podemos melhorar. Há também um polyfill que oferece a mesma API, mas não oferece o isolamento de desempenho.
Lembre-se de que as transições e animações CSS ainda são opções válidas e podem ser muito mais simples para animações básicas. Mas, se você precisar de algo mais sofisticado, o AnimationWorklet está aqui para ajudar.