panapanorama
v0.1.8
Published
React panorama component for building interactive 3D panorama experiences.
Readme
PanaPanorama
React panorama component for building branded, interactive 3D panorama demos.
The main idea is simple: PanaPanorama renders the panorama, and each anchor can render any React node you provide. The included anchor helpers are convenience APIs, not required styling.
Install
npm install panapanoramaPhone Testing Through ngrok
Start the Vite dev server:
npm run devIn another terminal, start the named ngrok tunnel:
npm run dev:tunnelThis uses ngrok.yml and expects ngrok to be installed locally and available on your PATH. It also expects panapanorama.ngrok.app to be reserved in your ngrok account. If that domain is not reserved yet, either reserve it in the ngrok dashboard or use a temporary generated URL:
npm run dev:tunnel:randomImport the package styles once in your app:
import 'panapanorama/style.css'Custom Anchors
Use createPanoramaAnchor when each demo needs its own anchor design.
import {
PanaPanorama,
createPanoramaAnchor,
usePanoramaAnchorLayout,
type PanoramaAnchor,
} from 'panapanorama'
function CarFeatureAnchor() {
const layout = usePanoramaAnchorLayout()
return (
<article className="car-feature-anchor" data-placement={layout?.placement}>
<p>Powertrain</p>
<h2>Twin-Turbo V6</h2>
<p>Track-ready response with daily-driver refinement.</p>
</article>
)
}
const anchors: PanoramaAnchor[] = [
createPanoramaAnchor({
id: 'powertrain',
yaw: 1.2,
pitch: -0.08,
scrollOrder: 1,
content: <CarFeatureAnchor />,
}),
]
export function CarDemo() {
return (
<PanaPanorama
source="/demo/car-panorama.jpg"
anchors={anchors}
initialView={{ yaw: 0, pitch: 0, zoom: 1 }}
/>
)
}Your component owns its markup, styles, animation, buttons, and business-specific copy. PanaPanorama only handles projection, visibility, navigation, and layout context.
Included Presets
Use these when you want the current connected-card look:
import {
createPanoramaCardAnchor,
createPanoramaInfoAnchor,
PanoramaAnchoredCard,
PanoramaAnchorRingMarker,
PanoramaInfoAnchor,
} from 'panapanorama'createPanoramaAnchor: plain custom React content.createPanoramaCardAnchor: wraps custom content in the included connected-card shell.createPanoramaInfoAnchor: creates the included eyebrow/title/body info card.PanoramaAnchoredCard: reusable connected-card shell.PanoramaAnchorRingMarker: reusable ring marker.PanoramaInfoAnchor: reusable text-card preset.
The current demo uses createPanoramaInfoAnchor because it wants the included info-card preset. A different business demo can use createPanoramaAnchor and provide completely different React.
Anchor Options
All helpers return a normal PanoramaAnchor, so the same anchor configuration applies:
createPanoramaAnchor({
id: 'wheel',
yaw: 0.82,
pitch: -0.24,
placement: 'left',
offset: { x: -16, y: 8 },
scale: 0.9,
marker: 'beacon',
hiddenMarker: 'beacon',
markerGoToAnchor: true,
content: <WheelAnchor />,
})Useful options:
yawandpitch: where the anchor lives in the panorama.scrollOrder: where it appears in stepped timeline navigation.placement: where preset card shells place content relative to the marker.offset: fine-tunes card position.scaleandmarkerScale: size content or marker.marker,hiddenMarker, andrenderHiddenMarker: marker behavior.markerGoToAnchor: clicking marker navigates to that anchor.
Authoring
Enable anchor creation while building a demo:
<PanaPanorama
source="/demo/panorama.jpg"
anchors={anchors}
mode="authoring"
onAnchorCreate={(anchor) => {
console.log(anchor)
}}
debug={{
enabled: true,
allowAnchorCreation: true,
copyTools: true,
}}
/>The copied anchor coordinates can be added to your scene data, then rendered with any custom React component.
Postprocessing
Enable restrained screen-space lighting effects from the core package with a preset:
<PanaPanorama
source="/demo/campus-night.jpg"
postprocessing={{ preset: 'cinematic-neon' }}
debug={{
enabled: true,
postprocessingControls: true,
}}
/>Presets include off, soft-glow, cinematic-neon, and premium-night. You can override bloom, emissive masking, shimmer, chromatic strength, and quality per scene while the panorama projection remains unchanged.
Each effect group can also oscillate its primary strength:
postprocessing={{
preset: 'cinematic-neon',
emissive: {
targetColor: '#22d8ff',
colorTolerance: 0.35,
colorInfluence: 0.8,
},
bloom: {
oscillation: {
enabled: true,
min: 0.22,
max: 0.38,
durationSeconds: 8,
},
},
}}Mapping Image Coordinates
Use getPanoramaAnglesFromImagePoint when you have one x/y point from the supplied image and need anchor coordinates:
import { getPanoramaAnglesFromImagePoint } from 'panapanorama'
const { yaw, pitch } = getPanoramaAnglesFromImagePoint(
{ x: 1320, y: 420 },
{ width: 2000, height: 1000 },
{
sourceOptions: {
flipX: true,
},
}
)Use the same sourceOptions you pass to PanaPanorama. For more detail, mapImagePointToPanorama also returns normalized panorama coordinates, normalized output size, and whether the point is visible after source adaptation.
For repeated scene data, create anchors directly from image points:
import {
createPanoramaAnchorsFromImagePoints,
createPanoramaInfoAnchor,
type PanoramaImageAnchorPoint,
type CreatePanoramaInfoAnchorOptions,
} from 'panapanorama'
const scenes: PanoramaImageAnchorPoint<CreatePanoramaInfoAnchorOptions>[] = [
{
id: 'mapped-point',
x: 1320,
y: 420,
eyebrow: 'Feature',
title: 'Mapped Anchor',
body: 'This anchor is authored from image coordinates.',
},
]
const anchors = createPanoramaAnchorsFromImagePoints({
imageSize: { width: 2000, height: 1000 },
sourceOptions: { flipX: true },
points: scenes,
createAnchor: createPanoramaInfoAnchor,
})