Image slider
Dalam pelajaran ini kita akan belajar cara memuat dan menggunakan gambar tekstur dalam shader kita untuk membuat slider gambar responsif ini:
Dan berikut adalah hasil akhirnya di perangkat mobile:
Proyek ini terinspirasi oleh Codepen oleh Sikriti Dakua.
Saya harap Anda termotivasi untuk belajar cara membuat efek ini, mari kita mulai!
Proyek awal
Proyek awal kami berisi bagian layar penuh yang terdiri dari logo, tombol menu dan komponen <Canvas>
dengan kubus putih di tengah adegan.
Kita akan menggunakan Framer Motion untuk menganimasikan elemen HTML tetapi Anda bisa menggunakan pustaka lain atau bahkan CSS biasa untuk menganimasikannya. Kita hanya akan menggunakan versi default dari Framer Motion, tidak perlu menginstal paket 3D.
Untuk UI saya memilih Tailwind CSS tetapi silakan gunakan solusi yang paling Anda nyaman.
Folder public/textures/optimized
berisi gambar yang akan kita gunakan dalam slider. Saya menghasilkan gambar-gambar ini menggunakan AI dengan Leonardo.Ai dan mengoptimasinya dengan Squoosh. Saya memilih rasio 3:4 untuk memiliki orientasi potret yang akan terlihat baik di perangkat mobile.
Salah satu gambar yang akan kita gunakan dioptimalkan dengan Squoosh dari 3.9mb menjadi 311kb.
Komponen Image slider
Mari kita mulai dengan mengganti kubus putih dengan bidang yang akan digunakan untuk menampilkan gambar. Kita buat sebuah komponen baru bernama ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
Sesuaikan lebar dan tinggi dengan rasio aspek dari gambar yang akan Anda gunakan.
Properti fillPercent
akan digunakan untuk menyesuaikan ukuran bidang agar hanya mengambil persentase dari tinggi/lebar layar.
Di App.jsx
kita impor komponen ImageSlider
dan mengganti kubus putih dengan itu:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
Dan inilah hasilnya:
Bidang mengambil terlalu banyak ruang
Kami ingin bidang kami bisa responsif dan hanya mengambil 75%(fillPercent
) dari tinggi layar. Kami bisa mencapainya dengan menggunakan hook useThree
untuk mendapatkan dimensi viewport
dan membuat faktor skala untuk menyesuaikan ukuran bidang:
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> ); };
Untuk menghitung faktor skala kita, kita membagi viewport.height
dengan height
bidang yang dibagi dengan fillPercent
. Ini akan memberi kita rasio yang bisa kita gunakan untuk menskalakan bidang.
Untuk memahami matematika di balik ini, kita bisa memikirkan
viewport.height
sebagai ketinggian maksimum bidang. Jika ketinggian viewport kita adalah 3 dan ketinggian bidang kita adalah 4, kita perlu menskalakan bidang dengan3 / 4
agar sesuai dengan layar. Namun karena kita hanya ingin mengambil 75% dari ketinggian layar, kita membagi ketinggian bidang denganfillPercent
untuk mendapatkan referensi ketinggian baru. Yang menghasilkan4 / 0.75 = 5.3333
.
Kemudian kita kalikan width
dan height
dengan ratio
untuk mendapatkan dimensi baru.
Ini bekerja baik saat kita mengubah ukuran secara vertikal tetapi tidak secara horizontal. Kita perlu menyesuaikan lebar bidang untuk hanya mengambil 75% dari lebar layar ketika tinggi viewport lebih besar dari lebar.
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> ); };
Jangan lupa untuk mengganti ratio
dari const
menjadi let
agar bisa diganti. (Atau gunakan operator ternary sebagai gantinya)
Sekarang bidang tersebut responsif dan hanya mengambil 75% dari tinggi atau lebar layar tergantung pada dimensi layar.
Kita siap menampilkan gambar di bidang tersebut.
Tekstur gambar shader kustom
Pertama, mari kita muat salah satu gambar dan tampilkan pada <meshBasicMaterial>
saat ini menggunakan useTexture
hook dari 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> ); };
Gambar terlihat dengan baik pada plane.
Sekarang, karena kita ingin menambahkan efek kreatif selama transisi antara gambar dan saat hover, kita akan membuat custom shader material untuk dapat menampilkan dua gambar sekaligus dan menganimasikannya.
ImageSliderMaterial
Mari kita buat custom shader material yang dinamai ImageSliderMaterial
. Saya memilih untuk menyimpannya dalam file yang sama dengan komponen ImageSlider
karena sangat terkait dengannya. Namun, Anda dapat membuat file terpisah jika diinginkan.
// ... 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, }); // ...
Kita menyimpan tekstur kita dalam uniform
bernama uTexture
dan kita berikan ke fragment shader untuk menampilkannya.
Tipe dari uTexture
uniform adalah sampler2D
yang digunakan untuk menyimpan tekstur 2D.
Untuk mengekstrak warna tekstur di posisi tertentu, kita menggunakan fungsi texture2D
dan memberikannya uTexture
dan koordinat vUv
.
Mari kita ganti meshBasicMaterial
kita dengan ImageSliderMaterial
yang baru:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
Gambar ditampilkan menggunakan shader material kustom kita.
Koreksi Warna
Saya tahu Anda mulai memiliki mata yang tajam 🦅 dan Anda memperhatikan bahwa koreksi warna gambar terlihat berbeda!
Ini karena <meshBasicMaterial/>
melakukan beberapa pemrosesan tambahan di dalam fragment shader untuk menyesuaikan warna berdasarkan pilihan tone mapping dan color space pada renderer.
Meskipun ini adalah sesuatu yang bisa kita tiru secara manual dalam custom shader kita, ini bukanlah tujuan dari pelajaran ini dan termasuk topik lanjutan.
Sebagai gantinya, kita dapat menggunakan fragmen siap pakai untuk mengaktifkan efek yang sama seperti material standar Three.js. Jika Anda melihat pada kode sumber meshBasicMaterial, Anda akan melihat bahwa itu adalah campuran dari #include
statements dan kode custom.
Untuk membuat kode yang mudah digunakan kembali dan dikelola, Three.js menggunakan preprocessing untuk memasukkan kode dari file lain. Untungnya, kita bisa menggunakan potongan shader tersebut dalam custom shader material kita juga!
Mari tambahkan dua baris ini di akhir fragment shader kita:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
Untuk memahami lebih baik bagaimana potongan shader bekerja, alat ini memungkinkan Anda untuk mengklik pada include statement untuk melihat kode yang disertakan: ycw.github.io/three-shaderlib-skim
Koreksi warna sekarang sama seperti meshBasicMaterial. 🎨
Sebelum melanjutkan dengan shader, mari kita siapkan UI kita.
Manajemen Keadaan dengan Zustand
Zustand adalah sebuah pustaka manajemen keadaan yang kecil, cepat, dan skalabel yang memungkinkan kita untuk membuat sebuah store global untuk mengelola keadaan aplikasi kita.
Ini adalah alternatif untuk Redux atau solusi konteks kustom untuk berbagi keadaan antara komponen dan mengelola logika keadaan yang kompleks. (Meskipun bukan demikian dalam proyek kita. Logika kita sederhana.)
Mari tambahkan Zustand ke dalam proyek kita:
yarn add zustand
Dan buat file baru bernama useSlider.js
di dalam folder hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
Fungsi create
mengambil sebuah fungsi sebagai argumen yang akan menerima fungsi set
untuk memperbarui dan menggabungkan keadaan untuk kita. Kita dapat menempatkan keadaan dan metode kita di dalam objek yang dikembalikan.
Pertama, data yang kita butuhkan:
// ... 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
akan menyimpan indeks slide saat ini.direction
akan menyimpan arah transisi.items
akan menyimpan data dari slides. (path ke gambar, nama singkat, judul, warna latar belakang, dan deskripsi)
Sekarang kita dapat membuat metode untuk ke slide sebelumnya dan berikutnya:
// ... 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", })), }));
Fungsi set
akan menggabungkan keadaan baru dengan keadaan sebelumnya. Kami menggunakan operator modulo untuk kembali ke slide pertama ketika kita mencapai yang terakhir dan sebaliknya.
Keadaan kita sudah siap, mari kita siapkan UI kita.
Antarmuka Slider
Kita akan membuat komponen baru bernama Slider
untuk menampilkan detail teks slide dan tombol navigasi. Dalam 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> ); };
Kita tidak akan membahas detail CSS yang digunakan, tetapi biarkan saya jelaskan poin-poin utamanya:
- Kontainer tengah adalah
div
yang mereproduksi dimensi dan rasio aspek pesawat 3D kita. Dengan cara ini, kita dapat memposisikan teks dan tombol relatif terhadap pesawat. - Kita menggunakan
aspect-square
untuk menjaga rasio aspek kontainer. - Tombol panah berasal dari Heroicons.
- Judul dan nama pendek memiliki dimensi tetap dan overflow tersembunyi untuk membuat efek teks menarik nantinya.
- Kelas
md:
digunakan untuk menyesuaikan tata letak pada layar yang lebih besar.
Mari tambahkan komponen Slider
kita di samping Canvas
dalam 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;
Slider ditampilkan sebelum kanvas.
Kita perlu mengubah gaya Canvas
agar ditampilkan sebagai latar belakang dan mengambil lebar dan tinggi layar penuh:
{/* ... */} <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", }} > {/* ... */}
Mari tambahkan font dan gaya kustom ke index.css
kita:
@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; } }
Kelas text-outline
digunakan untuk membuat garis luar pada teks.
Untuk menambahkan font kustom, kita perlu memperbarui tailwind.config.js
kita:
/** @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: [], };
Sekarang kita memiliki antarmuka yang terlihat bagus:
Efek Teks
Untuk membuat transisi lebih menarik, kita akan menambahkan beberapa efek teks pada judul, nama pendek, dan deskripsi.
Pertama, kita perlu mendapatkan arah untuk mengetahui apakah kita akan ke slide berikutnya atau sebelumnya. Kita bisa mendapatkannya dari hook useSlider
:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
Agar bisa menganimasikan teks yang ditampilkan sebelumnya keluar dan teks baru masuk, kita memerlukan indeks dari slide sebelumnya. Kita bisa menghitungnya dengan mudah:
// ... 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; } // ... };
Sekarang kita bisa menambahkan efek teks dengan bantuan Framer Motion. Mari mulai dengan judul di kanan bawah:
// ... 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"> {/* KONTENER TENGAH */} <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]"> {/* ... */} {/* KANAN BAWAH */} <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" // untuk membuat transform bekerja (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> ); };
Kita menggunakan animate
prop untuk beralih antara berbagai keadaan dan kita mendefinisikan berbagai properti untuk setiap keadaan di variants
prop.
Untuk menganimasikan setiap karakter, kita membagi judul menjadi array karakter dan menggunakan staggerChildren
prop untuk menunda animasi setiap karakter.
Prop from
digunakan untuk mendefinisikan posisi awal animasi.
Mari hapus overflow-hidden
dari judul untuk melihat efeknya:
Teks judul dianimasikan masuk dan keluar.
Mari tambahkan efek yang sama pada nama pendek:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* KONTENER TENGAH */} <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]"> {/* KIRI ATAS */} <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> ); };
Dan efek fade in dan out sederhana untuk deskripsi:
// ... export export Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* KONTENER TENGAH */} <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]"> {/* ... */} {/* KANAN BAWAH */} {/* ... */} <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> ); };
Antarmuka kita sekarang sudah dianimasikan dan siap digunakan.
Kita siap menuju bagian paling menarik dari pelajaran ini: efek transisi shader! 🎉
Efek transisi gambar
Seperti yang kita lakukan untuk melakukan animasi pada teks, untuk melakukan transisi antara gambar-gambar tersebut, kita memerlukan tekstur gambar saat ini dan sebelumnya.
Mari kita simpan jalur gambar sebelumnya di komponen ImageSlider
kita:
// ... 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]); // ... };
Dengan useEffect
hook, kita menyimpan jalur gambar saat ini di dalam lastImage
state dan ketika gambar berubah, kita memperbarui lastImage
state dengan jalur gambar yang baru.
Sebelum menggunakan prevTexture
dalam shader kita, dan sebelum lupa, mari kita muat terlebih dahulu semua gambar agar terhindar dari tampilan yang berkedip saat kita mengubah slide:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
Dengan melakukan ini, kita memuat semua gambar terlebih dahulu, kita bisa dengan aman menambahkan layar pemuatan di awal situs web kita untuk menghindari tampilan yang berkedip.
Sekarang, mari tambahkan dua uniforms ke ImageSliderMaterial
kita untuk menyimpan tekstur sebelumnya dan progres transisi:
// ... 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> ); };
Kita menggunakan fungsi mix
untuk menginterpolasi antara tekstur sebelumnya dan yang saat ini berdasarkan uniform uProgression
.
Kita bisa melihat percampuran antara gambar sebelumnya dan saat ini.
Efek Memudar Masuk dan Keluar
Mari kita animasikan uProgression
uniform untuk menciptakan transisi yang mulus antara gambar-gambar tersebut.
Pertama, kita perlu referensi ke material kita agar bisa memperbarui uProgression
uniform:
// ... 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> ); };
Kita bisa menghapus prop uProgression
karena kita akan memperbaruinya secara manual.
Sekarang di dalam useEffect
ketika gambar berubah, kita bisa mengatur uProgression
ke 0
dan menganimasikannya ke 1
dalam 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 ); }); // ... };
Transisi antara gambar-gambar sekarang menjadi lebih mulus.
Mari kita bangun lebih lanjut dari ini untuk menciptakan efek yang lebih menarik.
Posisi Terdistorsi
Untuk membuat transisi lebih menarik, kita akan mendorong gambar ke arah transisi.
Kita akan menggunakan koordinat vUv
untuk mendistorsi posisi gambar. Mari tambahkan uniform uDistortion
ke ImageSliderMaterial
dan menggunakannya untuk mendistorsi koordinat 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.