使用 React Three Fiber 和 GSAP 實現 WebGL 輪播動畫

来源:https://www.cnblogs.com/EnSnail/archive/2023/05/22/17419761.html
-Advertisement-
Play Games

參考:[Building a WebGL Carousel with React Three Fiber and GSAP](https://tympanus.net/codrops/2023/04/27/building-a-webgl-carousel-with-react-three-fibe ...


參考:Building a WebGL Carousel with React Three Fiber and GSAP

效果來源於由 Eum Ray 創建的網站 alcre.co.kr,具有迷人的視覺效果和交互性,具有可拖動或滾動的輪播,具有有趣的圖像展示效果。

本文將使用 WebGL、React Three Fiber 和 GSAP 實現類似的效果。通過本文,可以瞭解如何使用 WebGL、React Three Fiber 和 GSAP 創建互動式 3D 輪播。

準備

首先,用 createreact app 創建項目

npx create-react-app webgl-carsouel
cd webgl-carsouel
npm start

然後安裝相關依賴

npm i @react-three/fiber @react-three/drei gsap leva react-use -S
  • @react-three/fiber: 用 react 實現的簡化 three.js 寫法的一個非常出名的庫
  • @react-three/drei:@react-three/fiber 生態中的一個非常有用的庫,是對 @react-three/fiber 的增強
  • gsap: 一個非常出名的動畫庫
  • leva: @react-three/fiber 生態中用以在幾秒鐘內創建GUI控制項的庫
  • react-use: 一個流行的 react hooks 庫

1. 生成具有紋理的 3D 平面

首先,創建一個任意大小的平面,放置於原點(0, 0, 0)並面向相機。然後,使用 shaderMaterial 材質將所需圖像插入到材質中,修改 UV 位置,讓圖像填充整個幾何體錶面。

為了實現這一點,需要使用一個 glsl 函數,函數將平面和圖像的比例作為轉換參數:

/* 
--------------------------------
Background Cover UV
--------------------------------
u = basic UV
s = plane size
i = image size
*/
vec2 CoverUV(vec2 u, vec2 s, vec2 i) {
  float rs = s.x / s.y; // aspect plane size
  float ri = i.x / i.y; // aspect image size
  vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); // new st
  vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st; // offset
  return u * s / st + o;
}

接下來,將定義2個 uniformsuResuImageRes。每當改變視口大小時,這2個變數將會隨之改變。使用 uRes 以像素為單位存儲片面的大小,使用 uImageRes 存儲圖像紋理的大小。

下麵是創建平面和設置著色器材質的代碼:

// Plane.js

import { useEffect, useMemo, useRef } from "react"
import { useThree } from "@react-three/fiber"
import { useTexture } from "@react-three/drei"
import { useControls } from 'leva'

const Plane = () => {
  const $mesh = useRef()
  const { viewport } = useThree()
  const tex = useTexture(
    'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/1.jpg'
  )

  const { width, height } = useControls({
    width: {
      value: 2,
      min: 0.5,
      max: viewport.width,
    },
    height: {
      value: 3,
      min: 0.5,
      max: viewport.height,
    }
  })

  useEffect(() => {
    if ($mesh.current.material) {
      $mesh.current.material.uniforms.uRes.value.x = width
      $mesh.current.material.uniforms.uRes.value.y = height
    }
  }, [viewport, width, height])

  const shaderArgs = useMemo(() => ({
    uniforms: {
      uTex: { value: tex },
      uRes: { value: { x: 1, y: 1 } },
      uImageRes: {
        value: { x: tex.source.data.width, y: tex.source.data.height }
      }
    },
    vertexShader: /* glsl */ `
      varying vec2 vUv;

      void main() {
        vUv = uv;
        vec3 pos = position;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
      }
    `,
    fragmentShader: /* glsl */ `
      uniform sampler2D uTex;
      uniform vec2 uRes;
      uniform vec2 uImageRes;

      /*
      -------------------------------------
      background Cover UV
      -------------------------------------
      u = basic UV
      s = screen size
      i = image size
      */
      vec2 CoverUV(vec2 u, vec2 s, vec2 i) {
        float rs = s.x / s.y; // aspect screen size
        float ri = i.x / i.y; // aspect image size
        vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); // new st
        vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st; // offset
        return u * s / st + o;
      }

      varying vec2 vUv;

      void main() {
        vec2 uv = CoverUV(vUv, uRes, uImageRes);
        vec3 tex = texture2D(uTex, uv).rgb;
        gl_FragColor = vec4(tex, 1.0);
      }
    `
  }), [tex])

  return (
    <mesh ref={$mesh}>
      <planeGeometry args={[width, height, 30, 30]} />
      <shaderMaterial args={[shaderArgs]} />
    </mesh>
  )
}

export default Plane

2、向平面添加縮放效果

首先, 設置一個新組件來包裹 <Plane />,用以管理縮放效果的激活和停用。

使用著色器材質 shaderMaterial 調整 mesh 大小可保持幾何空間的尺寸。因此,激活縮放效果後,必須顯示一個新的透明平面,其尺寸與視口相當,方便點擊整個圖像恢復到初始狀態。

此外,還需要在平面的著色器中實現波浪效果。

因此,在 uniforms 中添加一個新欄位 uZoomScale,存儲縮放平面的值 xy,從而得到頂點著色器的位置。縮放值通過在平面尺寸和視口尺寸比例來計算:

$mesh.current.material.uniforms.uZoomScale.value.x = viewport.width / width
$mesh.current.material.uniforms.uZoomScale.value.y = viewport.height / height

接下來,在 uniforms 中添加一個新欄位 uProgress,來控制波浪效果的數量。通過使用 GSAP 修改 uProgress,動畫實現平滑的緩動效果。

創建波形效果,可以在頂點著色器中使用 sin 函數,函數在平面的 x 和 y 位置上添加波狀運動。

// CarouselItem.js

import { useEffect, useRef, useState } from "react"
import { useThree } from "@react-three/fiber"
import gsap from "gsap"
import Plane from './Plane'

const CarouselItem = () => {
  const $root = useRef()
  const [hover, setHover] = useState(false)
  const [isActive, setIsActive] = useState(false)
  const { viewport } = useThree()

  useEffect(() => {
    gsap.killTweensOf($root.current.position)
    gsap.to($root.current.position, {
      z: isActive ? 0 : -0.01,
      duration: 0.2,
      ease: "power3.out",
      delay: isActive ? 0 : 2
    })
  }, [isActive])

  // hover effect
  useEffect(() => {
    const hoverScale = hover && !isActive ? 1.1 : 1
    gsap.to($root.current.scale, {
      x: hoverScale,
      y: hoverScale,
      duration: 0.5,
      ease: "power3.out"
    })
  }, [hover, isActive])

  const handleClose = (e) => {
    e.stopPropagation()
    if (!isActive) return
    setIsActive(false)
  }

  const textureUrl = 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/1.jpg'


  return (
    <group
      ref={$root}
      onClick={() => setIsActive(true)}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <Plane
        width={1}
        height={2.5}
        texture={textureUrl}
        active={isActive}
      />

      {isActive ? (
        <mesh position={[0, 0, 0]} onClick={handleClose}>
          <planeGeometry args={[viewport.width, viewport.height]} />
          <meshBasicMaterial transparent={true} opacity={0} color="red" />
        </mesh>
      ) : null}
    </group>
  )
}

export default CarouselItem

<Plane /> 組件也要進行更改,支持參數及參數變更處理,更改後:

// Plane.js

import { useEffect, useMemo, useRef } from "react"
import { useThree } from "@react-three/fiber"
import { useTexture } from "@react-three/drei"
import gsap from "gsap"

const Plane = ({ texture, width, height, active, ...props}) => {
  const $mesh = useRef()
  const { viewport } = useThree()
  const tex = useTexture(texture)

  useEffect(() => {
    if ($mesh.current.material) {
      // setting
      $mesh.current.material.uniforms.uZoomScale.value.x = viewport.width / width
      $mesh.current.material.uniforms.uZoomScale.value.y = viewport.height / height

      gsap.to($mesh.current.material.uniforms.uProgress, {
        value: active ? 1 : 0,
        duration: 2.5,
        ease: 'power3.out'
      })

      gsap.to($mesh.current.material.uniforms.uRes.value, {
        x: active ? viewport.width : width,
        y: active? viewport.height : height,
        duration: 2.5,
        ease: 'power3.out'
      })
    }
  }, [viewport, active]);

  const shaderArgs = useMemo(() => ({
    uniforms: {
      uProgress: { value: 0 },
      uZoomScale: { value: { x: 1, y: 1 } },
      uTex: { value: tex },
      uRes: { value: { x: 1, y: 1 } },
      uImageRes: {
        value: { x: tex.source.data.width, y: tex.source.data.height }
      }
    },
    vertexShader: /* glsl */ `
      varying vec2 vUv;
      uniform float uProgress;
      uniform vec2 uZoomScale;

      void main() {
        vUv = uv;
        vec3 pos = position;
        float angle = uProgress * 3.14159265 / 2.;
        float wave = cos(angle);
        float c = sin(length(uv - .5) * 15. + uProgress * 12.) * .5 + .5;
        pos.x *= mix(1., uZoomScale.x + wave * c, uProgress);
        pos.y *= mix(1., uZoomScale.y + wave * c, uProgress);

        gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
      }
    `,
    fragmentShader: /* glsl */ `
      uniform sampler2D uTex;
      uniform vec2 uRes;
      // uniform vec2 uZoomScale;
      uniform vec2 uImageRes;

      /*
      -------------------------------------
      background Cover UV
      -------------------------------------
      u = basic UV
      s = screen size
      i = image size
      */
      vec2 CoverUV(vec2 u, vec2 s, vec2 i) {
        float rs = s.x / s.y; // aspect screen size
        float ri = i.x / i.y; // aspect image size
        vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); // new st
        vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st; // offset
        return u * s / st + o;
      }

      varying vec2 vUv;

      void main() {
        vec2 uv = CoverUV(vUv, uRes, uImageRes);
        vec3 tex = texture2D(uTex, uv).rgb;
        gl_FragColor = vec4(tex, 1.0);
      }
    `
  }), [tex])

  return (
    <mesh ref={$mesh} {...props}>
      <planeGeometry args={[width, height, 30, 30]} />
      <shaderMaterial args={[shaderArgs]} />
    </mesh>
  )
}

export default Plane

3、實現可以用滑鼠滾動或拖動移動的圖像輪播

這部分是最有趣的,但也是最複雜的,因為必須考慮很多事情。

首先,需要使用 renderSlider 創建一個組用以包含所有圖像,圖像用 <CarouselItem /> 渲染。
然後,需要使用 renderPlaneEvent() 創建一個片面用以管理事件。

輪播最重要的部分在 useFrame() 中,需要計算滑塊進度,使用 displayItems() 函數設置所有item 位置。

另一個需要考慮的重要方面是 <CarouselItem />z 位置,當它變為活動狀態時,需要使其 z 位置更靠近相機,以便縮放效果不會與其他 meshs 衝突。這也是為什麼當退出縮放時,需要 mesh 足夠小以將 z 軸位置恢復為 0 (詳見 <CarouselItem />)。也是為什麼禁用其他 meshs 的點擊,直到縮放效果被停用。

// data/images.js

const images = [
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/1.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/2.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/3.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/4.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/5.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/6.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/7.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/8.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/9.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/10.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/11.jpg' },
  { image: 'https://raw.githubusercontent.com/supahfunk/webgl-carousel/main/public/img/12.jpg' }
]

export default images
// Carousel.js

import { useEffect, useRef, useState, useMemo } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { usePrevious } from 'react-use'
import gsap from 'gsap'
import CarouselItem from './CarouselItem'
import images from '../data/images'

// Plane settings
const planeSettings = {
  width: 1,
  height: 2.5,
  gap: 0.1
}

// gsap defaults
gsap.defaults({
  duration: 2.5,
  ease: 'power3.out'
})

const Carousel = () => {
  const [$root, setRoot] = useState();

  const [activePlane, setActivePlane] = useState(null);
  const prevActivePlane = usePrevious(activePlane)
  const { viewport } = useThree()

  // vars
  const progress = useRef(0)
  const startX = useRef(0)
  const isDown = useRef(false)
  const speedWheel = 0.02
  const speedDrag = -0.3
  const $items = useMemo(() => {
    if ($root) return $root.children
  }, [$root])

  const displayItems = (item, index, active) => {
    gsap.to(item.position, {
      x: (index - active) * (planeSettings.width + planeSettings.gap),
      y: 0
    })
  }

  useFrame(() => {
    progress.current = Math.max(0, Math.min(progress.current, 100))

    const active = Math.floor((progress.current / 100) * ($items.length - 1))
    $items.forEach((item, index) => displayItems(item, index, active))
  })

  const handleWheel = (e) => {
    if (activePlane !== null) return
    const isVerticalScroll = Math.abs(e.deltaY) > Math.abs(e.deltaX)
    const wheelProgress = isVerticalScroll ? e.deltaY : e.deltaX
    progress.current = progress.current + wheelProgress * speedWheel
  }

  const handleDown = (e) => {
    if (activePlane !== null) return
    isDown.current = true
    startX.current = e.clientX || (e.touches && e.touches[0].clientX) || 0
  }

  const handleUp = () => {
    isDown.current = false
  }

  const handleMove = (e) => {
    if (activePlane !== null || !isDown.current) return
    const x = e.clientX || (e.touches && e.touches[0].clientX) || 0
    const mouseProgress = (x - startX.current) * speedDrag
    progress.current = progress.current + mouseProgress
    startX.current = x
  }

  // click
  useEffect(() => {
    if (!$items) return
    if (activePlane !== null && prevActivePlane === null) {
      progress.current = (activePlane / ($items.length - 1)) * 100
    }
  }, [activePlane, $items]);

  const renderPlaneEvents = () => {
    return (
      <mesh
        position={[0, 0, -0.01]}
        onWheel={handleWheel}
        onPointerDown={handleDown}
        onPointerUp={handleUp}
        onPointerMove={handleMove}
        onPointerLeave={handleUp}
        onPointerCancel={handleUp}
      >
        <planeGeometry args={[viewport.width, viewport.height]} />
        <meshBasicMaterial transparent={true} opacity={0} />
      </mesh>
    )
  }

  const renderSlider = () => {
    return (
      <group ref={setRoot}>
        {images.map((item, i) => (
          <CarouselItem
            width={planeSettings.width}
            height={planeSettings.height}
            setActivePlane={setActivePlane}
            activePlane={activePlane}
            key={item.image}
            item={item}
            index={i}
          />
        ))}
      </group>
    )
  }

  return (
    <group>
      {renderPlaneEvents()}
      {renderSlider()}
    </group>
  )
}

export default Carousel

<CarouselItem> 需要更改,以便根據參數顯示不同的圖像,及其他細節處理,更改後如下:

// CarouselItem.js

import { useEffect, useRef, useState } from "react"
import { useThree } from "@react-three/fiber"
import gsap from "gsap"
import Plane from './Plane'

const CarouselItem = ({
  index,
  width,
  height,
  setActivePlane,
  activePlane,
  item
}) => {
  const $root = useRef()
  const [hover, setHover] = useState(false)
  const [isActive, setIsActive] = useState(false)
  const [isCloseActive, setIsCloseActive] = useState(false);
  const { viewport } = useThree()
  const timeoutID = useRef()

  useEffect(() => {
    if (activePlane === index) {
      setIsActive(activePlane === index)
      setIsCloseActive(true)
    } else {
      setIsActive(null)
    }
  }, [activePlane]);
  
  useEffect(() => {
    gsap.killTweensOf($root.current.position)
    gsap.to($root.current.position, {
      z: isActive ? 0 : -0.01,
      duration: 0.2,
      ease: "power3.out",
      delay: isActive ? 0 : 2
    })
  }, [isActive])

  // hover effect
  useEffect(() => {
    const hoverScale = hover && !isActive ? 1.1 : 1
    gsap.to($root.current.scale, {
      x: hoverScale,
      y: hoverScale,
      duration: 0.5,
      ease: "power3.out"
    })
  }, [hover, isActive])

  const handleClose = (e) => {
    e.stopPropagation()
    if (!isActive) return
    setActivePlane(null)
    setHover(false)
    clearTimeout(timeoutID.current)
    timeoutID.current = setTimeout(() => {
      setIsCloseActive(false)
    }, 1500);
    // 這個計時器的持續時間取決於 plane 關閉動畫的時間
  }

  return (
    <group
      ref={$root}
      onClick={() => setActivePlane(index)}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <Plane
        width={width}
        height={height}
        texture={item.image}
        active={isActive}
      />

      {isCloseActive ? (
        <mesh position={[0, 0, 0.01]} onClick={handleClose}>
          <planeGeometry args={[viewport.width, viewport.height]} />
          <meshBasicMaterial transparent={true} opacity={0} color="red" />
        </mesh>
      ) : null}
    </group>
  )
}

export default CarouselItem

4、實現後期處理效果,增強輪播體驗

真正吸引我眼球並激發我複製次輪播的是視口邊緣拉伸像素的效果。

過去,我通過 @react-three/postprocessing 來自定義著色器多次實現此效果。然而,最近我一直在使用 MeshTransmissionMaterial,因此有了一個想法,嘗試用這種材料覆蓋 mesh 並調整設置實現效果。效果幾乎相同!

訣竅是將 materialthickness 屬性與輪播滾動進度的速度聯繫起來,僅此而已。

// PostProcessing.js

import { forwardRef } from "react";
import { useThree } from "@react-three/fiber";
import { MeshTransmissionMaterial } from "@react-three/drei";
import { Color } from "three";
import { useControls } from 'leva'

const PostProcessing = forwardRef((_, ref) => {
  const { viewport } = useThree()

  const { active, ior } = useControls({
    active: { value: true },
    ior: {
      value: 0.9,
      min: 0.8,
      max: 1.2
    }
  })

  return active ? (
    <mesh position={[0, 0, 1]}>
      <planeGeometry args={[viewport.width, viewport.height]} />
      <MeshTransmissionMaterial
        ref={ref}
        background={new Color('white')}
        transmission={0.7}
        roughness={0}
        thickness={0}
        chromaticAberration={0.06}
        anisotropy={0}
        ior={ior}
      />
    </mesh>
  ) : null
})

export default PostProcessing

因為後處理作用於 <Carousel /> 組件,所以需要進行相應的更改,更改後如下:

// Carousel.js

import { useEffect, useRef, useState, useMemo } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { usePrevious } from 'react-use'
import gsap from 'gsap'
import PostProcessing from "./PostProcessing";
import CarouselItem from './CarouselItem'
import { lerp, getPiramidalIndex } from "../utils";
import images from '../data/images'

// Plane settings
const planeSettings = {
  width: 1,
  height: 2.5,
  gap: 0.1
}

// gsap defaults
gsap.defaults({
  duration: 2.5,
  ease: 'power3.out'
})

const Carousel = () => {
  const [$root, setRoot] = useState();
  const $post = useRef()

  const [activePlane, setActivePlane] = useState(null);
  const prevActivePlane = usePrevious(activePlane)
  const { viewport } = useThree()

  // vars
  const progress = useRef(0)
  const startX = useRef(0)
  const isDown = useRef(false)
  const speedWheel = 0.02
  const speedDrag = -0.3
  const oldProgress = useRef(0)
  const speed = useRef(0)
  const $items = useMemo(() => {
    if ($root) return $root.children
  }, [$root])

  const displayItems = (item, index, active) => {
    const piramidalIndex = getPiramidalIndex($items, active)[index]
    gsap.to(item.position, {
      x: (index - active) * (planeSettings.width + planeSettings.gap),
      y: $items.length * -0.1 + piramidalIndex * 0.1
    })
  }

  useFrame(() => {
    progress.current = Math.max(0, Math.min(progress.current, 100))

    const active = Math.floor((progress.current / 100) * ($items.length - 1))
    $items.forEach((item, index) => displayItems(item, index, active))

    speed.current = lerp(speed.current, Math.abs(oldProgress.current - progress.current), 0.1)

    oldProgress.current = lerp(oldProgress.current, progress.current, 0.1)

    if ($post.current) {
      $post.current.thickness = speed.current
    }
  })

  const handleWheel = (e) => {
    if (activePlane !== null) return
    const isVerticalScroll = Math.abs(e.deltaY) > Math.abs(e.deltaX)
    const wheelProgress = isVerticalScroll ? e.deltaY : e.deltaX
    progress.current = progress.current + wheelProgress * speedWheel
  }

  const handleDown = (e) => {
    if (activePlane !== null) return
    isDown.current = true
    startX.current = e.clientX || (e.touches && e.touches[0].clientX) || 0
  }

  const handleUp = () => {
    isDown.current = false
  }

  const handleMove = (e) => {
    if (activePlane !== null || !isDown.current) return
    const x = e.clientX || (e.touches && e.touches[0].clientX) || 0
    const mouseProgress = (x - startX.current) * speedDrag
    progress.current = progress.current + mouseProgress
    startX.current = x
  }

  // click
  useEffect(() => {
    if (!$items) return
    if (activePlane !== null && prevActivePlane === null) {
      progress.current = (activePlane / ($items.length - 1)) * 100
    }
  }, [activePlane, $items]);

  const renderPlaneEvents = () => {
    return (
      <mesh
        position={[0, 0, -0.01]}
        onWheel={handleWheel}
        onPointerDown={handleDown}
        onPointerUp={handleUp}
        onPointerMove={handleMove}
        onPointerLeave={handleUp}
        onPointerCancel={handleUp}
      >
        <planeGeometry args={[viewport.width, viewport.height]} />
        <meshBasicMaterial transparent={true} opacity={0} />
      </mesh>
    )
  }

  const renderSlider = () => {
    return (
      <group ref={setRoot}>
        {images.map((item, i) => (
          <CarouselItem
            width={planeSettings.width}
            height={planeSettings.height}
            setActivePlane={setActivePlane}
            activePlane={activePlane}
            key={item.image}
            item={item}
            index={i}
          />
        ))}
      </group>
    )
  }

  return (
    <group>
      {renderPlaneEvents()}
      {renderSlider()}
      <PostProcessing ref={$post} />
    </group>
  )
}

export default Carousel
// utils/index.js

/**
 * 返回 v0, v1 之間的一個值,可以根據 t 進行計算
 * 示例:
 * lerp(5, 10, 0) // 5
 * lerp(5, 10, 1) // 10
 * lerp(5, 10, 0.2) // 6
 */
export const lerp = (v0, v1, t) => v0 * (1 - t) + v1 * t

/**
 * 以金字塔形狀返回索引值遞減的數組,從具有最大值的指定索引開始。這些索引通常用於在元素之間創建重疊效果
 * 示例:array = [0, 1, 2, 3, 4, 5]
 * getPiramidalIndex(array, 0) // [ 6, 5, 4, 3, 2, 1 ]
 * getPiramidalIndex(array, 1) // [ 5, 6, 5, 4, 3, 2 ]
 * getPiramidalIndex(array, 2) // [ 4, 5, 6, 5, 4, 3 ]
 * getPiramidalIndex(array, 3) // [ 3, 4, 5, 6, 5, 4 ]
 * getPiramidalIndex(array, 4) // [ 2, 3, 4, 5, 6, 5 ]
 * getPiramidalIndex(array, 5) // [ 1, 2, 3, 4, 5, 6 ]
 */
export const getPiramidalIndex = (array, index) => {
  return array.map((_, i) => index === i ? array.length : array.length - Math.abs(index - i))
}

總之,通過使用 React Three Fiber 、GSAP 和一些創造力,可以在 WebGL 中創建令人驚嘆的視覺效果和互動式組件,就像受 alcre.co.kr 啟發的輪播一樣。希望這篇文章對您自己的項目有所幫助和啟發!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 《1萬多條司法資格考試題庫ACCESS版》搜集了大量司法資格考試試題,包括試卷一、試卷二、試卷三、試卷四等科目。同類的資料庫有《9萬多條執業醫師資格考試題庫ACCESS資料庫》、《6萬多條會計從業資格考試題庫ACCESS版》、《近7萬多條證券從業資格考試題庫ACCESS版》、《1萬多條一級建造師資格 ...
  • 摘要:MySQL一張表最多能存多少數據? 本文分享自華為雲社區《為什麼MySQL單表不能超過2000萬行?》,作者: GaussDB 資料庫 。 最近看到一篇《我說MySQL每張表最好不要超過2000萬數據,面試官讓我回去等通知》的文章,非常有趣。 文中提到,他朋友在面試的過程中說,自己的工作就是把 ...
  • 數學上有一個“計算漢明重量”的問題,即求取一個二進位位中非 0 的數量。使用 Redis 提供的 Bitmap 統計時恰恰是這樣一個問題,學習後能發現解決辦法卻是如此巧妙。 ...
  • 2022年的程式員節, #大齡程式員去哪兒了#成為了社交媒體上最火的話題之一,程式員的職場成長問題在社會上引起了廣泛關註。 有2位在技術領域摸爬滾打很多年的開發者,35歲後的他們,有70後,有80後,依然在編程開發,依然有離職創業的勇氣,努力實現自己的人生價值。走進他們的故事,你會發現,這個世上沒有 ...
  • # React筆記-Hooks(九) ## Hooks ### 概念 >React Hooks 的意思是 組件儘量寫成純函數 如果需要外部功能和副作用 就用鉤子把外部代碼"鉤"進來 ### 函數組件和類組件區別 >- 函數組件沒有狀態(state) 類組件有 >- 函數組件沒有生命周期 類組件有(掛 ...
  • OpenAI於前幾天發佈了IOS版ChatGPT智能App應用。預示著ChatGPT正式踏入了移動設備領域。 現在可以去AppStore下載這款免費、帶有語音設別功能的ChatGPT應用了。 基於vite4.x+vue3+pinia2模仿chatgpt手機端聊天模板Vue3-MobileGPT。 運 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在實際的開發工作過程中,積累了一些常見又超級好用的 Javascript 技巧和代碼片段,包括整理的其他大神的 JS 使用技巧,今天篩選了 9 個,以供大家參考。 1、動態載入 JS 文件 在一些特殊的場景下,特別是一些庫和框架的開 ...
  • HTTP 1.1相比HTTP 1.0具有以下優點: 1. 持久連接 :HTTP 1.1引入了持久連接機制,允許多個請求和響應復用同一個TCP連接。這樣可以減少建立和關閉連接的開銷,提高性能和效率。2. 流水線處理 :HTTP 1.1支持流水線處理,即可以同時發送多個請求,不需要等待前一個請求的響應。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...