The CircularGallery component is an advanced WebGL-based image gallery that displays images in a circular/curved layout with smooth physics-based scrolling and 3D effects.
Overview
Key features:
WebGL rendering using OGL (Open Graphics Library)
Circular/curved image arrangement with customizable bend
Physics-based scrolling with momentum
Mouse and touch support
Shader-based image rendering with rounded corners
Text labels for each image
Infinite scrolling with wrapping
Responsive design
Props
Array of gallery items with image (URL) and text (label) properties
Curvature amount - positive bends down, negative bends up, 0 is flat
Color of text labels below each image
Corner radius for images (0-0.5)
font
string
default: "bold 30px Figtree"
Font specification for text labels
Scrolling speed multiplier
Easing factor for smooth scrolling (0-1)
Implementation
import CircularGallery from './components/CircularGallery/CircularGallery' ;
function App () {
const galleryItems = [
{ image: '/images/guitar1.jpg' , text: 'Fender Stratocaster' },
{ image: '/images/piano1.jpg' , text: 'Yamaha Grand Piano' },
{ image: '/images/drums1.jpg' , text: 'Pearl Export Series' },
{ image: '/images/bass1.jpg' , text: 'Fender Jazz Bass' }
];
return (
< div className = "w-full h-screen" >
< CircularGallery
items = { galleryItems }
bend = { 3 }
textColor = "#ffffff"
borderRadius = { 0.05 }
scrollSpeed = { 2 }
scrollEase = { 0.05 }
/>
</ div >
);
}
Item Structure
Each item in the items array should have:
{
image : string ; // URL to the image
text : string ; // Label displayed below the image
}
Example:
const items = [
{
image: 'https://example.com/guitar.jpg' ,
text: 'Electric Guitar'
},
{
image: 'https://example.com/piano.jpg' ,
text: 'Grand Piano'
}
];
WebGL Architecture
The component uses OGL (Open Graphics Library) for WebGL rendering:
Renderer
Camera
Geometry
Shaders
const renderer = new Renderer ({
alpha: true ,
antialias: true ,
dpr: Math . min ( window . devicePixelRatio || 1 , 2 )
});
Creates a WebGL renderer with transparency and antialiasing. const camera = new Camera ( gl );
camera . fov = 45 ;
camera . position . z = 20 ;
Sets up a perspective camera with 45° field of view. const planeGeometry = new Plane ( gl , {
heightSegments: 50 ,
widthSegments: 100
});
High-segment plane geometry for smooth curved deformation. Custom vertex shader applies wave distortion: vec3 p = position;
p.z = ( sin (p.x * 4.0 + uTime) * 1.5 +
cos (p.y * 2.0 + uTime) * 1.5 ) *
( 0.1 + uSpeed * 0.5 );
Fragment shader handles rounded corners and image aspect ratio.
Circular Bend Effect
The bend prop controls the curvature:
const R = ( H * H + B_abs * B_abs ) / ( 2 * B_abs );
const arc = R - Math . sqrt ( R * R - effectiveX * effectiveX );
if ( bend > 0 ) {
plane . position . y = - arc ;
plane . rotation . z = - Math . sign ( x ) * Math . asin ( effectiveX / R );
} else {
plane . position . y = arc ;
plane . rotation . z = Math . sign ( x ) * Math . asin ( effectiveX / R );
}
bend > 0 : Images curve downward (convex)
bend < 0 : Images curve upward (concave)
bend = 0 : Images remain flat
The gallery supports multiple input methods:
Mouse Wheel
window . addEventListener ( 'wheel' , ( e ) => {
const delta = e . deltaY ;
scroll . target += ( delta > 0 ? scrollSpeed : - scrollSpeed ) * 0.2 ;
});
Mouse Drag
window . addEventListener ( 'mousedown' , ( e ) => {
isDown = true ;
start = e . clientX ;
});
window . addEventListener ( 'mousemove' , ( e ) => {
if ( ! isDown ) return ;
const distance = ( start - e . clientX ) * ( scrollSpeed * 0.025 );
scroll . target = scroll . position + distance ;
});
Touch
window . addEventListener ( 'touchstart' , ( e ) => {
start = e . touches [ 0 ]. clientX ;
});
window . addEventListener ( 'touchmove' , ( e ) => {
const distance = ( start - e . touches [ 0 ]. clientX ) * ( scrollSpeed * 0.025 );
scroll . target = scroll . position + distance ;
});
The gallery creates seamless infinite scrolling by duplicating items:
const galleryItems = items && items . length ? items : defaultItems ;
this . mediasImages = galleryItems . concat ( galleryItems ); // Duplicate array
As items scroll off-screen, they wrap around:
if ( direction === 'right' && isBefore ) {
extra -= widthTotal ;
}
if ( direction === 'left' && isAfter ) {
extra += widthTotal ;
}
Device Pixel Ratio Capped at 2x to prevent excessive GPU load on high-DPI displays
Request Animation Frame Uses RAF for smooth 60fps rendering
Texture Mipmaps Automatically generates mipmaps for better performance
Segment Count 50x100 segments provide smooth curves without excessive vertices
Cleanup
The component properly cleans up WebGL resources:
useEffect (() => {
const app = new App ( containerRef . current , { items , bend , ... });
return () => {
app . destroy (); // Removes event listeners and canvas element
};
}, [ items , bend , textColor , borderRadius , font , scrollSpeed , scrollEase ]);
Styling
The container uses cursor styles for drag interaction:
className = "w-full h-full overflow-hidden cursor-grab active:cursor-grabbing"
The CircularGallery requires a defined width and height from its parent container. Use w-full h-screen or specific dimensions.
Default Items
If no items are provided, the component uses placeholder images from picsum.photos:
const defaultItems = [
{ image: 'https://picsum.photos/seed/1/800/600?grayscale' , text: 'Bridge' },
{ image: 'https://picsum.photos/seed/2/800/600?grayscale' , text: 'Desk Setup' },
// ... 12 total items
];