UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

354 lines (320 loc) 9.31 kB
/* eslint-disable react/no-unknown-property */ import * as THREE from 'three' import { useRef, useState, useEffect, memo } from 'react' import { Canvas, createPortal, useFrame, useThree } from '@react-three/fiber' import { useFBO, useGLTF, useScroll, Image, Scroll, Preload, ScrollControls, MeshTransmissionMaterial, Text, } from '@react-three/drei' import { easing } from 'maath' export default function FluidGlass({ mode = 'lens', lensProps = {}, barProps = {}, cubeProps = {}, }) { const Wrapper = mode === 'bar' ? Bar : mode === 'cube' ? Cube : Lens const rawOverrides = mode === 'bar' ? barProps : mode === 'cube' ? cubeProps : lensProps const { navItems = [ { label: 'Home', link: '' }, { label: 'About', link: '' }, { label: 'Contact', link: '' }, ], ...modeProps } = rawOverrides return ( <Canvas camera={{ position: [0, 0, 20], fov: 15 }} gl={{ alpha: true }} > <ScrollControls damping={0.2} pages={3} distance={0.4}> {mode === 'bar' && <NavItems items={navItems} />} <Wrapper modeProps={modeProps}> <Scroll> <Typography /> <Images /> </Scroll> <Scroll html /> <Preload /> </Wrapper> </ScrollControls> </Canvas> ) } const ModeWrapper = memo(function ModeWrapper({ children, glb, geometryKey, lockToBottom = false, followPointer = true, modeProps = {}, ...props }) { const ref = useRef() const { nodes } = useGLTF(glb) const buffer = useFBO() const { viewport: vp } = useThree() const [scene] = useState(() => new THREE.Scene()) const geoWidthRef = useRef(1) useEffect(() => { const geo = nodes[geometryKey]?.geometry geo.computeBoundingBox() geoWidthRef.current = geo.boundingBox.max.x - geo.boundingBox.min.x || 1 }, [nodes, geometryKey]) useFrame((state, delta) => { const { gl, viewport, pointer, camera } = state const v = viewport.getCurrentViewport(camera, [0, 0, 15]) const destX = followPointer ? (pointer.x * v.width) / 2 : 0 const destY = lockToBottom ? -v.height / 2 + 0.2 : followPointer ? (pointer.y * v.height) / 2 : 0 easing.damp3(ref.current.position, [destX, destY, 15], 0.15, delta) if (modeProps.scale == null) { const maxWorld = v.width * 0.9 const desired = maxWorld / geoWidthRef.current ref.current.scale.setScalar(Math.min(0.15, desired)) } gl.setRenderTarget(buffer) gl.render(scene, camera) gl.setRenderTarget(null) // Background Color gl.setClearColor(0x5227ff, 1) }) const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps return ( <> {createPortal(children, scene)} <mesh scale={[vp.width, vp.height, 1]}> <planeGeometry /> <meshBasicMaterial map={buffer.texture} transparent /> </mesh> <mesh ref={ref} scale={scale ?? 0.15} rotation-x={Math.PI / 2} geometry={nodes[geometryKey]?.geometry} {...props} > <MeshTransmissionMaterial buffer={buffer.texture} ior={ior ?? 1.15} thickness={thickness ?? 5} anisotropy={anisotropy ?? 0.01} chromaticAberration={chromaticAberration ?? 0.1} {...extraMat} /> </mesh> </> ) }) function Lens({ modeProps, ...p }) { return ( <ModeWrapper glb="/assets/3d/lens.glb" geometryKey="Cylinder" followPointer modeProps={modeProps} {...p} /> ) } function Cube({ modeProps, ...p }) { return ( <ModeWrapper glb="/assets/3d/cube.glb" geometryKey="Cube" followPointer modeProps={modeProps} {...p} /> ) } function Bar({ modeProps = {}, ...p }) { const defaultMat = { transmission: 1, roughness: 0, thickness: 10, ior: 1.15, color: '#ffffff', attenuationColor: '#ffffff', attenuationDistance: 0.25, } return ( <ModeWrapper glb="/assets/3d/bar.glb" geometryKey="Cube" lockToBottom followPointer={false} modeProps={{ ...defaultMat, ...modeProps }} {...p} /> ) } function NavItems({ items }) { const group = useRef() const { viewport, camera } = useThree() const DEVICE = { mobile: { max: 639, spacing: 0.2, fontSize: 0.035 }, tablet: { max: 1023, spacing: 0.24, fontSize: 0.045 }, desktop: { max: Infinity, spacing: 0.3, fontSize: 0.045 }, } const getDevice = () => { const w = window.innerWidth return w <= DEVICE.mobile.max ? 'mobile' : w <= DEVICE.tablet.max ? 'tablet' : 'desktop' } const [device, setDevice] = useState(getDevice()) useEffect(() => { const onResize = () => setDevice(getDevice()) window.addEventListener('resize', onResize) return () => window.removeEventListener('resize', onResize) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const { spacing, fontSize } = DEVICE[device] useFrame(() => { if (!group.current) return const v = viewport.getCurrentViewport(camera, [0, 0, 15]) group.current.position.set(0, -v.height / 2 + 0.2, 15.1) group.current.children.forEach((child, i) => { child.position.x = (i - (items.length - 1) / 2) * spacing }) }) const handleNavigate = (link) => { if (!link) return link.startsWith('#') ? (window.location.hash = link) : (window.location.href = link) } return ( <group ref={group} renderOrder={10}> {items.map(({ label, link }) => ( <Text key={label} fontSize={fontSize} color="white" anchorX="center" anchorY="middle" font="/assets/fonts/figtreeblack.ttf" depthWrite={false} outlineWidth={0} outlineBlur="20%" outlineColor="#000" outlineOpacity={0.5} depthTest={false} renderOrder={10} onClick={(e) => { e.stopPropagation() handleNavigate(link) }} onPointerOver={() => (document.body.style.cursor = 'pointer')} onPointerOut={() => (document.body.style.cursor = 'auto')} > {label} </Text> ))} </group> ) } function Images() { const group = useRef() const data = useScroll() const { height } = useThree((s) => s.viewport) useFrame(() => { group.current.children[0].material.zoom = 1 + data.range(0, 1 / 3) / 3 group.current.children[1].material.zoom = 1 + data.range(0, 1 / 3) / 3 group.current.children[2].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2 group.current.children[3].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2 group.current.children[4].material.zoom = 1 + data.range(1.15 / 3, 1 / 3) / 2 }) return ( <group ref={group}> <Image position={[-2, 0, 0]} scale={[3, height / 1.1, 1]} url="https://images.unsplash.com/photo-1595001354022-29103be3b73a?q=80&w=3270&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <Image position={[2, 0, 3]} scale={3} url="https://images.unsplash.com/photo-1478436127897-769e1b3f0f36?q=80&w=3270&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <Image position={[-2.05, -height, 6]} scale={[1, 3, 1]} url="https://images.unsplash.com/photo-1513682121497-80211f36a7d3?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <Image position={[-0.6, -height, 9]} scale={[1, 2, 1]} url="https://images.unsplash.com/photo-1516205651411-aef33a44f7c2?q=80&w=2843&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <Image position={[0.75, -height, 10.5]} scale={1.5} url="https://images.unsplash.com/photo-1505069190533-da1c9af13346?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> </group> ) } function Typography() { const DEVICE = { mobile: { fontSize: 0.2 }, tablet: { fontSize: 0.40 }, desktop: { fontSize: 0.7 }, } const getDevice = () => { const w = window.innerWidth return w <= 639 ? 'mobile' : w <= 1023 ? 'tablet' : 'desktop' } const [device, setDevice] = useState(getDevice()) useEffect(() => { const onResize = () => setDevice(getDevice()) window.addEventListener('resize', onResize) return () => window.removeEventListener('resize', onResize) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const { fontSize } = DEVICE[device] return ( <Text position={[0, 0, 12]} font="/assets/fonts/figtreeblack.ttf" fontSize={fontSize} letterSpacing={-0.05} outlineWidth={0} outlineBlur="20%" outlineColor="#000" outlineOpacity={0.5} color="white" anchorX="center" anchorY="middle" > React Bits </Text> ) }