• 使用 React Three Fiber 和 GSAP 实现 WebGL 轮播动画


    参考: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、向平面添加缩放效果

    首先, 设置一个新组件来包裹 ,用以管理缩放效果的激活和停用。

    使用着色器材质 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.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 创建一个组用以包含所有图像,图像用 渲染。
    然后,需要使用 renderPlaneEvent() 创建一个片面用以管理事件。

    轮播最重要的部分在 useFrame() 中,需要计算滑块进度,使用 displayItems() 函数设置所有item 位置。

    另一个需要考虑的重要方面是 z 位置,当它变为活动状态时,需要使其 z 位置更靠近相机,以便缩放效果不会与其他 meshs 冲突。这也是为什么当退出缩放时,需要 mesh 足够小以将 z 轴位置恢复为 0 (详见 )。也是为什么禁用其他 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.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.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 启发的轮播一样。希望这篇文章对您自己的项目有所帮助和启发!

  • 相关阅读:
    GEE错误——Line 2: ee.Image(...).filterBounds is not a function
    性价比较高的无线蓝牙耳机,300以内高音质蓝牙耳机推荐
    Redis的两种持久化方式RDB和AOF
    SDXL微调5分钟入门
    C++11标准模板(STL)- 算法(std::stable_sort)
    人工神经网络教学视频第三版,人工神经网络教程
    详解JVM类加载
    OpenAI 创始人奥特曼:做你自己喜欢的事情
    在fastapi中实现异步
    力扣2596
  • 原文地址:https://www.cnblogs.com/EnSnail/p/17419761.html