UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

201 lines (200 loc) 9.85 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; const ThreeDImageGallery = ({ images = [ 'https://images.pexels.com/photos/2514035/pexels-photo-2514035.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', 'https://images.pexels.com/photos/816608/pexels-photo-816608.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', 'https://images.pexels.com/photos/1271620/pexels-photo-1271620.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1' ], width = 800, height = 600, boxWidth = 1, boxHeight = 1.4, parallaxStrength = 0.3, animationSpeed = 3, spacing = 1, rotationAngle = 0.1, borderRadius = 0.08, edgeSoftness = 0.001, autoRotate = false, autoRotateSpeed = 0.5, ambientLightIntensity = 0.5, enableMouseControl = true, enableTouchControl = true, perspective = 75, cameraDistance = 3, backgroundColor = 'transparent', className = '', style = {}, onImageClick, onSceneReady }) => { const mountRef = useRef(null); const sceneRef = useRef(null); const rendererRef = useRef(null); const cameraRef = useRef(null); const planeGroupRef = useRef(null); const mouseRef = useRef(new THREE.Vector2()); const clockRef = useRef(new THREE.Clock()); const raycasterRef = useRef(new THREE.Raycaster()); const [isReady, setIsReady] = useState(false); useEffect(() => { if (!mountRef.current) return; // Scene setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(perspective, width / height, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.setClearColor(backgroundColor === 'transparent' ? 0x000000 : backgroundColor, backgroundColor === 'transparent' ? 0 : 1); camera.position.set(0, 0, cameraDistance); camera.lookAt(0, 0, 0); scene.add(camera); // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, ambientLightIntensity); scene.add(ambientLight); // Geometry const planeGeometry = new THREE.PlaneGeometry(boxWidth, boxHeight); const planeGroup = new THREE.Group(); // Create rounded plane function const createRoundedPlane = (texture) => { const material = new THREE.MeshMatcapMaterial({ matcap: texture, transparent: true, }); material.onBeforeCompile = (shader) => { shader.vertexShader = shader.vertexShader.replace('#include <common>', ` #include <common> varying vec4 vPosition; varying vec2 vUv; `); shader.vertexShader = shader.vertexShader.replace('#include <fog_vertex>', ` #include <fog_vertex> vPosition = mvPosition; vUv = uv; `); shader.fragmentShader = shader.fragmentShader.replace(`#include <common>`, ` #include <common> varying vec4 vPosition; varying vec2 vUv; float roundedBoxSDF(vec2 CenterPosition, vec2 Size, float Radius) { return length(max(abs(CenterPosition)-Size+Radius,0.0))-Radius; } `); shader.fragmentShader = shader.fragmentShader.replace(`#include <dithering_fragment>`, ` #include <dithering_fragment> vec2 size = vec2(1.0, 1.0); float edgeSoftness = ${edgeSoftness.toFixed(6)}; float radius = ${borderRadius.toFixed(6)}; float distance = roundedBoxSDF(vUv.xy - (size/2.0), size/2.0, radius); float smoothedAlpha = 1.0-smoothstep(0.0, edgeSoftness * 2.0, distance); gl_FragColor = vec4(outgoingLight, smoothedAlpha); `); }; return new THREE.Mesh(planeGeometry, material); }; // Load textures and create planes const textureLoader = new THREE.TextureLoader(); const loadPromises = images.map((imageSrc, index) => { return new Promise((resolve) => { textureLoader.load(imageSrc, (texture) => { const plane = createRoundedPlane(texture); const offset = (index - (images.length - 1) / 2) * spacing; plane.position.set(offset, 0, index === Math.floor(images.length / 2) ? 0.5 : 1); plane.rotation.y = index === 0 ? Math.PI * rotationAngle : index === images.length - 1 ? Math.PI * -rotationAngle : 0; plane.userData = { index }; planeGroup.add(plane); resolve(); }); }); }); Promise.all(loadPromises).then(() => { scene.add(planeGroup); setIsReady(true); onSceneReady?.(); }); // Mouse/Touch handling const handlePointerMove = (clientX, clientY) => { if (!enableMouseControl && !enableTouchControl) return; mouseRef.current.x = (clientX / width) * 2 - 1; mouseRef.current.y = -((clientY / height) * 2 - 1); }; const handleClick = (event) => { if (!onImageClick || !planeGroupRef.current || !cameraRef.current) return; const clientX = 'touches' in event ? event.touches[0]?.clientX : event.clientX; const clientY = 'touches' in event ? event.touches[0]?.clientY : event.clientY; if (!clientX || !clientY) return; const rect = mountRef.current?.getBoundingClientRect(); if (!rect) return; const mouse = new THREE.Vector2(); mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1; raycasterRef.current.setFromCamera(mouse, cameraRef.current); const intersects = raycasterRef.current.intersectObjects(planeGroupRef.current.children); if (intersects.length > 0) { const clickedPlane = intersects[0].object; onImageClick(clickedPlane.userData.index); } }; // Event listeners if (enableMouseControl) { mountRef.current.addEventListener('mousemove', (e) => handlePointerMove(e.clientX, e.clientY)); mountRef.current.addEventListener('click', handleClick); } if (enableTouchControl) { mountRef.current.addEventListener('touchmove', (e) => { e.preventDefault(); if (e.touches[0]) { handlePointerMove(e.touches[0].clientX, e.touches[0].clientY); } }, { passive: false }); mountRef.current.addEventListener('touchstart', handleClick); } // Animation loop let previousTime = 0; const animate = () => { const elapsedTime = clockRef.current.getElapsedTime(); const deltaTime = elapsedTime - previousTime; previousTime = elapsedTime; if (planeGroup) { if (autoRotate) { planeGroup.rotation.y += autoRotateSpeed * deltaTime; } else if (enableMouseControl || enableTouchControl) { const parallaxX = mouseRef.current.x * -parallaxStrength; const parallaxY = mouseRef.current.y * parallaxStrength; planeGroup.rotation.y += (parallaxX - planeGroup.rotation.y) * animationSpeed * deltaTime; planeGroup.rotation.x += (parallaxY - planeGroup.rotation.x) * animationSpeed * deltaTime; } } renderer.render(scene, camera); }; renderer.setAnimationLoop(animate); mountRef.current.appendChild(renderer.domElement); // Store refs sceneRef.current = scene; rendererRef.current = renderer; cameraRef.current = camera; planeGroupRef.current = planeGroup; // Cleanup return () => { renderer.setAnimationLoop(null); renderer.dispose(); planeGeometry.dispose(); scene.clear(); if (mountRef.current && renderer.domElement) { mountRef.current.removeChild(renderer.domElement); } }; }, [ images, width, height, boxWidth, boxHeight, parallaxStrength, animationSpeed, spacing, rotationAngle, borderRadius, edgeSoftness, autoRotate, autoRotateSpeed, ambientLightIntensity, enableMouseControl, enableTouchControl, perspective, cameraDistance, backgroundColor ]); // Handle resize useEffect(() => { const handleResize = () => { if (!rendererRef.current || !cameraRef.current) return; const newWidth = mountRef.current?.clientWidth || width; const newHeight = mountRef.current?.clientHeight || height; cameraRef.current.aspect = newWidth / newHeight; cameraRef.current.updateProjectionMatrix(); rendererRef.current.setSize(newWidth, newHeight); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [width, height]); return (_jsx("div", { ref: mountRef, className: `3d-image-gallery ${className}`, style: { width: '100%', height: '100%', minWidth: width, minHeight: height, overflow: 'hidden', ...style }, "aria-label": "3D Image Gallery", role: "img" })); }; export default ThreeDImageGallery;