Xlera8Name

Um efeito de foco sofisticado para o seu avatar

Você conhece aquele tipo de efeito em que a cabeça de alguém está saindo de um círculo ou buraco? A famosa animação Porky Pig, onde ele dá adeus enquanto sai de uma série de anéis vermelhos, é o exemplo perfeito, e Kilian Valkhof realmente recriou isso aqui no CSS-Tricks um tempo atrás.

Eu tenho uma ideia semelhante, mas abordada de uma maneira diferente e com uma pitada de animação. Eu acho que é bastante prático e cria um efeito de foco que você pode usar em algo como seu próprio avatar.

Veja isso? Faremos uma animação em escala onde o avatar parece sair do círculo em que está. Legal, certo? Não olhe para o código e vamos construir esta animação juntos passo a passo.

O HTML: apenas um elemento

Se você ainda não conferiu o código do demo e está se perguntando quantos divs isso levará, então pare por aí, porque nossa marcação nada mais é do que um único elemento de imagem:

<img src="" alt="">

Sim, um único elemento! A parte desafiadora deste exercício é usar a menor quantidade de código possível. Se você foi me seguindo por um tempo, você deve estar acostumado com isso. Eu tento muito encontrar soluções CSS que possam ser alcançadas com o código menor e mais sustentável possível.

Escrevi uma série de artigos aqui no CSS-Tricks, onde exploro diferentes efeitos de foco usando a mesma marcação HTML contendo um único elemento. Eu entro em detalhes sobre gradientes, máscaras, recortes, contornos e até mesmo técnicas de layout. Eu recomendo verificar isso porque vou reutilizar muitos dos truques deste post.

Um arquivo de imagem quadrado com fundo transparente funcionará melhor para o que estamos fazendo. Aqui está o que estou usando, se você quiser começar com isso.

Projetado por cang

Espero ver muitos exemplos disso possíveis usando imagens reais - então, por favor, compartilhe seu resultado final nos comentários quando terminar, para que possamos criar uma coleção!

Antes de entrar no CSS, vamos primeiro dissecar o efeito. A imagem fica maior ao passar o mouse, então com certeza usaremos transform: scale() lá. Há um círculo atrás do avatar e um gradiente radial deve funcionar. Por fim, precisamos criar uma borda na parte inferior do círculo que crie a aparência do avatar atrás do círculo.

Vamos ao trabalho!

O efeito de escala

Vamos começar adicionando a transformação:

img { width: 280px; aspect-ratio: 1; cursor: pointer; transition: .5s;
}
img:hover { transform: scale(1.35);
}

Nada complicado ainda, certo? Vamos continuar.

O circulo

Dissemos que o fundo seria um gradiente radial. Isso é perfeito porque podemos criar limites rígidos entre as cores de um gradiente radial, o que faz parecer que estamos desenhando um círculo com linhas sólidas.

img { --b: 5px; /* border width */ width: 280px; aspect-ratio: 1; background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), #C02942 calc(100% - var(--b)) 99%, #0000 ); cursor: pointer; transition: .5s;
}
img:hover { transform: scale(1.35);
}

Observe a variável CSS, --b, estou usando lá. Ele representa a espessura da “borda” que na verdade está sendo usada apenas para definir as paradas de cores sólidas para a parte vermelha do gradiente radial.

O próximo passo é brincar com o tamanho do gradiente ao passar o mouse. O círculo precisa manter seu tamanho à medida que a imagem cresce. Como estamos aplicando um scale() transformação, nós realmente precisamos diminuir o tamanho do círculo porque, caso contrário, ele aumenta com o avatar. Então, enquanto a imagem aumenta, precisamos que o gradiente diminua.

Vamos começar definindo uma variável CSS, --f, que define o “fator de escala”, e use-o para definir o tamanho do círculo. estou a usar 1 como o valor padrão, pois é a escala inicial para a imagem e o círculo do qual transformamos.

Aqui está uma demonstração para ilustrar o truque. Passe o mouse para ver o que está acontecendo nos bastidores:

Eu adicionei uma terceira cor ao radial-gradient para identificar melhor a área do gradiente ao passar o mouse:

radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), #C02942 calc(100% - var(--b)) 99%, lightblue
);

Agora temos que posicionar nosso plano de fundo no centro do círculo e garantir que ele ocupe toda a altura. Gosto de declarar tudo direto no background propriedade abreviada, para que possamos adicionar nosso posicionamento de plano de fundo e garantir que ele não se repita, acrescentando esses valores logo após o radial-gradient():

background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;

O fundo é colocado no centro (50%), tem uma largura igual a calc(100%/var(--f)), e tem uma altura igual a 100%.

Nada escala quando --f é igual a 1 — novamente, nossa escala inicial. Enquanto isso, o gradiente ocupa toda a largura do contêiner. quando nós aumentarmos --f, o tamanho do elemento aumenta — graças ao scale() transform — e o tamanho do gradiente diminui.

Aqui está o que obtemos quando aplicamos tudo isso à nossa demonstração:

Estamos chegando mais perto! Temos o efeito de estouro na parte superior, mas ainda precisamos ocultar a parte inferior da imagem, para que pareça que ela está saindo do círculo em vez de ficar na frente dele. Essa é a parte complicada de tudo isso e é o que faremos a seguir.

A borda inferior

Primeiro tentei resolver isso com o border-bottom propriedade, mas não consegui encontrar uma maneira de corresponder o tamanho da borda ao tamanho do círculo. Aqui está o melhor que consegui e você pode ver imediatamente que está errado:

A solução real é usar o outline propriedade. Sim, outline, não border. em um artigo anterior, eu mostro como outline é poderoso e nos permite criar efeitos legais de foco. Combinado com outline-offset, temos exatamente o que precisamos para o nosso efeito.

A ideia é definir um outline na imagem e ajuste seu deslocamento para criar a borda inferior. O deslocamento dependerá do fator de escala da mesma forma que o tamanho do gradiente.

Agora temos nossa “borda” inferior (na verdade, uma outline) combinado com a “borda” criada pelo gradiente para criar um círculo completo. Ainda precisamos ocultar partes do outline (do topo e dos lados), que veremos em um momento.

Aqui está nosso código até agora, incluindo mais algumas variáveis ​​CSS que você pode usar para configurar o tamanho da imagem (--s) e a cor da “borda” (--c):

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ width: var(--s); aspect-ratio: 1; cursor: pointer; border-radius: 0 0 999px 999px; outline: var(--b) solid var(--c); outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b)); background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000 ) 50% / calc(100% / var(--f)) 100% no-repeat; transform: scale(var(--f)); transition: .5s;
}
img:hover { --f: 1.35; /* hover scale */
}

Como precisamos de uma borda inferior circular, adicionamos um border-radius na parte inferior, permitindo outline para corresponder à curvatura do gradiente.

O cálculo usado em outline-offset é muito mais simples do que parece. Por padrão, outline está desenhado lado de fora da caixa do elemento. E no nosso caso, precisamos que sobreposição o elemento. Mais precisamente, precisamos que ele siga o círculo criado pelo gradiente.

Diagrama da transição de fundo.

Quando dimensionamos o elemento, vemos o espaço entre o círculo e a borda. Não esqueçamos que a ideia é manter o círculo do mesmo tamanho após a execução da transformação de escala, o que nos deixa com o espaço que usaremos para definir o deslocamento do contorno conforme ilustrado na figura acima.

Não vamos esquecer que o segundo elemento é dimensionado, então nosso resultado também é dimensionado... o que significa que precisamos dividir o resultado por f para obter o valor de deslocamento real:

Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2

Adicionamos um sinal negativo, pois precisamos que o contorno vá de fora para dentro:

Offset = (1/f - 1) * S/2

Aqui está uma demonstração rápida que mostra como o contorno segue o gradiente:

Você já pode vê-lo, mas ainda precisamos que o contorno inferior se sobreponha ao círculo, em vez de deixá-lo sangrar por ele. Podemos fazer isso removendo o tamanho da borda do deslocamento:

outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b));

Agora precisamos descobrir como remover a parte superior do contorno. Em outras palavras, queremos apenas a parte inferior da imagem outline.

Primeiro, vamos adicionar espaço na parte superior com preenchimento para ajudar a evitar a sobreposição na parte superior:

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ width: var(--s); aspect-ratio: 1; padding-block-start: calc(var(--s)/5); /* etc. */
}
img:hover { --f: 1.35; /* hover scale */
}

Não há lógica específica para esse preenchimento superior. A ideia é garantir que o contorno não toque na cabeça do avatar. Usei o tamanho do elemento para definir aquele espaço para ter sempre a mesma proporção.

Note que eu adicionei o content-box valor para o background:

background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000 ) 50%/calc(100%/var(--f)) 100% no-repeat content-box;

Precisamos disso porque adicionamos preenchimento e queremos apenas o plano de fundo definido para a caixa de conteúdo, portanto, devemos dizer explicitamente ao plano de fundo para parar aí.

Adicionando máscara CSS à mistura

Chegamos à última parte! Tudo o que precisamos fazer é esconder algumas peças e pronto. Para isso, contaremos com a mask propriedade e, claro, gradientes.

Aqui está uma figura para ilustrar o que precisamos esconder ou o que precisamos mostrar para ser mais preciso

Mostrando como a máscara se aplica à parte inferior do círculo.

A imagem da esquerda é a que temos atualmente e a da direita é a que queremos. A parte verde ilustra a máscara que devemos aplicar na imagem original para obter o resultado final.

Podemos identificar duas partes da nossa máscara:

  • Uma parte circular na parte inferior que tem a mesma dimensão e curvatura do gradiente radial que usamos para criar o círculo atrás do avatar
  • Um retângulo na parte superior que cobre a área dentro do contorno. Observe como o contorno está fora da área verde na parte superior — essa é a parte mais importante, pois permite que o contorno seja cortado de forma que apenas a parte inferior fique visível.

Aqui está o nosso CSS final:

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ --_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; --_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b)); width: var(--s); aspect-ratio: 1; padding-top: calc(var(--s)/5); cursor: pointer; border-radius: 0 0 999px 999px; outline: var(--b) solid var(--c); outline-offset: var(--_o); background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000) var(--_g); mask: linear-gradient(#000 0 0) no-repeat 50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%, radial-gradient( circle closest-side, #000 99%, #0000) var(--_g); transform: scale(var(--f)); transition: .5s;
}
img:hover { --f: 1.35; /* hover scale */
}

Vamos quebrar isso mask propriedade. Para começar, observe que um radial-gradient() do background propriedade está lá. Eu criei uma nova variável, --_g, para as partes comuns tornarem as coisas menos confusas.

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; mask: radial-gradient( circle closest-side, #000 99%, #0000) var(--_g);

A seguir, há um linear-gradient() lá também:

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; mask: linear-gradient(#000 0 0) no-repeat 50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%, radial-gradient( circle closest-side, #000 99%, #0000) var(--_g);

Isso cria a parte retangular da máscara. Sua largura é igual à largura do gradiente radial menos o dobro da espessura da borda:

calc(100% / var(--f) - 2 * var(--b))

A altura do retângulo é igual à metade, 50%, do tamanho do elemento.

Também precisamos do gradiente linear colocado no centro horizontal (50%) e deslocado do topo pelo mesmo valor que o deslocamento do contorno. Eu criei outra variável CSS, --_o, para o deslocamento que definimos anteriormente:

--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

Uma das coisas confusas aqui é que precisamos de um negativo deslocamento para o contorno (para movê-lo de fora para dentro), mas um positivo deslocamento para o gradiente (para mover de cima para baixo). Então, se você está se perguntando por que multiplicamos o deslocamento, --_o, de -1, bem, agora você sabe!

Aqui está uma demonstração para ilustrar a configuração de gradiente da máscara:

Passe o mouse acima e veja como tudo se move junto. A caixa do meio ilustra a camada de máscara composta por dois gradientes. Imagine-o como a parte visível da imagem à esquerda e você obterá o resultado final à direita!

Resumindo

Ufa, terminamos! E não apenas terminamos com uma animação de foco suave, mas fizemos tudo com um único HTML <img> elemento. Apenas isso e menos de 20 linhas de truques de CSS!

Claro, contamos com alguns pequenos truques e fórmulas matemáticas para chegar a um efeito tão complexo. Mas sabíamos exatamente o que fazer, pois identificamos antecipadamente as peças de que precisávamos.

Poderíamos ter simplificado o CSS se nos permitíssemos mais HTML? Absolutamente. Mas estamos aqui para aprender novos truques de CSS! Este foi um bom exercício para explorar gradientes CSS, mascaramento, o outline comportamento da propriedade, transformações e muito mais. Se você se sentiu perdido em algum momento, definitivamente confira minha série que usa os mesmos conceitos gerais. Às vezes, ajuda ver mais exemplos e casos de uso para enfatizar o assunto.

Vou deixar você com uma última demonstração que usa fotos de desenvolvedores populares de CSS. Não se esqueça de me mostrar uma demonstração com sua própria imagem para que eu possa adicioná-la à coleção!

Fale Conosco

Olá! Como posso ajudá-lo?