Scene Configuration
Every Pannellum scene option passes through the tour prop — plus the lookAt fly-to pattern for event-driven camera moves
How configuration flows
A scene dictionary inside tour is passed to Pannellum untouched — anything in the Pannellum configuration reference works, even keys this page doesn't mention. Two rules of thumb:
- Scene/world changes (panorama, limits, autoRotate, titles) go in the
tour config. Outputting a new tour re-initializes the viewer — since 0.3.0 the container is transparent, so a rebuild dips to your page background instead of flashing white.
- Camera-only changes (point somewhere, zoom) use the
lookAtprop —
imperative, animated, no rebuild. That's the fly-to section below.
Configuration playground
Live controls re-outputting the tour — switch the initial-view preset and set autoRotate for idle cinematics:
# File: docs/scenes/scene_config_example.py
import dash_mantine_components as dmc
from dash import Input, Output, callback, html
from dash_pannellum import DashPannellum
VIEW_PRESETS = {
"ridge": {"label": "Sunrise ridge", "pitch": 2, "yaw": 5, "hfov": 90},
"plateau": {"label": "Across the plateau", "pitch": -8, "yaw": 135, "hfov": 110},
"zenith": {"label": "Straight up", "pitch": 55, "yaw": 0, "hfov": 100},
}
def build_tour(preset_key, auto_rotate):
view = VIEW_PRESETS[preset_key]
return {
"default": {"firstScene": "toco"},
"scenes": {
"toco": {
# Identity shown in the viewer's info bar
"title": "Cerro Toco",
"author": "Pip Install Python",
# The panorama itself
"type": "equirectangular",
"panorama": "https://pannellum.org/images/cerro-toco-0.jpg",
"autoLoad": True,
# Initial camera — pitch/yaw/hfov are per-scene config
"pitch": view["pitch"],
"yaw": view["yaw"],
"hfov": view["hfov"],
# Zoom limits the user can't escape
"minHfov": 60,
"maxHfov": 120,
# Idle cinematics: degrees/second, negative pans the other way
"autoRotate": auto_rotate or None,
}
},
}
component = html.Div(
[
DashPannellum(
id="sc-pano",
tour=build_tour("ridge", 0),
autoLoad=True,
width="100%",
height="420px",
),
dmc.Group(
[
dmc.Select(
id="sc-preset",
label="Initial view preset",
value="ridge",
data=[
{"value": k, "label": v["label"]}
for k, v in VIEW_PRESETS.items()
],
w=220,
size="sm",
),
dmc.Stack(
[
dmc.Text("autoRotate (°/s)", size="sm", fw=500),
dmc.Slider(
id="sc-rotate",
value=0,
min=-8,
max=8,
step=1,
w=240,
marks=[
{"value": -8, "label": "-8"},
{"value": 0, "label": "off"},
{"value": 8, "label": "8"},
],
),
],
gap=4,
),
],
mt="md",
gap="xl",
align="flex-end",
),
]
)
@callback(
Output("sc-pano", "tour"),
Input("sc-preset", "value"),
Input("sc-rotate", "value"),
prevent_initial_call=True,
)
def reconfigure(preset_key, auto_rotate):
# Config props are LIVE: a new tour re-initializes the viewer with the
# new scene settings. (For camera-only moves, use `lookAt` instead —
# see the fly-to example below.)
return build_tour(preset_key, auto_rotate)
:defaultExpanded: false :withExpandedButton: true
The scene config vocabulary
| Key | Type | What it does |
|---|---|---|
panorama | str | Equirectangular image URL (or data URI) |
panoramaCanvasId | str | 0.3.0 — bind the scene to a live <canvas> instead (see Joystick Arena) |
title / author | str | Shown in the viewer's info bar |
pitch / yaw / hfov | number | Initial camera |
minHfov / maxHfov | number | Zoom limits |
minPitch / maxPitch | number | Clamp vertical look (e.g. hide a tripod) |
minYaw / maxYaw | number | Clamp horizontal look (partial panoramas) |
autoRotate | number | Idle rotation in °/s; sign sets direction |
autoRotateInactivityDelay | number | ms of inactivity before rotation resumes |
preview | str | Image shown before load when autoLoad=False |
vaov / vOffset | number | Vertical angle of view for partial panoramas |
backgroundColor | [r,g,b] | Fill color around partial panoramas |
hotSpots | list | Scene-switch and info hotspots (Virtual Tours) |
sceneFadeDuration | number | Crossfade ms on scene change (tour default block) |
Fly-to: event-driven camera triggers
The pattern you want for "something happened — look over there": keep a dictionary of named targets (pitch/yaw/hfov per place), and have any trigger — a button, a clicked hotspot, an alert from a websocket or an Interval poll — output a lookAt with a duration. Pannellum eases pitch, yaw and zoom together, which reads exactly like a map flyTo:
# File: docs/scenes/flyto_example.py
import random
import dash_mantine_components as dmc
from dash import Input, Output, callback, ctx, html, no_update
from dash_pannellum import DashPannellum
# Named places in the scene. A fly-to is just a lookAt with a duration:
# pitch/yaw aim the camera, hfov zooms it, animated eases the whole move.
TARGETS = {
"telescope-array": {
"label": "📡 Telescope array",
"pitch": -1.2,
"yaw": 122.0,
"hfov": 55,
"story": "Dust on the east dishes — camera dispatched to the array.",
},
"snowy-peaks": {
"label": "🏔 Snowy peaks",
"pitch": 1.5,
"yaw": 60.0,
"hfov": 60,
"story": "Weather alert on the ridge — checking the snow line.",
},
"western-ridge": {
"label": "🌅 Western ridge",
"pitch": 0.5,
"yaw": -120.0,
"hfov": 70,
"story": "Motion detected on the western approach.",
},
}
def fly_to(name, animated=1200):
target = TARGETS[name]
return {
"pitch": target["pitch"],
"yaw": target["yaw"],
"hfov": target["hfov"],
"animated": animated,
}
component = html.Div(
[
DashPannellum(
id="fly-pano",
tour={
"default": {"firstScene": "alma"},
"scenes": {
"alma": {
"title": "ALMA Observatory",
"type": "equirectangular",
"panorama": "https://pannellum.org/images/alma.jpg",
"autoLoad": True,
"yaw": 117,
"pitch": -3,
"hfov": 100,
}
},
},
# The same targets double as clickable hotspots — clicking one
# also flies the camera to it (see the second callback).
callbackHotspots={
"alma": [
{
"pitch": t["pitch"],
"yaw": t["yaw"],
"type": "info",
"text": t["label"],
"name": name,
}
for name, t in TARGETS.items()
]
},
autoLoad=True,
compass=True,
width="100%",
height="420px",
),
dmc.Group(
[
*[
dmc.Button(
t["label"],
id={"type": "fly-btn", "target": name},
variant="light",
size="xs",
)
for name, t in TARGETS.items()
],
dmc.Button(
"🚨 Simulate event",
id="fly-event",
color="red",
variant="filled",
size="xs",
),
],
mt="md",
gap="xs",
wrap="wrap",
),
dmc.Alert(
"Click a target button, a ⓘ hotspot in the scene, or simulate an "
"event — the camera flies to the spot.",
id="fly-status",
color="gray",
mt="sm",
),
dmc.Code("camera idle", id="fly-readout", mt="xs"),
]
)
@callback(
Output("fly-pano", "lookAt"),
Output("fly-status", "children"),
Output("fly-status", "color"),
Input({"type": "fly-btn", "target": "telescope-array"}, "n_clicks"),
Input({"type": "fly-btn", "target": "snowy-peaks"}, "n_clicks"),
Input({"type": "fly-btn", "target": "western-ridge"}, "n_clicks"),
Input("fly-event", "n_clicks"),
Input("fly-pano", "lastClickedHotspot"),
prevent_initial_call=True,
)
def dispatch_camera(*_):
trigger = ctx.triggered_id
if trigger == "fly-event":
# The "external event" pattern: ANY server-side trigger (a websocket
# message, an Interval poll, a queue consumer…) can output a lookAt
# to point the panorama at the area of interest.
name = random.choice(list(TARGETS))
return fly_to(name, animated=1500), TARGETS[name]["story"], "red"
if trigger == "fly-pano":
name = ctx.inputs["fly-pano.lastClickedHotspot"]
if name not in TARGETS:
return no_update, no_update, no_update
return (
fly_to(name, animated=900),
f"Centered on {TARGETS[name]['label']}.",
"teal",
)
name = trigger["target"]
return fly_to(name), f"Flying to {TARGETS[name]['label']}…", "blue"
@callback(
Output("fly-readout", "children"),
Input("fly-pano", "pitch"),
Input("fly-pano", "yaw"),
Input("fly-pano", "hfov"),
)
def show_camera(pitch, yaw, hfov):
return (
f"pitch {pitch or 0:+.1f}° · yaw {yaw or 0:+.1f}° · "
f"hfov {hfov or 0:.0f}° (zoom)"
)
:defaultExpanded: false :withExpandedButton: true
The three triggers in the example, all landing in one callback:
- Buttons — pattern-matching IDs, one per target.
- The scene itself — clicking a ⓘ callback hotspot flies to and zooms
into that target (lastClickedHotspot → lookAt).
- A simulated external event — the 🚨 button stands in for any
server-side trigger (queue message, sensor threshold, websocket push): the callback picks the area of interest and dispatches the camera.
Target coordinates are authored the same way as hotspots: turn on showCenterDot=True, stream pitch/yaw (and now hfov) into a readout, aim, copy. The workflow is demonstrated on the Callback Hotspots page.
When to rebuild vs when to fly
| You want to… | Use | Cost |
|---|---|---|
| Point/zoom the camera | lookAt | none — animated in place |
| Jump to another scene | loadScene (+ preloadScenes) | none — instant if preloaded |
| Move through a world continuously | panoramaCanvasId + dynamicUpdate | a canvas redraw |
| Change the world's configuration | re-output tour | a rebuild — treat it as a real transition |
Source: /components/scenes
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /components/scenes/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt