npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gallery-universe

v0.1.7

Published

A zoomable, clusterable 3D canvas for displaying large collections of items

Downloads

1,025

Readme

gallery-universe

A zoomable, clusterable 3D canvas for displaying large collections of items. Navigate thousands of items in a perspective space — scatter them freely or group them into clusters, with smooth pinch-to-zoom, pan, and animated transitions between layouts.

Features

  • Perspective 3D canvas — items exist at real world coordinates with depth-based projection
  • Pinch-to-zoom and two-finger pan (trackpad and touch) with content boundary clamping
  • Cluster mode — group items by any property with animated transitions
  • Navigate between clusters with smooth camera pans
  • CategoryNav component — scrollable category bar that auto-highlights the nearest cluster as you pan
  • Configurable cluster label position (up, down, center)
  • Custom item rendering — bring your own draw function
  • Built-in image renderer with loading states and selection highlight
  • Click and double-click hit detection

Install

npm install gallery-universe gsap

gsap is a required peer for smooth camera animation.

Basic usage

import { useUniverseCore, UniverseCanvas, createItems, createImageRenderer } from 'gallery-universe'

type Track = {
  title: string
  artist: string
  coverUrl: string
}

// Create items once, outside the component
const items = createItems(800, (i) => ({
  title: `Track ${i}`,
  artist: `Artist ${i % 20}`,
  coverUrl: `https://example.com/covers/${i}.jpg`,
}))

const renderItem = createImageRenderer<Track>('coverUrl')

export default function App() {
  const core = useUniverseCore<Track>({
    items,
    onItemClick: (item) => console.log(item.data.title),
    onItemDoubleClick: (item) => console.log('double:', item.data.title),
  })

  return (
    <UniverseCanvas
      core={core}
      width={window.innerWidth}
      height={window.innerHeight}
      renderItem={renderItem}
    />
  )
}

Grouping and cluster navigation

export default function App() {
  const core = useUniverseCore<Track>({ items })

  return (
    <>
      <div style={{ position: 'fixed', top: 16, left: 16, zIndex: 10, display: 'flex', gap: 8 }}>
        <button onClick={() => core.setGroupBy(null)}>Scatter</button>

        <button onClick={() => core.setGroupBy((item) => item.data.artist)}>
          By Artist
        </button>
      </div>

      <UniverseCanvas
        core={core}
        width={window.innerWidth}
        height={window.innerHeight}
        renderItem={renderItem}
        groupBy={(item) => item.data.artist}
        clusterLabelPosition="up"
      />
    </>
  )
}

CategoryNav

CategoryNav renders a scrollable pill bar that auto-highlights the cluster nearest to the camera as you pan. Clicking a pill navigates to that cluster.

import { CategoryNav } from 'gallery-universe'

const groupBy = (item) => item.data.artist
const groups = [...new Set(items.map(groupBy))].map((key) => ({
  key,
  count: items.filter((i) => groupBy(i) === key).length,
}))

export default function App() {
  const core = useUniverseCore<Track>({ items })

  return (
    <>
      <CategoryNav
        groups={groups}
        cameraRef={core.cameraRef}
        groupCentersRef={core.groupCentersRef}
        onSelect={(key) => core.navigateToGroup(key)}
      />

      <UniverseCanvas
        core={core}
        width={window.innerWidth}
        height={window.innerHeight}
        renderItem={renderItem}
        groupBy={groupBy}
      />
    </>
  )
}

CategoryNav props

| Prop | Type | Description | |---|---|---| | groups | { key: string; count: number }[] | List of groups to render as pills. | | cameraRef | RefObject<Camera> | From core.cameraRef. Used to track camera position. | | groupCentersRef | RefObject<Map<string, { x: number; y: number }>> | From core.groupCentersRef. Maps group keys to world positions. | | onSelect | (key: string) => void | Called when a pill is clicked — typically core.navigateToGroup. | | trackStyle | React.CSSProperties | Optional styles for the scrollable track container. | | buttonStyle | React.CSSProperties | Optional styles applied to every pill button. | | buttonActiveStyle | React.CSSProperties | Optional extra styles applied to the active pill (merged with buttonStyle). |

Custom item renderer

createImageRenderer is a convenience wrapper. You can pass any draw function instead:

import type { RenderItem } from 'gallery-universe'

function renderItem(ctx: CanvasRenderingContext2D, item: RenderItem<Track>, isSelected: boolean) {
  const { screenX, screenY, screenSize } = item

  ctx.fillStyle = isSelected ? '#1db954' : '#333'
  ctx.beginPath()
  ctx.arc(screenX, screenY, screenSize / 2, 0, Math.PI * 2)
  ctx.fill()

  if (screenSize > 30) {
    ctx.fillStyle = '#fff'
    ctx.font = `${Math.round(screenSize * 0.2)}px sans-serif`
    ctx.textAlign = 'center'
    ctx.fillText(item.data.title, screenX, screenY + screenSize / 2 + 14)
  }
}

API

useUniverseCore(options)

The main hook. Returns a core object to pass to UniverseCanvas.

| Option | Type | Description | |---|---|---| | items | UniverseItem<T>[] | Items to display. Create with createItems. | | onItemClick | (item) => void | Fired on click or tap. | | onItemDoubleClick | (item) => void | Fired on double-click or double-tap. |

core object:

| Property / Method | Description | |---|---| | setGroupBy(fn) | Group items by a string key. Pass null to return to scatter. | | navigateToGroup(key) | Smooth camera pan to the named cluster. | | cameraRef | RefObject<Camera> — live camera state. Pass to CategoryNav. | | groupCentersRef | RefObject<Map<string, { x: number; y: number }>> — world-space center per group. Pass to CategoryNav. |

createItems(count, getData)

Generates count items distributed in a 3D sphere. getData(index) returns your custom data for each item.

createImageRenderer(urlKey)

Returns a canvas draw function that renders images from item.data[urlKey]. Shows a placeholder while loading and a green border when selected.

UniverseCanvas

| Prop | Type | Default | Description | |---|---|---|---| | core | UniverseCoreExtended<T> | — | From useUniverseCore. | | width | number | — | Canvas width in pixels. | | height | number | — | Canvas height in pixels. | | renderItem | (ctx, item, isSelected) => void | — | Draw function called per visible item per frame. | | groupBy | (item) => string \| null | null | Used to render cluster labels on canvas. Should match the fn passed to setGroupBy. | | clusterLabelPosition | 'up' \| 'down' \| 'center' | 'up' | Vertical position of cluster labels relative to the cluster center. |