Turbine as animações da sua app da Web
Resumo:o worklet de animação permite que você escreva animações imperativas que são executadas na taxa de frames nativa do dispositivo para uma fluidez extra sem engasgos, torne suas animações mais resistentes contra engasgos da linha de execução principal e que podem ser vinculadas à 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 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 DOM na Web hoje, tem duas opções e meia: Transições CSS para transições simples de A para B, Animações CSS para animações baseadas em tempo potencialmente cíclicas e mais complexas e a 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. Implementar um scroller de paralaxe com bom desempenho na Web hoje em dia é surpreendentemente difícil.
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 é estilizar barras de rolagem. Eles não podem ser estilizados, ou pelo menos não o 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.
O ponto é que todas essas coisas são estranhas e difíceis ou impossíveis de
implementar de maneira eficiente. 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.
O Animation Worklet 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.
Introdução às animações e linhas do tempo
A WAAPI e a 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 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 deixar as coisas um pouco mais concretas, 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
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 nossa animação.
Quando a animação alcançar o último frame-chave, ela vai voltar ao primeiro
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 você quiser que a animação
nunca pare, escreva iterations: Number.POSITIVE_INFINITY
. Confira o
resultado do código
acima.
A WAAPI é incrivelmente poderosa e tem muitos outros recursos, como easing, início de deslocamento, ponderação de keyframe e comportamento de preenchimento que excederiam o escopo deste artigo. Se quiser saber mais, leia este artigo sobre animações CSS no CSS Tricks.
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 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, transmitimos esse tempo para o efeito. Mas, como este é 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 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 mudar os números dele. 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 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ê atualiza esta demonstração, há uma chance de 50/50
de que o quadrado gire em uma direção. Se o navegador desinstalar
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 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. Mas até agora, nosso cronograma sempre foi document.timeline
, que
rastreia o tempo.
O ScrollTimeline
abre novas possibilidades e permite que você execute 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 uma nova ScrollTimeline
.
Você pode ter 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 uma 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ê precisa estar sempre preparado para NaN
como currentTime
. Muitas vezes,
é recomendável usar o valor padrão 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ê pode saber que algumas propriedades CSS são animadas rapidamente, enquanto outras não são. Algumas propriedades precisam apenas de algum trabalho na GPU para serem animadas, enquanto outras forçam o navegador a refazer o layout de todo o documento.
No Chrome (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 o mais regularmente possível, de preferência tão rápido quanto possível (normalmente 60 Hz). Dependendo de quais propriedades 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 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.
Dar uma lição
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 para trás do compositor, e o compositor pode reutilizar os dados do último frame para manter a taxa de frames. 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 animações e novas maneiras de gerar animações para trazer um novo nível de fidelidade visual à 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.