O desfoque é uma ótima maneira de redirecionar o foco do usuário. Fazer com que alguns elementos visuais pareçam desfocados enquanto mantém outros em foco direciona naturalmente o foco do usuário. Os usuários ignoram o conteúdo desfocado e se concentram no conteúdo que podem ler. Um exemplo seria uma lista de ícones que mostram detalhes sobre os itens individuais quando o cursor passa por eles. Durante esse período, as opções restantes podem ser desfocadas para redirecionar o usuário às informações recém-exibidas.
Texto longo, leia o resumo
Animar um desfoque não é uma opção, porque é muito lento. Em vez disso, pré-calcule uma série de versões cada vez mais desfocadas e faça um crossfade entre elas. Meu colega Yi Gu escreveu uma biblioteca para cuidar de tudo para você. Confira nossa demonstração.
No entanto, essa técnica pode ser bastante perturbadora quando aplicada sem nenhum período de transição. Animar um desfoque, fazendo a transição de desfocado para focado, parece uma escolha razoável, mas se você já tentou fazer isso na Web, provavelmente descobriu que as animações não são nada suaves, como mostra esta demonstração se você não tiver uma máquina potente. Podemos melhorar?
O problema
No momento, não é possível fazer a animação de um desfoque funcionar de maneira eficiente. No entanto, podemos
encontrar uma solução alternativa que parece boa o suficiente, mas que, tecnicamente falando, não
é um desfoque animado. Para começar, vamos entender por que o desfoque animado
é lento. Para desfocar elementos na Web, há duas técnicas: a propriedade CSS filter
e os filtros SVG. Graças ao aumento do suporte e à facilidade de uso, os filtros
CSS são normalmente usados. Se você precisar oferecer suporte ao Internet
Explorer, não terá outra escolha a não ser usar filtros SVG, já que o IE 10 e o 11 oferecem suporte
a eles, mas não aos filtros CSS. A boa notícia é que nossa solução alternativa para animar um
desfoque funciona com as duas técnicas. Vamos tentar encontrar o gargalo usando o DevTools.
Se você ativar a opção "Paint Flashing" nas Ferramentas do desenvolvedor, não vai aparecer nenhum flash. Parece que não há repintagens acontecendo. Isso é tecnicamente correto, já que uma "repintura" se refere à CPU que precisa repintar a textura de um elemento promovido. Sempre que um elemento é promovido e desfocado, o desfoque é aplicado pela GPU usando um sombreador.
Os filtros SVG e CSS usam filtros de convolução para aplicar um desfoque. Os filtros de convolução são bastante caros, já que, para cada pixel de saída, é preciso considerar um número de pixels de entrada. Quanto maior a imagem ou o raio de desfoque, mais caro será o efeito.
E é aí que está o problema. Estamos executando uma operação de GPU bastante cara em cada frame, excedendo nosso orçamento de frame de 16 ms e, portanto, terminando bem abaixo de 60 fps.
No fundo do poço
O que podemos fazer para que isso funcione bem? Podemos usar um truque! Em vez
de animar o valor de desfoque real (o raio do desfoque), pré-calculamos algumas
cópias desfocadas em que o valor de desfoque aumenta exponencialmente e, em seguida,
fazemos a transição entre elas usando opacity
.
O cross-fade é uma série de transições de opacidade sobrepostas. Se temos quatro estágios de desfoque, por exemplo, desfocamos o primeiro e ativamos o segundo ao mesmo tempo. Quando o segundo estágio atinge 100% de opacidade e o primeiro atinge 0%, o segundo desaparece enquanto o terceiro aparece. Depois disso, finalmente desfocamos a terceira etapa e desfocamos na quarta e última versão. Nesse cenário, cada etapa levaria ¼ da duração total desejada. Visualmente, isso se parece muito com um desfoque animado real.
Em nossos experimentos, aumentar o raio de desfoque exponencialmente por fase gerou
os melhores resultados visuais. Exemplo: se tivermos quatro estágios de desfoque, aplicaremos
filter: blur(2^n)
a cada um deles, ou seja, estágio 0: 1px, estágio 1: 2px, estágio 2: 4px
e estágio 3: 8px. Se forçarmos cada uma dessas cópias desfocadas na própria camada
(chamada de "promoção") usando will-change: transform
, a mudança de opacidade nesses
elementos será super-rápida. Em teoria, isso nos permitiria
carregar antecipadamente o trabalho caro de desfoque. Acontece que a lógica está errada. Se
você executar esta demonstração,
vai notar que a taxa de frames ainda está abaixo de 60 fps e que o desfoque está
pior do que antes.
Uma rápida olhada nas Ferramentas do desenvolvedor revela que a GPU ainda está extremamente ocupada e esticando cada frame para cerca de 90 ms. Mas por quê? Não estamos mais mudando o valor de desfoque, apenas a opacidade. O que está acontecendo? O problema está, mais uma vez, na natureza do efeito de desfoque: como explicado anteriormente, se o elemento for promovado e desfocado, o efeito será aplicado pela GPU. Portanto, mesmo que não estejamos mais animando o valor de desfoque, a textura ainda não está desfocada e precisa ser desfocada novamente em cada frame pela GPU. A razão pela qual a taxa de frames está ainda pior do que antes vem do fato de que, em comparação com a implementação simples, a GPU tem mais trabalho do que antes, já que a maior parte do tempo duas texturas estão visíveis e precisam ser desfocadas de forma independente.
O que criamos não é bonito, mas deixa a animação extremamente rápida. Voltamos a não promover o elemento a ser desfocado, mas promovemos um wrapper pai. Se um elemento for desfocado e promovido, o efeito será aplicado pela GPU. Isso é o que deixou nossa demonstração lenta. Se o elemento estiver desfocado, mas não promovido, o desfoque será rasterizado para a textura mãe mais próxima. No nosso caso, esse é o elemento de wrapper pai promovido. A imagem desfocada agora é a textura do elemento pai e pode ser reutilizada para todos os frames futuros. Isso só funciona porque sabemos que os elementos desfocados não são animados e armazená-los em cache é realmente benéfico. Confira uma demonstração que implementa essa técnica. O que o Moto G4 acha dessa abordagem? Alerta de spoiler: ele acha que é ótimo:
Agora temos muito espaço na GPU e 60 fps. Conseguimos!
Como colocar em produção
Na nossa demonstração, duplicamos uma estrutura DOM várias vezes para ter cópias do conteúdo para desfocar com diferentes intensidades. Talvez você esteja se perguntando como isso funcionaria em um ambiente de produção, já que isso pode ter alguns efeitos colaterais inesperados com os estilos CSS do autor ou até mesmo com o JavaScript. Você tem razão. Entre no Shadow DOM.
Embora a maioria das pessoas pense no Shadow DOM como uma maneira de anexar elementos "internos" aos elementos personalizados, ele também é uma
primitiva de isolamento e desempenho. O JavaScript e o CSS não podem perfurar os limites do DOM
Shadow, o que nos permite duplicar conteúdo sem interferir nos
estilos ou na lógica do aplicativo do desenvolvedor. Já temos um elemento <div>
para
cada cópia rasterizar e agora usamos esses <div>
s como hosts de sombra. Criamos
um ShadowRoot
usando attachShadow({mode: 'closed'})
e anexamos uma cópia do
conteúdo ao ShadowRoot
em vez do <div>
. Precisamos
copiar todas as folhas de estilo para o ShadowRoot
para garantir que nossas
cópias tenham o mesmo estilo que o original.
Alguns navegadores não oferecem suporte ao Shadow DOM v1. Para esses casos, duplicamos o conteúdo e torcemos para que nada quebre. Poderíamos usar o polyfill de DOM de sombra com ShadyCSS, mas não implementamos isso na nossa biblioteca.
Pronto. Depois de nossa jornada pelo pipeline de renderização do Chrome, descobrimos como podemos animar desfoque de forma eficiente em todos os navegadores.
Conclusão
Esse tipo de efeito não deve ser usado de forma leviana. Como copiamos elementos
DOM e os forçamos na própria camada, podemos ultrapassar os limites de dispositivos
de baixo custo. Copiar todas as folhas de estilo em cada ShadowRoot
também é um possível
risco de desempenho. Portanto, decida se você prefere ajustar
sua lógica e estilos para não serem afetados por cópias no LightDOM
ou usar nossa
técnica ShadowDOM
. No entanto, às vezes, nossa técnica pode ser um investimento
valioso. Confira o código no nosso repositório do GitHub
e a
demonstração.
Entre em contato comigo no Twitter se tiver dúvidas.