Estudando Efeito de Chuva Rainify
Essa é a explicação do funcionamento do efeito de chuva desenvolvido thiagotnon
. Eu analisei o projeto completo dele e reproduzi em uma versão simplificada, a fim de entender cada parte.
O componente que o thiagotnon
criou se encontra nesse link: Rainify.
O repositório de testes que fiz para entendimento a fundo do que acontece se encontra aqui: rainify-studing-logic. Existem apenas arquivos html
, css
e javasccript
, por simplificação.
Elementos Principais
Antes de ir para o código é importante entender o que é necessário fazer para que o efeito de chuva ocorra.
Primeiro, para simular a chuva é necessário ter duas entidades: a gota (drop
) e o respingo (splash
). A animação das gotas de chuva irá ocorrer sempre no meio da tela e os respingos irão ser animados sempre que as gotas chegarem na posição mais baixa da tela.
Os atributos que constituem uma gota são:
x
: Posição horizontalmente da gotay
: Posição verticalmente da gotalength
: Comprimento da gotavelocityX
: Velocidade no eixo horizontalvelocityY
: Velocidade no eixo vertical
Com isso, uma gota nada mais é do que uma linhas com uma posição e velocidade na tela.
Já os atributos que constituem um respingo são:
x
: Posição horizontalmente do respingoy
: Posição verticalmente do respingor
*: Raio do respingoopacity
*: Opacidade do respingoangle
*: Ângulo do respingovelocity
*: Velocidade de crescimento do respingo
Você deve ter ficado um pouco confuso nessa parte, pois um respingo com raio, opacidate, ângulo e velocidade. O que cada uma dessas coisas significa?
Bom, o que acontece é que a simulação do respingo é feita como uma círculo de raio r
que cresce a uma taxa igual a velocity
definida. Além disso, o efeito de desaparecimento do respingo ocorre por causa da opacidade. Além disso, quando uma gota de chuva chega ao solo, são criados 5 objetos respingos em ângulos diferentes. Se você visitar o site https://rainify.thiagonatan.dev.br/ e colocar o parâmetro splash
igual a 50, você vai notar exatamente esse efeito:
Sabendo desses elementos, agora é necessário saber quais as funções necessarias para montar o efeito da chuva.
Bom, é necessário criar a gota e o respingo. Depois fazer o update das posições de cada um deles, para obter o efeito de animação. Por fim, juntar tudo em algum lugar a animar tudo de uma vez.
É isso que vamos entender daqui para frente agora com o código.
Implementação (Código)
Estrutura (HTML
e CSS
)
Estruturei meu projeto em um projeto html
, css
e js
.
.
├── index.html
├── index.css
└── index.js
No html
eu coloquei os links para o css
e o js
e a tag canvas.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rainify Studing Logic</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<canvas id="rain"></canvas>
<script src="index.js"></script>
</body>
</html>
No css
, fiz umas estilizações básicas e coloquei o canvas para ocupar todo o tamanho da tela.
:root {
--bg-color: hsl(189 50% 5%);
}
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
}
canvas {
position: fixed;
bottom: 0;
left: 0;
z-index: 0;
pointer-events: none;
}
Implementação (JS
)
Agora no arquivo js
, eu comecei colocando os parâmetros de input
para controler de algumas propriedades da chuva:
const isRaining = true;
const intensity = 300;
const speed = 4;
const wind = - 4;
const thickness = 0.5;
const color = "rgba(255, 255, 255, 1)";
const splashColor = "rgba(255, 255, 255, 1)";
const splashDuration = 10;
Depois obtive o canvas
do html
, para poder usar as propriedades no js
.
const canvas = document.getElementById('rain');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Agora, é a renderização os elementos que compões a chuva, que são as gotas (drops
) e os respingos (splashes
).
Algo que achei bem interessante da implementação disso foi que as gotas são fixas, então não é necessário ficar criando e apagando objetos do html
.
const raindrops = Array.from({ length: intensity }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * -canvas.height,
length: Math.random() * 20 + 10,
velocityY: (Math.random() * 2 + 2) * speed,
velocityX: wind,
}))
const splashes = [];
Uma coisa a se atentar é são posições y
das gotas. Elas são inicialmente criadas em uma posição acima da tela, no qual o eixo y
é negativo. Isso permite um efeito mais natural de início de chuva.
De detalhes adicionais é que a velocidade de vento é igual a velocidade no eixo x e a velocidade no eixo y é aumentada segundo um fator para ficar mais rápida.
Continuando, para desenhar uma gota usa-se o a posição inicial (drop.x, drop.y)
até a posição final (drop.x + drop.velocityX, drop.y + drop.length)
e depois desenha uma linha ctx.stroke()
. Isso faz o formato da gota ser uma linha entre essas duas posições.
const drawRaindrop = (drop) => {
ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x + drop.velocityX, drop.y + drop.length);
ctx.strokeStyle = color;
ctx.lineWidth = thickness;
ctx.stroke();
}
Para desenhar o respingo usa-se o ctx.arc
, que é uma propriedade que desenha círculos no canvas. Os dois primeiros parâmetros são a posição do centro do círculo. O terceito parâmetro é o raio. Os dois ultimos são o início e o fim do arco.
Um detalhe importante aqui é essa regex no splashColor
. No caso, ele está mudando o valor numérico do ultimo número, que controla a opacidade, para a opacidade atual. Essa variável foi definida no início: const splashColor = "rgba(255, 255, 255, 1)"
.
const drawSplash = (splash) => {
ctx.beginPath();
ctx.arc(
splash.x + splash.radius * Math.cos(splash.angle),
splash.y + splash.radius * Math.sin(splash.angle),
splash.radius / 4,
0,
Math.PI * 2
);
ctx.fillStyle = splashColor.replace(/[\d.]+\)$/g, `${splash.opacity})`);
ctx.fill();
}
Agora, para o update da gota é executado o código abaixo.
const updateRaindrop = (drop) => {
drop.y += drop.velocityY;
drop.x += drop.velocityX;
if(drop.y > canvas.height) {
// Cria 5 respingos
for(let i = 0; i < 5; i++) {
splashes.push({
x: drop.x,
y: canvas.height,
radius: Math.random() * 2 + 1,
opacity: 1,
angle: Math.random() * 2 * Math.PI,
velocity: Math.random() * 2 + 0.5,
})
}
// Volta para a posição inicial
drop.y = 0 - drop.length;
drop.x = Math.random() * canvas.width;
}
// Efeito pacman
if(drop.x > canvas.width) {
drop.x = 0;
} else if (drop.x < 0) {
drop.x = canvas.width;
}
}
Explicando um pouco ele, nas primeiras linhas é feito o update de x
e y
baseado na velocidade da gota. Depois chega-se em dois casos interessantes.
O primeiro é caso a gota ultrapasse a altura, ou seja, chegue no solo (drop.y > canvas.height
). Nesse caso, ele cria 5 respingos, segundo o que comentei anteriormente. Depois ele volta a gota para a posição inicial, no topo. Só que não no formato anterior, no qual a gota era gerada em uma posição negativa grande da tela, mas sim de acordo com o comprimento da gota, a fim de deixar a gota o mais próxima possível do topo e dar um efeito natural de chuva.
O segundo caso é o que chamei de efeito pacman
, no qual a gota chega em alguma das extremidades da tela. Nesse caso, ela vai para a extremidade oposta. Isso é bom para manter uma fluidez das gotas caindo.
O update de respingo não é tão complexo. Ele só aumenta o raio e diminui a opacidade. Caso a opacidade seja menor que zero, então ele remove o respingo do array splashes
.
const updateSplash = (splash, index) => {
splash.radius += splash.velocity;
splash.opacity -= 1 / splashDuration;
if(splash.opacity <= 0) {
splashes.splice(index, 1);
}
}
Com tudo isso feito, agora é animar. Antes de tudo ele remove qualquer desenho anterior com o clearRect
. Depois ele faz um for nas gotas e nas respingos, atualizando cada um deles. E depois chega a parte mais importante para animar, que é feita pelo window.requestAnimationFrame(animate)
.
Essa função agenda uma próxima chamada para a função animate
, que vai ser executada antes do próximo repintar da tela. Isso permite que a função de animate
seja executada de maneira suave e sincronizada com a taxa de atualização do display.
const animate = () => {
if(!isRaining) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
raindrops.forEach((drop) => {
drawRaindrop(drop);
updateRaindrop(drop);
});
splashes.forEach((splash, index) => {
drawSplash(splash);
updateSplash(splash, index);
});
window.requestAnimationFrame(animate);
}
Agora, para iniciar toda a animação, basta colocar as linhas abaixo:
const handleResize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
const init = () => {
window.addEventListener('resize', handleResize)
animate();
}
init()
O resultado final é para ser algo assim:
Conclusão
Fazer animações com CSS
é muito legal e esse de efeito de chuva trabalho com vários conceitos legais de canvas
e css
. Eu curto bastante isso e fico contente em aprender sobre, por mais que eu goste bastante de back-end
.
Notas do Autor
Eu costumava fazer projetos assim para treinar. Caso queira dar uma olhada em mais animações aqui o repositório e o site. Tem o canal do YouTube lá também de onde eu aprendi: