**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  

> **Full documentation:** [https://pannellum.2plot.dev](https://pannellum.2plot.dev) — the dedicated dash-pannellum documentation site, with the complete API reference and deeper examples. This page is the quick-start overview.



`dash-pannellum` is a Dash component library that integrates the Pannellum panorama viewer into your Dash applications. It allows you to display interactive 360° panoramas, including equirectangular images, cube maps, and 360° videos. The component features tour mode with multiple scenes and hotspots, customizable camera controls, multi-resolution panorama support, and keyboard navigation for an immersive viewing experience.

Since 0.2.0 the viewer is also fully **imperative**: the `lookAt` prop pans and zooms the live camera, `loadScene` switches tour scenes, and `callbackHotspots` moves markers in real time — all without rebuilding the viewer. As of **0.4.0**, the `orientation` prop turns on gyroscope look-around on mobile devices, so the device itself becomes the camera.

### Installation

[Visit GitHub Repo](https://github.com/pip-install-python/dash_pannellum)

⭐️ Star this component on GitHub! Stay up to date on new releases and browse the codebase.

```bash
pip install dash-pannellum
```

> **Note:** `dash-pannellum` 0.1.0+ requires **Dash 4.2+** and is built against React 18. The Pannellum/video.js runtime loads from CDN at first render, so the browser needs internet access.

---

### Quick Start

Display an interactive 360° equirectangular panorama with camera controls and adjustable viewing parameters.



```python
# File: docs/dash_pannellum/simple.py

from dash import *
import dash_mantine_components as dmc
from dash_pannellum import DashPannellum


component = dmc.SimpleGrid(
    cols={"base": 1, "sm": 1, "lg": 4},
    children=[
        dmc.Paper(dmc.Stack([
            html.Div(id="view-pannellum"),
            html.Label(id="pannellum-output"),
            ]),
            id="intro-wrapper-dem",
            style={"gridColumn": "1 / 4"},
        ),
        dmc.Stack(
            [
                dmc.TextInput(
                    id='pannellum-panorama-url',
                    value='/assets/images/art_museum.jpg',
                    label='Panorama URL',
                    placeholder='Enter URL',
                ),
                dmc.Select(
                        label="Select Panorama Type",
                        placeholder="Select one",
                        id="panorama-type",
                        value="equirectangular",
                        data=[
                            {"value": "equirectangular", "label": "Equirectangular"},
                            {"value": "tour", "label": "Tour"},
                            {"value": "multires", "label": "MultiRes"},
                            {"value": "video", "label": "Video"},
                        ],
                    ),
                dmc.NumberInput(
                            id='pannellum-haov', label="haov prop", hideControls=True, mb=10, value=360, min=0, max=360,
                        ),
                dmc.NumberInput(
                            id='pannellum-vaov', label="vaov prop", hideControls=True, mb=10, value=180, min=0, max=180,
                        ),
                dmc.NumberInput(
                            id='pannellum-vOffset', label="vOffset prop", hideControls=True, mb=10, value=1, min=-90, max=90,
                        ),
                    # dynamicWidth
                # dmc.Checkbox(
                #     id="pannellum-custom-controls", label="customControls", checked=False, mb=10
                # ),
                dmc.Checkbox(
                    id="pannellum-show-center-dot", label="showCenterDot", checked=True, mb=10
                ),
                dmc.Checkbox(
                    id="pannellum-autoload", label="autoLoad", checked=False, mb=10
                ),

            ],
            style={'overflow-y': 'auto', 'max-height': '500px'},
        ),
        dcc.Interval(id='interval-component', interval=100, n_intervals=0)
    ],
    spacing="2rem",
)

@callback(
    Output("view-pannellum", "children"),
    Input("pannellum-panorama-url", "value"),
    Input("panorama-type", "value"),
    Input("pannellum-haov", "value"),
    Input("pannellum-vaov", "value"),
    Input("pannellum-vOffset", "value"),
    # Input("pannellum-custom-controls", "checked"),
    Input("pannellum-show-center-dot", "checked"),
    Input("pannellum-autoload", "checked"),
)
def update_pannellum_output(url, panorama_type, haov, vaov, vOffset, showCenterDot, autoload):
    print('selected props')
    print(url, panorama_type, haov, vaov, vOffset, showCenterDot, autoload)
    if not url:
        return html.Div("No URL provided")
    elif panorama_type == "equirectangular":
        config = {
            "type": "equirectangular",
            "panorama": f"{url}",
            "haov": float(haov),
            "vaov": float(vaov),
            "vOffset": float(vOffset),
        }

        return DashPannellum(
            id=f"pannellum-example",
            tour={"default": {"firstScene": "scene1"}, "scenes": {"scene1": config}},
            showCenterDot=showCenterDot,
            autoLoad=autoload,
            width='100%',
            height='400px',
        )
    elif panorama_type == 'tour':
        tour_config = {
            "default": {
                "firstScene": "circle",
                "author": "Pip Install Python",
                "sceneFadeDuration": 1000,
            },
            "scenes": {
                "circle": {
                    "title": "Dash Pannellum",
                    "hfov": 110,
                    "pitch": -3,
                    "yaw": 117,
                    "type": "equirectangular",
                    "panorama": f"{url}",
                    "hotSpots": [
                        {
                            "pitch": -2.1,
                            "yaw": 132.9,
                            "type": "scene",
                            "text": "Spring House or Dairy",
                            "sceneId": "house"
                        }
                    ]
                },
                "house": {
                    "title": "Spring House or Dairy",
                    "hfov": 110,
                    "yaw": 5,
                    "type": "equirectangular",
                    "panorama": "https://pannellum.org/images/bma-0.jpg",
                    "hotSpots": [
                        {
                            "pitch": -0.6,
                            "yaw": 37.1,
                            "type": "scene",
                            "text": "Mason Circle",
                            "sceneId": "circle",
                            "targetYaw": -23,
                            "targetPitch": 2
                        }
                    ]
                }
            }
        }

        return DashPannellum(
            id='pannellum-example',
            tour=tour_config,
            showCenterDot=showCenterDot,
            width='100%',
            height='400px',
            autoLoad=autoload
        )
    elif panorama_type == 'multires':
        multiRes_config = {
            "basePath": "https://pannellum.org/images/multires/library",
            "path": "/%l/%s%y_%x",
            "fallbackPath": "/fallback/%s",
            "extension": "jpg",
            "tileResolution": 512,
            "maxLevel": 6,
            "cubeResolution": 8432,
        }

        return DashPannellum(
            id='pannellum-example',
            multiRes=multiRes_config,
            showCenterDot=showCenterDot,
            width='100%',
            height='400px',
            autoLoad=autoload
        )
    elif panorama_type == 'video':
        video_config = {
            "sources": [
                {"src": "https://bitmovin-a.akamaihd.net/content/playhouse-vr/progressive.mp4", "type": "video/mp4"},
            ],
            "poster": "https://bitmovin-a.akamaihd.net/content/playhouse-vr/poster.jpg"
        }

        return DashPannellum(
            id='pannellum-example',
            video=video_config,
            showCenterDot=showCenterDot,
            width='100%',
            height='400px',
            autoLoad=autoload
        )
    return html.Div("Something went wrong")


@callback(
    Output('pannellum-output', 'children'),
    Input('pannellum-example', 'pitch'),
    Input('pannellum-example', 'yaw'),
    Input('interval-component', 'n_intervals'),
    prevent_initial_call=True
)
def update_video_output(pitch, yaw, n):
    if pitch is not None and yaw is not None:
        return f'Camera Position - Pitch: {pitch:.2f}, Yaw: {yaw:.2f}'
    return 'Camera Position - Pitch: 0.00, Yaw: 0.00'
```


---

### Basic Panorama

Create a simple panorama viewer with basic configuration.



```python
# File: docs/dash_pannellum/basic.py

from dash import html
import dash_pannellum

component = html.Div([
    dash_pannellum.DashPannellum(
        id='panorama',
        tour={
            "default": {
                "firstScene": "scene1",
                "sceneFadeDuration": 1000
            },
            "scenes": {
                "scene1": {
                    "title": "Example Panorama",
                    "hfov": 110,
                    "pitch": -3,
                    "yaw": 117,
                    "type": "equirectangular",
                    "panorama": "/assets/images/landscape.jpg"
                }
            }
        },
        autoLoad=True,
        width='100%',
        height='400px',
    )
])
```


---

### Tour Mode

Tour mode enables navigation between multiple connected panorama scenes with interactive hotspots. Users can click hotspots to jump between different viewpoints, creating an immersive multi-scene experience.



```python
# File: docs/dash_pannellum/tours.py

from dash import html
import dash_pannellum

component = html.Div([
    dash_pannellum.DashPannellum(
        id='panorama',
        tour={
                "default": {
                    "firstScene": "circle",
                    "author": "Pip Install Python",
                    "sceneFadeDuration": 1000,
                },
                "scenes": {
                    "circle": {
                        "title": "Dash Pannellum",
                        "hfov": 110,
                        "pitch": -3,
                        "yaw": 117,
                        "type": "equirectangular",
                        "panorama": "https://pannellum.org/images/alma.jpg",
                        "hotSpots": [
                            {
                                "pitch": -2.1,
                                "yaw": 132.9,
                                "type": "scene",
                                "text": "Spring House or Dairy",
                                "sceneId": "house"
                            }
                        ]
                    },
                    "house": {
                        "title": "Spring House or Dairy",
                        "hfov": 110,
                        "yaw": 5,
                        "type": "equirectangular",
                        "panorama": "https://pannellum.org/images/bma-0.jpg",
                        "hotSpots": [
                            {
                                "pitch": -0.6,
                                "yaw": 37.1,
                                "type": "scene",
                                "text": "Mason Circle",
                                "sceneId": "circle",
                                "targetYaw": -23,
                                "targetPitch": 2
                            }
                        ]
                    }
                }
            },
        autoLoad=True,
        width='100%',
        height='400px',
    )
])
```


**Tour Configuration:**

Each tour consists of a `default` object and a `scenes` dictionary:

- **`default.firstScene`**: ID of the initial scene to display
- **`default.sceneFadeDuration`**: Transition duration in milliseconds between scenes
- **`scenes`**: Dictionary of scene configurations, each with its own panorama and hotspots

**Hotspot Configuration:**

- `pitch`: Vertical position in degrees
- `yaw`: Horizontal position in degrees
- `type`: `"scene"` for scene navigation hotspots
- `text`: Tooltip text displayed on hover
- `sceneId`: Target scene ID to navigate to
- `targetYaw` (optional): Camera yaw after transition
- `targetPitch` (optional): Camera pitch after transition

---

### Partial Panorama

Display panoramas that don't cover the full 360° horizontally or 180° vertically by specifying viewing extents using horizontal angle of view (haov), vertical angle of view (vaov), and vertical offset (vOffset).



```python
# File: docs/dash_pannellum/partial_panorama.py

from dash import html
import dash_pannellum

component = html.Div([
    dash_pannellum.DashPannellum(
        id='partial-panorama-component',
        tour={"default": {"firstScene": "scene1"}, "scenes": {"scene1":
                    {
                        "type": "equirectangular",
                        "panorama": "https://archive.org/download/SalinaKansas1916Postcard/Salina%20Kansas%2C%201916%20Postcard%2C%20Front.jpg",
                        "haov": 149.87,
                        "vaov": 54.15,
                        "vOffset": 1.17
                    }
                }
              },
        width='100%',
        height='400px',
        autoLoad=True
    )
])
```


**Viewing Parameters:**

- **`haov`**: Horizontal angle of view in degrees (e.g., 149.87)
- **`vaov`**: Vertical angle of view in degrees (e.g., 54.15)
- **`vOffset`**: Vertical offset in degrees, useful when panorama is not vertically centered (e.g., 1.17)

These parameters allow precise control over which portion of the panorama is visible and how it's framed within the viewer.

---

### 360° Video Panorama

Display interactive 360° video content with standard video controls.



```python
# File: docs/dash_pannellum/video.py

from dash import html
import dash_pannellum

component = html.Div([
    dash_pannellum.DashPannellum(
        id='panorama',
        video={
            "sources": [
                {"src": "https://cdn.bitmovin.com/content/assets/playhouse-vr/progressive.mp4", "type": "video/mp4"},
            ],
            "poster": "https://cdn.bitmovin.com/content/assets/playhouse-vr/poster.jpg"
        },
        autoLoad=True,
        width='100%',
        height='400px',
    )
])
```


**Video Configuration:**

- **`sources`**: Array of video source objects with `src` (URL) and `type` (MIME type)
- **`poster`**: URL to poster image displayed before video loads

The component supports standard HTML5 video formats. Users can interact with the video using typical video controls while maintaining the ability to pan around the 360° view.

---

### Using Callbacks

Track the viewer's current state (camera position, zoom, loaded status, active scene) using Dash callbacks. The component provides read-only properties that update as the user interacts with the panorama. View-state updates are throttled to 4 per second and change-detected, so idle viewers fire nothing.

```python
from dash import callback, Input, Output

@callback(
    Output('output-div', 'children'),
    Input('panorama', 'loaded'),
    Input('panorama', 'pitch'),
    Input('panorama', 'yaw'),
    Input('panorama', 'hfov'),
    Input('panorama', 'currentScene')
)
def update_output(loaded, pitch, yaw, hfov, current_scene):
    """Display current panorama state"""
    if loaded and pitch is not None and yaw is not None:
        return f'Scene: {current_scene}, Pitch: {pitch:.2f}°, Yaw: {yaw:.2f}°, Zoom: {hfov:.1f}°'
    return 'Loading panorama...'
```

**Available Read-Only Callback Properties:**

- **`loaded`**: Boolean indicating if panorama has finished loading
- **`pitch`**: Current vertical camera angle (-90° to 90°)
- **`yaw`**: Current horizontal camera angle (-180° to 180°)
- **`hfov`**: Current horizontal field of view — the zoom level, in degrees
- **`currentScene`**: ID of the active scene in tour mode
- **`lastClickedHotspot`**: `name` of the last clicked callback hotspot (see [Callback Hotspots](#callback-hotspots))
- **`orientationSupported`** / **`orientationActive`**: gyroscope truth props (see [Gyroscope Look-Around](#gyroscope-look-around))

> **Warning:** These properties are **outputs only** — use them as callback `Input`s, never as `Output`s. Writing to `pitch`/`yaw`/`hfov` does not move the camera. To drive the camera, write the `lookAt` prop instead; set the initial orientation in the scene config.

---

### Driving the Camera with `lookAt`

The `lookAt` prop (added in 0.2.0) is an **imperative camera write**: set `{pitch, yaw, hfov, animated}` from any callback and the live viewer pans and zooms in place — **no rebuild, no flash, no camera reset**. Omitted fields keep their current value, and `animated` is the transition duration in milliseconds. Combined with the read-only `pitch`/`yaw`/`hfov` outputs, this gives you a full "fly-to" pattern from buttons, clicked hotspots, or any external event.



```python
# File: docs/dash_pannellum/camera_control.py

import dash_mantine_components as dmc
from dash import callback, ctx, Input, Output
from dash_pannellum import DashPannellum

# Named camera targets. Omitted fields keep their current value,
# so "Zoom In" only touches hfov and leaves pitch/yaw alone.
TARGETS = {
    "pannellum-lookat-btn-house": {"pitch": -2.1, "yaw": 132.9, "hfov": 60},
    "pannellum-lookat-btn-sky": {"pitch": 55, "hfov": 100},
    "pannellum-lookat-btn-zoom": {"hfov": 50},
    "pannellum-lookat-btn-reset": {"pitch": -3, "yaw": 117, "hfov": 110},
}

component = dmc.Stack(
    [
        DashPannellum(
            id="pannellum-lookat-viewer",
            tour={
                "default": {"firstScene": "circle"},
                "scenes": {
                    "circle": {
                        "type": "equirectangular",
                        "panorama": "https://pannellum.org/images/alma.jpg",
                        "hfov": 110,
                        "pitch": -3,
                        "yaw": 117,
                    }
                },
            },
            autoLoad=True,
            width="100%",
            height="400px",
        ),
        dmc.Group(
            [
                dmc.Button("Spring House", id="pannellum-lookat-btn-house", size="xs"),
                dmc.Button("Look Up", id="pannellum-lookat-btn-sky", size="xs"),
                dmc.Button("Zoom In", id="pannellum-lookat-btn-zoom", size="xs"),
                dmc.Button(
                    "Reset View",
                    id="pannellum-lookat-btn-reset",
                    size="xs",
                    variant="outline",
                ),
            ]
        ),
        dmc.Text(id="pannellum-lookat-readout", size="sm", c="dimmed"),
    ],
    gap="sm",
)


@callback(
    Output("pannellum-lookat-viewer", "lookAt"),
    Input("pannellum-lookat-btn-house", "n_clicks"),
    Input("pannellum-lookat-btn-sky", "n_clicks"),
    Input("pannellum-lookat-btn-zoom", "n_clicks"),
    Input("pannellum-lookat-btn-reset", "n_clicks"),
    prevent_initial_call=True,
)
def fly_to(*_):
    """Write lookAt — the live viewer pans/zooms in place, no rebuild."""
    return {**TARGETS[ctx.triggered_id], "animated": 1000}


@callback(
    Output("pannellum-lookat-readout", "children"),
    Input("pannellum-lookat-viewer", "pitch"),
    Input("pannellum-lookat-viewer", "yaw"),
    Input("pannellum-lookat-viewer", "hfov"),
    prevent_initial_call=True,
)
def readout(pitch, yaw, hfov):
    """pitch / yaw / hfov are read-only Inputs that report the camera back."""
    if pitch is None or yaw is None:
        return "Camera: waiting for viewer..."
    hfov_text = f", hfov {hfov:.1f}°" if hfov is not None else ""
    return f"Camera: pitch {pitch:.1f}°, yaw {yaw:.1f}°{hfov_text}"
```


**`lookAt` keys:**

- **`pitch`** (optional): Target vertical angle in degrees
- **`yaw`** (optional): Target horizontal angle in degrees
- **`hfov`** (optional): Target zoom (horizontal field of view) in degrees
- **`animated`** (optional): Transition duration in ms (default 1000; use a small value like 220 for joystick-style continuous steering)

For high-frequency steering (a joystick, keyboard, or game loop), skip the server round-trip and write from a clientside callback or plain JS:

```js
window.dash_clientside.set_props('panorama', {lookAt: {yaw: bearing, animated: 220}});
```

`lookAt` works in image panorama (tour) mode; it does not reach `multiRes` or `video` viewers. A `lookAt` write that lands while the viewer is still booting is dropped — initial orientation belongs in the scene config.

**Related imperative props** — the same no-rebuild philosophy applies to:

- **`loadScene`**: set to a scene ID from the tour config to switch scenes without rebuilding the viewer. Unknown IDs and the already-active scene are ignored, so it's safe to reflect. Pair with `preloadScenes` (default on) for instant jumps.
- **`callbackHotspots`**: move or replace hotspot markers live (see next section).

---

### Callback Hotspots

`callbackHotspots` adds hotspots that report clicks back to Python — separate from the tour's own `hotSpots`. Keys are scene IDs; each entry's `name` is written to the read-only `lastClickedHotspot` prop on click:

```python
from dash import callback, Input, Output
from dash_pannellum import DashPannellum

DashPannellum(
    id='panorama',
    tour=tour,
    callbackHotspots={
        "lobby": [
            {"pitch": -1.2, "yaw": 122.0, "type": "info",
             "text": "Reception desk", "name": "reception"},
        ],
    },
)

@callback(
    Output('info-panel', 'children'),
    Input('panorama', 'lastClickedHotspot'),
    prevent_initial_call=True,
)
def on_hotspot(name):
    return INFO[name]
```

**Live updates with per-name diffing (0.3.1):** outputting a new `callbackHotspots` dict from a callback never rebuilds the viewer, and the update is diffed **per `name`**:

- Unchanged hotspots are left untouched
- **Position-only changes move the existing DOM node in place** — the marker keeps its hover state and stays clickable mid-flight
- Changes to `text`/`type`/`cssClass` recreate just that one hotspot
- Names that disappear are removed

This means markers can drift every tick as real DOM hotspots:

```python
@callback(Output('panorama', 'callbackHotspots'), Input('tick', 'n_intervals'))
def drift(n):
    return {"main": [project_to_yaw_pitch(e) for e in world.entities]}
```

Keep `name` stable per entity — it's the diff key. If you encode a tick counter into `name`, every update becomes a remove-and-re-add again.

**Tip — authoring hotspot coordinates:** set `showCenterDot=True` (a crosshair at screen center), stream `pitch`/`yaw` into a readout callback, aim the dot at the target, and copy the values into your hotspot config. Remove the dot for production.

---

### Gyroscope Look-Around

*New in 0.4.0.* The `orientation` prop requests **gyroscope steering** — point the phone around and the panorama follows. It's imperative (no rebuild): set it to `True` to engage, `False` to release. Two read-only props report the truth: `orientationSupported` (the device/browser can gyro-steer at all) and `orientationActive` (the gyro is steering right now — `False` if permission was denied, and Pannellum pauses it while the user drags the panorama).



```python
# File: docs/dash_pannellum/gyro.py

import dash_mantine_components as dmc
from dash import callback, clientside_callback, Input, Output
from dash_pannellum import DashPannellum

component = dmc.Stack(
    [
        DashPannellum(
            id="pannellum-gyro-viewer",
            tour={
                "default": {"firstScene": "house"},
                "scenes": {
                    "house": {
                        "type": "equirectangular",
                        "panorama": "https://pannellum.org/images/bma-0.jpg",
                        "hfov": 110,
                    }
                },
            },
            autoLoad=True,
            width="100%",
            height="400px",
        ),
        dmc.Group(
            [
                dmc.Switch(
                    id="pannellum-gyro-switch",
                    label="Gyroscope look-around",
                    checked=False,
                ),
                dmc.Badge(
                    "Checking sensors...",
                    id="pannellum-gyro-supported",
                    color="gray",
                    variant="light",
                ),
                dmc.Badge(
                    "Gyro idle",
                    id="pannellum-gyro-active",
                    color="gray",
                    variant="light",
                ),
            ]
        ),
        dmc.Text(
            "Gyro steering needs a mobile device with motion sensors, served over "
            "HTTPS. On desktop the switch is a no-op and the panorama stays "
            "drag-to-look — nothing breaks.",
            size="sm",
            c="dimmed",
        ),
    ],
    gap="sm",
)

# The orientation write MUST be clientside: iOS 13+ only shows the
# motion-permission prompt inside a user-gesture window, so the prop has
# to be set synchronously on the tap — a server round-trip is too late.
clientside_callback(
    "function(on) { return Boolean(on); }",
    Output("pannellum-gyro-viewer", "orientation"),
    Input("pannellum-gyro-switch", "checked"),
    prevent_initial_call=True,
)


@callback(
    Output("pannellum-gyro-supported", "children"),
    Output("pannellum-gyro-supported", "color"),
    Output("pannellum-gyro-active", "children"),
    Output("pannellum-gyro-active", "color"),
    Input("pannellum-gyro-viewer", "orientationSupported"),
    Input("pannellum-gyro-viewer", "orientationActive"),
)
def gyro_status(supported, active):
    """orientationSupported / orientationActive are read-only truth props."""
    if supported:
        supported_badge = ("Gyro supported", "green")
    else:
        supported_badge = ("Not supported on this device", "gray")
    if active:
        active_badge = ("Gyro steering", "teal")
    else:
        active_badge = ("Gyro idle", "gray")
    return (*supported_badge, *active_badge)
```


**iOS requires a clientside callback on a direct tap.** iOS 13+ only shows the motion-permission prompt inside a user-gesture window, so the `orientation` write must happen synchronously on the tap — a server callback round-trip is too late:

```python
from dash import clientside_callback, Input, Output

clientside_callback(
    "function(on) { return Boolean(on); }",
    Output('panorama', 'orientation'),
    Input('gyro-switch', 'checked'),
    prevent_initial_call=True,
)
```

**Gyro support constraints** — Pannellum gates orientation on all three of:

1. `DeviceOrientationEvent` being available (motion sensors)
2. A **mobile user agent**
3. **Literal `https:`** — Pannellum checks `location.protocol`, so plain-HTTP `localhost` reports unsupported even though browsers would allow the sensor there. Use a TLS dev cert or a tunnel to test on a phone.

On desktop the feature degrades gracefully: `orientationSupported` stays `False`, the switch is a no-op, and the panorama remains drag-to-look. Turn `orientation` off before running directed `lookAt` sequences — a camera fighting the gyroscope feels broken.

---

### Keyboard Controls

The component supports keyboard navigation for improved user experience:

- **Arrow Keys**: Pan the view (←→ horizontal, ↑↓ vertical)
- **Shift**: Zoom in
- **Control/Ctrl**: Zoom out
- **F**: Toggle fullscreen mode

---

### Component Properties

| Property               | Type      | Default     | Description                                                                                                                       |
| :--------------------- | :-------- | :---------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| **`id`**               | `string`  | **Required** | Unique identifier for the component used in Dash callbacks.                                                                       |
| `width`                | `string`  | `'600px'`   | Width of the panorama viewer (any CSS size, e.g., '100%', '800px').                                                               |
| `height`               | `string`  | `'400px'`   | Height of the panorama viewer (any CSS size, e.g., '400px', '100vh').                                                             |
| `tour`                 | `dict`    | `None`      | Tour mode config: `{default: {firstScene, ...}, scenes: {sceneId: {...}}}`. A single panorama is a tour with one scene. A scene may set `panoramaCanvasId` (DOM id of a `<canvas>`) instead of `panorama` for dynamic canvas mode. Live — changing it rebuilds the viewer. |
| `multiRes`             | `dict`    | `None`      | Multi-resolution (tiled) panorama config: `basePath`, `path`, `fallbackPath`, `extension`, `tileResolution`, `maxLevel`, `cubeResolution`. Live. |
| `video`                | `dict`    | `None`      | 360° video config: `{sources: [{src, type}], poster}`. Rendered through video.js. Live.                                            |
| `autoLoad`             | `bool`    | `True`      | If true, the panorama loads automatically. If false, user must click to load.                                                     |
| `compass`              | `bool`    | `False`     | If true, displays a compass heading indicator in the viewer.                                                                      |
| `northOffset`          | `number`  | `0`         | Offset, in degrees, of the center of the panorama from North.                                                                     |
| `customControls`       | `bool`    | `False`     | If true, hides the built-in zoom/fullscreen controls so you can build your own with Dash components.                              |
| `showCenterDot`        | `bool`    | `False`     | If true, displays a center dot — useful as a crosshair when authoring hotspot positions.                                          |
| `useHttpStreaming`     | `bool`    | `False`     | If true, loads the video.js HTTP streaming plugin so HLS/DASH sources (e.g. live streams) can play as 360° video.                 |
| `callbackHotspots`     | `dict`    | `{}`        | Hotspots that report clicks to Dash: `{sceneId: [{pitch, yaw, type, text, name}]}`. Diffed per-name in place — no rebuild (0.3.1). |
| `lookAt`               | `dict`    | `None`      | Imperative camera write: `{pitch, yaw, hfov, animated}` pans/zooms the live viewer with no rebuild. Omitted keys keep their value; `animated` is ms (default 1000). Image panorama modes only. |
| `loadScene`            | `string`  | `None`      | Imperative tour scene switch by scene ID — no rebuild. Unknown IDs and the active scene are ignored.                              |
| `preloadScenes`        | `bool`    | `True`      | Prefetch the other scenes' panoramas once the viewer is up, so tour jumps don't show a loading box.                               |
| `hideLoadingSpinner`   | `bool`    | `False`     | Suppress Pannellum's "Loading..." box for this viewer.                                                                            |
| `dynamicUpdate`        | `bool`    | `False`     | Re-upload the panorama texture every frame — required for live `panoramaCanvasId` canvas scenes. Leave false for static panoramas. |
| `orientation`          | `bool`    | `False`     | Request gyroscope look-around (0.4.0). Mobile + HTTPS only; on iOS, set from a clientside callback on a direct user tap.          |
| `orientationSupported` | `bool`    | (read-only) | True when the device/browser can drive the camera from the gyroscope (motion sensors + mobile browser + literal `https:`).       |
| `orientationActive`    | `bool`    | (read-only) | True while gyro look-around is actively steering (false if permission was denied or while the user drags).                        |
| `loaded`               | `bool`    | (read-only) | Indicates whether the panorama has finished loading.                                                                              |
| `pitch`                | `number`  | (read-only) | Current vertical camera angle in degrees (-90 to 90). Throttled to 4 updates/s.                                                   |
| `yaw`                  | `number`  | (read-only) | Current horizontal camera angle in degrees (-180 to 180). Throttled to 4 updates/s.                                               |
| `hfov`                 | `number`  | (read-only) | Current horizontal field of view (zoom) in degrees. Throttled to 4 updates/s.                                                     |
| `currentScene`         | `string`  | (read-only) | ID of the currently active scene in tour mode.                                                                                    |
| `lastClickedHotspot`   | `string`  | (read-only) | `name` of the last clicked callback hotspot.                                                                                      |

Read-only props are callback **Inputs** — never write to them from an `Output`. Use `lookAt`, `loadScene` and `callbackHotspots` to act on a running viewer; changing `tour`/`multiRes`/`video` (and other "live" config props) tears down and re-initializes it.

**Multi-Resolution Configuration:**

When using `multiRes` for high-quality panoramas, the configuration requires:

- `basePath`: Base URL path to tile directory
- `path`: Tile path pattern (e.g., `"/%l/%s%y_%x"`)
- `fallbackPath`: Fallback path for missing tiles
- `extension`: Image file extension (e.g., `"jpg"`)
- `tileResolution`: Resolution of each tile in pixels
- `maxLevel`: Maximum zoom level available
- `cubeResolution`: Total resolution of cube faces

---

### Contributing

Contributions to dash-pannellum are welcome! Please refer to the project's issues on GitHub for any feature requests or bug reports.

### License

This project is licensed under the MIT License.
