@supuwoerc/masonry
High-performance image grid/masonry layout library powered by Canvas + Web Worker + OffscreenCanvas.
All layout calculations and rendering run in a Worker thread — zero main-thread blocking. Supports infinite scrolling, inertia scrolling, seamless loop, viewport culling, click interaction, and adaptive DPR scaling.
中文文档
Features
Web Worker rendering — Layout and Canvas bindings execute in a Worker; the main thread only proxies events
Dual layout modes — Equal-height grid and masonry (waterfall) layout
Inertia scrolling — Physics-based scrolling with friction decay
Seamless loop — Automatic infinite loop scrolling once all data is loaded
Viewport culling — Only renders elements within the visible area + configurable buffer
Image loader — Concurrency control, timeout, exponential backoff retry, custom fetcher
Placeholder animation — Built-in breathing gradient and spinning dots loaders
Click interaction — Worker-side hit detection, returns precise row/column index
Adaptive scaling — Listens for DPR changes and container resize, auto re-renders
Installation
npm install @supuwoerc/masonry # or pnpm add @supuwoerc/masonry
Copy
Quick Start
Basic Usage (ImageBitmap)
import { MasonryBuilder } from '@supuwoerc/masonry' const canvas = document . getElementById ( 'canvas' ) as HTMLCanvasElement new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 , gap: 10 , radius: 8 }, items: bitmaps , // ImageBitmap[] backgroundColor: '#f5f5f5' , }) . withEvents ({ onReady : ( instance ) => console . log ( 'Ready' , instance ), }) . build ()
Copy
URL Loading
new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 , gap: 10 , radius: 8 }, items: [ 'https://example.com/image1.jpg' , 'https://example.com/image2.jpg' , ], }) . build ()
Copy
URL Loading with Dimensions (Recommended for Masonry)
new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 , gap: 10 }, layout: 'masonry' , items: [ { url: 'https://example.com/1.jpg' , width: 800 , height: 1200 }, { url: 'https://example.com/2.jpg' , width: 600 , height: 400 }, ], }) . build ()
Copy
new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 , gap: 10 , radius: 8 }, }) . withLoader ({ pageSize: 20 , loadMore : async ( page , pageSize ) => { const res = await fetch ( `/api/images?page= ${ page } &size= ${ pageSize } ` ) const data = await res . json () return data . list // string[] | ItemDescriptor[] | ImageBitmap[] }, }) . build ()
Copy
Custom Image Fetcher (Auth / Proxy)
const masonry = new Masonry ({ core: { canvas , style: { width: 200 , height: 300 }, items: urls }, imageLoad: { concurrency: 4 , timeout: 8000 , maxRetries: 2 , fetcher : async ( url , signal ) => { const res = await fetch ( url , { signal , headers: { Authorization: 'Bearer token' }, }) return await res . blob () }, }, })
Copy
Click Interaction
new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 }, items: bitmaps }) . withInteraction ({ onClick : ({ item , index , row , column , event }) => { console . log ( `Clicked item ${ index } at row ${ row } , column ${ column } ` ) }, }) . build ()
Copy
Placeholder Animations
import { BreathingPlaceholderRenderer , SpinPlaceholderRenderer } from '@supuwoerc/masonry' // Breathing gradient new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 }, items: urls }) . withPlaceholder ( new BreathingPlaceholderRenderer ({ backgroundColor: '#e0e0e0' , highlightColor: 'rgba(255, 255, 255, 0.6)' , duration: 1500 , radius: 8 , }), ) . build () // Spinning dots new MasonryBuilder () . withCore ({ canvas , style: { width: 200 , height: 300 }, items: urls }) . withPlaceholder ( new SpinPlaceholderRenderer ({ backgroundColor: '#f2f2f2' , }), ) . build ()
Copy
import { StatsMonitor } from '@supuwoerc/masonry' const monitor = new StatsMonitor ( 'fps' ) // 'fps' | 'ms' | 'mb' // Double-click to toggle visibility document . addEventListener ( 'dblclick' , () => monitor . toggle ())
Copy
API Reference
MasonryBuilder
Fluent builder — the recommended way to create instances.
Method
Parameter
Description
withCore(config)
Core
Set core configuration (canvas, style, data source, etc.)
withInteraction(config)
Interaction
Set interaction configuration (click, scroll, layout callback)
withLoader(config)
LoadMoreConfig
Set infinite scroll loader
withPlaceholder(renderer)
PlaceholderRenderer
Set placeholder renderer
withEvents(config)
{ onReady?, onError? }
Set event callbacks
build()
—
Build and return a Masonry instance
Masonry
Direct instantiation:
const masonry = new Masonry ( config : MasonryConfiguration ) masonry . destroy () // Destroy instance and release all resources
Copy
Configuration
Core
Parameter
Type
Required
Default
Description
canvas
HTMLCanvasElement
✓
—
Canvas DOM element
style
GridItemStyle
✓
—
Grid item style configuration
items
ImageBitmap[] | string[] | ItemDescriptor[]
—
Image data source
backgroundColor
string | GradientBackground
'#fff'
Background color or gradient
layout
'grid' | 'masonry'
'grid'
Layout mode
limit
number
—
Concurrency limit
timeout
number
—
Request timeout (ms)
GridItemStyle
Parameter
Type
Required
Default
Description
width
number
✓
—
Item width (px)
height
number
✓
—
Item height (px)
gap
number
0
Gap between items (px)
radius
number
0
Border radius (px)
ItemDescriptor
Parameter
Type
Required
Default
Description
url
string
✓
—
Image URL
width
number
—
Original image width (used in masonry layout)
height
number
—
Original image height (used in masonry layout)
Interaction
Parameter
Type
Required
Default
Description
onClick
(event: ClickEvent) => void
—
Click event callback
onLayoutUpdate
(event: LayoutUpdateEvent) => void
—
Layout update callback
scroll
ScrollConfig
—
Scroll configuration
ClickEvent
Parameter
Type
Description
item
GridItem
The clicked grid item
index
number
Index in the data source
row
number
Row number
column
number
Column number
event
MouseEvent
Native event object
LayoutUpdateEvent
Parameter
Type
Description
contentWidth
number
Total content width (px)
contentHeight
number
Total content height (px)
Parameter
Type
Required
Default
Description
disabled
{ horizontal?: boolean; vertical?: boolean }
{ horizontal: false, vertical: false }
Disabled scroll directions
inertia
boolean
true
Enable inertia scrolling
buffer
number
1.0
Viewport culling buffer multiplier (extends N viewport sizes above/below)
threshold
number
200
Distance threshold to trigger loadMore (px)
loop
boolean
true
Enable seamless loop scrolling when all data is loaded
LoadMoreConfig
Parameter
Type
Required
Default
Description
pageSize
number
✓
—
Items per page
loadMore
(page: number, pageSize: number) => Promise<...>
✓
—
Async function to load more data
ImageLoadConfig
Parameter
Type
Required
Default
Description
concurrency
number
6
Concurrent load count
maxRetries
number
3
Maximum retry attempts
retryDelay
number
500
Base retry delay (ms), uses exponential backoff
timeout
number
10000
Single image timeout (ms)
fetcher
ImageFetcher
built-in fetch
Custom request function
GradientBackground
Parameter
Type
Required
Description
type
'linear' | 'radial'
✓
Gradient type
stops
ColorStop[]
✓
Color stop array { offset: number; color: string }
linear
{ start: [x, y]; end: [x, y] }
Linear gradient parameters
radial
{ start: [x, y]; end: [x, y]; r0: number; r1: number }
Radial gradient parameters
PlaceholderRenderer Interface
Method
Signature
Description
render
(width, height, id) => ImageBitmap | Promise<ImageBitmap>
Render one frame
remove
(id: string) => void
Remove specific placeholder
dispose
() => void
Release all resources
Built-in implementations:
StatsMonitor
new StatsMonitor ( panel ?: 'fps' | 'ms' | 'mb' | 'custom' , dom ?: HTMLElement , start ?: boolean )
Copy
Method
Description
start()
Start monitoring
stop()
Stop monitoring
enable()
Show panel
disable()
Hide panel
toggle()
Toggle visibility
customizeStyle(style)
Customize panel DOM style
Architecture
┌─────────────────────────────────────────────────────────────┐ │ Main Thread │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ Masonry │ │ ImageLoader │ │ PlaceholderRenderer │ │ │ │ / Builder │ │ │ │ │ │ │ └─────┬─────┘ └──────┬───────┘ └─────────┬─────────┘ │ │ │ │ │ │ │ │ postMessage │ ImageBitmap │ ImageBitmap │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Worker Message Channel ( postMessage ) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ transferControlToOffscreen │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Worker Thread ( Web Worker ) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ LayoutStrategy │ │ ScrollState │ │ OffscreenCanvas │ │ │ │ Grid / Masonry │ │ + Inertia │ │ Rendering │ │ │ └──────────────┘ └──────────────┘ └─────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ ViewportCulling │ │ HitTest │ │ Loop Scroll │ │ │ │ + Buffer │ │ Detection │ │ (1 D Modulo ) │ │ │ └──────────────┘ └──────────────┘ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
Copy
Message Flow
Initialization : Main → Worker : Setup ( OffscreenCanvas + config ) Worker → Main : SetupResponse Main → Worker : Render Image Loading : Main → Worker : ImageLoaded ( index + ImageBitmap ) Worker → Main : RenderLoading ( placeholder ID list ) Main → Worker : RenderLoadingResponse ( placeholder ImageBitmap ) Worker → Main : RemoveLoading ( loaded , remove placeholder ) Scrolling : Main → Worker : Scroll ( deltaX , deltaY ) Worker internal : inertia calculation → viewport culling → repaint Infinite Scroll : Worker → Main : LoadMore ( scrolled to threshold ) Main → Worker : LoadMoreResponse ( new data ) Click : Main → Worker : Click ( x , y ) Worker → Main : ClickResult ( item , index , row , column ) Container Changes : Main → Worker : Resize ( clientWidth , clientHeight , dpr ) Layout Changes : Worker → Main : LayoutUpdated ( contentWidth , contentHeight )
Copy
Rendering Pipeline
Initialization — Main thread transfers Canvas control to Worker via transferControlToOffscreen()
Layout Calculation — Worker computes each item's position using the configured strategy (Grid/Masonry)
Viewport Culling — Each frame only draws items within the current viewport + buffer zone
Loop Scrolling — Once all data is loaded, 1D modulo arithmetic maps infinite coordinates to the finite dataset
Inertia Scrolling — After pointer release, velocity decays per-frame with friction (0.95) until stop
Animation Loop — Starts/stops requestAnimationFrame on demand; zero CPU when idle
Browser Compatibility
Feature
Requirement
Canvas 2D
✓ Required
Web Worker
✓ Required
OffscreenCanvas
✓ Required
ImageBitmap
✓ Required
ResizeObserver
✓ Required
Supports all modern browsers (Chrome 69+, Firefox 105+, Safari 16.4+, Edge 79+).
License
MIT