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

这篇具有很好参考价值的文章主要介绍了使用 React Three Fiber 和 GSAP 实现 WebGL 轮播动画。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

  • 在线 demo
  • github 源码

效果来源于由 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 启发的轮播一样。希望这篇文章对您自己的项目有所帮助和启发!文章来源地址https://www.toymoban.com/news/detail-454304.html

到了这里,关于使用 React Three Fiber 和 GSAP 实现 WebGL 轮播动画的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 如何使用webgl(three.js)实现煤矿隧道、井下人员定位、掘进面、纵采面可视化解决方案——第十九课(一)

    序: 又是很久没有更新文章了,这次索性将之前做的三维煤矿项目拿出来讲讲,一是回顾技术,二是锻炼一下文笔。 随着科技的不断发展,越来越多的人开始关注煤矿采集的安全和效率问题。为了更好地展示煤矿采集的过程和效率,可视化展示系统成为了一个非常重要的工

    2024年02月08日
    浏览(66)
  • 使用Three.js创建令人惊叹的WebGL 3D可视化

    WebGL 可视化 3D 绘图是一项新兴技术,具有广阔的应用前景。它允许开发人员在 Web 浏览器中创建和渲染 3D 图形,而无需安装额外的插件或软件。 本博客将介绍 Three.js,Three.js 是一个功能强大的 WebGL 框架,提供了丰富的 API 用于创建和渲染 3D 图形,接下来让我们通

    2024年01月19日
    浏览(60)
  • 深入理解React中fiber

    Fiber是对React核心算法的重写,Fiber是React内部定义的一种数据结构,将更新渲染耗时长的大任务,分为许多的小片。Fiber节点保存啦组件需要更新的状态和副作用,一个Fiber代表一个工作单元。 在react中,主要做了下面这些操作: 为每个增加了优先级,优先级高的任务可以中断

    2024年02月07日
    浏览(32)
  • three.js 使用 tweenjs绘制相机运动动画

    效果: 代码:  

    2024年01月18日
    浏览(53)
  • React 底层 Fiber 架构 简单理解

    JS 是引擎是 单线程运行 的;严格来说,JS 引擎和页面渲染引擎在同一渲染线程,两者 互斥 。那么就会遇到这样的一种情况:当前面一个任务长期霸占CPU,后面啥事也干不了,浏览器卡死,造成极差的用户体验。 对于前端而言,主要的解决方向: 优化每个任务,让它有多快

    2024年02月05日
    浏览(46)
  • Qt+C++实现灯带动画运动位置变换移动跑马灯图片轮播

     程序示例精选 Qt+C++实现灯带动画运动位置变换移动跑马灯图片轮播 如需安装运行环境或远程调试,见文章底部个人 QQ 名片,由专业技术人员远程协助! 这篇博客针对Qt+C++实现灯带动画运动位置变换移动跑马灯图片轮播编写代码,代码整洁,规则,易读。 学习与应用推荐首

    2024年02月13日
    浏览(35)
  • vue3结合three.js实现3D带有交互的动画

    three.js引入 安装轨道控件插件: 安装渲染器插件: vue文件中引用: 在页面中创建场景 创建一个透视相机 初始化渲染器 初始动画混合器 参数:rootObject 混合器播放的动画所属的对象。就是包含动画模型的场景对象。 常用参数和属性: .time 全局的混合器时间。 .clipAction(Ani

    2024年02月04日
    浏览(52)
  • 学习篇之React Fiber概念及原理

    React Fiber 是 React 框架的一种底层架构,为了改进 React 的渲染引擎,使其更加高效、灵活和可扩展。 传统上,React 使用一种称为堆栈调和递归算法来处理虚拟 DOM 的更新,这种方法在大型应用或者频繁更新的情况下可能会产生性能问题。React Fiber 则是基于一种增量渲染的思想

    2024年02月12日
    浏览(38)
  • 手写react--fiber架构如何中断和恢复

    React 17 版本开始提供了一个新的JSX转换方案。在之前,JSX 代码会被默认编译成  React.createElement(...)  ,而使用新 JSX 方案后,会从一个独立的模块 react/jsx-runtime 中引入 JSX 函数。 babel插件@babel/plugin-transform-react-jsx /** @jsxRuntime classic */  babel运行时转换 render 创建最初始的fiber对

    2024年02月16日
    浏览(46)
  • React18原理: React核心对象之ReactElement对象和Fiber对象

    React中的核心对象 在React应用中,有很多特定的对象或数据结构.了解这些内部的设计,可以更容易理解react运行原理 列举从react启动到渲染过程出现频率较高,影响范围较大的对象,它们贯穿整个react运行时 如 ReactElement 对象 如 Fiber 对象 其他过程的重要对象 如事件对象(位

    2024年02月19日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包