Turbine as animações do seu web app
TL;DR:o Animation Worklet permite escrever animações imperativas que são executadas na taxa de frames nativa do dispositivo para uma suavidade extra sem travamentos™, tornar as animações mais resilientes contra travamentos da linha de execução principal e podem ser vinculadas à rolagem em vez de tempo. O Animation Worklet está no Chrome Canary (atrás da flag "Recursos experimentais da plataforma da Web"), e estamos planejando um teste de origem para o Chrome 71. Você pode começar a usar 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 você quiser animar qualquer elemento DOM na Web hoje, terá duas opções e meia: transições CSS para transições simples de A para B, animações CSS para animações potencialmente 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á 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. Mas alguns dos efeitos que os desenvolvedores estão tentando não são controlados pelo tempo nem sem estado. Por exemplo, o famoso scroller de paralaxe é, como o nome sugere, controlado por rolagem. Implementar um scroller de paralaxe eficiente na Web hoje em dia é surpreendentemente difícil.
E quanto ao uso de elementos sem estado? Pense na barra de endereço do Chrome no Android, por exemplo. Se você rolar a tela para baixo, ele 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 é com estado.
Outro problema é estilizar barras de rolagem. Eles são notoriamente difíceis de estilizar, ou pelo menos não são estilizados o suficiente. E se eu quiser um nyan cat como minha barra de rolagem? Qualquer técnica que você escolha, criar uma barra de rolagem personalizada não é eficiente nem fácil.
O problema é que todas essas coisas são difíceis e quase impossíveis de implementar com eficiência. A maioria deles depende de eventos e/ou
requestAnimationFrame
, o que pode manter você em 60 fps, mesmo quando a tela é
capaz de rodar a 90 fps, 120 fps ou mais e usa uma fração do
precioso orçamento de frames da linha de execução principal.
O worklet de animação amplia os recursos da pilha de animações da Web para facilitar esse tipo de efeito. Antes de começarmos, vamos garantir que estamos atualizados sobre os conceitos básicos de animações.
Uma introdução sobre animações e linhas do tempo
A WAAPI e o Animation Worklet usam muito as linhas do tempo para permitir que você orquestre animações e efeitos da maneira que quiser. Esta seção é uma atualização rápida ou uma 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 deixar as coisas um pouco mais concretas, 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 horário de início. Nossa animação tem um atraso de 3.000 ms, o que significa que ela vai começar (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 the
durationoptions. 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 a linha do tempo controla onde estamos na animação.
Quando a animação chegar ao último frame-chave, ela vai voltar para o primeiro e iniciar a próxima iteração. Esse processo se repete um total de três vezes, já que definimos iterations: 3
. Se quisermos que a animação nunca pare, vamos escrever iterations: Number.POSITIVE_INFINITY
. Confira o resultado do código acima.
A WAAPI é muito poderosa e tem muitos outros recursos, como facilitação, deslocamentos de início, ponderações de keyframe e comportamento de preenchimento, que excedem o escopo deste artigo. Se quiser saber mais, recomendo ler este artigo sobre animações CSS no CSS Tricks (em inglês).
Como escrever 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 mexer com as linhas do tempo. A API Animation Worklet não é apenas baseada na WAAPI, mas é, no sentido da Web extensível, uma primitiva de nível mais baixo que explica como as funções da WAAPI. 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. Portanto, verifique se o
código não espera apenas que AnimationWorklet
esteja lá. Portanto, antes de carregar o
worklet, detecte se o navegador do usuário tem suporte para
AnimationWorklet
com uma verificação simples:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Como carregar um worklet
Worklets são um novo conceito introduzido pelo grupo de trabalho do Houdini para facilitar a criação e o escalonamento de muitas das novas APIs. Vamos abordar os detalhes dos worklets um pouco mais tarde, mas, por simplicidade, pense neles como threads baratos 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()
do 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 o AnimationWorklet acima se comportam exatamente da mesma forma, como você pode ver na demonstração.
Tempo
O parâmetro currentTime
do método animate()
é o currentTime
da linha do tempo que transmitimos ao construtor WorkletAnimation()
. No exemplo anterior, apenas transmitimos 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)
);
}
}
);
Estamos usando o Math.sin()
do currentTime
e remapeando esse valor para o intervalo [0; 2000], que é o período em que nosso efeito é definido. Agora
a animação está muito diferente, sem que os frames-chave ou as opções da animação tenham sido alterados. O código do worklet pode ser arbitrariamente complexo e permite definir de forma programática quais efeitos são reproduzidos, em qual ordem e em qual extensão.
Opções sobre opções
Talvez você queira reutilizar um worklet e mudar os números dele. Por isso, 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 geradas com o 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 as
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
outra linha de execução 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 chamado antes da destruição de um worklet, que pode ser usado para retornar um objeto de estado. Esse objeto será transmitido ao 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 de em qual direção o quadrado vai girar. Se o navegador desmontar o worklet e migrá-lo para uma linha de execução diferente, haverá outra chamada Math.random()
na criação, o que pode causar uma mudança repentina de direção. Para evitar isso, retornamos a direção escolhida aleatoriamente das animações como um estado e a usamos no construtor, se fornecida.
Conectando-se ao contínuo espaço-tempo: ScrollTimeline
Como mostrado na seção anterior, o AnimationWorklet permite definir programaticamente 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
acompanha o tempo.
O ScrollTimeline
abre novas possibilidades e permite controlar 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 novo ScrollTimeline
.
Como você já deve ter imaginado, ScrollTimeline
não usa o tempo, mas a posição de rolagem do
scrollSource
para definir o currentTime
no worklet. Rolar até o topo (ou esquerda) significa currentTime = 0
, enquanto rolar 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 rola, o
currentTime
da linha do tempo será NaN
. Portanto, especialmente com o design responsivo em mente, você sempre deve estar preparado para NaN
como seu currentTime
. Muitas vezes, é sensato usar o valor padrão 0.
Vincular animações à posição de rolagem é algo que sempre foi procurado, mas nunca foi realmente alcançado nesse nível de fidelidade, exceto por 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 paralaxe como este demonstração mostra que agora são necessárias apenas algumas linhas para definir uma animação controlada por 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 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 threads conforme necessário. Isso é especialmente importante para AnimationWorklet.
Compositor NSync
Você sabe que algumas propriedades CSS são rápidas para animar, enquanto outras não. Algumas propriedades só precisam de um pouco de trabalho na GPU para serem animadas, enquanto outras forçam o navegador a reestruturar todo o documento.
No Chrome (assim como em muitos outros navegadores), temos um processo chamado compositor, cuja função é (e estou simplificando muito aqui) organizar camadas e texturas e usar a GPU para atualizar a tela com a maior frequência possível, idealmente tão rápido quanto a tela pode atualizar (normalmente 60 Hz). Dependendo das propriedades CSS que estão sendo animadas, o navegador pode precisar apenas que o compositor faça o trabalho dele, enquanto outras propriedades precisam executar o layout, que é uma operação que apenas 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.
Punição leve
Geralmente, há apenas um processo de compositor, que pode ser compartilhado em várias guias, já que a GPU é um recurso muito disputado. Se o compositor ficar bloqueado de alguma forma, todo o navegador vai parar de funcionar e não vai responder à entrada do usuário. Isso precisa ser evitado a todo custo. O que acontece se o worklet não conseguir entregar os dados de que o compositor precisa a tempo para a renderização do frame?
Se isso acontecer, o worklet poderá "escorregar", de acordo com a especificação. Ele fica atrás do compositor, que pode reutilizar os dados do último frame para manter a taxa de frames alta. Visualmente, isso vai parecer um jank, mas a grande diferença é que o navegador ainda responde à entrada do usuário.
Conclusão
O AnimationWorklet tem muitas facetas e benefícios para a Web. Os benefícios óbvios são mais controle sobre as animações e novas maneiras de impulsionar 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 a instabilidades e, ao mesmo tempo, ter acesso a todos os novos recursos.
O Animation Worklet está no Canary, e nosso objetivo é fazer um teste de origem com o Chrome 71. Estamos ansiosos para conhecer suas novas experiências incríveis na Web e saber o que podemos melhorar. Há também um polyfill que oferece a mesma API, mas não o isolamento de performance.
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 vai ajudar.