Skip to main content
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

items
Array
default:"[]"
Array of gallery items with image (URL) and text (label) properties
bend
number
default:"3"
Curvature amount - positive bends down, negative bends up, 0 is flat
textColor
string
default:"#ffffff"
Color of text labels below each image
borderRadius
number
default:"0.05"
Corner radius for images (0-0.5)
font
string
default:"bold 30px Figtree"
Font specification for text labels
scrollSpeed
number
default:"2"
Scrolling speed multiplier
scrollEase
number
default:"0.05"
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:
const renderer = new Renderer({
  alpha: true,
  antialias: true,
  dpr: Math.min(window.devicePixelRatio || 1, 2)
});
Creates a WebGL renderer with transparency and antialiasing.

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

Scrolling & Interaction

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;
});

Infinite Scrolling

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;
}

Performance Optimization

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
];

Build docs developers (and LLMs) love