Fundamentals
Core
Master
Shaders
Deslizador de imágenes
En esta lección aprenderemos cómo cargar y utilizar imágenes de textura en nuestros shaders para crear este deslizador de imágenes responsivo:
Y aquí está el resultado final en móvil:
El proyecto está inspirado en este Codepen por Sikriti Dakua.
Espero que estés motivado para aprender cómo crear este efecto, ¡comencemos!
Proyecto inicial
Nuestro proyecto inicial contiene una sección de pantalla completa que incluye un logo, un botón de menú y un componente <Canvas>
con un cubo blanco en el centro de la escena.
Utilizaremos Framer Motion para animar los elementos HTML pero puedes usar cualquier otra biblioteca o incluso CSS puro para animarlos. Solo usaremos la versión predeterminada de Framer Motion, no es necesario instalar el paquete 3D.
Para la UI elegí Tailwind CSS pero siéntete libre de usar la solución con la que te sientas más cómodo.
La carpeta public/textures/optimized
contiene las imágenes que utilizaremos en el deslizador. Las generé utilizando IA con Leonardo.Ai y las optimicé con Squoosh. Elegí una proporción de 3:4 para tener una orientación de retrato que se verá bien en móvil.
Una de las imágenes que usaremos, optimizada con Squoosh de 3.9mb a 311kb.
Componente de carrusel de imágenes
Vamos a empezar reemplazando el cubo blanco con un plano que será usado para mostrar las imágenes. Creamos un nuevo componente llamado ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Ajusta el ancho y la altura a la relación de aspecto de las imágenes que usarás.
La prop fillPercent
se usará para ajustar el tamaño del plano de modo que ocupe solo un porcentaje de la altura/anchura de la pantalla.
En App.jsx
importamos el componente ImageSlider
y reemplazamos el cubo blanco con él:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
Y aquí está el resultado:
El plano está ocupando demasiado espacio
Queremos que nuestro plano sea responsive y ocupe solo 75%(fillPercent
) de la altura de la pantalla. Podemos lograr esto usando el hook useThree
para obtener las dimensiones del viewport
y crear un factor de escala para ajustar el tamaño del 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 nuestro factor de escala, dividimos el viewport.height
por la height
del plano dividida por fillPercent
. Esto nos dará una proporción que podemos usar para escalar el plano.
Para entender las matemáticas detrás de esto, podemos pensar en el
viewport.height
como la altura máxima del plano. Si la altura de nuestro viewport es 3 y la altura de nuestro plano es 4, necesitamos escalar el plano por3 / 4
para que se ajuste a la pantalla. Pero como queremos ocupar solo el 75% de la altura de la pantalla, dividimos la altura del plano porfillPercent
para obtener la nueva altura de referencia. Lo que nos da4 / 0.75 = 5.3333
.
Luego multiplicamos el width
y height
por el ratio
para obtener las nuevas dimensiones.
Funciona bien cuando redimensionamos verticalmente pero no horizontalmente. Necesitamos ajustar el ancho del plano para que ocupe solo el 75% del ancho de la pantalla cuando la altura del viewport es mayor que el ancho.
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> ); };
No olvides cambiar el ratio
de const
a let
para poder reasignarlo. (O usa un operador ternario en su lugar)
Ahora el plano es responsive y ocupa solo el 75% de la altura o anchura de la pantalla dependiendo de las dimensiones de la pantalla.
Estamos listos para mostrar las imágenes en el plano.
Textura de imagen con shader personalizado
Primero, carguemos una de las imágenes y mostrémosla en el <meshBasicMaterial>
actual usando el hook useTexture
de 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> ); };
La imagen se muestra de manera agradable en el plano.
Ahora, porque queremos agregar efectos creativos durante la transición entre imágenes y al pasar el cursor, vamos a crear un material shader personalizado para poder mostrar dos imágenes al mismo tiempo y animarlas.
ImageSliderMaterial
Vamos a crear nuestro material shader personalizado llamado ImageSliderMaterial
. He decidido mantenerlo en el mismo archivo que el componente ImageSlider
ya que está estrechamente relacionado con él. Pero puedes crear un archivo separado si lo prefieres.
// ... 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, }); // ...
Almacenamos nuestra textura en un uniform
llamado uTexture
y lo pasamos al fragment shader para mostrarla.
El tipo del uniform
uTexture
es sampler2D
, que se utiliza para almacenar texturas 2D.
Para extraer el color de la textura en una posición específica, usamos la función texture2D
y le pasamos el uTexture
y las coordenadas vUv
.
Reemplacemos nuestro meshBasicMaterial
con nuestro nuevo ImageSliderMaterial
:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
La imagen se muestra usando nuestro material shader personalizado.
Color grading
¡Sé que estás desarrollando una vista aguda 🦅 y has notado que la gradación del color de la imagen se ve diferente!
Esto se debe a que <meshBasicMaterial/>
realiza un procesamiento adicional dentro del fragment shader para ajustar el color basado en el tone mapping y el color space elegidos en el renderer.
Aunque esto es algo que podríamos replicar manualmente en nuestro custom shader, no es el objetivo de esta lección y es un tema avanzado.
En su lugar, podemos usar fragments listos para habilitar los mismos efectos que los materiales estándar de Three.js. Si examinas el código fuente de meshBasicMaterial, verás que es una mezcla de declaraciones #include
y código personalizado.
Para hacer el código fácilmente reutilizable y mantenible, Three.js usa un preprocesador para incluir código de otros archivos. Por suerte, ¡podemos usar esos shader chunks en nuestro custom shader material también!
Vamos a añadir esas dos líneas al final de nuestro fragment shader:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
Para entender mejor cómo funcionan los shader chunks, esta herramienta te permite hacer clic en las declaraciones de include para ver el código que se incluye: ycw.github.io/three-shaderlib-skim
La gradación de color ahora es la misma que la de meshBasicMaterial. 🎨
Antes de profundizar en el shader, vamos a preparar nuestra UI.
Zustand State Management
Zustand es una biblioteca de gestión de estado pequeña, rápida y escalable que nos permite crear una tienda global para gestionar el estado de nuestra aplicación.
Es una alternativa a Redux o una solución de contexto personalizada para compartir estado entre componentes y gestionar lógica de estado compleja. (Aunque no sea el caso en nuestro proyecto. Nuestra lógica es sencilla.)
Vamos a agregar Zustand a nuestro proyecto:
yarn add zustand
Y crear un nuevo archivo llamado useSlider.js
en una carpeta hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
La función create
toma una función como argumento que recibirá una función set
para actualizar y fusionar el estado por nosotros. Podemos poner nuestro estado y métodos dentro del objeto devuelto.
Primero los datos que necesitamos:
// ... 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
almacenará el índice de la diapositiva actual.direction
almacenará la dirección de la transición.items
almacenará los datos de las diapositivas. (ruta a la imagen, nombre corto, título, color de fondo y descripción)
Ahora podemos crear los métodos para ir a la diapositiva anterior y siguiente:
// ... 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", })), }));
La función set
fusionará el nuevo estado con el anterior. Usamos el operador de módulo para volver a la primera diapositiva cuando llegamos a la última y viceversa.
Nuestro estado está listo, vamos a preparar nuestra UI.
Slider UI
Crearemos un nuevo componente llamado Slider
para mostrar los detalles del texto de la diapositiva y los botones de navegación. En 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> ); };
No profundizaremos en los detalles del CSS utilizado, pero permíteme explicarte los puntos principales:
- El contenedor del medio es un
div
que reproduce las dimensiones y la relación de aspecto de nuestro plano 3D. De esa manera podemos posicionar el texto y los botones en relación al plano. - Usamos
aspect-square
para mantener la relación de aspecto del contenedor. - Los botones de flecha provienen de Heroicons.
- El título y el nombre corto tienen dimensiones fijas y desbordamiento oculto para crear efectos de texto interesantes más adelante.
- Las clases
md:
se usan para ajustar el diseño en pantallas más grandes.
Agreguemos nuestro componente Slider
junto al Canvas
en 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;
El slider se muestra antes que el canvas.
Necesitamos cambiar el estilo del Canvas
para que se muestre como un fondo y ocupe el ancho y alto completo de la pantalla:
{/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }} className="top-0 left-0" style={{ // Overriding the default style applied by R3F width: "100%", height: "100%", position: "absolute", }} > {/* ... */}
Agreguemos fuentes y estilos personalizados a nuestro 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; } }
Las clases text-outline
se utilizan para crear un contorno alrededor del texto.
Para agregar las fuentes personalizadas, necesitamos actualizar nuestro 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: [], };
Ahora tenemos una interfaz de usuario con buen aspecto:
Efectos de texto
Para hacer las transiciones más interesantes, añadiremos algunos efectos de texto al título, nombre corto y descripción.
Primero, necesitamos obtener la dirección para saber si vamos a la siguiente o a la diapositiva previa. Podemos obtenerla del hook useSlider
:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
Para poder animar el texto que se mostró anteriormente y el nuevo texto, necesitamos el índice de la diapositiva anterior. Podemos calcularlo fácilmente:
// ... 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; } // ... };
Ahora podemos añadir los efectos de texto con la ayuda de Framer Motion. Empecemos con el título en la esquina inferior derecha:
// ... 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"> {/* 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]"> {/* ... */} {/* 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.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" // to make the transform work (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 el prop animate
para cambiar entre los diferentes estados y definimos las diferentes propiedades para cada estado en el prop variants
.
Para animar cada carácter, dividimos el título en un array de caracteres y usamos el prop staggerChildren
para retrasar la animación de cada carácter.
El prop from
se usa para definir la posición inicial de la animación.
Vamos a eliminar el overflow-hidden
del título para ver el efecto:
El texto del título se anima entrando y saliendo.
Vamos a añadir el mismo efecto al nombre corto:
// ... export const Slider = () => { // ... 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.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> ); };
Y un simple efecto de desvanecimiento para la descripción:
// ... export const Slider = () => { // ... 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]"> {/* ... */} {/* BOTTOM RIGHT */} {/* ... */} <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> ); };
Nuestra interfaz de usuario ahora está animada y lista para ser usada.
Estamos listos para saltar a la parte más interesante de esta lección: el efecto de transición con shader! 🎉
Efecto de transición de imágenes
Como hicimos para animar el texto, para hacer la transición entre las imágenes, necesitaremos las texturas de la imagen actual y la anterior.
Vamos a guardar la ruta de la imagen anterior en nuestro 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]); // ... };
Con el hook useEffect
, guardamos la ruta de la imagen actual en el estado lastImage
y cuando la imagen cambia, actualizamos el estado lastImage
con la nueva ruta de la imagen.
Antes de usar la prevTexture
en nuestro shader, y antes de que se nos olvide, vamos a precargar todas las imágenes para evitar parpadeos cuando cambiamos la diapositiva:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
Haciendo esto, estamos precargando todas las imágenes, podríamos agregar de manera segura una pantalla de carga al principio de nuestro sitio web para evitar cualquier parpadeo.
Ahora, vamos a agregar dos uniforms a nuestro ImageSliderMaterial
para guardar la textura anterior y el progreso de la transición:
// ... 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 la función mix
para interpolar entre la textura anterior y la actual basado en el uniform uProgression
.
Podemos ver una mezcla entre la imagen anterior y la actual.
Efecto de desvanecimiento
Vamos a animar el uProgression
uniforme para crear una transición suave entre las imágenes.
Primero, necesitamos una referencia a nuestro material para poder actualizar el uProgression
uniforme:
// ... 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 prescindir de la prop uProgression
ya que la actualizaremos manualmente.
Ahora en el useEffect
cuando la imagen cambie, podemos establecer el uProgression
a 0
y animarlo a 1
en un bucle 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 ); }); // ... };
Ahora tenemos una transición suave entre las imágenes.
Vamos a construir sobre esto para crear un efecto más interesante.
Posición distorsionada
Para hacer la transición más interesante, moveremos las imágenes en la dirección de la transición.
Usaremos las coordenadas vUv
para distorsionar la posición de las imágenes. Agreguemos un uDistortion
uniform a nuestro ImageSliderMaterial
y usémoslo para distorsionar las coordenadas vUv
:
End of lesson preview
To get access to the entire lesson, you need to purchase the course.