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

@lancercomet/zoom-pan

v0.4.0

Published

Yet another web 2D rendering lib.

Readme

ZoomPan

ZoomPan is a 2D rendering viewport / render-pipeline library.

It is not a scene-graph engine. If you are looking for a full display tree, rich shape primitives, filters, batching, and a large ecosystem, you probably want PixiJS / Fabric.js / Konva.

It provides a consistent world/screen coordinate model, pan/zoom interactions, a pluggable render pipeline (RenderPipeline/Pass), a layer system, and optional rendering backends (Canvas2D/WebGL).

Use it as a simple “image viewer with pan & zoom”, or as the rendering core of a “drawing app / annotation tool / editor”.

ZoomPan vs Pixi / Fabric / Konva

| Topic | ZoomPan | Pixi / Fabric / Konva | | --- | --- | --- | | Primary goal | Viewport + render orchestration for editor-style apps | General-purpose 2D rendering engine / scene graph | | Best for | Zoom/pan canvas viewers, drawing apps, editors, annotation tools | Complex object trees, shape primitives, filters, rich rendering features | | Extension model | Explicit render pipeline passes + plugins | Scene graph + framework lifecycle + ecosystem plugins | | World/screen split | Built-in phases: world and screen | Often requires custom conventions / layers / containers | | What it does not try to be | A full engine with display tree, batching, filters | A small viewport kernel |

Features

  • Selectable renderer backend: renderer: 'canvas2d' | 'webgl' | 'auto' | IRenderer
  • Pluggable render pipeline: 4 phases beforeWorld / world / afterWorld / screen + stable ordering via order
  • Viewport transform: pan/zoom + world ↔ screen coordinate conversion
  • Interaction plugin: mouse/touch/pen input + inertia + cancel() for focus-loss cleanup
  • Layer system: split world/screen rendering (you can insert passes between them)
  • Bounds (document bounds): background/shadow/clip/border, pan clamping, zoomToFit({ maxScale })
  • History (Undo/Redo): provided via plugin/examples (see examples/)

Install

npm install @lancercomet/zoom-pan

Quick Start

Image Viewer

import { ViewManager, ContentLayerManager, createInteractionPlugin } from '@lancercomet/zoom-pan'

const content = new ContentLayerManager()

const view = new ViewManager(canvas, {
  renderer: 'auto' // 'canvas2d' | 'webgl' | 'auto'
})

view.registerLayerManager(content)
view.use(createInteractionPlugin())

await content.createImageLayer({
  src: 'image.png',
  x: 0,
  y: 0
})

Drawing App (simplified)

import {
  ViewManager,
  ContentLayerManager,
  CanvasLayer,
  HistoryManager,
  createInteractionPlugin,
  createBoundsPlugin,
  createSnapshotCommand
} from '@lancercomet/zoom-pan'

const content = new ContentLayerManager()
const history = new HistoryManager({ maxHistorySize: 50 })

const view = new ViewManager(canvas, { renderer: 'auto' })
view.registerLayerManager(content)

// Interaction plugin controls pan/zoom.
// - Touch gestures (pan/pinch) always work
// - Mouse/pen pan is controlled by setPanEnabled()
const interaction = view.use(createInteractionPlugin())

// Bounds defines document rect/margins and clamps pan.
const bounds = view.use(createBoundsPlugin({
  rect: { x: 0, y: 0, width: 1200, height: 800 },
  margins: { left: 50, right: 50, top: 50, bottom: 50 }
}))

// Prevent upscaling small documents
bounds.zoomToFit({ maxScale: 1 })

const drawLayer = new CanvasLayer({ width: 1200, height: 800 })
content.addLayer(drawLayer)

// Drawing mode: disable mouse/pen pan (touch still works)
interaction.setPanEnabled(false)

// On focus loss, cancel any ongoing drag/pinch/inertia
window.addEventListener('blur', () => interaction.cancel())

let snapshotBefore: ImageData | null = null

canvas.onpointerdown = (e) => {
  if (e.pointerType === 'touch') return
  const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
  snapshotBefore = drawLayer.captureSnapshot()
  drawLayer.beginStroke(wx, wy)
}

canvas.onpointermove = (e) => {
  if (e.pointerType === 'touch') return
  if (e.buttons !== 1) return
  const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
  drawLayer.stroke(wx, wy, '#000', 10, e.pressure, 'brush')
  view.requestRender()
}

canvas.onpointerup = () => {
  drawLayer.endStroke()
  const snapshotAfter = drawLayer.captureSnapshot()
  const cmd = createSnapshotCommand(drawLayer, snapshotBefore, snapshotAfter)
  if (cmd) history.addCommand(cmd)
}

Core Concepts

ViewManager

The main controller: render loop, coordinate transforms, renderer backend, render pipeline, and plugins.

const view = new ViewManager(canvas, {
  minZoom: 0.2,
  maxZoom: 10,
  background: '#fff',
  renderer: 'auto'
})

const { wx, wy } = view.toWorld(screenX, screenY)
const { x, y } = view.toScreen(worldX, worldY)

view.zoomToAtScreen(anchorX, anchorY, 2.0)
view.zoomByFactorAtScreen(anchorX, anchorY, 1.5)

Renderer Selection

import { ViewManager } from '@lancercomet/zoom-pan'

new ViewManager(canvas, { renderer: 'canvas2d' })
new ViewManager(canvas, { renderer: 'webgl' })
new ViewManager(canvas, { renderer: 'auto' }) // tries WebGL first, falls back to Canvas2D

RenderPipeline / Pass

You can insert your own passes (e.g. draw a debug grid before world rendering):

view.addRenderPass({
  name: 'debug.grid',
  phase: 'beforeWorld',
  order: -50,
  render: ({ renderer }) => {
    const ctx = renderer.getContentContext()
    ctx.save()
    ctx.strokeStyle = 'rgba(0,0,0,0.08)'
    ctx.lineWidth = 1
    // draw grid / guides here
    ctx.restore()
  }
})

InteractionPlugin

import { createInteractionPlugin } from '@lancercomet/zoom-pan'

const interaction = view.use(createInteractionPlugin())

interaction.setPanEnabled(true)  // mouse/pen can pan
interaction.setPanEnabled(false) // mouse/pen cannot pan (drawing mode)

interaction.setZoomEnabled(true) // wheel zoom + pinch zoom

interaction.cancel() // cancel drag/pinch/inertia (blur/visibilitychange)

BoundsPlugin

BoundsPlugin draws background/shadow, clips the world, and draws border via pipeline passes. It also supports per-instance pass ordering (useful when you need to insert your own passes around the bounds clip).

import { createBoundsPlugin } from '@lancercomet/zoom-pan'

const bounds = view.use(createBoundsPlugin({
  rect: { x: 0, y: 0, width: 1200, height: 800 },
  margins: { left: 50, right: 50, top: 50, bottom: 50 },
  drawBorder: true,
  background: '#f0f0f0',
  shadow: { blur: 20, color: 'rgba(0,0,0,0.3)', offsetX: 0, offsetY: 5 },
  passOrder: {
    beforeWorld: -100,
    afterWorld: 100
  }
}))

bounds.zoomToFit({ maxScale: 1 })
bounds.setPanClampMode('minVisible') // 'margin' | 'minVisible'

Layers

import { ViewManager, ContentLayerManager, TopScreenLayerManager, CanvasLayer } from '@lancercomet/zoom-pan'

const view = new ViewManager(canvas, { renderer: 'auto' })

// World-space content
const content = new ContentLayerManager()
view.registerLayerManager(content)

// Screen-space overlay (UI / cursor / HUD)
const overlay = new TopScreenLayerManager()
view.registerLayerManager(overlay)

content.addLayer(new CanvasLayer({ width: 1200, height: 800 }))

Default rendering is split by pipeline phase:

  • world: calls each LayerManager's renderWorldLayersIn(view)
  • screen: calls each LayerManager's renderScreenLayersIn(view)

This means you can insert passes between world and screen (e.g. selection overlay after world, UI in screen).

Plugin Lifecycle

ViewManager.use(plugin) is transactional: if install() throws, it performs a best-effort rollback to avoid leaving a half-installed plugin behind.

view.hasPlugin('bounds')
view.listPlugins()
view.clearPlugins()

view.unuse('bounds')

Examples

See examples/:

  • examples/viewer/: image viewer
  • examples/painter/: drawing app (brush/eraser/layers/undo-redo)
  • examples/bounds/: bounds (background/shadow/zoomToFit)
  • examples/history/: history (undo/redo)