Menu fechado

Desbloqueie o Poder da Scroll-Driven Animation

scroll-driven

Introdução ao Projeto de Animação de Grid com Scroll

Desafio e Objetivos

O objetivo deste projeto é criar uma animação de grid que seja controlada pelo scroll, utilizando tecnologias como GSAP, ScrollTrigger e Lenis. A animação deve ser fluida, controlada e elegante, com movimentos e transições claros e precisos.

Arquitetura do Projeto

O projeto consiste em uma estrutura HTML simples, com um bloco principal que contém o grid de imagens. O CSS define a aparência e a posição dos elementos, enquanto o JavaScript utiliza GSAP, ScrollTrigger e Lenis para criar a animação controlada pelo scroll.

HTML Structure

<section class="block block--main">
  <div class="block__wrapper">
    <div class="content">
      <h2 class="content__title">Sticky Grid Scroll</h2>
      <p class="content__description">...</p>
      <button class="content__button">...</button>
    </div>
    <div class="gallery">
      <ul class="gallery__grid">
        <li class="gallery__item">
          <img class="gallery__image" src="../1.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../2.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../3.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../4.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../5.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../6.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../7.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../8.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../9.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../10.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../11.webp" alt="..." />
        </li>
        <li class="gallery__item">
          <img class="gallery__image" src="../12.webp" alt="..." />
        </li>
      </ul>
    </div>
  </div>
</section>

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 utiliza GSAP, ScrollTrigger e Lenis para criar a animação controlada pelo scroll. A animação é dividida em várias partes, incluindo a revelação do grid, o zoom do grid e a troca de conteúdo.




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()
  new StickyGridScroll()
})

class StickyGridScroll {
  constructor() {
    this.getElements()
    this.initContent()
    this.groupItemsByColumn()
    this.addParallaxOnScroll()
    this.animateTitleOnScroll()
    this.animateGridOnScroll()
  }
}

StickyGridScroll.prototype.getElements = function() {
  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")
  }
}

StickyGridScroll.prototype.initContent = function() {
  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 })
  }
}

StickyGridScroll.prototype.groupItemsByColumn = function() {
  this.numColumns = 3
  this.columns = Array.from({ length: this.numColumns }, () => [])
  this.items.forEach((item, index) => {
    this.columns[index % this.numColumns].push(item)
  })
}

StickyGridScroll.prototype.animateGridOnScroll = function() {
  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")
}

StickyGridScroll.prototype.gridRevealTimeline = function(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
}

StickyGridScroll.prototype.gridZoomTimeline = function(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) ? -1 : 1) * 40,
      duration: 0.5,
      ease: "power1.inOut",
    }, "-=0.5")
  return timeline
}

StickyGridScroll.prototype.toggleContent = function(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" : "<")
}

StickyGridScroll.prototype.addParallaxOnScroll = function() {
  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,
    },
  })
}

StickyGridScroll.prototype.animateTitleOnScroll = function() {
  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

class StickyGridScroll {
  constructor() {
    this.getElements()
    this.initContent()
    this.groupItemsByColumn()
    this.addParallaxOnScroll()
    this.animateTitleOnScroll()
    this.animateGridOnScroll()
  }
}

Métodos da Classe

getElements

StickyGridScroll.prototype.getElements = function() {
  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

StickyGridScroll.prototype.initContent = function() {
  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

StickyGridScroll.prototype.groupItemsByColumn = function() {
  this.numColumns = 3
  this.columns = Array.from({ length: this.numColumns }, () => [])
  this.items.forEach((item, index) => {
    this.columns[index % this.numColumns].push(item)
  })
}

Animações

animateGridOnScroll

StickyGridScroll.prototype.animateGridOnScroll = function() {
  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

StickyGridScroll.prototype.gridRevealTimeline = function(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

StickyGridScroll.prototype.gridZoomTimeline = function(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) ? -1 : 1) * 40,
      duration: 0.5,
      ease: "power1.inOut",
    }, "-=0.5")
  return timeline
}

toggleContent

StickyGridScroll.prototype.toggleContent = function(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" : "<")
}

HTML e CSS

Estrutura HTML Simples

<section class="block block--main">
<div class="block__wrapper">
<div class="content">
<h2 class="content__title">Sticky Grid Scroll</h2>
<p class="content__description">...</p>
<button class="content__button">...</button>
</div>
<div class="gallery">
<ul class="gallery__grid">
<li class="gallery__item">
<img class="gallery__image" src="../1.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../2.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../3.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../4.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../5.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../6.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../7.webp" alt="..." />
</li>
<li class="gallery__item">
<img class="gallery__image" src="../8.webp" alt="..." />
</li>
<li class="gallery__item">
&lt


Redação YTI&W-News

Redação Developers | Yassutaro TI & Web

Notícias do universo do Desenvolvimento Web, dicas e tutoriais para Webmasters.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Publicado em:APIs e Integrações,Desenvolvimento de Software,Web Design
Fale Conosco
×

Inscreva-se em nossa Newsletter!


Receba nossos lançamentos e artigos em primera mão!