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

use-canvas-masonry

v0.0.3

Published

60fps infinite canvas with masonry layout and viewport virtualization

Readme

use-canvas-masonry

A React hook for building 60fps infinite canvas layouts with masonry or grid arrangement, viewport virtualization, and infinite scroll in any direction.

Features

  • Masonry & grid layouts — automatic column sizing based on item count
  • Pan & zoom — pointer drag with inertia, pinch-to-zoom, scroll wheel
  • Viewport virtualization — only visible tiles are in the DOM
  • Infinite wrap-around — toroidal scrolling on X and/or Y axes
  • Click detection — tap vs drag discrimination, hit-tested in canvas space
  • Imperative APIpanTo, zoomTo, fitToView

Installation

npm install use-canvas-masonry

Peer dependency: React 18+

Usage

import { useCanvasMasonry, type CanvasItem } from 'use-canvas-masonry'

type Photo = CanvasItem & { id: number; src: string }

const photos: Photo[] = [
  { id: 1, width: 400, height: 600, src: '/photo1.jpg' },
  { id: 2, width: 600, height: 400, src: '/photo2.jpg' },
  // ...
]

export default function Gallery() {
  const { containerRef, transformRef, containerProps, transformProps, tileProps, visibleTiles } =
    useCanvasMasonry<Photo>({
      items: photos,
      columnWidth: 300,
      gap: 12,
      wrap: true,
      onItemClick: (item) => console.log('clicked', item.id),
    })

  return (
    <div ref={containerRef} {...containerProps}>
      <div ref={transformRef} {...transformProps}>
        {visibleTiles.map((tile) => (
          <div key={tile.wrapKey ?? tile.index} style={tileProps(tile).style}>
            <img src={tile.item.src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
          </div>
        ))}
      </div>
    </div>
  )
}

Items must satisfy CanvasItem:

type CanvasItem = { width: number; height: number }

Options

| Option | Type | Default | Description | |---|---|---|---| | items | T[] | — | Array of items to render. Each must have width and height. | | columnWidth | number | 300 | Fixed column width in px. | | gap | number | 30 | Gap between columns and rows in px. | | layout | 'masonry' \| 'grid' | 'masonry' | Layout algorithm. | | minZoom | number | 0.5 | Minimum zoom level. | | maxZoom | number | 3 | Maximum zoom level. | | overscan | number | 200 | Extra px rendered outside the viewport edges. | | initialViewport | Partial<Viewport> | — | Starting { x, y, zoom }. | | wrap | boolean | false | Enable infinite horizontal wrap-around. | | wrapY | boolean | true | Also wrap vertically. Requires wrap: true. | | onItemClick | (item, tile, event) => void | — | Called on tap (pointer displacement < 5px). Not fired after drags. |

Return value

| Property | Type | Description | |---|---|---| | containerRef | RefObject<HTMLDivElement> | Attach to the outer container div. | | transformRef | RefObject<HTMLDivElement> | Attach to the inner transform div. | | containerProps | object | Spread onto the outer container div. | | transformProps | object | Spread onto the inner transform div. | | tileProps(tile) | (tile) => { style } | Returns absolute-position style for a tile. | | visibleTiles | TileLayout<T>[] | Tiles currently visible in the viewport — render these. | | allTiles | TileLayout<T>[] | All computed tile positions. | | viewport | Viewport | Read-only snapshot of current { x, y, zoom }. | | totalSize | { width, height } | Total canvas dimensions (one period). | | panTo(x, y) | function | Pan to an absolute canvas position. | | zoomTo(zoom, center?) | function | Zoom to a level, optionally anchored to a screen point. | | fitToView() | function | Fit all content into the viewport. |

TileLayout

type TileLayout<T> = {
  item: T        // original item
  index: number  // position in items array
  x: number      // canvas x
  y: number      // canvas y
  width: number
  height: number
  wrapKey?: string // use as key={tile.wrapKey ?? tile.index} when wrap is enabled
}

Rendering tiles

Use tileProps(tile) to position each tile and tile.wrapKey ?? tile.index as the React key:

{visibleTiles.map((tile) => (
  <div key={tile.wrapKey ?? tile.index} style={tileProps(tile).style}>
    {/* tile content */}
  </div>
))}

When wrap is enabled, the same item appears at multiple canvas positions simultaneously. wrapKey encodes the position offset so React treats each copy as a distinct node.

Infinite wrap-around

useCanvasMasonry({
  items,
  wrap: true,   // infinite horizontal scroll
  wrapY: true,  // + infinite vertical scroll (default when wrap is true)
})

When wrap is on, the minimum zoom is clamped so you can't zoom out far enough to see the whole canvas at once — that would break the tiling illusion.

Imperative control

const { panTo, zoomTo, fitToView } = useCanvasMasonry({ items })

// Pan to canvas origin
panTo(0, 0)

// Zoom to 2× anchored to screen center
zoomTo(2, { x: window.innerWidth / 2, y: window.innerHeight / 2 })

// Fit all content
fitToView()

License

MIT