Como criar efeitos de cursor animado com JavaScript

Aprenda a criar efeitos de cursor animado com JavaScript

Cursores animados viraram tendência esse ano, eu mesmo usei em vários projetos nos últimos meses. Por isso, hoje decidi trazer mais um tutorial feito pelo incrível time de designers da Codrops. Você pode ver o tutorial original (em inglês) clicando aqui.

A chamada para o cursor

O markup será dividido em dois elementos. Uma <div> bem simples pra esse pontinho branco e um elemento <canvas> pra desenhar aquela borda animada.

<body class="tutorial">
<main class="page">
<div class="page__inner">

<!-- elementos do cursor -->
<div class="cursor cursor--small"></div>
<canvas class="cursor cursor--canvas" resize></canvas>

</div>
</main>
</body>

Personalizando o layout

Pra dar um pouco de vida pra nossa demo, iremos definir o estilo do nosso layout usando CSS.

body.tutorial {
--color-text: #F4F0E7;
--color-bg: #1E2D40;
--color-link: #FF196A;
background-color: var(--color-bg);
}
.page {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.page__inner {
display: flex;
justify-content: center;
width: 100%;
}

O estilo do cursor

Entenda, é bem simples: os dois elementos do cursor terão uma posição fixa. Dessa maneira, é só configurar eles pra que fiquem no ponteiro do mouse. O canvas irá fazer todo o processo.

.cursor {
position: fixed;
left: 0;
top: 0;
pointer-events: none;
}
.cursor--small {
width: 5px;
height: 5px;
left: -2.5px;
top: -2.5px;
border-radius: 50%;
z-index: 11000;
background: var(--color-text);
}
.cursor--canvas {
width: 100vw;
height: 100vh;
z-index: 12000;
}

Os links

Pra gente não perder tempo, vamos criar um link junto com um ícone SVG pra que possamos animar quando passar em cima. Lembrando que aqui é apenas um exemplo, você pode se inspirar nessa aula pra criar coisas fantásticas. A falta de energia elétrica é o limite.

<nav class="nav">
<a href="#" class="link">
<svg class="settings-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="settings-icon__group settings-icon__group--1">
<line class="settings-icon__line" x1="79.69" y1="16.2" x2="79.69" y2="83.8"/>
<rect class="settings-icon__rect" x="73.59" y="31.88" width="12.19" height="12.19"/>
</g>
<g class="settings-icon__group settings-icon__group--2">
<line class="settings-icon__line" x1="50.41" y1="16.2" x2="50.41" y2="83.8"/>
<rect class="settings-icon__rect" x="44.31" y="54.33" width="12.19" height="12.19"/>
</g>
<g class="settings-icon__group settings-icon__group--3">
<line class="settings-icon__line" x1="20.31" y1="16.2" x2="20.31" y2="83.8"/>
<rect class="settings-icon__rect" x="14.22" y="26.97" width="12.19" height="12.19"/>
</g>
</svg>
</a>
<!-- você pode adicionar mais links aqui -->
</nav>

Estilo dos links e seus efeitos de transição

Aqui iremos definir os estilos de navegação e também os itens de transição dos links.

.nav {
display: flex;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.link {
display: flex;
width: 75px;
height: 75px;
margin: 0 5px;
justify-content: center;
align-items: center;
}
.settings-icon {
display: block;
width: 40px;
height: 40px;
}
.settings-icon__line {
stroke: var(--color-text);
stroke-width: 5px;
transition: all 0.2s ease 0.05s;
}
.settings-icon__rect {
stroke: var(--color-text);
fill: var(--color-bg);
stroke-width: 5px;
transition: all 0.2s ease 0.05s;
}
.link:hover .settings-icon__line,
.link:hover .settings-icon__rect {
stroke: var(--color-link);
transition: all 0.2s ease 0.05s;
}
.link:hover .settings-icon__group--1 .settings-icon__rect {
transform: translateY(20px);
}
.link:hover .settings-icon__group--2 .settings-icon__rect {
transform: translateY(-20px);
}
.link:hover .settings-icon__group--3 .settings-icon__rect {
transform: translateY(25px);
}
Esse é o resultado que teremos até agora.

Adicionando o Paper e o SimplexNoise

Como eu disse antes, nós iremos usar o Paper.js. Pra fazer aquele efeito animado na borda, usaremos o Simplex Noise.

<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>

Ocultando o cursor padrão

Como estamos criando nosso próprio cursor animado, meio que precisamos esconder o cursor do sistema, né? 🙂

.page, .page a {
cursor: none;
}

Animando o cursor

Aqui iremos colocar aquele efeito suave no cursor, pra isso usaremos o requestAnimationFrame()-loop.

let clientX = -100;
let clientY = -100;
const innerCursor = document.querySelector(".cursor--small");
const initCursor = () => {
document.addEventListener("mousemove", e => {
clientX = e.clientX;
clientY = e.clientY;
});
const render = () => {
innerCursor.style.transform = `translate(${clientX}px, ${clientY}px)`;
// se você já estiver usando o TweenMax no seu projeto,
// é preciso colocar TweenMax.set()
// TweenMax.set(innerCursor, {
// x: clientX,
// y: clientY
// });

requestAnimationFrame(render);
};
requestAnimationFrame(render);
};
initCursor();

Configurando a borda no Canvas

Agora iremos configurar aquela bordinha que tem em volta do pontinho branco. Pra fazer ela se mover daquela maneira, usaremos uma coisa chamada interpolação linear.

let lastX = 0;
let lastY = 0;
let isStuck = false;
let showCursor = false;
let group, stuckX, stuckY, fillOuterCursor;
const initCanvas = () => {
const canvas = document.querySelector(".cursor--canvas");
const shapeBounds = {
width: 75,
height: 75
};
paper.setup(canvas);
const strokeColor = "rgba(255, 25, 106)";
const strokeWidth = 1;
const segments = 8;
const radius = 15;
// nós iremos precisar disso para animar a borda
const noiseScale = 150; // velocidade
const noiseRange = 4; // campo de distorção
let isNoisy = false; // comando
// elemento que irá determinar a forma da borda
const polygon = new paper.Path.RegularPolygon(
new paper.Point(0, 0),
segments,
radius
);
polygon.strokeColor = strokeColor;
polygon.strokeWidth = strokeWidth;
polygon.smooth();
group = new paper.Group([polygon]);
group.applyMatrix = false;
const noiseObjects = polygon.segments.map(() => new SimplexNoise());
let bigCoordinates = [];
const lerp = (a, b, n) => {
return (1 - n) * a + n * b;
};
const map = (value, in_min, in_max, out_min, out_max) => {
return (
((value - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
);
};
paper.view.onFrame = event => {
// usando a interpolação linear, o círculo se moverá 0,2 (20%)
// da distância entre sua posição atual com a do mouse
// as coordenadas são definidas por frame
lastX = lerp(lastX, clientX, 0.2);
lastY = lerp(lastY, clientY, 0.2);
group.position = new paper.Point(lastX, lastY);
}
}
initCanvas();
Se você fez tudo certo, verá que agora o cursor estará se movendo da maneira que configuramos.

Configurando o estado hover

const initHovers = () => {
// vá até ao centro na parte do link e coloque stuckX e stuckY
// isso é necessário pra definir a posição da animação na borda
const handleMouseEnter = e => {
const navItem = e.currentTarget;
const navItemBox = navItem.getBoundingClientRect();
stuckX = Math.round(navItemBox.left + navItemBox.width / 2);
stuckY = Math.round(navItemBox.top + navItemBox.height / 2);
isStuck = true;
};
// resetar o isStuck quando o mouse sair de cima
const handleMouseLeave = () => {
isStuck = false;
};
const linkItems = document.querySelectorAll(".link");
linkItems.forEach(item => {
item.addEventListener("mouseenter", handleMouseEnter);
item.addEventListener("mouseleave", handleMouseLeave);
});
};
initHovers();

Animando a borda

Abaixo temos a versão estendida do método paper.view.onFrame mencionado acima.

// loop da animação do Paper.js
// (60fps por frame com o requestAnimationFrame)
paper.view.onFrame = event => {
// usando a interpolação linear, o círculo se moverá 0,2 (20%)
// da distância entre sua posição atual com a do mouse
// as coordenadas são definidas por frame
if (!isStuck) {
// mover a borda normalmente
lastX = lerp(lastX, clientX, 0.2);
lastY = lerp(lastY, clientY, 0.2);
group.position = new paper.Point(lastX, lastY);
} else if (isStuck) {
// fixar a posição da nav
lastX = lerp(lastX, stuckX, 0.2);
lastY = lerp(lastY, stuckY, 0.2);
group.position = new paper.Point(lastX, lastY);
}

if (isStuck && polygon.bounds.width < shapeBounds.width) { // aumentar o tamanho polygon.scale(1.08); } else if (!isStuck && polygon.bounds.width > 30) {
// remover animação da borda
if (isNoisy) {
polygon.segments.forEach((segment, i) => {
segment.point.set(bigCoordinates[i][0], bigCoordinates[i][1]);
});
isNoisy = false;
bigCoordinates = [];
}
// personalizar a forma
const scaleDown = 0.92;
polygon.scale(scaleDown);
}

// quando estiver fixa, animar com simplex noise
if (isStuck && polygon.bounds.width >= shapeBounds.width) {
isNoisy = true;
// pegar as coordenadas da borda maior
if (bigCoordinates.length === 0) {
polygon.segments.forEach((segment, i) => {
bigCoordinates[i] = [segment.point.x, segment.point.y];
});
}

// criar loop em todos os pontos do polígono
polygon.segments.forEach((segment, i) => {

// conseguir novos efeitos na animação
// iremos dividir o event.count com o noiseScale pra ter um efeito bem suave
const noiseX = noiseObjects[i].noise2D(event.count / noiseScale, 0);
const noiseY = noiseObjects[i].noise2D(event.count / noiseScale, 1);

// mapear o valor da borda para o range
const distortionX = map(noiseX, -1, 1, -noiseRange, noiseRange);
const distortionY = map(noiseY, -1, 1, -noiseRange, noiseRange);

// aplicar distorção nas coordenadas
const newX = bigCoordinates[i][0] + distortionX;
const newY = bigCoordinates[i][1] + distortionY;

// definir uma nova coordenada (animada) no pontinho
segment.point.set(newX, newY);
});

}
polygon.smooth();
};
Temos então esse resultado supimpa.

Considerações finais

Como o grande Stefan disse, criar efeitos de cursor animado é uma ótima maneira de brincar com JavaScript. Mas venho te lembrar que você deve usar com moderação, não vá exagerar nos efeitos pois pode deixar seu sistema lento.

Após anos de experiência em criação web, resolvi compartilhar dicas sobre a carreira em uma agência de desenvolvimento. — https://studioromes.com

Após anos de experiência em criação web, resolvi compartilhar dicas sobre a carreira em uma agência de desenvolvimento. — https://studioromes.com