Wawa Sensei logo

Image slider

Starter pack

इस पाठ में हम सीखेंगे कि अपने शेडर्स में टेक्सचर इमेज को कैसे लोड और उपयोग करें ताकि हम यह प्रतिक्रियाशील इमेज स्लाइडर बना सकें:

और यहाँ मोबाइल पर अंतिम परिणाम है:

यह प्रोजेक्ट इस Codepen द्वारा Sikriti Dakua से प्रेरित है।

मुझे उम्मीद है कि आप इस प्रभाव को बनाने के लिए प्रेरित हैं, आइए शुरू करें!

Starter project

हमारे स्टार्टर प्रोजेक्ट में एक फुलस्क्रीन सेक्शन है जिसमें एक लोगो, एक मेनू बटन और एक <Canvas> कंपोनेंट है जिसमें दृश्य के मध्य में एक सफेद घन है।

हम Framer Motion का उपयोग HTML तत्वों को एनिमेट करने के लिए करेंगे, लेकिन आप किसी अन्य लाइब्रेरी या यहाँ तक कि साधारण CSS का उपयोग कर सकते हैं उन्हें एनिमेट करने के लिए। हम केवल Framer Motion के डिफ़ॉल्ट संस्करण का उपयोग करेंगे, 3D पैकेज को इंस्टॉल करने की आवश्यकता नहीं है।

UI के लिए मैंने Tailwind CSS चुना है, लेकिन आप अपना मनपसंद समाधान इस्तेमाल कर सकते हैं।

public/textures/optimized फ़ोल्डर में वे चित्र हैं जिन्हें हम स्लाइडर में उपयोग करेंगे। मैंने उन्हें AI के साथ Leonardo.Ai का उपयोग करके उत्पन्न किया है और Squoosh के साथ अनुकूलित किया है। मैंने पोर्ट्रेट ओरिएंटेशन के लिए 3:4 अनुपात चुना जो मोबाइल पर अच्छा दिखेगा।

AI Generated Image

हम जो छवि उपयोग करेंगे, उसे 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>
  );
};

इमेज के एस्पेक्ट रेशियो के अनुसार width और height को समायोजित करें।

fillPercent prop का इस्तेमाल प्लेन के आकार को समायोजित करने के लिए किया जाएगा ताकि यह स्क्रीन की ऊंचाई/चौड़ाई का केवल एक प्रतिशत ही ले।

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>
      {/* ... */}
    </>
  );
}

// ...

और यहाँ परिणाम है:

Image Slider Plane

प्लेन बहुत ज्यादा जगह ले रहा है

हम चाहते हैं कि हमारा प्लेन उत्तरदायी हो और स्क्रीन की ऊंचाई का केवल 75% (fillPercent) ही ले। हम इसे useThree hook का उपयोग करके 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>
  );
};

const से let में ratio को बदलना न भूलें ताकि इसे पुनः असाइन किया जा सके। (या इसके बजाय टर्नरी ऑपरेटर का उपयोग करें)

अब प्लेन उत्तरदायी है और स्क्रीन की ऊंचाई या चौड़ाई का केवल 75% लेता है, स्क्रीन आयामों के आधार पर।

हम प्लेन पर इमेज डिस्प्ले करने के लिए तैयार हैं।

कस्टम शेडर इमेज टेक्सचर

पहले, आइए एक इमेज लोड करते हैं और इसे वर्तमान <meshBasicMaterial> पर Drei के useTexture hook का उपयोग करके प्रदर्शित करते हैं:

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>
  );
};

Image Slider Plane with Texture

इमेज प्लेन पर अच्छी तरह प्रदर्शित होती है।

अब, क्योंकि हम इमेजेस के बीच ट्रांज़िशन और होवर पर क्रिएटिव इफेक्ट्स जोड़ना चाहते हैं, हम एक कस्टम शेडर material बनाएंगे ताकि एक ही समय में दो इमेजेस को प्रदर्शित कर सकें और उन्हें ऐनिमेट कर सकें।

ImageSliderMaterial

आइए हमारा कस्टम शेडर material बनाते हैं जिसका नाम ImageSliderMaterial है। मैंने इसे ImageSlider component के साथ ही रखने का चयन किया है क्योंकि यह इसके साथ कड़ाई से संबंधित है। लेकिन यदि आप चाहें तो एक अलग फाइल बना सकते हैं।

// ...
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,
});

// ...

हम अपने texture को एक uniform में संग्रहीत करते हैं जिसका नाम uTexture है और इसे fragment शेडर को दिखाने के लिए पास करते हैं।

uTexture uniform का प्रकार sampler2D है जिसका उपयोग 2D textures को संग्रहीत करने के लिए किया जाता है।

texture के एक विशेष स्थान पर रंग निकालने के लिए, हम texture2D फ़ंक्शन का उपयोग करते हैं और इसमें uTexture और vUv coordinates पास करते हैं।

आइए हमारे meshBasicMaterial को हमारे नए ImageSliderMaterial से बदलते हैं:

// ...

export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => {
  // ...
  return (
    <mesh>
      <planeGeometry args={[width * ratio, height * ratio]} />
      <imageSliderMaterial uTexture={texture} />
    </mesh>
  );
};

Image Slider Plane with ImageSliderMaterial

इमेज हमारे कस्टम शेडर material का उपयोग करके प्रदर्शित होती है।

रंग ग्रेडिंग

मुझे पता है कि आपकी नज़रें तेज़ हैं 🦅 और आपने देखा कि इमेज का रंग ग्रेडिंग अलग दिख रहा है!

ऐसा इसलिए है क्योंकि <meshBasicMaterial/> फ्रैगमेंट शेडर के अंदर अतिरिक्त प्रोसेसिंग करता है ताकि चुने गए tone mapping और color space के आधार पर रंग समायोजित किया जा सके।

जबकि हम इसे अपने कस्टम शेडर में मैन्युअली पुनः उत्पन्न कर सकते हैं, यह इस पाठ का लक्ष्य नहीं है और यह एक उन्नत विषय है।

इसके बजाय, हम तैयार फ्रैगमेंट्स का उपयोग कर सकते हैं ताकि Three.js के मानक materials के समान प्रभाव सक्षम किया जा सके। यदि आप meshBasicMaterial के सोर्स कोड को देखें, तो आप पाएंगे कि यह #include स्टेटमेंट्स और कस्टम कोड का एक मिश्रण है।

meshBasicMaterial source code

कोड को आसानी से पुन: प्रयोज्य और बनाए रखने योग्य बनाने के लिए, Three.js कोड को अन्य फाइलों से शामिल करने के लिए एक प्रीप्रोसेसर का उपयोग करता है। सौभाग्य से, हम उन शेडर क्षुधाओं (chunks) का उपयोग हमारे कस्टम शेडर material में भी कर सकते हैं!

आइए हमारे फ्रैगमेंट शेडर के अंत में उन दो लाइनों को जोड़ें:

  void main() {
    // ...
    #include <tonemapping_fragment>
    #include <encodings_fragment>
  }

बेहतर समझने के लिए कि शेडर क्षुधाएं कैसे काम करती हैं, यह टूल आपको शामिल स्टेटमेंट पर क्लिक करने की अनुमति देता है ताकि आप देख सकें कि कौन सा कोड शामिल किया गया है: ycw.github.io/three-shaderlib-skim

Image Slider Material with Shader Chunks

रंग ग्रेडिंग अब meshBasicMaterial के समान है। 🎨

शेडर पर आगे बढ़ने से पहले, आइए हमारे UI को तैयार करें।

Zustand स्टेट मैनेजमेंट

Zustand एक छोटा, तेज़ और स्केलेबल स्टेट मैनेजमेंट लाइब्रेरी है जो हमें हमारे एप्लिकेशन स्टेट को मैनेज करने के लिए एक ग्लोबल स्टोर बनाने की अनुमति देता है।

यह Redux या कस्टम कांटेक्स्ट सॉल्यूशन का एक विकल्प है जो घटकों के बीच स्टेट साझा करने और जटिल स्टेट लॉजिक को मैनेज करने के लिए है। (यहां तक कि हमारे परियोजना में यह आवश्यक नहीं है। हमारा लॉजिक सरल है।)

आइए Zustand को हमारे परियोजना में जोड़ते हैं:

yarn add zustand

और hooks फ़ोल्डर में एक नई फ़ाइल useSlider.js नामक बनाएं:

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 स्लाइड के डेटा को स्टोर करेगा। (image का path, छोटा नाम, शीर्षक, बैकग्राउंड कलर और विवरण)

अब हम पिछली और अगली स्लाइड पर जाने के लिए विधियों को बना सकते हैं:

// ...

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 फ़ंक्शन नए स्टेट को पिछले स्टेट के साथ मर्ज कर देगा। हम मॉड्यूलो ऑपरेटर का उपयोग पहले स्लाइड पर वापस लूप करने के लिए करते हैं जब हम अंतिम स्लाइड पर पहुंचते हैं और इसके विपरीत।

हमारा स्टेट तैयार है, अब हम अपने UI को तैयार करते हैं।

Slider UI

हम एक नया component "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">
      {/* 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>
  );
};

हम CSS के विवरण में नहीं जाएंगे लेकिन आइए मुख्य बिंदुओं को समझते हैं:

  • मिडल कंटेनर एक div है जो हमारे 3D plane के आयाम और पहलू अनुपात को पुन: उत्पन्न करता है। इस तरह हम टेक्स्ट और बटन को plane के सापेक्ष स्थिति में रख सकते हैं।
  • हम aspect-square का उपयोग करते हैं ताकि कंटेनर का पहलू अनुपात समान रहे।
  • एरो बटन Heroicons से आते हैं।
  • शीर्षक और छोटा नाम निश्चित आयाम और ओवरफ्लो छुपा हुआ है ताकि बाद में रोचक टेक्स्ट प्रभाव उत्पन्न किया जा सके।
  • बड़ी स्क्रीन पर लेआउट समायोजित करने के लिए md: क्लासेस का उपयोग किया जाता है।

आइए हमारे Slider component को 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;

Slider canvas से पहले प्रदर्शित होता है।

हमें Canvas की शैली को बदलने की आवश्यकता है ताकि इसे बैकग्राउंड के रूप में प्रदर्शित किया जा सके और स्क्रीन की पूरी चौड़ाई और ऊंचाई ले सके:

{/* ... */}
<Canvas
  camera={{ position: [0, 0, 5], fov: 30 }}
  className="top-0 left-0"
  style={{
    // 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 क्लास का उपयोग टेक्स्ट के चारों ओर एक 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: [],
};

अब हमारे पास एक अच्छा दिखने वाला UI है:

टेक्स्ट प्रभाव

परिवर्तनों को और दिलचस्प बनाने के लिए, हम शीर्षक, छोटा नाम और विवरण में कुछ टेक्स्ट प्रभाव जोड़ेंगे।

पहले, हमें यह जानने के लिए दिशा प्राप्त करने की आवश्यकता है कि हम अगले स्लाइड पर जा रहे हैं या पिछले स्लाइड पर। हम इसे 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" // 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>
  );
};

हम विभिन्न अवस्थाओं के बीच स्विच करने के लिए 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>
  );
};

हमारा UI अब एनिमेटेड है और उपयोग के लिए तैयार है।

हम इस पाठ के सबसे दिलचस्प भाग में छलांग लगाने के लिए तैयार हैं: शेडर ट्रांजिशन प्रभाव! 🎉

इमेज ट्रांजिशन इफेक्ट

जैसे हमने टेक्स्ट को एनिमेट करने के लिए किया था, इमेज के बीच ट्रांजिशन करने के लिए, हमें वर्तमान और पिछले इमेज टेक्सचर्स की आवश्यकता होगी।

आइए हमारे 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 स्टेट को अपडेट करते हैं।

हमारे shader में prevTexture का उपयोग करने से पहले, और इससे पहले कि हम भूल जाएं, सभी इमेज को प्रीलोड कर लें ताकि जब हम स्लाइड बदलें तो फ्लिकरिंग न हो:

// ...

export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => {
  // ...
};

useSlider.getState().items.forEach((item) => {
  useTexture.preload(item.image);
});

ऐसा करने से हम सभी इमेज को प्रीलोड कर रहे हैं, हम सुरक्षित रूप से अपनी वेबसाइट की शुरुआत में एक लोडिंग स्क्रीन जोड़ सकते हैं ताकि किसी भी प्रकार की फ्लिकरिंग से बचा जा सके।

अब, चलिए हमारे ImageSliderMaterial में दो uniforms जोड़ते हैं ताकि पिछले texture और ट्रांजिशन की प्रगति को स्टोर कर सकें:

// ...

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 फ़ंक्शन का उपयोग करके पिछले और वर्तमान texture के बीच uProgression uniform के आधार पर इंटरपोलेट करते हैं।

हम पिछले और वर्तमान इमेज के बीच मिक्स देख सकते हैं।

Fade in and out effect

आइए हम uProgression uniform को एनिमेट करके इमेजेस के बीच एक स्मूथ ट्रांज़िशन बनाते हैं।

पहले, हमें अपने material का संदर्भ चाहिए ताकि हम 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 prop को हटा सकते हैं क्योंकि हम इसे मैन्युअली अपडेट करेंगे।

अब जब इमेज बदलती है, तो 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 समन्वय का उपयोग करके छवियों की स्थिति को विकृत करेंगे। आइए ImageSliderMaterial में एक uDistortion uniform जोड़ें और इसका उपयोग vUv समन्वय को विकृत करने के लिए करें:

End of lesson preview

To get access to the entire lesson, you need to purchase the course.