Fundamentals
Core
Master
Shaders
Image slider
このレッスンでは、シェーダーでテクスチャイメージを読み込み、使用してレスポンシブな画像スライダーを作成する方法を学びます。
モバイルでの最終結果はこちらです:
このプロジェクトは Sikriti Dakua による Codepen からインスピレーションを得ました。
この効果を作成する方法を学ぶことにやる気を感じていることを願っています。さあ、始めましょう!
スタータープロジェクト
私たちのスタータープロジェクトには、ロゴ、メニューボタン、シーンの中心に白いキューブがある <Canvas>
コンポーネントを含むフルスクリーンセクションが含まれています。
HTML要素 をアニメーション化するために Framer Motion を使用しますが、他のライブラリや単純なCSSを使用しても構いません。Framer Motion のデフォルトバージョンだけを使用し、3Dパッケージ をインストールする必要はありません。
UI のために Tailwind CSS を選びましたが、自分が最も快適に感じるソリューションを使用しても構いません。
public/textures/optimized
フォルダーには、スライダーで使用する画像が含まれています。これらの画像は Leonardo.Ai でAIを使って生成し、Squoosh で最適化しました。ポートレート向きでモバイルで見栄えが良い 3:4 の比率を選びました。
Squooshで3.9mbから311kbに最適化された使用する画像の一つ。
Image slider component
まず、白いキューブを画像を表示するためのplaneに置き換えます。ImageSlider
という新しいコンポーネントを作成します:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
使用する画像のアスペクト比に合わせてwidthとheightを調整します。
fillPercent
プロップは、planeのサイズを画面の高さ/幅の一部だけに調整するために使用されます。
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> {/* ... */} </> ); } // ...
このような結果になります:
planeが大きすぎる
planeをレスポンシブにし、画面の高さの75%(fillPercent
)だけを占めるようにします。これを実現するために、useThree
フックを使ってviewport
の寸法を取得し、planeのサイズを調整するためのスケールファクターを作成します:
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
をplaneのheight
をfillPercent
で割った値で割ります。これにより、planeをスケールするための比率が得られます。
この数学を理解するために、
viewport.height
をplaneの最大高さと考えることができます。もしviewportの高さが3でplaneの高さが4の場合、3 / 4
でplaneをスケールして画面に収める必要があります。しかし、画面の高さの75%だけを占めたい場合、planeの高さをfillPercent
で割って新しい参照高さを得ます。つまり、4 / 0.75 = 5.3333
です。
その後、width
とheight
に比率を掛けて新しい寸法を得ます。
縦にリサイズするときはうまく動作しますが、横にはうまく動作しません。viewportの高さが幅より大きい場合、planeの幅を画面の幅の75%に調整する必要があります。
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
に変更することを忘れないでください。(または三項演算子を使用することもできます)
これで、planeはレスポンシブになり、画面の高さや幅に応じて75%だけを占めるようになります。
これで、planeに画像を表示する準備が整いました。
カスタムシェーダー画像テクスチャ
まず、Drei の useTexture
フックを使用して画像を読み込み、現在の <meshBasicMaterial>
に表示してみましょう:
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> ); };
この画像は、プレーンにきれいに表示されます。
次に、画像間の移行中およびホバー時にクリエイティブなエフェクトを追加したいので、カスタムシェーダーmaterialを作成し、2つの画像を同時に表示してアニメーションするようにします。
ImageSliderMaterial
ImageSliderMaterial
という名前のカスタムシェーダーmaterialを作成します。これは 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, }); // ...
テクスチャは uTexture
という名前の uniform
に保存され、フラグメントシェーダーに渡されて表示されるようにします。
uTexture
uniform のタイプは sampler2D
で、これは2Dテクスチャを保存するために使用されます。
特定の位置でテクスチャの色を抽出するには、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> ); };
画像はカスタムシェーダーmaterialを使用して表示されています。
Color grading
目ざとい方はお気づきかと思いますが🦅、画像のカラーグレーディングが異なっています!
これは、<meshBasicMaterial/>
がレンダラーで選択されたtone mappingおよびcolor spaceに基づいて色を調整するために、フラグメントシェーダ内で追加の処理を行うためです。
このレッスンの目標ではありませんし、高度なトピックですが、カスタムシェーダでこれを手動で再現することは可能です。
代わりに、標準のThree.js materialsと同じ効果を有効にするための、使いやすいフラグメントを利用できる準備ができています。meshBasicMaterialのソースコードを見れば、それが#include
文とカスタムコードのミックスであることがわかります。
コードを簡単に再利用可能かつ保守可能にするために、Three.jsはプリプロセッサを使用して他のファイルからコードをインクルードします。幸いなことに、これらのシェーダーチャンクをカスタムシェーダーマテリアルでも使用できます!
フラグメントシェーダの最後にこれら2行を追加しましょう:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
シェーダーチャンクの働きをよりよく理解するために、このツールを使用すると、インクルード文をクリックして含まれるコードを見ることができます:ycw.github.io/three-shaderlib-skim
カラーグレーディングがmeshBasicMaterialと同じになりました。🎨
シェーダについてこれ以上進む前に、UIの準備をしましょう。
Zustand State Management
Zustand は、私たちのアプリケーションの状態を管理するためのグローバルストアを作成できる、小さくて高速かつスケーラブルな状態管理ライブラリです。
これは、Reduxやカスタムコンテキストソリューションの代替として、コンポーネント間で状態を共有し、複雑な状態ロジック(プロジェクトではシンプルなロジックしか使用していませんが)を管理するための方法です。
プロジェクトにZustandを追加しましょう:
yarn add zustand
そして、hooks
フォルダーにuseSlider.js
という新しいファイルを作成します:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
create
関数は、引数として関数を受け取り、この関数に対してset
関数を渡します。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
関数は、新しい状態を以前の状態とマージします。モジュロ演算子を使用して、最後のスライドに達したときに最初のスライドに戻ったり、その逆も可能にしています。
状態が準備できましたので、UIの準備をしましょう。
Slider UI
新しいコンポーネント 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の詳細には触れませんが、主なポイントを説明します:
- 中央のコンテナは、3Dプレーンの寸法とアスペクト比を再現する
div
です。 これにより、テキストとボタンをプレーンに対して相対的に配置できます。 - コンテナのアスペクト比を保持するために
aspect-square
を使用します。 - 矢印ボタンは Heroicons から来ています。
- タイトルと短い名前には固定の寸法があり、オーバーフローは非表示にして、後で興味深いテキスト効果を作成します。
md:
クラスは、より大きな画面でレイアウトを調整するために使用されます。
Slider
コンポーネントをApp.jsx
のCanvas
の隣に追加しましょう:
// ... 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 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
クラスはテキストの周りにアウトラインを作成するために使用されます。
カスタムフォントを追加するために、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が完成しました:
テキストエフェクト
トランジションをより興味深くするために、タイトル、短い名前、説明にいくつかのテキストエフェクトを追加します。
まず、次のスライドに移動するのか前のスライドに戻るのかを判断するためにdirectionを取得する必要があります。これはuseSlider
フックから取得できます:
// ... 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がアニメーション化され、使用準備ができました。
さて、このレッスンの最も興味深い部分であるシェーダートランジション効果に進む準備が整いました! 🎉
Image transition effect
テキストのアニメーションと同様に、画像間のトランジションを行うためには、現在の画像テクスチャと前の画像テクスチャが必要です。
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
を使う前に、そして忘れる前に、スライドを変更するときのちらつきを避けるためにすべての画像をプリロードしましょう:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
これにより、すべての画像をプリロードするので、ちらつきを避けるためにウェブサイトの最初に読み込み画面を安全に追加できます。
次に、前のテクスチャとトランジションの進行状況を保存するために、ImageSliderMaterial
に2つのuniformを追加しましょう:
// ... 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 を更新できるように material を参照する必要があります:
// ... 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
に設定し、useFrame
ループで 1
にアニメートしましょう:
// ... 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
ユニフォームを追加し、それを使用してvUv
座標を歪ませましょう。
End of lesson preview
To get access to the entire lesson, you need to purchase the course.