Fundamentals
Core
Master
Shaders
Image slider
Nesta lição, aprenderemos como carregar e usar imagens de textura em nossos shaders para criar este slider de imagem responsivo:
E aqui está o resultado final no celular:
O projeto é inspirado neste Codepen por Sikriti Dakua.
Espero que você esteja motivado a aprender como criar este efeito, vamos começar!
Projeto inicial
Nosso projeto inicial contém uma seção em tela cheia com um logo, um botão de menu e um componente <Canvas>
com um cubo branco no meio da cena.
Usaremos o Framer Motion para animar os elementos HTML, mas você pode usar qualquer outra biblioteca ou até mesmo CSS puro para animá-los. Usaremos apenas a versão padrão do Framer Motion, não é necessário instalar o pacote 3D.
Para a UI escolhi Tailwind CSS, mas sinta-se à vontade para usar a solução com a qual você se sinta mais confortável.
A pasta public/textures/optimized
contém as imagens que usaremos no slider. Eu as gerei usando IA com Leonardo.Ai e otimizei com Squoosh. Escolhi uma proporção de 3:4 para ter uma orientação de retrato que ficará boa no celular.
Uma das imagens que usaremos, otimizada com Squoosh de 3.9mb para 311kb.
Componente de slider de imagem
Vamos começar substituindo o cubo branco por um plano que será utilizado para exibir as imagens. Criamos um novo componente chamado ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Ajuste a largura e a altura de acordo com a proporção das imagens que você usará.
A prop fillPercent
será usada para ajustar o tamanho do plano para ocupar apenas uma porcentagem da altura/largura da tela.
No App.jsx
importamos o componente ImageSlider
e substituímos o cubo branco por ele:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
E aqui está o resultado:
O plano está ocupando muito espaço
Queremos que nosso plano seja responsivo e ocupe apenas 75% (fillPercent
) da altura da tela. Podemos conseguir isso usando o hook useThree
para obter as dimensões do viewport
e criar um fator de escala para ajustar o tamanho do plano:
import { useThree } from "@react-three/fiber"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const viewport = useThree((state) => state.viewport); const ratio = viewport.height / (height / fillPercent); return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Para calcular nosso fator de escala, dividimos a viewport.height
pela height
do plano dividida por fillPercent
. Isso nos dará um ratio que podemos usar para escalar o plano.
Para entender a matemática por trás disso, podemos pensar na
viewport.height
como a altura máxima do plano. Se nossa altura da viewport for 3 e a altura do plano for 4, precisamos escalar o plano por3 / 4
para que ele se encaixe na tela. Porém, como queremos ocupar apenas 75% da altura da tela, dividimos a altura do plano porfillPercent
para obter a nova altura de referência. O que dá4 / 0.75 = 5.3333
.
Então multiplicamos a width
e a height
pelo ratio
para obter as novas dimensões.
Funciona bem quando redimensionamos verticalmente, mas não horizontalmente. Precisamos ajustar a largura do plano para ocupar apenas 75% da largura da tela quando a altura da viewport for maior que a largura.
import { useThree } from "@react-three/fiber"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const viewport = useThree((state) => state.viewport); let ratio = viewport.height / (height / fillPercent); if (viewport.width < viewport.height) { ratio = viewport.width / (width / fillPercent); } return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Não se esqueça de mudar o ratio
de const
para let
para poder reassinalá-lo. (Ou use um operador ternário em vez disso)
Agora o plano é responsivo e ocupa apenas 75% da altura ou largura da tela, dependendo das dimensões da tela.
Estamos prontos para exibir as imagens no plano.
Custom shader image texture
Primeiro, vamos carregar uma das imagens e exibi-la no <meshBasicMaterial>
atual usando o hook useTexture
da Drei:
import { useTexture } from "@react-three/drei"; // ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const image = "textures/optimized/Default_authentic_futuristic_cottage_with_garden_outside_0.jpg"; const texture = useTexture(image); // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" map={texture} /> </mesh> ); };
A imagem é exibida de forma agradável no plano.
Agora, como queremos adicionar efeitos criativos durante a transição entre imagens e no hover, vamos criar um material de shader personalizado para poder exibir duas imagens ao mesmo tempo e animá-las.
ImageSliderMaterial
Vamos criar nosso material de shader personalizado chamado ImageSliderMaterial
. Eu escolhi mantê-lo no mesmo arquivo do componente ImageSlider
pois está intimamente relacionado a ele. Mas você pode criar um arquivo separado, se preferir.
// ... import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const ImageSliderMaterial = shaderMaterial( { uTexture: undefined, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /*glsl*/ ` varying vec2 vUv; uniform sampler2D uTexture; void main() { vec2 uv = vUv; vec4 curTexture = texture2D(uTexture, vUv); gl_FragColor = curTexture; }` ); extend({ ImageSliderMaterial, }); // ...
Armazenamos nossa textura em um uniform
chamado uTexture
e o passamos para o fragment shader para exibi-lo.
O tipo do uniform uTexture
é sampler2D
que é utilizado para armazenar texturas 2D.
Para extrair a cor da textura em uma posição específica, usamos a função texture2D
e passamos uTexture
e as coordenadas vUv
.
Vamos substituir nosso meshBasicMaterial
pelo nosso novo ImageSliderMaterial
:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
A imagem é exibida usando nosso material de shader personalizado.
Color grading
Eu sei que você começou a ter olhos de águia 🦅 e notou que a gradação de cores da imagem está diferente!
Isso acontece porque o <meshBasicMaterial/>
faz um processamento extra dentro do fragment shader para ajustar a cor com base no tone mapping e color space escolhidos no renderizador.
Embora isso seja algo que poderíamos replicar manualmente em nosso shader personalizado, esse não é o objetivo desta lição e é um tópico avançado.
Em vez disso, podemos usar fragmentos prontos para habilitar os mesmos efeitos que os materiais padrão do Three.js. Se você olhar o código-fonte do meshBasicMaterial, verá que é uma mistura de declarações #include
e código personalizado.
Para tornar o código facilmente reutilizável e sustentável, o Three.js usa um pré-processador para incluir código de outros arquivos. Felizmente, podemos usar esses chunks de shader no nosso material de shader personalizado também!
Vamos adicionar essas duas linhas no final do nosso fragment shader:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
Para entender melhor como os chunks de shader funcionam, esta ferramenta permite que você clique nas declarações de inclusões para ver o código que é incluído: ycw.github.io/three-shaderlib-skim
A gradação de cores agora é a mesma que a do meshBasicMaterial. 🎨
Antes de prosseguir com o shader, vamos preparar nossa UI.
Zustand State Management
Zustand é uma biblioteca de gerenciamento de estado pequena, rápida e escalável que nos permite criar uma loja global para gerenciar o estado da nossa aplicação.
É uma alternativa ao Redux ou a uma solução de contexto customizada para compartilhar estado entre componentes e gerenciar lógica de estado complexa. (Mesmo se não for o caso no nosso projeto. Nossa lógica é simples.)
Vamos adicionar Zustand ao nosso projeto:
yarn add zustand
E criar um novo arquivo chamado useSlider.js
em uma pasta hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
A função create
recebe uma função como argumento que receberá uma função set
para atualizar e mesclar o estado para nós. Podemos colocar nosso estado e métodos dentro do objeto retornado.
Primeiro, os dados que precisamos:
// ... export const useSlider = create((set) => ({ curSlide: 0, direction: "start", items: [ { image: "textures/optimized/Default_authentic_futuristic_cottage_with_garden_outside_0.jpg", short: "PH", title: "Relax", description: "Enjoy your peace of mind.", color: "#201d24", }, { image: "textures/optimized/Default_balinese_futuristic_villa_with_garden_outside_jungle_0.jpg", short: "TK", title: "Breath", color: "#263a27", description: "Feel the nature surrounding you.", }, { image: "textures/optimized/Default_desert_arabic_futuristic_villa_with_garden_oasis_outsi_0.jpg", short: "OZ", title: "Travel", color: "#8b6d40", description: "Brave the unknown.", }, { image: "textures/optimized/Default_scandinavian_ice_futuristic_villa_with_garden_outside_0.jpg", short: "SK", title: "Calm", color: "#72a3ca", description: "Free your mind.", }, { image: "textures/optimized/Default_traditional_japanese_futuristic_villa_with_garden_outs_0.jpg", short: "AU", title: "Feel", color: "#c67e90", description: "Emotions and experiences.", }, ], }));
curSlide
vai armazenar o índice do slide atual.direction
vai armazenar a direção da transição.items
vai armazenar os dados dos slides. (caminho para a imagem, nome curto, título, cor de fundo e descrição)
Agora podemos criar os métodos para ir para o slide anterior e próximo slide:
// ... export const useSlider = create((set) => ({ // ... nextSlide: () => set((state) => ({ curSlide: (state.curSlide + 1) % state.items.length, direction: "next", })), prevSlide: () => set((state) => ({ curSlide: (state.curSlide - 1 + state.items.length) % state.items.length, direction: "prev", })), }));
A função set
vai mesclar o novo estado com o anterior. Usamos o operador módulo para voltar ao primeiro slide quando atingimos o último e vice-versa.
Nosso estado está pronto, vamos preparar nossa UI.
Slider UI
Vamos criar um novo componente chamado Slider
para exibir os detalhes do texto do slide e os botões de navegação. Em Slider.jsx
:
import { useSlider } from "./hooks/useSlider"; export const Slider = () => { const { curSlide, items, nextSlide, prevSlide } = useSlider(); return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* MIDDLE CONTAINER */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* TOP LEFT */} <div className="w-48 md:w-72 left-4 md:left-0 md:-translate-x-1/2 absolute -top-8 "> <h1 className="relative antialiased overflow-hidden font-display text-[5rem] h-[4rem] leading-[4rem] md:text-[11rem] md:h-[7rem] md:leading-[7rem] font-bold text-white block" > {items[curSlide].short} </h1> </div> {/* MIDDLE ARROWS */} <button className="absolute left-4 md:-left-14 top-1/2 -translate-y-1/2 pointer-events-auto" onClick={prevSlide} > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 stroke-white hover:opacity-50 transition-opacity duration-300 ease-in-out" > <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /> </svg> </button> <button className="absolute right-4 md:-right-14 top-1/2 -translate-y-1/2 pointer-events-auto" onClick={nextSlide} > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 stroke-white hover:opacity-50 transition-opacity duration-300 ease-in-out" > <path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /> </svg> </button> {/* BOTTOM RIGHT */} <div className="absolute right-4 md:right-auto md:left-full md:-ml-20 bottom-8"> <h2 className="antialiased font-display font-bold text-transparent text-outline-0.5 block overflow-hidden relative w-[50vw] text-5xl h-16 md:text-8xl md:h-28" > {items[curSlide].title} </h2> </div> <div className="absolute right-4 md:right-auto md:left-full md:top-full md:-mt-10 bottom-8 md:bottom-auto"> <p className="text-white w-64 text-sm font-thin italic ml-4 relative"> {items[curSlide].description} </p> </div> </div> </div> ); };
Não entraremos nos detalhes do CSS usado, mas deixe-me explicar os pontos principais:
- O contêiner do meio é um
div
que reproduz as dimensões e a proporção do nosso plano 3D. Dessa forma, podemos posicionar o texto e os botões em relação ao plano. - Usamos
aspect-square
para manter a proporção do contêiner. - Os botões de seta vêm de Heroicons.
- O título e o nome curto têm dimensões fixas e
overflow hidden
para criar efeitos de texto interessantes posteriormente. - Classes
md:
são usadas para ajustar o layout em telas maiores.
Vamos adicionar nosso componente Slider
ao lado do Canvas
em App.jsx
:
// ... import { Slider } from "./Slider"; function App() { return ( <> <main className="bg-black"> <section className="w-full h-screen relative"> {/* ... */} <Slider /> <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> </section> {/* ... */} </main> </> ); } export default App;
O slider é exibido antes do canvas.
Precisamos alterar o estilo do Canvas
para ser exibido como um fundo e ocupar toda a largura e altura da tela:
{/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }} className="top-0 left-0" style={{ // Sobrescrevendo o estilo padrão aplicado pelo R3F width: "100%", height: "100%", position: "absolute", }} > {/* ... */}
Vamos adicionar fontes e estilos personalizados ao nosso index.css
:
@import url("https://fonts.googleapis.com/css2?family=Red+Rose:wght@700&display=swap&family=Poppins:ital,wght@1,100&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; @layer base { .text-outline-px { -webkit-text-stroke: 1px white; } .text-outline-0\.5 { -webkit-text-stroke: 2px white; } .text-outline-1 { -webkit-text-stroke: 4px white; } }
As classes text-outline
são usadas para criar um contorno ao redor do texto.
Para adicionar as fontes personalizadas, precisamos atualizar nosso tailwind.config.js
:
/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, fontFamily: { sans: ["Poppins", "sans-serif"], display: ["Red Rose", "sans-serif"], }, }, plugins: [], };
Agora temos uma UI de aparência agradável:
Efeitos de texto
Para tornar as transições mais interessantes, adicionaremos alguns efeitos de texto ao título, nome curto e descrição.
Primeiro, precisamos obter a direção para saber se estamos indo para o próximo ou o slide anterior. Podemos obtê-la a partir do hook useSlider
:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
Para animar o texto exibido anteriormente para fora e o novo texto para dentro, precisamos do índice do slide anterior. Podemos calculá-lo facilmente:
// ... export const Slider = () => { // ... let prevIdx = direction === "next" ? curSlide - 1 : curSlide + 1; if (prevIdx === items.length) { prevIdx = 0; } else if (prevIdx === -1) { prevIdx = items.length - 1; } // ... };
Agora podemos adicionar os efeitos de texto com a ajuda do Framer Motion. Vamos começar com o título no canto inferior direito:
// ... import { motion } from "framer-motion"; const TEXT_TRANSITION_HEIGHT = 150; export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* CONTAINER DO MEIO */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* ... */} {/* INFERIOR DIREITO */} <div className="absolute right-4 md:right-auto md:left-full md:-ml-20 bottom-8"> <h2 className="antialiased font-display font-bold text-transparent text-outline-0.5 block overflow-hidden relative w-[50vw] text-5xl h-16 md:text-8xl md:h-28" > {items.map((item, idx) => ( <motion.div key={idx} className="absolute top-0 left-0 w-full text-right md:text-left" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } variants={{ current: { transition: { delay: 0.4, staggerChildren: 0.06, }, }, }} > {item.title.split("").map((char, idx) => ( <motion.span key={idx} className="inline-block" // para fazer o transform funcionar (translateY) variants={{ current: { translateY: 0, transition: { duration: 0.8, from: direction === "prev" ? -TEXT_TRANSITION_HEIGHT : TEXT_TRANSITION_HEIGHT, type: "spring", bounce: 0.2, }, }, prev: { translateY: direction === "prev" ? TEXT_TRANSITION_HEIGHT : -TEXT_TRANSITION_HEIGHT, transition: { duration: 0.8, from: direction === "start" ? -TEXT_TRANSITION_HEIGHT : 0, }, }, next: { translateY: TEXT_TRANSITION_HEIGHT, transition: { from: TEXT_TRANSITION_HEIGHT, }, }, }} > {char} </motion.span> ))} </motion.div> ))} </h2> </div> {/* ... */} </div> </div> ); };
Usamos a prop animate
para alternar entre os diferentes estados e definimos as diferentes propriedades para cada estado na prop variants
.
Para animar cada caractere, dividimos o título em um array de caracteres e usamos a prop staggerChildren
para atrasar a animação de cada caractere.
A prop from
é usada para definir a posição inicial da animação.
Vamos remover o overflow-hidden
do título para ver o efeito:
O texto do título é animado para entrar e sair.
Vamos adicionar o mesmo efeito ao nome curto:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* CONTAINER DO MEIO */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* SUPERIOR ESQUERDO */} <div className="w-48 md:w-72 left-4 md:left-0 md:-translate-x-1/2 absolute -top-8 "> <h1 className="relative antialiased overflow-hidden font-display text-[5rem] h-[4rem] leading-[4rem] md:text-[11rem] md:h-[7rem] md:leading-[7rem] font-bold text-white block" > {items.map((_item, idx) => ( <motion.span key={idx} className="absolute top-0 left-0 md:text-center w-full" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } variants={{ current: { translateY: 0, transition: { duration: 0.8, from: direction === "prev" ? -TEXT_TRANSITION_HEIGHT : TEXT_TRANSITION_HEIGHT, type: "spring", bounce: 0.2, delay: 0.4, }, }, prev: { translateY: direction === "prev" ? TEXT_TRANSITION_HEIGHT : -TEXT_TRANSITION_HEIGHT, transition: { type: "spring", bounce: 0.2, delay: 0.2, from: direction === "start" ? -TEXT_TRANSITION_HEIGHT : 0, }, }, next: { translateY: TEXT_TRANSITION_HEIGHT, transition: { from: TEXT_TRANSITION_HEIGHT, }, }, }} > {items[idx].short} </motion.span> ))} </h1> </div> {/* ... */} </div> </div> ); };
E um efeito simples de fade in e out para a descrição:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* CONTAINER DO MEIO */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* ... */} {/* INFERIOR DIREITO */} {/* ... */} <div className="absolute right-4 md:right-auto md:left-full md:top-full md:-mt-10 bottom-8 md:bottom-auto"> <p className="text-white w-64 text-sm font-thin italic ml-4 relative"> {items.map((item, idx) => ( <motion.span key={idx} className="absolute top-0 left-0 w-full text-right md:text-left" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } initial={{ opacity: 0, }} variants={{ current: { opacity: 1, transition: { duration: 1.2, delay: 0.6, from: 0, }, }, }} > {item.description} </motion.span> ))} </p> </div> </div> </div> ); };
Nossa interface de usuário agora está animada e pronta para ser usada.
Estamos prontos para pular para a parte mais interessante desta lição: o efeito de transição com shader! 🎉
Efeito de transição de imagem
Como fizemos para animar o texto, para fazer a transição entre as imagens, precisaremos das texturas da imagem atual e da anterior.
Vamos armazenar o caminho da imagem anterior em nosso componente ImageSlider
:
// ... import { useSlider } from "./hooks/useSlider"; import { useEffect, useState } from "react"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const { items, curSlide } = useSlider(); const image = items[curSlide].image; const texture = useTexture(image); const [lastImage, setLastImage] = useState(image); const prevTexture = useTexture(lastImage); useEffect(() => { const newImage = image; return () => { setLastImage(newImage); }; }, [image]); // ... };
Com o hook useEffect
, armazenamos o caminho da imagem atual no estado lastImage
e, quando a imagem muda, atualizamos o estado lastImage
com o novo caminho da imagem.
Antes de usar a prevTexture
em nosso shader, e antes que esqueçamos, vamos pré-carregar todas as imagens para evitar piscadas ao mudar o slide:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
Fazendo isso, estamos pré-carregando todas as imagens, e podemos adicionar com segurança uma tela de carregamento no início de nosso site para evitar qualquer piscada.
Agora, vamos adicionar dois uniforms ao nosso ImageSliderMaterial
para armazenar a textura anterior e o progresso da transição:
// ... const ImageSliderMaterial = shaderMaterial( { uProgression: 1.0, uTexture: undefined, uPrevTexture: undefined, }, /*glsl*/ ` // ... `, /*glsl*/ ` varying vec2 vUv; uniform sampler2D uTexture; uniform sampler2D uPrevTexture; uniform float uProgression; void main() { vec2 uv = vUv; vec4 curTexture = texture2D(uTexture, vUv); vec4 prevTexture = texture2D(uPrevTexture, vUv); vec4 finalTexture = mix(prevTexture, curTexture, uProgression); gl_FragColor = finalTexture; #include <tonemapping_fragment> #include <encodings_fragment> }` ); // ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} uPrevTexture={prevTexture} uProgression={0.5} /> </mesh> ); };
Usamos a função mix
para interpolar entre a textura anterior e a atual com base no uniforme uProgression
.
Podemos ver uma mistura entre a imagem anterior e a atual.
Efeito de Fade in e out
Vamos animar a uniform uProgression
para criar uma transição suave entre as imagens.
Primeiro, precisamos de uma referência ao nosso material para poder atualizar o uniform uProgression
:
// ... import { useRef } from "react"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... const material = useRef(); // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial ref={material} uTexture={texture} uPrevTexture={prevTexture} /> </mesh> ); };
Podemos nos livrar da prop uProgression
já que vamos atualizá-la manualmente.
Agora no useEffect
quando a imagem mudar, podemos definir uProgression
para 0
e animá-lo para 1
em um loop useFrame
:
// ... import { useFrame } from "@react-three/fiber"; import { MathUtils } from "three/src/math/MathUtils.js"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... useEffect(() => { const newImage = image; material.current.uProgression = 0; return () => { setLastImage(newImage); }; }, [image]); useFrame(() => { material.current.uProgression = MathUtils.lerp( material.current.uProgression, 1, 0.05 ); }); // ... };
Agora temos uma transição suave entre as imagens.
Vamos construir em cima disso para criar um efeito mais interessante.
Posição distorcida
Para tornar a transição mais interessante, vamos empurrar as imagens na direção da transição.
Utilizaremos as coordenadas vUv
para distorcer a posição das imagens. Vamos adicionar um uniform uDistortion
ao nosso ImageSliderMaterial
e usá-lo para distorcer as coordenadas vUv
:
End of lesson preview
To get access to the entire lesson, you need to purchase the course.