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

juniper-iiif

v1.2.2

Published

High-performance IIIF viewer with WebGPU/WebGL rendering, annotations, and comparison mode

Readme

Juniper IIIF

A high-performance IIIF image viewer with WebGPU, WebGL, and Canvas 2D rendering backends. Built for smooth interaction with deep-zoom images, annotations, and comparison workflows.

Features

  • Multiple renderers — WebGPU (4x MSAA), WebGL, Canvas 2D with automatic fallback
  • Tiled rendering — multi-level tile management with LRU caching, priority loading, and GPU batching
  • Spring physics camera — smooth zoom-to-cursor, pan, pinch, and keyboard navigation
  • IIIF v2 & v3 — manifest parsing, multi-canvas navigation, ranges/TOC, metadata
  • Annotations — IIIF annotations + custom HTML annotations with popups
  • HTML overlays — arbitrary DOM elements positioned in world coordinates
  • Minimap — draggable viewport navigator with rotate and mirror controls
  • Comparison mode — synchronized side-by-side viewing of multiple images
  • Responsive — per-breakpoint panel visibility for desktop, tablet, and mobile
  • Fully themeable — CSS custom properties for colors, sizing, and layout
  • TypeScript — full type definitions included

Installation

npm install juniper-iiif

Quick Start

import { IIIFViewer } from 'juniper-iiif';
import 'juniper-iiif/style.css';

const container = document.getElementById('viewer');
const viewer = await IIIFViewer.create(container, 'https://example.org/manifest.json');

With Options

const viewer = await IIIFViewer.create(container, url, {
  preset: 'full',            // 'minimal' | 'viewer' | 'full'
  renderer: 'auto',          // 'webgpu' | 'webgl' | 'canvas2d' | 'auto'
  camera: {
    wheelZoomFactor: 1.5,    // Wheel/button/keyboard zoom multiplier
    pinchSensitivity: 1.0,   // Pinch-to-zoom sensitivity (>1 amplifies, <1 dampens)
    doubleTapZoomFactor: 2.0, // Double-tap zoom multiplier
    minZoom: 0.2,            // Min zoom (multiplier of fit-to-view scale)
    maxZoom: 10,             // Max zoom (multiplier of fit-to-view scale)
    springStiffness: 6.5,    // Animation spring tension
    animationTime: 1.25,     // Spring duration (seconds)
    zoomThrottle: 80,        // Min ms between wheel events
  },
  distanceDetail: 0.65,      // Tile detail (0-1, lower = sharper)
  panels: {
    navigation: 'show',
    pages: 'show',
    minimap: { visibility: 'show', dock: 'bottom-left' },
    annotations: 'show',
    settings: 'show-closed',
    manifest: 'show-closed',
    compare: { visibility: 'show', dock: 'bottom-right' },
    gesture: 'show-closed',
  },
  enableOverlays: true,
  enableCompare: true,
});

JSON Configuration

Load images and settings from a config object:

const viewer = new IIIFViewer(container, { preset: 'full', autoStart: true });

await viewer.loadConfig({
  images: [
    { url: 'https://example.org/iiif/image1/info.json', label: 'Image 1' },
    { url: 'https://example.org/iiif/image2/info.json', placement: { x: 1000, y: 0 } },
  ],
  settings: {
    backgroundColor: '#1a1a1a',
    theme: 'dark',
  },
  viewport: {
    centerX: 500,
    centerY: 500,
    zoom: 1.5,
  },
  annotations: [
    { x: 100, y: 200, content: 'Note here', type: 'Notes' },
  ],
});

Navigation

// Zoom
viewer.zoom(targetScale, duration?);
viewer.zoomByFactor(2.0, duration?);

// Pan
viewer.pan(deltaX, deltaY, duration?);

// Navigate to point
viewer.lookAt(x, y);
viewer.lookAt(x, y, { zoom: 3, duration: 600 });

// Fit a rectangular region (image pixel coordinates)
viewer.fitBounds(x, y, width, height);
viewer.fitBounds(100, 200, 400, 300, { padding: 80, duration: 800 });

// Fit entire image in view
viewer.fitToWorld();

// Navigate to absolute position
viewer.to(worldX, worldY, cameraZ, duration?);

// Canvas navigation (multi-page manifests)
await viewer.loadCanvas(index);
await viewer.nextCanvas();
await viewer.previousCanvas();
viewer.canvasCount;    // total canvases
viewer.currentCanvas;  // current index

Events

const off = viewer.on('zoom', ({ zoom, scale }) => {
  console.log('Zoom level:', zoom);
});

// Available events:
viewer.on('load',            ({ url, type }) => {});
viewer.on('canvasChange',    ({ index, label }) => {});
viewer.on('viewportChange',  ({ centerX, centerY, zoom, scale }) => {});
viewer.on('zoom',            ({ zoom, scale }) => {});
viewer.on('rendererReady',   ({ type }) => {});
viewer.on('tileLoadStart',   ({ totalPending }) => {});
viewer.on('tileLoadEnd',     () => {});
viewer.on('error',           ({ message, source, originalError }) => {});
viewer.on('destroy',         () => {});

// Unsubscribe
off();

Annotations

Add custom HTML annotations positioned in image pixel coordinates:

// Simple text label
viewer.addAnnotation(200, 100, 400, 60, 'Detail of interest', {
  id: 'label-1',
  type: 'Notes',
  color: '#3e73c9',
  style: {
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    color: '#fff',
    padding: '8px 12px',
    borderRadius: '6px',
  },
});

// Point marker with popup
viewer.addAnnotation(800, 600, 0, 0, 'pin', {
  id: 'pin-1',
  type: 'Markers',
  scaleWithZoom: false,
  popup: '<h4>Point of Interest</h4><p>Description here</p>',
  popupPosition: { x: 28, y: 0 },
});

// Custom HTML element
const el = document.createElement('div');
el.innerHTML = '<div class="hotspot-pulse"></div>';
viewer.addAnnotation(500, 300, 0, 0, el, {
  scaleWithZoom: true,
  style: { overflow: 'visible' },
});

// Remove / clear
viewer.removeAnnotation('label-1');
viewer.clearAnnotations();

Annotation Options

| Option | Type | Description | |---|---|---| | id | string | Unique identifier | | type | string | Category (shown in annotation panel) | | color | string | Panel indicator color | | scaleWithZoom | boolean \| { min, max } | Scale with zoom, fixed size, or clamped between bounds | | style | Record<string, string> | CSS styles applied to the element | | activeClass | string | CSS class when annotation is visible | | inactiveClass | string | CSS class when annotation is hidden | | targetUrl | string | Only show for this manifest URL | | targetPage | number | Only show on this canvas index | | popup | string \| HTMLElement | Popup content on click (independent div, not clipped by annotation) | | popupPosition | { x, y } | Popup offset from annotation's top-right corner | | popupScale | { min, max } | Clamp popup scale between bounds (e.g. { min: 0.5, max: 2 }) |

State

viewer.getZoom();          // Current scale (1 = 1:1 pixels)
viewer.getCenter();        // { x, y } in world coordinates
viewer.getBounds();        // { left, top, right, bottom }
viewer.getRendererType();  // 'webgpu' | 'webgl' | 'canvas2d'
viewer.isLoading();        // Whether tiles are being fetched

Layout Persistence

Save and restore the full viewer state including viewport, panel positions, and settings:

const state = viewer.saveLayout();
localStorage.setItem('viewer-layout', JSON.stringify(state));

// Later:
const saved = JSON.parse(localStorage.getItem('viewer-layout'));
await viewer.loadLayout(saved);

Theming

CSS Custom Properties

Import the default stylesheet, then override variables:

import 'juniper-iiif/style.css';
:root {
  /* Colors (RGB format for opacity control) */
  --iiif-color-primary: 100, 200, 150;
  --iiif-panel-bg: rgba(20, 20, 30, 0.9);
  --iiif-text-primary: #f0f0f0;

  /* Layout */
  --iiif-toolbar-height: 3rem;
  --iiif-border-radius: 12px;
  --iiif-nav-btn-size: 32px;

  /* Typography */
  --iiif-font-family: 'Inter', sans-serif;
}

Theme File

A complete, documented theme file is shipped for direct editing:

# Copy from node_modules into your project
cp node_modules/juniper-iiif/dist/iiif-theme.css ./src/my-theme.css

Or import it to see all available variables:

import 'juniper-iiif/theme.css';

Light Theme

Add the theme-light class to the container:

container.classList.add('theme-light');

CSS Variables Reference

Colors: --iiif-color-primary, --iiif-color-danger, --iiif-color-success, --iiif-color-info, --iiif-color-error

Panel: --iiif-panel-bg, --iiif-panel-blur, --iiif-panel-shadow, --iiif-panel-z-index, --iiif-panel-header-font-size, --iiif-panel-body-font-size

Text: --iiif-text-primary, --iiif-text-secondary, --iiif-text-muted, --iiif-text-dimmed, --iiif-text-hover

Borders: --iiif-border-color, --iiif-border-light, --iiif-border-radius

Interactive: --iiif-hover-bg, --iiif-active-bg, --iiif-active-border, --iiif-focus-color

Buttons: --iiif-button-bg, --iiif-button-bg-hover, --iiif-button-border

Inputs: --iiif-input-bg, --iiif-input-border, --iiif-input-placeholder

Layout: --iiif-toolbar-height, --iiif-toolbar-btn-min-width, --iiif-toolbar-offset, --iiif-nav-btn-size, --iiif-canvas-nav-width, --iiif-canvas-nav-max-height, --iiif-toc-min-width, --iiif-toc-max-width, --iiif-annotation-max-height

Typography: --iiif-font-family, --iiif-font-mono

Transitions: --iiif-transition-duration, --iiif-transition-fast

Presets

Three built-in presets configure which UI elements are available:

| Preset | Toolbar | Panels | Compare | Overlays | |---|---|---|---|---| | minimal | No | None | No | No | | viewer | Yes | Navigation, Pages, Map, Settings | No | Yes | | full | Yes | All | Yes | Yes |

IIIFViewer.create(container, url, { preset: 'minimal' });

Individual options override preset defaults.

Responsive Panel Visibility

Each panel can be configured differently for desktop, tablet, and mobile breakpoints:

const viewer = await IIIFViewer.create(container, url, {
  panels: {
    // Simple string - same on all breakpoints
    navigation: 'show',

    // Responsive object - different per breakpoint
    pages: { desktop: 'show', tablet: 'show-closed', mobile: 'hide' },
    minimap: { desktop: 'show', mobile: 'hide' },
    annotations: { desktop: 'show', tablet: 'hide' },
    settings: { desktop: 'show-closed', mobile: 'hide' },
    manifest: 'show-closed',

    // With dock position override
    compare: { visibility: 'show', dock: 'top-left' },
  },
});

Breakpoints

| Breakpoint | Width | |---|---| | mobile | <= 480px | | tablet | 481px - 1024px | | desktop | > 1024px |

Omitted breakpoints fall back to the next larger size: mobile falls back to tablet, which falls back to desktop. If desktop is omitted it defaults to 'show'.

Visibility Values

| Value | Behavior | |---|---| | 'show' | Panel visible and expanded | | 'show-closed' | Panel visible but collapsed | | 'show-open' | Alias for 'show' | | 'hide' | Panel hidden at this breakpoint |

Dock Positions

Each panel can be assigned to a dock via the dock property in the config object:

| Dock | Default Panels | |---|---| | 'top-left' | Gesture | | 'top-right' | Contents, Manifest, Annotations, Settings | | 'bottom-left' | Pages, Map | | 'bottom-right' | Compare | | 'top-center' | — | | 'bottom-center' | — |

Panels can be combined with responsive visibility and dock: { desktop: 'show', mobile: 'hide', dock: 'top-left' }.

Minimap

The Map panel provides a thumbnail overview of the current canvas with a draggable viewport rectangle for quick navigation.

  • Click or drag anywhere on the minimap to navigate
  • Viewport rectangle shows the current visible area in real time
  • Rotate — 90 degree clockwise and counter-clockwise rotation
  • Mirror — horizontal and vertical flip

All transforms work across all three renderers and input controls automatically compensate so panning and zooming remain natural.

The Map panel can be toggled at runtime from the Settings panel.

Comparison Mode

// Enter comparison mode
await viewer.enterCompareMode();

// Add a URL to compare (enters compare mode automatically if needed)
await viewer.addCompareUrl('https://example.org/iiif/manifest.json');

// Exit
viewer.exitCompareMode();

Comparison mode creates synchronized side-by-side viewers. Use the Compare panel UI to type in URLs, or call addCompareUrl() programmatically — it uses the same code path as the panel's "Load" button. If compare mode isn't active yet, it enters automatically.

Note: The compare panel must be enabled in your panels config (e.g. panels: { compare: 'show' }) for programmatic compare to work.

Keyboard Shortcuts

| Key | Action | |---|---| | + / = | Zoom in | | - | Zoom out | | 0 | Fit to view | | Arrow keys | Pan | | PageUp / [ | Previous canvas | | PageDown / ] | Next canvas | | F | Toggle fullscreen |

Cleanup

viewer.destroy();

Removes all event listeners, stops the render loop, and cleans up GPU resources.

Browser Support

| Renderer | Requirements | |---|---| | WebGPU | Chrome 113+, Edge 113+, Firefox behind flag | | WebGL | All modern browsers | | Canvas 2D | All browsers (fallback) |

The viewer automatically selects the best available renderer when using renderer: 'auto' (default).

License

MIT