Introdução ao Projeto de Animação de Grid com Scroll
Desafio e Objetivos
O objetivo principal deste projeto é desenvolver uma animação de grid controlada pelo scroll, utilizando tecnologias como GSAP, ScrollTrigger e Lenis, proporcionando uma experiência de usuário agradável e imersiva.
Arquitetura do Projeto
A estrutura do projeto é composta por uma combinação de HTML, CSS e JavaScript, onde o HTML fornece a estrutura básica, o CSS define a aparência e a posição dos elementos, e o JavaScript é utilizado para criar a animação controlada pelo scroll.
Estrutura HTML
Sticky Grid Scroll
...
-
-
-
-
-
-
-
-
CSS Setup
html {
font-size: calc(100vw / 1440);
}
body {
font-size: 1.6rem;
}
.block.block--main {
height: 425vh;
}
.block__wrapper {
position: sticky;
top: 0;
padding: 0 24rem;
overflow: hidden;
}
.content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
text-align: center;
z-index: 1;
}
.gallery {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 73.6rem;
}
.gallery__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 3.2rem;
row-gap: 4rem;
}
.gallery__item {
width: 100%;
aspect-ratio: 1;
}
.gallery__image {
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript + GSAP + Lenis
O JavaScript é responsável por criar a animação controlada pelo scroll, utilizando as bibliotecas GSAP, ScrollTrigger e Lenis. A implementação inclui:
- Configuração do Lenis para controle suave do scroll.
- Utilização do GSAP para animações e efeitos.
- Integração do ScrollTrigger para animações baseadas no scroll.
import Lenis from "lenis";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { preloadImages } from "./utils.js";
gsap.registerPlugin(ScrollTrigger);
function initSmoothScrolling() {
const lenis = new Lenis({
lerp: 0.08,
wheelMultiplier: 1.4,
});
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
}
preloadImages().then(() => {
document.body.classList.remove("loading");
initSmoothScrolling();
});
class StickyGridScroll {
constructor() {
this.getElements();
this.initContent();
this.groupItemsByColumn();
this.addParallaxOnScroll();
this.animateTitleOnScroll();
this.animateGridOnScroll();
}
getElements() {
this.block = document.querySelector(".block--main");
if (this.block) {
this.wrapper = this.block.querySelector(".block__wrapper");
this.content = this.block.querySelector(".content");
this.title = this.block.querySelector(".content__title");
this.description = this.block.querySelector(".content__description");
this.button = this.block.querySelector(".content__button");
this.grid = this.block.querySelector(".gallery__grid");
this.items = this.block.querySelectorAll(".gallery__item");
}
}
initContent() {
if (this.description && this.button) {
gsap.set([this.description, this.button], {
opacity: 0,
pointerEvents: "none",
});
}
if (this.content && this.title) {
const dy = (this.content.offsetHeight - this.title.offsetHeight) / 2;
this.titleOffsetY = (dy / this.content.offsetHeight) * 100;
gsap.set(this.title, { yPercent: this.titleOffsetY });
}
}
groupItemsByColumn() {
this.numColumns = 3;
this.columns = Array.from({ length: this.numColumns }, () => []);
this.items.forEach((item, index) => {
this.columns[index % this.numColumns].push(item);
});
}
animateGridOnScroll() {
const timeline = gsap.timeline({
scrollTrigger: {
trigger: this.block,
start: "top 25%",
end: "bottom bottom",
scrub: true,
},
});
timeline
.add(this.gridRevealTimeline())
.add(this.gridZoomTimeline(), "-=0.6")
.add(() => this.toggleContent(timeline.scrollTrigger.direction === 1), "-=0.32");
}
gridRevealTimeline(columns = this.columns) {
const timeline = gsap.timeline();
const wh = window.innerHeight;
const dy = wh - (wh - this.grid.offsetHeight) / 2;
columns.forEach((column, colIndex) => {
const fromTop = colIndex % 2 === 0;
timeline.from(column, {
y: dy * (fromTop ? -1 : 1),
stagger: {
each: 0.06,
from: fromTop ? "end" : "start",
},
ease: "power1.inOut",
}, "grid-reveal");
});
return timeline;
}
gridZoomTimeline(columns = this.columns) {
const timeline = gsap.timeline({ defaults: { duration: 1, ease: "power3.inOut" } });
timeline
.to(this.grid, { scale: 2.05 })
.to(columns[0], { xPercent: -40 }, "<")
.to(columns[2], { xPercent: 40 }, "<")
.to(columns[1], {
yPercent: (index) => (index < Math.floor(columns[1].length / 2) ? -40 : 40),
duration: 0.5,
ease: "power1.inOut",
}, "-=0.5");
return timeline;
}
toggleContent(isVisible = true) {
if (!this.title || !this.description || !this.button) {
return;
}
gsap.timeline({ defaults: { overwrite: true } })
.to(this.title, {
yPercent: isVisible ? 0 : this.titleOffsetY,
duration: 0.7,
ease: "power2.inOut",
})
.to([this.description, this.button], {
opacity: isVisible ? 1 : 0,
duration: 0.4,
ease: power1.${isVisible ? "inOut" : "out"},
pointerEvents: isVisible ? "all" : "none",
}, isVisible ? "-=0.9" : "-=0.4");
}
addParallaxOnScroll() {
if (!this.block || !this.wrapper) {
return;
}
gsap.from(this.wrapper, {
yPercent: -100,
ease: "none",
scrollTrigger: {
trigger: this.block,
start: "top bottom",
end: "top top",
scrub: true,
},
});
}
animateTitleOnScroll() {
if (!this.block || !this.title) {
return;
}
gsap.from(this.title, {
opacity: 0,
duration: 0.7,
ease: "power1.out",
scrollTrigger: {
trigger: this.block,
start: "top 57%",
toggleActions: "play none none reset",
},
});
}
}
new StickyGridScroll();
Estrutura do Projeto
Classe StickyGridScroll
A classe StickyGridScroll gerencia a animação da grid, incluindo a revelação do grid, o zoom do grid e a troca de conteúdo.
class StickyGridScroll {
constructor() {
this.getElements();
this.initContent();
this.groupItemsByColumn();
this.addParallaxOnScroll();
this.animateTitleOnScroll();
this.animateGridOnScroll();
}
}
Fonte: Documentação do Projeto de Animação de Grid com Scroll.