عارض الصور
في هذا الدرس سنتعلم كيفية تحميل واستخدام صور القوام في الشيدر لإنشاء هذا عارض الصور المستجيب:
وهذا هو النتيجة النهائية على الجوال:
المشروع مستوحى من هذا Codepen من Sikriti Dakua.
آمل أن تكون متحمسًا لتعلم كيفية إنشاء هذا التأثير، دعونا نبدأ!
مشروع البداية
يحتوي مشروع البداية الخاص بنا على قسم ملء الشاشة يحتوي على شعار، زر قائمة، و <Canvas>
مع مكعب أبيض في منتصف المشهد.
سنستخدم Framer Motion لتحريك عناصر HTML لكن يمكنك استخدام أي مكتبة أخرى أو حتى CSS عادي لتحريكها. سنستخدم فقط النسخة الافتراضية من Framer Motion، لا حاجة لتثبيت حزمة 3D.
بالنسبة لـ الواجهة اخترت Tailwind CSS لكن لا تتردد في استخدام الحل الذي تكون مرتاحًا له.
المجلد public/textures/optimized
يحتوي على الصور التي سنستخدمها في العارض. لقد قمت بتوليدها باستخدام الذكاء الاصطناعي مع Leonardo.Ai وقمت بتحسينها باستخدام Squoosh. اخترت نسبة 3:4 للحصول على توجيه رأسي سيبدو جيدًا على الجوال.
إحدى الصور التي سنستخدمها تم تحسينها باستخدام Squoosh من 3.9mb إلى 311kb.
مكون عارض الصور
لنبدأ باستبدال المكعب الأبيض بمستوى سيتم استخدامه لعرض الصور. نقوم بإنشاء مكون جديد يسمى ImageSlider
:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
قم بضبط العرض والارتفاع بناءً على نسبة العرض إلى الارتفاع للصور التي ستستخدمها.
ستُستخدم خاصية fillPercent
لضبط حجم المستوى ليأخذ فقط نسبة معينة من ارتفاع/عرض الشاشة.
في App.jsx
نقوم باستيراد مكون ImageSlider
واستبدال المكعب الأبيض به:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
وهذه هي النتيجة:
المستوى يأخذ مساحة كبيرة جدا
نود أن يكون المستوى متجاوبًا ويأخذ فقط 75%(fillPercent
) من ارتفاع الشاشة. يمكننا تحقيق ذلك باستخدام هوك useThree
للحصول على أبعاد viewport
وإنشاء معامل مقياس لضبط حجم المستوى:
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> ); };
لحساب معامل المقياس، نقسم viewport.height
على height
للمستوى مقسومًا على fillPercent
. سيعطينا ذلك نسبة يمكننا استخدامها لقياس حجم المستوى.
لفهم الرياضيات خلف ذلك، يمكننا التفكير في
viewport.height
كأقصى ارتفاع للمستوى. إذا كان ارتفاع الviewport لدينا هو 3 وارتفاع المستوى لدينا هو 4، فنحن بحاجة إلى قياس المستوى بمقدار3 / 4
ليكون مناسبًا للشاشة. ولكن لأننا نريد أن نأخذ فقط 75% من ارتفاع الشاشة، نقسم ارتفاع المستوى علىfillPercent
للحصول على الارتفاع المرجعي الجديد. مما يعطي4 / 0.75 = 5.3333
.
ثم نضرب width
وheight
في ratio
للحصول على الأبعاد الجديدة.
يعمل بشكل جيد عندما نقوم بتغيير الحجم رأسيًا ولكن ليس أفقيًا. نحتاج إلى ضبط عرض المستوى ليأخذ فقط 75% من عرض الشاشة عند كون ارتفاع viewport
أكبر من العرض.
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> ); };
لا تنسَ تغيير ratio
من const
إلى let
لتتمكن من إعادة تعيينه. (أو استخدم مشغل ثلاثي بدلاً من ذلك)
الآن المستوى متجاوب ويأخذ فقط 75% من ارتفاع أو عرض الشاشة حسب أبعاد الشاشة.
نحن جاهزون لعرض الصور على المستوى.
نسيج الصورة بخطاطة مخصصة
أولاً، دعونا نحمل إحدى الصور ونعرضها على <meshBasicMaterial>
الحالي باستخدام خطاف useTexture
من 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> ); };
تم عرض الصورة بشكل جميل على الطائرة.
الآن، لأننا نريد إضافة تأثيرات إبداعية أثناء الانتقال بين الصور وعند التمرير بالماوس، سنقوم بإنشاء خطاطة مخصصة لعرض صورتين في نفس الوقت وتحريكهما.
ImageSliderMaterial
دعونا ننشئ خطاطة مخصصة تُسمى ImageSliderMaterial
. اخترت الاحتفاظ بها في نفس الملف مثل مكون ImageSlider
لأنها مرتبطة به ارتباطًا وثيقًا. ولكن يمكنك إنشاء ملف منفصل إذا كنت تفضل ذلك.
// ... 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, }); // ...
نقوم بتخزين النسيج لدينا في uniform
باسم uTexture
ونمرره إلى المُظلل التجزيئي لعرضه.
نوع الـ uniform
uTexture
هو sampler2D
والذي يُستخدم لتخزين النسيج ثنائي الأبعاد.
لاستخراج لون النسيج في موضع معين، نستخدم وظيفة texture2D
ونمرر لها uTexture
وإحداثيات vUv
.
دعونا نستبدل meshBasicMaterial
بـ ImageSliderMaterial
الجديدة لدينا:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
تم عرض الصورة باستخدام خطاطتنا المخصصة.
ضبط الألوان
أعلم أنك بدأت تلاحظ بحدة 🦅 وقد لاحظت أن ضبط ألوان الصورة يبدو مختلفًا!
هذا لأن <meshBasicMaterial/>
تقوم ببعض المعالجة الإضافية داخل الـ fragment shader لتعديل اللون بناءً على tone mapping و color space المختارين في الـ renderer.
بينما يمكننا تكرار ذلك يدويًا في الـ shader المخصص لدينا، إلا أن هذا ليس هدف هذه الدرس وموضوعًا متقدمًا.
بدلاً من ذلك، يمكننا استخدام fragments جاهزة لتمكين نفس التأثيرات كالـ materials القياسية في Three.js. إذا نظرت إلى كود المصدر لـ meshBasicMaterial، سترى أنه مزيج من تصريحات #include
وكود مخصص.
لجعل الكود قابلًا لإعادة الاستخدام والصيانة بسهولة، يستخدم Three.js مسبقاَ معالجة تشمل الكود من ملفات أخرى. لحسن الحظ، يمكننا استخدام تلك الـ shader chunks في الـ shader المخصص لدينا أيضًا!
لنضف تلك السطور في نهاية الـ fragment shader الخاص بنا:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
لفهم أفضل لكيفية عمل الـ shader chunks، هذه الأداة تسمح لك بالنقر على تصريحات الـ include لرؤية الكود الذي يتم تضمينه: ycw.github.io/three-shaderlib-skim
الآن ضبط الألوان هو نفسه كالـ meshBasicMaterial. 🎨
قبل المضي قدمًا في الـ shader، دعونا نجهز الـ UI الخاص بنا.
إدارة الحالة باستخدام Zustand
Zustand هي مكتبة صغيرة وسريعة وقابلة للتوسع لإدارة الحالة تتيح لنا إنشاء مخزن عالمي لإدارة حالة التطبيق الخاص بنا.
إنها بديل لـ Redux أو لحل مخصص لتمرير السياق للمشاركة بين المكونات وإدارة منطق الحالة المعقدة. (حتى لو لم يكن الأمر كذلك في مشروعنا. منطقنا بسيط.)
لنقم بإضافة Zustand إلى مشروعنا:
yarn add zustand
ونقوم بإنشاء ملف جديد باسم useSlider.js
في مجلد hooks
:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
تأخذ الدالة create
دالة كمعامل وستتلقى دالة set
لتحديث ودمج الحالة نيابة عنا. يمكننا وضع حالتنا والطرق داخل الكائن العائد.
أولاً البيانات التي نحتاجها:
// ... 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
ستخزن مؤشر الشريحة الحالية.direction
ستخزن اتجاه الانتقال.items
ستخزن بيانات الشرائح. (المسار إلى الصورة، الاسم المختصر، العنوان، لون الخلفية والوصف)
الآن يمكننا إنشاء الطرق للذهاب إلى الشريحة السابقة والتالية:
// ... 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", })), }));
ستدمج الدالة set
الحالة الجديدة مع السابقة. نستخدم معامل باقي القسمة للعودة إلى الشريحة الأولى عند الوصول إلى الأخيرة والعكس بالعكس.
حالتنا جاهزة، دعونا نحضر واجهة المستخدم.
واجهة المستخدم للمنزلق
سنقوم بإنشاء مكون جديد يسمى Slider
لعرض تفاصيل نص الشريحة وأزرار التنقل. في 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"> {/* الحاوية الوسطى */} <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]"> {/* أعلى اليسار */} <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> {/* الأسهم الوسطى */} <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> {/* الأسفل اليمين */} <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> ); };
لن نمر بتفاصيل CSS المستخدمة ولكن دعني أشرح لك النقاط الرئيسية:
- الحاوية الوسطى هي
div
تعيد إنتاج الأبعاد ونسبة العرض إلى الارتفاع لسطحنا ثلاثي الأبعاد. وبهذه الطريقة يمكننا وضع النص والأزرار بشكل نسبي للطائرة. - نستخدم
aspect-square
للحفاظ على نسبة العرض إلى الارتفاع للحاوية. - تأتي أزرار السهم من Heroicons.
- لديك العنوان والاسم القصير أبعاد ثابتة وتدفق مخفي لإنشاء تأثيرات نصية مثيرة لاحقًا.
- تستخدم
md:
الفئات لضبط التخطيط على الشاشات الأكبر حجمًا.
دعونا نضيف مكون Slider
بجانب Canvas
في 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;
يتم عرض المنزلق قبل الـ canvas.
نحن بحاجة إلى تغيير نمط Canvas
ليتم عرضها كخلفية ولتأخذ عرض وارتفاع الشاشة بالكامل:
{/* ... */} <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", }} > {/* ... */}
دعونا نضيف الخطوط والأساليب المخصصة إلى 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; } }
تستخدم فئات text-outline
لإنشاء خط خارجي حول النص.
لإضافة الخطوط المخصصة، نحن بحاجة إلى تحديث 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: [], };
الآن، لدينا واجهة مستخدم جذابة:
تأثيرات النص
لجعل الانتقالات أكثر إثارة، سنضيف بعض تأثيرات النص إلى العنوان والاسم القصير والوصف.
أولاً، نحتاج إلى الحصول على الاتجاه لمعرفة ما إذا كنا نتجه إلى الشريحة التالية أو الشريحة السابقة. يمكننا الحصول عليه من الـ useSlider
hook:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
لكي نتمكن من تحريك النص الذي كان يعرض سابقاً للخارج والنص الجديد للداخل، نحتاج إلى فهرس الشريحة السابقة. يمكننا حسابه بسهولة:
// ... 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; } // ... };
الآن يمكننا إضافة تأثيرات النص بمساعدة Framer Motion. لنبدأ بالعنوان في الأسفل على اليمين:
// ... 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" 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> ); };
نستخدم الخاصية animate
للتبديل بين الحالات المختلفة ونحدد الخصائص المختلفة لكل حالة في خاصية variants
.
لتحريك كل حرف، نقوم بتقسيم العنوان إلى مصفوفة من الأحرف ونستخدم خاصية staggerChildren
لتأخير حركة كل حرف.
تستخدم خاصية from
لتعريف الموضع البداية للحركة.
دعونا نزيل خاصية overflow-hidden
من العنوان لرؤية التأثير:
يتم تحريك نص العنوان دخولاً وخروجاً.
لنضيف نفس التأثير على الاسم القصير:
// ... 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> ); };
وتأثير بسيط للظهور والاختفاء للوصف:
// ... 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> ); };
الآن أصبحت واجهة المستخدم الخاصة بنا محركة وجاهزة للاستخدام.
نحن جاهزون للانتقال إلى الجزء الأكثر إثارة في هذا الدرس: تأثير انتقال shader! 🎉
تأثير الانتقال بين الصور
كما فعلنا مع تحريك النص، للقيام بالانتقال بين الصور، سنحتاج إلى الملمس الحالي والسابق للصورة.
لنقم بتخزين مسار الصورة السابقة في مكون 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]); // ... };
باستخدام useEffect
، نقوم بتخزين مسار الصورة الحالي في الحالة lastImage
وعند تغيير الصورة، نقوم بتحديث حالة lastImage
بمسار الصورة الجديدة.
قبل استخدام prevTexture
في الـ shader الخاص بنا، وقبل أن ننسى، دعونا نقوم بتحميل جميع الصور مسبقاً لتجنب التذبذب عند تغيير الشريحة:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
من خلال القيام بذلك، نحن نحمل جميع الصور مسبقاً، يمكننا بأمان إضافة شاشة تحميل في بداية موقعنا لتجنب أي تذبذب.
الآن، دعونا نضيف متغيرين إلى ImageSliderMaterial
لتخزين الملمس السابق وتقدم الانتقال:
// ... 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> ); };
نستخدم دالة mix
للتداخل بين الملمس السابق والحالي بناءً على المتغير uProgression
.
يمكننا رؤية دمج بين الصورة السابقة والحالية.
تأثير التلاشي للداخل والخارج
لنقم بتحريك الـ uProgression
uniform لإنشاء انتقال سلس بين الصور.
أولاً، نحتاج إلى مرجع للمادة الخاصة بنا لنتمكن من تحديث الـ 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> ); };
يمكننا التخلص من الخاصية uProgression
حيث سنقوم بتحديثها يدوياً.
الآن في الـ useEffect
عندما تتغير الصورة، يمكننا ضبط الـ uProgression
على 0
وتحريكه إلى 1
في حلقة 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 ); }); // ... };
لدينا الآن انتقال سلس بين الصور.
دعونا نبني على هذا لإنشاء تأثير أكثر إثارة.
الموضع المشوه
لجعل الانتقال أكثر جاذبية، سنقوم بدفع الصور في اتجاه الانتقال.
سوف نستخدم إحداثيات vUv
لتشويه موضع الصور. دعونا نضيف ثابت uDistortion
إلى ImageSliderMaterial
ونستخدمه لتشويه إحداثيات 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.