⚡️ Limited Black Friday Deal
Get 50% off on the React Three Fiber Ultimate Course with the promo code ULTIMATE50
Buy Now
Fundamentals
Core
Master
Shaders
Slider d'images
Dans cette leçon, nous allons apprendre à charger et utiliser des textures d'images dans nos shaders pour créer ce slider d'images réactif :
Et voici le résultat final sur mobile :
Le projet est inspiré de ce Codepen par Sikriti Dakua.
J'espère que vous êtes motivés pour apprendre comment créer cet effet, commençons !
Projet de départ
Notre projet de départ contient une section en plein écran avec un logo, un bouton de menu et un composant <Canvas>
avec un cube blanc au milieu de la scène.
Nous utiliserons Framer Motion pour animer les éléments HTML mais vous pouvez utiliser n'importe quelle autre bibliothèque ou même du CSS pur pour les animer. Nous n'utiliserons que la version par défaut de Framer Motion, inutile d'installer le package 3D.
Pour l'UI, j'ai choisi Tailwind CSS mais n'hésitez pas à utiliser la solution avec laquelle vous êtes le plus à l'aise.
Le dossier public/textures/optimized
contient les images que nous utiliserons dans le slider. Je les ai générées en utilisant l'IA avec Leonardo.Ai et les ai optimisées avec Squoosh. J'ai choisi un ratio de 3:4 pour avoir une orientation portrait qui rendra bien sur mobile.
Une des images que nous utiliserons, optimisée avec Squoosh de 3,9mb à 311kb.
Composant de diaporama d'images
Commençons par remplacer le cube blanc par un plan qui sera utilisé pour afficher les images. Nous créons un nouveau composant nommé ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Ajustez la largeur et la hauteur au ratio d'aspect des images que vous utiliserez.
La prop fillPercent
sera utilisée pour ajuster la taille du plan afin qu'il ne prenne qu'un pourcentage de la hauteur/largeur de l'écran.
Dans App.jsx
, nous importons le composant ImageSlider
et remplaçons le cube blanc par celui-ci :
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
Et voici le résultat :
Le plan prend trop de place
Nous voulons que notre plan soit réactif et ne prenne que 75% (fillPercent
) de la hauteur de l'écran. Nous pouvons y parvenir en utilisant le hook useThree
pour obtenir les dimensions du viewport
et créer un facteur d'échelle pour ajuster la taille du plan :
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> ); };
Pour calculer notre facteur d'échelle, nous divisons la viewport.height
par la height
du plan divisée par fillPercent
. Cela nous donnera un ratio que nous pouvons utiliser pour mettre à l'échelle le plan.
Pour comprendre les mathématiques derrière cela, nous pouvons penser à la
viewport.height
comme la hauteur maximale du plan. Si la hauteur de notre viewport est de 3 et la hauteur de notre plan est de 4, nous devons mettre à l'échelle le plan par3 / 4
pour le faire tenir dans l'écran. Mais comme nous voulons prendre seulement 75% de la hauteur de l'écran, nous divisons la hauteur du plan parfillPercent
pour obtenir la nouvelle hauteur de référence. Ce qui donne4 / 0.75 = 5.3333
.
Ensuite, nous multiplions la width
et la height
par le ratio
pour obtenir les nouvelles dimensions.
Cela fonctionne bien lorsque nous redimensionnons verticalement mais pas horizontalement. Nous devons ajuster la largeur du plan pour ne prendre que 75% de la largeur de l'écran lorsque la hauteur du viewport est supérieure à la largeur.
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'oubliez pas de changer le ratio
de const
à let
pour pouvoir le réassigner. (Ou utilisez un opérateur ternaire à la place)
Maintenant, le plan est réactif et ne prend que 75% de la hauteur ou de la largeur de l'écran en fonction des dimensions de l'écran.
Nous sommes prêts à afficher les images sur le plan.
Texture d'image de shader personnalisé
Tout d'abord, chargeons l'une des images et affichons-la sur le <meshBasicMaterial>
actuel en utilisant le 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> ); };
L'image est bien affichée sur le plan.
Maintenant, parce que nous voulons ajouter des effets créatifs lors de la transition entre les images et au survol, nous allons créer un material de shader personnalisé pour pouvoir afficher deux images en même temps et les animer.
ImageSliderMaterial
Créons notre material de shader personnalisé nommé ImageSliderMaterial
. J'ai choisi de le garder dans le même fichier que le composant ImageSlider
car il y est étroitement lié. Mais vous pouvez créer un fichier séparé si vous préférez.
// ... 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, }); // ...
Nous stockons notre texture dans un uniform
nommé uTexture
et nous le transmettons au fragment shader pour l'afficher.
Le type de l'uniform uTexture
est sampler2D
, lequel est utilisé pour stocker les textures 2D.
Pour extraire la couleur de la texture à une position spécifique, nous utilisons la fonction texture2D
et lui passons le uTexture
et les coordonnées vUv
.
Remplaçons notre meshBasicMaterial
par notre nouveau ImageSliderMaterial
:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
L'image est affichée en utilisant notre material de shader personnalisé.
Color grading
Je sais que tu commences à avoir une vision aiguisée 🦅 et tu as remarqué que le color grading de l'image semble différent !
C'est parce que le <meshBasicMaterial/>
effectue un traitement supplémentaire dans le fragment shader pour ajuster la couleur en fonction du tone mapping et du color space choisis sur le renderer.
Bien que cela soit quelque chose que nous pourrions reproduire manuellement dans notre shader personnalisé, ce n'est pas l'objectif de cette leçon et c'est un sujet avancé.
À la place, nous pouvons utiliser des fragments prêts à l'emploi pour activer les mêmes effets que les matériaux standard de Three.js. Si tu regardes le code source du meshBasicMaterial, tu verras que c'est un mélange d'instructions #include
et de code personnalisé.
Pour rendre le code facilement réutilisable et maintenable, Three.js utilise un préprocesseur pour inclure du code provenant d'autres fichiers. Heureusement, nous pouvons aussi utiliser ces shader chunks dans notre shader material personnalisé !
Ajoutons ces deux lignes à la fin de notre fragment shader :
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
Pour mieux comprendre comment fonctionnent les shader chunks, cet outil te permet de cliquer sur les instructions d'inclusion pour voir le code inclus : ycw.github.io/three-shaderlib-skim
Le color grading est maintenant le même que celui du meshBasicMaterial. 🎨
Avant d'aller plus loin sur le shader, préparons notre UI.
Gestion de l'État avec Zustand
Zustand est une bibliothèque de gestion d'état petite, rapide et évolutive qui nous permet de créer un store global pour gérer l'état de notre application.
C'est une alternative à Redux ou à une solution de context personnalisée pour partager l'état entre les composants et gérer une logique d'état complexe. (Même si ce n'est pas le cas dans notre projet. Notre logique est simple.)
Ajoutons Zustand à notre projet :
yarn add zustand
Et créons un nouveau fichier nommé useSlider.js
dans un dossier hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
La fonction create
prend une fonction comme argument qui recevra une fonction set
pour mettre à jour et fusionner l'état pour nous. Nous pouvons mettre notre état et nos méthodes à l'intérieur de l'objet retourné.
D'abord, les données dont nous avons besoin :
// ... 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
stockera l'index de la diapositive actuelle.direction
stockera la direction de la transition.items
stockera les données des diapositives. (chemin de l'image, nom court, titre, couleur de fond et description)
Nous pouvons maintenant créer les méthodes pour aller à la diapositive précédente et à la suivante :
// ... 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 fonction set
fusionnera le nouvel état avec le précédent. Nous utilisons l'opérateur modulo pour revenir à la première diapositive lorsque nous atteignons la dernière et vice versa.
Notre état est prêt, préparons notre UI.
Interface Slider
Nous allons créer un nouveau composant nommé Slider
pour afficher les détails du texte du slide et les boutons de navigation. Dans 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"> {/* MID 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> {/* MID 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> ); };
Nous n'allons pas entrer dans les détails du CSS utilisé, mais laissez-moi vous expliquer les points principaux :
- Le conteneur central est un
div
reproduisant les dimensions et le ratio d'aspect de notre plan 3D. De cette manière, nous pouvons positionner le texte et les boutons par rapport au plan. - Nous utilisons
aspect-square
pour conserver le ratio d'aspect du conteneur. - Les boutons en forme de flèche viennent de Heroicons.
- Le titre et le nom court ont des dimensions fixes et un débordement caché pour créer des effets de texte intéressants plus tard.
- Les classes
md:
sont utilisées pour ajuster la mise en page sur les écrans plus grands.
Ajoutons notre composant Slider
à côté du Canvas
dans 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;
Le slider est affiché avant le canvas.
Nous devons changer le style du Canvas
pour qu'il soit affiché comme un fond et qu'il prenne toute la largeur et la hauteur de l'écran :
{/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }} className="top-0 left-0" style={{ // Remplacement du style par défaut appliqué par R3F width: "100%", height: "100%", position: "absolute", }} > {/* ... */}
Ajoutons des polices et styles personnalisés à notre 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; } }
Les classes text-outline
sont utilisées pour créer un contour autour du texte.
Pour ajouter les polices personnalisées, nous devons mettre à jour notre 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: [], };
Nous avons maintenant une interface utilisateur élégante :
Effets de texte
Pour rendre les transitions plus intéressantes, nous ajouterons des effets de texte au titre, au nom court et à la description.
Tout d'abord, nous devons obtenir la direction pour savoir si nous allons à la diapositive suivante ou précédente. Nous pouvons l'obtenir à partir du hook useSlider
:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
Pour pouvoir animer le texte affiché précédemment et le nouveau texte, nous avons besoin de l'index de la diapositive précédente. Nous pouvons le calculer facilement :
// ... 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; } // ... };
Maintenant, nous pouvons ajouter les effets de texte avec l'aide de Framer Motion. Commençons par le titre en bas à droite :
// ... 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> ); };
Nous utilisons la prop animate
pour passer entre les différents états et nous définissons les différentes propriétés pour chaque état dans la prop variants
.
Pour animer chaque caractère, nous divisons le titre en un tableau de caractères et nous utilisons la prop staggerChildren
pour retarder l'animation de chaque caractère.
La prop from
est utilisée pour définir la position de départ de l'animation.
Retirons le overflow-hidden
du titre pour voir l'effet :
Le texte du titre est animé en entrée et sortie.
Ajoutons le même effet au nom court :
// ... 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> ); };
Et un effet simple de fondu pour la description :
// ... 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> ); };
Notre interface utilisateur est maintenant animée et prête à être utilisée.
Nous sommes prêts à passer à la partie la plus intéressante de cette leçon : l'effet de transition de shader ! 🎉
Effet de transition d'image
Comme nous l'avons fait pour l'animation du texte, pour effectuer la transition entre les images, nous aurons besoin des textures de l'image actuelle et de l'image précédente.
Stockons le chemin de l'image précédente dans notre composant 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]); // ... };
Avec le hook useEffect
, nous stockons le chemin de l'image actuelle dans l'état lastImage
et lorsque l'image change, nous mettons à jour l'état lastImage
avec le nouveau chemin de l'image.
Avant d'utiliser le prevTexture
dans notre shader, et avant d'oublier, préchargeons toutes les images pour éviter le scintillement lorsque nous changeons de diapositive :
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
En faisant cela, nous préchargeons toutes les images, nous pourrions ajouter en toute sécurité un écran de chargement au début de notre site Web pour éviter tout scintillement.
Ajoutons maintenant deux uniformes à notre ImageSliderMaterial
pour stocker la texture précédente et la progression de la transition :
// ... 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> ); };
Nous utilisons la fonction mix
pour interpoler entre la texture précédente et la texture actuelle en fonction de l'uniforme uProgression
.
On peut voir un mélange entre l'image précédente et l'image actuelle.
Effet de fondu entrant et sortant
Animons l'uniforme uProgression
pour créer une transition fluide entre les images.
Tout d'abord, nous avons besoin d'une référence à notre material pour pouvoir mettre à jour 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> ); };
Nous pouvons nous débarrasser de la prop uProgression
car nous la mettrons à jour manuellement.
Maintenant, dans le useEffect
lorsque l'image change, nous pouvons définir uProgression
à 0
et l'animer jusqu'à 1
dans une boucle 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 ); }); // ... };
Nous avons maintenant une transition fluide entre les images.
Construisons à partir de cela pour créer un effet plus intéressant.
Position déformée
Pour rendre la transition plus intéressante, nous allons pousser les images dans la direction de la transition.
Nous utiliserons les coordonnées vUv
pour déformer la position des images. Ajoutons un uniforme uDistortion
à notre ImageSliderMaterial
et utilisons-le pour déformer les coordonnées vUv
:
End of lesson preview
To get access to the entire lesson, you need to purchase the course.