Image slider
In questa lezione impareremo a caricare e utilizzare immagini texture nei nostri shader per creare questo slider di immagini reattivo:
Ecco il risultato finale su mobile:
Il progetto è ispirato da questo Codepen di Sikriti Dakua.
Spero che tu sia motivato ad imparare come creare questo effetto, iniziamo!
Progetto iniziale
Il nostro progetto iniziale contiene una sezione a schermo intero con un logo, un pulsante menu e un componente <Canvas>
con un cubo bianco al centro della scena.
Useremo Framer Motion per animare gli elementi HTML, ma puoi utilizzare qualsiasi altra libreria o anche solo CSS puro per animarli. Utilizzeremo solo la versione di default di Framer Motion, non è necessario installare il pacchetto 3D.
Per l'UI ho scelto Tailwind CSS, ma sentiti libero di utilizzare la soluzione che preferisci.
La cartella public/textures/optimized
contiene le immagini che utilizzeremo nello slider. Le ho generate utilizzando l'IA con Leonardo.Ai e ottimizzate con Squoosh. Ho scelto un rapporto di 3:4 per avere un'orientazione verticale che sarà visivamente gradevole su mobile.
Una delle immagini che utilizzeremo, ottimizzata con Squoosh da 3.9mb a 311kb.
Componente del cursore per immagini
Iniziamo sostituendo il cubo bianco con un piano che verrà utilizzato per visualizzare le immagini. Creiamo un nuovo componente chiamato ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Adjusta la larghezza e l'altezza al rapporto d'aspetto delle immagini che utilizzerai.
La prop fillPercent
verrà utilizzata per regolare la dimensione del piano in modo che occupi solo una percentuale dell'altezza/larghezza dello schermo.
In App.jsx
importiamo il componente ImageSlider
e sostituiamo il cubo bianco con esso:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
Ed ecco il risultato:
Il piano occupa troppo spazio
Vogliamo che il nostro piano sia reattivo e occupi solo il 75% (fillPercent
) dell'altezza dello schermo. Possiamo ottenere questo risultato utilizzando l'hook useThree
per ottenere le dimensioni del viewport
e creando un fattore di scala per regolare la dimensione del piano:
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> ); };
Per calcolare il nostro fattore di scala dividiamo la viewport.height
per l'altezza
del piano diviso per fillPercent
. Questo ci darà un rapporto che possiamo utilizzare per scalare il piano.
Per capire la matematica dietro a questo, possiamo pensare alla
viewport.height
come all'altezza massima del piano. Se l'altezza del nostro viewport è 3 e l'altezza del nostro piano è 4, dobbiamo scalare il piano di3 / 4
per farlo adattare allo schermo. Ma poiché vogliamo occupare solo il 75% dell'altezza dello schermo, dividiamo l'altezza del piano perfillPercent
per ottenere la nuova altezza di riferimento. Che ci dà4 / 0.75 = 5.3333
.
Poi moltiplichiamo la width
e la height
per il ratio
per ottenere le nuove dimensioni.
Funziona bene quando ridimensioniamo verticalmente ma non orizzontalmente. Dobbiamo regolare la larghezza del piano affinché occupi solo il 75% della larghezza dello schermo quando l'altezza del viewport è maggiore della larghezza.
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> ); };
Non dimenticare di cambiare il ratio
da const
a let
per poterlo riassegnare. (Oppure usa un operatore ternario)
Ora il piano è reattivo e occupa solo il 75% dell'altezza o della larghezza dello schermo a seconda delle dimensioni dello schermo.
Siamo pronti per visualizzare le immagini sul piano.
Texture dell'immagine con shader personalizzato
Per prima cosa, carichiamo una delle immagini e visualizziamola sul <meshBasicMaterial>
corrente utilizzando il hook useTexture
di 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> ); };
L'immagine è visualizzata con cura sul piano.
Ora, poiché vogliamo aggiungere effetti creativi durante la transizione tra le immagini e al passaggio del mouse, creeremo un materiale shader personalizzato per poter visualizzare due immagini contemporaneamente e animarle.
ImageSliderMaterial
Creiamo il nostro materiale shader personalizzato chiamato ImageSliderMaterial
. Ho scelto di mantenerlo nello stesso file del componente ImageSlider
poiché è strettamente correlato ad esso, ma puoi crearlo in un file separato se preferisci.
// ... 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, }); // ...
Conserviamo la nostra texture in un uniform
chiamato uTexture
e la passiamo allo shader di frammentazione per visualizzarla.
Il tipo di uniform
uTexture
è sampler2D
, utilizzato per memorizzare texture 2D.
Per estrarre il colore della texture in una posizione specifica, utilizziamo la funzione texture2D
e le passiamo il uTexture
e le coordinate vUv
.
Sostituiamo il nostro meshBasicMaterial
con il nostro nuovo ImageSliderMaterial
:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
L'immagine è visualizzata utilizzando il nostro materiale shader personalizzato.
Correzione del colore
So che inizi ad avere un occhio acuto 🦅 e hai notato che la correzione del colore dell'immagine appare diversa!
Questo accade perché il <meshBasicMaterial/>
esegue un'elaborazione extra all'interno dello shader di frammento per regolare il colore in base al tone mapping e allo color space scelti sul renderer.
Anche se potremmo replicare manualmente questa cosa nel nostro shader personalizzato, non è l'obiettivo di questa lezione ed è un argomento avanzato.
Invece, possiamo utilizzare frammenti pronti all'uso per abilitare gli stessi effetti dei materiali standard di Three.js. Se guardi al codice sorgente di meshBasicMaterial, vedrai che è un mix di dichiarazioni #include
e codice personalizzato.
Per rendere il codice facilmente riutilizzabile e manutenibile, Three.js utilizza un preprocessore per includere codice da altri file. Fortunatamente, possiamo utilizzare quegli shader chunks anche nel nostro shader material personalizzato!
Aggiungiamo queste due righe alla fine del nostro shader di frammento:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
Per capire meglio come funzionano gli shader chunks, questo strumento ti permette di cliccare sulle dichiarazioni include per vedere il codice che è incluso: ycw.github.io/three-shaderlib-skim
La correzione del colore è ora la stessa del meshBasicMaterial. 🎨
Prima di andare oltre sullo shader, prepariamo la nostra interfaccia utente.
Gestione dello Stato con Zustand
Zustand è una piccola libreria di gestione dello stato, veloce e scalabile, che ci permette di creare uno store globale per gestire lo stato della nostra applicazione.
È un'alternativa a Redux o a una soluzione custom di context per condividere lo stato tra i componenti e gestire logiche di stato complesse. (Anche se non è il caso del nostro progetto, dove la logica è semplice.)
Aggiungiamo Zustand al nostro progetto:
yarn add zustand
E creiamo un nuovo file chiamato useSlider.js
in una cartella hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
La funzione create
prende una funzione come argomento che riceverà una funzione set
per aggiornare e unire lo stato per noi. Possiamo inserire il nostro stato e i metodi all'interno dell'oggetto restituito.
Prima i dati di cui abbiamo bisogno:
// ... 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
memorizzerà l'indice della slide corrente.direction
memorizzerà la direzione della transizione.items
memorizzerà i dati delle slide. (percorso dell'immagine, nome breve, titolo, colore di sfondo e descrizione)
Ora possiamo creare i metodi per andare alla slide precedente e successiva:
// ... 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 funzione set
unirà il nuovo stato con quello precedente. Usiamo l'operatore modulo per tornare alla prima slide quando raggiungiamo l'ultima e viceversa.
Il nostro stato è pronto, prepariamo la nostra UI.
Slider UI
Creeremo un nuovo componente denominato Slider
per visualizzare i dettagli testuali delle diapositive e i pulsanti di navigazione. In 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"> {/* CONTENITORE CENTRALE */} <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]"> {/* IN ALTO A SINISTRA */} <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> {/* FRECCE AL CENTRO */} <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> {/* IN BASSO A DESTRA */} <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> ); };
Non passeremo attraverso i dettagli del CSS utilizzato, ma lasciatemi spiegare i punti principali:
- Il contenitore centrale è un
div
che riproduce le dimensioni e il rapporto d'aspetto del nostro piano 3D. In questo modo possiamo posizionare il testo e i pulsanti in relazione al piano. - Usiamo
aspect-square
per mantenere il rapporto d'aspetto del contenitore. - I pulsanti a freccia provengono da Heroicons.
- Il titolo e il nome breve hanno dimensioni fisse e overflow hidden per creare successivamente effetti di testo interessanti.
- Le classi
md:
sono usate per adattare il layout su schermi più grandi.
Aggiungiamo il nostro componente Slider
accanto al Canvas
in 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;
Lo slider è visualizzato prima del canvas.
Dobbiamo cambiare lo stile del Canvas
affinché sia visualizzato come sfondo e prenda la larghezza e l'altezza dell'intero schermo:
{/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }} className="top-0 left-0" style={{ // Sovrascrivere lo stile di default applicato da R3F width: "100%", height: "100%", position: "absolute", }} > {/* ... */}
Aggiungiamo font e stili personalizzati al nostro 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; } }
Le classi text-outline
sono usate per creare un contorno attorno al testo.
Per aggiungere i font personalizzati, dobbiamo aggiornare il nostro 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: [], };
Ora abbiamo un'interfaccia utente dall'aspetto gradevole:
Effetti di testo
Per rendere le transizioni più interessanti, aggiungeremo alcuni effetti di testo al titolo, nome breve e descrizione.
Per prima cosa, dobbiamo ottenere la direzione per sapere se stiamo andando alla diapositiva successiva o precedente. Possiamo ottenerla dal hook useSlider
:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
Per poter animare il testo precedentemente visualizzato in uscita e il nuovo testo in ingresso, abbiamo bisogno dell'indice della diapositiva precedente. Possiamo calcolarlo 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; } // ... };
Ora possiamo aggiungere gli effetti di testo con l'aiuto di Framer Motion. Iniziamo con il titolo in basso a destra:
// ... 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"> {/* CONTENITORE CENTRALE */} <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]"> {/* ... */} {/* IN BASSO A DESTRA */} <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" // per far funzionare il transform (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> ); };
Usiamo la prop animate
per passare tra i diversi stati e definiamo le diverse proprietà per ciascuno stato nella prop variants
.
Per animare ogni carattere, dividiamo il titolo in un array di caratteri e usiamo la prop staggerChildren
per ritardare l'animazione di ciascun carattere.
La prop from
è usata per definire la posizione iniziale dell'animazione.
Rimuoviamo l'overflow-hidden
dal titolo per vedere l'effetto:
Il testo del titolo è animato in entrata e in uscita.
Aggiungiamo lo stesso effetto al nome breve:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* CONTENITORE CENTRALE */} <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]"> {/* IN ALTO A SINISTRA */} <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 un semplice effetto di fade in e out per la descrizione:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* CONTENITORE CENTRALE */} <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]"> {/* ... */} {/* IN BASSO A DESTRA */} {/* ... */} <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> ); };
La nostra UI è ora animata e pronta per essere utilizzata.
Siamo pronti per passare alla parte più interessante di questa lezione: l'effetto di transizione shader! 🎉
Effetto di transizione delle immagini
Come abbiamo fatto per l'animazione del testo, per effettuare la transizione tra le immagini avremo bisogno delle texture dell'immagine corrente e di quella precedente.
Salviamo il percorso dell'immagine precedente nel nostro 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 l'hook useEffect
, memorizziamo il percorso dell'immagine corrente nello stato lastImage
e quando l'immagine cambia, aggiorniamo lo stato lastImage
con il nuovo percorso dell'immagine.
Prima di utilizzare il prevTexture
nel nostro shader, e prima che ce ne dimentichiamo, precarichiamo tutte le immagini per evitare sfarfallii quando cambiamo diapositiva:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
Facendo questo, precarichiamo tutte le immagini, e possiamo tranquillamente aggiungere una schermata di caricamento all'inizio del nostro sito web per evitare qualsiasi sfarfallio.
Ora, aggiungiamo due uniformi al nostro ImageSliderMaterial
per memorizzare la texture precedente e il progresso della transizione:
// ... 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> ); };
Utilizziamo la funzione mix
per interpolare tra la texture precedente e quella corrente basandoci sull'uniforme uProgression
.
Possiamo vedere una mescolanza tra l'immagine precedente e quella corrente.
Effetto di dissolvenza
Animiamo l'uniforme uProgression
per creare una transizione fluida tra le immagini.
Per prima cosa, abbiamo bisogno di un riferimento al nostro material per poter aggiornare l'uniforme 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> ); };
Possiamo eliminare la prop uProgression
dato che la aggiorneremo manualmente.
Ora nel useEffect
, quando l'immagine cambia, possiamo impostare uProgression
a 0
e animarla verso 1
in un 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 ); }); // ... };
Ora abbiamo una transizione fluida tra le immagini.
Costruiamo su questo per creare un effetto più interessante.
Posizione distorta
Per rendere la transizione più interessante, spingeremo le immagini nella direzione della transizione.
Utilizzeremo le coordinate vUv
per distorcere la posizione delle immagini. Aggiungiamo un uniform uDistortion
al nostro ImageSliderMaterial
e usiamolo per distorcere le coordinate vUv
:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.