参考: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 启发的轮播一样。希望这篇文章对您自己的项目有所帮助和启发!

使用 React Three Fiber 和 GSAP 实现 WebGL 轮播动画的更多相关文章

  1. 手写React的Fiber架构,深入理解其原理

    熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重 ...

  2. React视角下的轮播图

    天猫购物网站最显眼的就是轮播图了.我在学习一样新js库,一个新框架或新的编程思想的时候,总是感叹"入门必做选项卡,进阶须撸轮播图."作为一个React组件,它是状态操控行为的典型, ...

  3. React中使用CSSTransitionGroup插件实现轮播图

    动画效果,是一个页面上必不可少的功能,学习一个新的东西,当然就要学习,如何用新的东西,用它的方法去实现以前的东西啦.今天呢,我就在这里介绍一个试用react-addons-css-transition ...

  4. React Native学习(六)—— 轮播图

    本文基于React Native 0.52 Demo上传到Git了,有需要可以看看,写了新内容会上传的.Git地址 https://github.com/gingerJY/React-Native-D ...

  5. React 轮播图实现

    接到项目, 用react和material-ui实现轮播图. 搜索了一些方法参考, 不论语言/框架的使用,大体上分为两种思路 超宽列表实现法 在原生JS或者JQuery中,轮播图的实现一般是这样子的 ...

  6. reactjs-swiper react轮播图组件基于swiper

    react轮播图组件基于swiper demo地址:http://reactjs-ui.github.io/reactjs-swiper/simple.html 1. 下载安装 npm install ...

  7. react轮播图----react-slick

    1.安装 npm install react-slick; //安装样式 npm install slick carousel; 再在App.css中引入 @import "~slick-c ...

  8. React Native 如何做轮播图 react-native-swiper

    //:仿饿了么github:https://github.com/stoneWeb/elm-react-native 欢迎各位同学加入: React-Native群:397885169 大前端群:54 ...

  9. React Native 之轮播图swiper组件

    注释:swiper组件是第三方组件 所以在使用之前应该先在命令行安装,然后将第三方的模块引入(第三方模块地址:https://github.com/leecade/react-native-swipe ...

  10. 继上一篇随笔,优化3张以上图片轮播React组件

    import React from 'react'; import PropTypes from 'prop-types'; import {getSwipeWay} from '../utils/s ...

随机推荐

  1. 一起听、一起看、一起唱掀起Z世代青年社交浪潮

    6月5日,声网Agora 联合人人都是产品经理在成都举办了主题为"社交泛娱乐APP运营增长力和新玩法解析"的沙龙.现场围绕社交泛娱乐新玩法解析以及出海的新机遇.支付痛点.增长.运营 ...

  2. 在java中String类为什么要设计成final?Java面试常见问题

    2023Java面试题最经典的问题之一了,非常经典的Java基础知识,一定要学会! 在Java中,String类被设计成final,这意味着它的值在创建后不可更改.这是因为字符串在Java中使用广泛, ...

  3. ThreadPool实现机制

    Android中阻塞队列的应用有哪些 阻塞队列在 Android 中有很多应用,比如: 线程池:线程池任务的执行就是基于一个阻塞队列,如果线程池任务已满,则任务需要等待阻塞队列中的其他任务完成. Ha ...

  4. flutter---->flutter orientation

    Get the orientation 1. Media Query import 'package:flutter/material.dart'; void main() => runApp( ...

  5. vue中优雅的使用定时器

    1.常见方法.在data中声明一个变量,定时器绑定到变量中,然后在beforeDestory中销毁这个定时器 举个例子 首先我在data函数里面进行定义定时器名称 data() { return { ...

  6. DOM0级与DOM2级的区别

     1.DOM0级和DOM2级的共同优点:能添加多个事件处理程序,按顺序执行,HTML事件处理程序无法做到~2.关于dom0级和dom2级的区别DOM0级事件处理:同时绑定几个不同的事件,例如在绑定on ...

  7. VMWare无法从主机向虚拟机复制粘贴

    VMWare无法从主机向虚拟机复制粘贴 问题描述 无法在本机和虚拟机之间实现复制粘贴和文件拖到功能. 系统环境 本机 Windows10 虚拟机 Centos7 解决方法 多次尝试重新安装VMware ...

  8. LeeCode 二叉树问题(四)

    二叉搜索树的应用问题 二叉搜索树的定义 若左子树不空,则左子树上所有节点的值均小于根节点的值 若右子树不空,则右子树上所有节点的值均大于根节点的值 它的左右子树也均为二叉搜索树 中序遍历结果为一个升序 ...

  9. Go语言核心知识回顾(反射)

    有时要求写一个函数有能力统一处理各种值类型的函数,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在设计函数时并不存在,当我们无法透视一个未知类型的布局时,这段代码就无法继续,这是就 ...

  10. CVE-2015-5254漏洞复现

    1.漏洞介绍. Apache ActiveMQ 是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持 Java 消息服务,集群,Spring Framework 等.Apache ...