@ansonphong/360-viewer
v6.0.4
Published
Ultra-lightweight, headless 360 panorama renderer — Three.js, no DOM beyond canvas, 30KB core
Maintainers
Readme
@ansonphong/360-viewer
Ultra-lightweight, headless 360° panorama renderer.
Canvas-only Three.js viewer with no DOM beyond <canvas> — wraps the core renderer, multi-image switching, a typed event emitter, theme bridge, and library loader into a single public class.
Live demo → · Full docs → · Companion UI package →
Why this viewer
- 30KB core, no build step required — drop in a
<script>tag and you have mouse + touch + keyboard panning, pinch-zoom, projection switching, fullscreen, auto-rotation. - Headless on purpose — no sidebar, no toolbar, no opinion on layout. Pair with
@ansonphong/360-viewer-libraryfor batteries-included chrome, or wire it into your own UI. - Adaptive resolution loading — pick from 8K / 4K / 2K variants per image, swap at runtime, with smooth fade-through-black transitions.
- One Three.js instance — peer-deps cleanly with whatever Three the host page already loads.
- Latest-wins concurrency — calling
loadImage()mid-transition cancels the in-flight load instead of locking up.
Install
npm install @ansonphong/360-viewer threeThree.js r128 is a peer dependency. The viewer expects it on window.THREE (UMD) or as an ESM import (ESM bundle).
Quick start
Option A — Plain HTML + UMD (no build step)
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@ansonphong/360-viewer@6/dist/viewer.css" />
<script src="https://unpkg.com/@ansonphong/360-viewer@6/dist/viewer.umd.js"></script>
<div id="viewer" style="width: 100%; height: 600px;"></div>
<script>
const viewer = new Phong360Viewer({
container: 'viewer',
autoRotate: true,
autoRotationRate: 1.5,
});
viewer.loadImage('/panoramas/sunset.jpg');
</script>Option B — ESM (Vite / webpack / Rollup / Next.js)
import 'three'; // once into window.THREE
import { Phong360Viewer } from '@ansonphong/360-viewer';
import '@ansonphong/360-viewer/dist/viewer.css';
const viewer = new Phong360Viewer({
container: document.getElementById('viewer'),
autoRotate: true,
});
await viewer.loadImage('/panoramas/sunset.jpg');Option C — Standalone UMD (no peer-dep coordination)
If you want one self-contained file that bundles its own Three.js reference resolution:
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://unpkg.com/@ansonphong/360-viewer@6/dist/viewer.standalone.umd.js"></script>Same window.Phong360Viewer global. Internally uses a shim that resolves three to window.THREE at parse time — guarantees a single Three.js instance even on pages that already load Three.
Public API
Constructor
new Phong360Viewer({
container, // element or element id (required)
libraryUrl, // optional: library.json manifest for multi-image
projection: 'equirect',// 'equirect' | 'little-planet'
resolution: 'auto', // 'auto' | '2k' | '4k' | '8k'
theme: 'dark', // 'dark' | 'light' | 'auto'
accent: '#e13e13', // CSS color for UI accents
autoRotate: false,
autoRotationRate: 1.0,
fov: 75,
controls: {
enableZoom: true, // false → mouse wheel scrolls the host page
enablePan: true, // false → drag/keyboard pan disabled, pinch still zooms
},
loading: {
backgroundColor: '#000',
fadeInDuration: 500, // ms before swap
fadeOutDuration: 500,// ms after paint
showSpinner: true,
},
transition: { /* ... */ },
performance: {
maxPixelRatio: 2, // DPR clamp for high-DPI GPUs
maxAnisotropy: 8, // texture filtering ceiling
renderMode: 'on-demand', // 'continuous' | 'on-demand'
},
})Methods
// Image loading
await viewer.loadImage(url) // latest-wins
viewer.selectImage(id) // jump to image in library by id
viewer.nextImage()
viewer.prevImage()
viewer.getCurrentImage()
// Library
viewer.setLibrary(manifest) // pass a library.json object
viewer.setSections(sections)
viewer.getSections()
viewer.getImages()
viewer.getLibraryData()
// Runtime controls
viewer.setAutoRotate(true)
viewer.getAutoRotate()
viewer.setProjection('little-planet')
viewer.setResolution('4k')
viewer.getResolution()
viewer.setTheme('light')
viewer.setAccent('#6366f1')
viewer.setPerformance({ maxPixelRatio: 1 })
// Render lifecycle
viewer.pause()
viewer.resume()
viewer.isPaused()
// Three.js access (advanced)
viewer.getContext() // { scene, camera, renderer, controls }
// Cleanup
viewer.destroy()Events
Subscribe via viewer.on(name, handler) / viewer.off(name, handler):
| Event | Payload | When |
|---|---|---|
| image:loading | { url } | New image fetch started |
| image:loaded | { url, image } | GPU upload + first paint complete |
| image:visible | { url, image } | Fade-out finished — fully visible to user |
| image:error | { url, error } | Load failed |
| library:loaded | { manifest } | library.json parsed |
| projection:change | { projection } | After setProjection() |
| resolution:change | { resolution } | After setResolution() or auto-pick |
| theme:change | { theme } | After setTheme() |
The container element also dispatches bubbling CustomEvents for outside listeners that don't have a viewer reference:
phong-viewer-loaded—detail: { url }, fires after fade-out (image fully visible)phong-viewer-paused/phong-viewer-resumedphong-viewer-error—detail: { url, error }
Theming
All UI accents are driven by CSS custom properties on the container scope:
.my-viewer {
--pv-accent: #e13e13;
--pv-bg: #000;
--pv-text: #fff;
--pv-surface: rgba(20, 20, 20, 0.85);
}Or switch built-in themes at runtime: viewer.setTheme('dark' | 'light' | 'auto').
Engine CSS (viewer.css) is canvas-relevant only — overlay, transitions, theme variables. All sidebar/toolbar/info-bar styles belong to @ansonphong/360-viewer-library.
Bundles in dist/
| File | Purpose | Three.js |
|---|---|---|
| viewer.esm.js | ESM, modern bundlers | peer dep (external three) |
| viewer.umd.js | UMD, plain HTML | reads window.THREE via shim |
| viewer.standalone.umd.js | Self-contained UMD | reads window.THREE via shim |
| viewer.css | Canvas-relevant styles | — |
All bundles expose the global Phong360Viewer (UMD) or named export Phong360Viewer (ESM).
Multi-viewer pages
Each viewer instance creates its own loading overlay scoped to its container — no shared global state. To pre-place an overlay (e.g. to prevent FOUC), drop a <div id="loading-overlay"> inside the viewer's container element and the engine will adopt it.
loadImage() is latest-wins. A new call cancels the previous in-flight load; the prior Promise never resolves. Consumers tracking a specific call should await its Promise.
Performance controls
new Phong360Viewer({
container: 'viewer',
performance: {
maxPixelRatio: 1.5, // cap DPR on high-density mobile to cut fill cost
maxAnisotropy: 4, // lower texture filtering to free VRAM
renderMode: 'on-demand', // pause render loop when nothing changed
},
});The engine also pauses rendering automatically when the canvas is off-screen (IntersectionObserver) and clamps post-resume delta to 100ms so long pauses don't produce camera jumps.
Migration from 5.x → 6.x
Pure rename, no behavior changes:
| 5.x | 6.x |
|---|---|
| @ansonphong/360-engine | @ansonphong/360-viewer |
| Phong360Engine class | Phong360Viewer class |
| dist/engine.* | dist/viewer.* |
| --p360-* / .p360-* CSS | --pv-* / .pv-* |
| phong-360-loaded event | phong-viewer-loaded |
| p360-help event | pv-help |
The inner class Phong360ViewerCore is unchanged — still the core Three.js renderer.
npm uninstall @ansonphong/360-engine
npm install @ansonphong/360-viewer- import { Phong360Engine } from '@ansonphong/360-engine';
+ import { Phong360Viewer } from '@ansonphong/360-viewer';# Sweep CSS
sed -i 's/--p360-/--pv-/g; s/\.p360-/.pv-/g' your-styles.cssThe old package remains installable indefinitely with a deprecation notice — existing lockfiles do not break.
Pair with the library UI
Need a full gallery experience — sidebar with sections, toolbar with controls, glassmorphic info bar with prev/next, deep-linking, theme toggle? Install the companion package:
npm install @ansonphong/360-viewer @ansonphong/360-viewer-library threeSee @ansonphong/360-viewer-library.
Browser support
Modern evergreen browsers (Chrome, Firefox, Safari, Edge). Requires WebGL 1.0 and ES2017+. No IE support.
Links
- GitHub: ansonphong/360-VIEWER — full docs, examples, gallery template, deploy scripts
- Live demo: 360.phong.com
- Companion UI package:
@ansonphong/360-viewer-library - Author: Phong
- Issues: GitHub issues
License
MIT — see LICENSE.
