@vuvandinh203/krpano-iframe-toolkit
v1.0.1
Published
React client toolkit for controlling a krpano tour inside an iframe.
Readme
@vuvandinh203/krpano-iframe-toolkit
React toolkit for controlling a krpano tour running inside an iframe.
The package includes:
dist/krpano-iframe-toolkit.js: a browser bridge script loaded by the krpano tour HTML.- React APIs:
KrpanoTour,useKrpanoTour, andcreateKrpanoIframeClient. - TypeScript hints for common krpano variables such as
view.fov,xml.scene,hotspot[name].visible, and more.
Installation
npm install @vuvandinh203/krpano-iframe-toolkitReact must be installed by the host app:
npm install react react-domHow It Works
Your React app renders a tour page inside an iframe. The tour page loads krpano-iframe-toolkit.js, which exposes window.KrpanoIframeBridge. React and krpano then communicate through postMessage.
React app
-> iframe
-> tour.html
-> krpano
-> KrpanoIframeBridge1. Add The Bridge To The krpano Tour
Copy the bridge file from the installed package into the public/static directory used by the tour HTML:
cp node_modules/@vuvandinh203/krpano-iframe-toolkit/dist/krpano-iframe-toolkit.js public/krpano-iframe-toolkit.jsThen load it in tour.html and initialize it inside embedpano.onready:
<div id="pano" style="width: 100%; height: 100%;"></div>
<script src="./tour.js"></script>
<script src="./krpano-iframe-toolkit.js"></script>
<script>
embedpano({
xml: "./tour.xml",
target: "pano",
html5: "only",
onready: function (krpano) {
KrpanoIframeBridge.init(krpano, {
allowedOrigins: [
"http://localhost:5173",
"https://app.example.com"
],
allowRawCall: false,
autoEmitCommonEvents: true
});
}
});
</script>You can also load the bridge from a CDN after publishing the package:
<script src="https://unpkg.com/@vuvandinh203/krpano-iframe-toolkit/dist/krpano-iframe-toolkit.js"></script>or:
<script src="https://cdn.jsdelivr.net/npm/@vuvandinh203/krpano-iframe-toolkit/dist/krpano-iframe-toolkit.js"></script>Bridge Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| allowedOrigins | string[] | [] | React app origins allowed to control the tour. Must match protocol, host, and port. |
| debug | boolean | false | Enables bridge logs in the iframe console. |
| allowRawCall | boolean | true | Allows krpano.call(...) through tour.runAction(...). Disable it in production unless needed. |
| autoEmitCommonEvents | boolean | false | Enables built-in hotspotClick, sceneChange, and viewChange events. |
| autoEmitHotspotClicks | boolean | false | Automatically emits hotspot click events. |
| autoEmitSceneChanges | boolean | false | Automatically emits scene change events. |
| autoEmitViewChanges | boolean | false | Automatically emits view change events. |
| hotspotClickEventName | string | "hotspotClick" | Custom event name for auto hotspot clicks. |
| sceneChangeIntervalMs | number | 500 | Polling interval for scene changes. |
| viewChangeIntervalMs | number | 250 | Polling interval for view changes. |
| viewChangePrecision | number | 2 | Decimal precision used when comparing view changes. |
2. Quick Start In React
Use KrpanoTour when you want the simplest React integration. Pass the tour URL to src, then receive the connected client in onTourReady.
import { useState } from "react";
import {
KrpanoTour,
type KrpanoIframeClient
} from "@vuvandinh203/krpano-iframe-toolkit";
export default function TourPage() {
const [tour, setTour] = useState<KrpanoIframeClient | null>(null);
async function loadLobby() {
await tour?.loadScene("scene_lobby", {
blend: 1,
flags: "MERGE"
});
}
async function lookAtReception() {
await tour?.lookAt({
hlookat: 120,
vlookat: 0,
fov: 80,
duration: 1
});
}
return (
<div style={{ height: 640 }}>
<KrpanoTour
title="360 Tour"
src="https://tour.example.com/tour.html"
onTourReady={setTour}
/>
<button disabled={!tour} onClick={loadLobby}>
Load lobby
</button>
<button disabled={!tour} onClick={lookAtReception}>
Look at reception
</button>
</div>
);
}If src is an absolute URL, iframeOrigin is detected automatically. Provide iframeOrigin only for relative URLs or when you need to override it:
<KrpanoTour
src="/tour/tour.html"
iframeOrigin="https://tour.example.com"
onTourReady={(tour) => {
tour.loadScene("scene_lobby");
}}
/>3. Use A Ref Instead Of State
import { useRef } from "react";
import {
KrpanoTour,
type KrpanoTourHandle
} from "@vuvandinh203/krpano-iframe-toolkit";
export default function TourPage() {
const tourRef = useRef<KrpanoTourHandle>(null);
async function zoomIn() {
await tourRef.current?.client?.zoomIn({
step: 10,
minFov: 30
});
}
return (
<>
<KrpanoTour
ref={tourRef}
title="360 Tour"
src="https://tour.example.com/tour.html"
style={{ height: 640 }}
/>
<button onClick={zoomIn}>Zoom in</button>
</>
);
}4. Built-In Tour Events
For common tour events, enable autoEmitCommonEvents once in tour.html:
KrpanoIframeBridge.init(krpano, {
allowedOrigins: ["http://localhost:5173"],
autoEmitCommonEvents: true
});Then listen in React without editing each XML hotspot and without calling subscribeEvent:
<KrpanoTour
src="https://tour.example.com/tour.html"
onTourReady={(tour) => {
tour.onHotspotClick((payload) => {
console.log("Hotspot:", payload.name);
console.log("Scene:", payload.scene);
console.log("Position:", payload.ath, payload.atv);
});
tour.onSceneChange((payload) => {
console.log("Scene changed:", payload.scene);
});
tour.onViewChange((payload) => {
console.log("View:", payload.hlookat, payload.vlookat, payload.fov);
});
}}
/>To enable only selected built-in events:
KrpanoIframeBridge.init(krpano, {
allowedOrigins: ["http://localhost:5173"],
autoEmitHotspotClicks: true,
autoEmitSceneChanges: true,
autoEmitViewChanges: false
});If hotspots are created dynamically after the tour is ready, bind them again:
await tour.enableHotspotClickEvents();5. Custom Events
Use custom events for application-specific behavior, such as opening a product modal, tracking analytics, or opening a contact form.
In the tour HTML or in a JS function called by krpano XML:
KrpanoIframeBridge.emitAlways("openProduct", {
id: "product_123",
title: "Sofa",
scene: krpano.get("xml.scene")
});In React:
tour.onEvent("openProduct", (payload) => {
console.log("Open product:", payload);
});If you want the event to be sent only when React explicitly subscribes, use emit with subscribeEvent:
KrpanoIframeBridge.emit("openProduct", {
id: "product_123"
});tour.onEvent("openProduct", (payload) => {
console.log(payload);
});
tour.subscribeEvent("openProduct");6. TypeScript Hints For krpano Variables
getValue and setValue provide autocomplete for many common krpano paths:
const fov = await tour.getValue("view.fov");
const scene = await tour.getValue("xml.scene");
const isMobile = await tour.getValue("device.mobile");
await tour.setValue("view.fov", 90);
await tour.setValue("view.hlookat", 120);
await tour.setValue("hotspot[spot_1].visible", true);
await tour.setValue("layer[menu].alpha", 0.8);Known paths get basic return types:
| Path | Type |
| --- | --- |
| view.fov | number |
| xml.scene | string |
| device.mobile | boolean | "true" | "false" |
| hotspot[name].visible | boolean | "true" | "false" |
Custom tour variables are still allowed:
const value = await tour.getValue("my_custom_plugin.some_value");
await tour.setValue("my_custom_plugin.some_value", "hello");7. Hook API
Use useKrpanoTour when you want to render the iframe yourself.
import { useEffect, useState } from "react";
import {
useKrpanoTour,
type KrpanoView
} from "@vuvandinh203/krpano-iframe-toolkit";
export default function CustomTourPage() {
const [view, setView] = useState<KrpanoView | null>(null);
const {
iframeRef,
client: tour,
isReady,
isConnected,
lastError
} = useKrpanoTour({
tourUrl: "https://tour.example.com/tour.html",
requestTimeoutMs: 10000,
autoConnect: true,
onTourReady: (client) => {
client.getView().then(setView);
}
});
async function zoomIn() {
await tour?.zoomIn({ step: 10, minFov: 30 });
if (tour) {
setView(await tour.getView());
}
}
return (
<section>
<iframe
ref={iframeRef}
title="360 Tour"
src="https://tour.example.com/tour.html"
style={{ width: "100%", height: 640, border: 0 }}
/>
<button disabled={!isReady || !isConnected} onClick={zoomIn}>
Zoom in
</button>
{view && <pre>{JSON.stringify(view, null, 2)}</pre>}
{lastError && <p>{lastError.message}</p>}
</section>
);
}For lower-level iframe control, use useKrpanoIframe and pass iframeOrigin manually.
const { iframeRef, client } = useKrpanoIframe({
iframeOrigin: "https://tour.example.com",
requestTimeoutMs: 10000,
autoConnect: true
});8. Plain Client API
Use createKrpanoIframeClient when you are not using the React component or hook.
import { createKrpanoIframeClient } from "@vuvandinh203/krpano-iframe-toolkit";
const iframe = document.querySelector<HTMLIFrameElement>("#tour");
if (iframe) {
const tour = createKrpanoIframeClient(iframe, {
iframeOrigin: "https://tour.example.com",
requestTimeoutMs: 10000
});
await tour.connect();
await tour.loadScene("scene_lobby", { blend: 1 });
await tour.lookAt({ hlookat: 90, vlookat: 0, fov: 75, duration: 1 });
}9. Client Methods
| Method | Description |
| --- | --- |
| connect() | Connects to the bridge. |
| ping() | Checks whether the bridge is responding. |
| getValue(name) | Reads a krpano variable using krpano.get(name). |
| setValue(name, value) | Writes a krpano variable using krpano.set(name, value). |
| runAction(action) | Runs a raw krpano action using krpano.call(action). |
| loadScene(scene, options) | Loads a scene. |
| getCurrentScene() | Returns the current scene name. |
| getView() | Returns { hlookat, vlookat, fov, scene }. |
| setView(options) | Tweens the current view. |
| lookAt(options) | Moves the view with lookto. |
| zoomIn(options) | Zooms in by reducing view.fov. |
| zoomOut(options) | Zooms out by increasing view.fov. |
| enableHotspotClickEvents() | Rebinds auto hotspot click events. Useful for dynamic hotspots. |
| onHotspotClick(handler) | Listens for built-in hotspot click events. |
| onSceneChange(handler) | Listens for built-in scene change events. |
| onViewChange(handler) | Listens for built-in view change events. |
| onEvent(eventName, handler) | Listens for a custom event. |
| subscribeEvent(eventName, options) | Enables a custom event when the tour uses emit. |
| unsubscribeEvent(eventName) | Disables a custom event subscription. |
| listSubscribedEvents() | Lists active event subscriptions in the bridge. |
| onReady(handler) | Listens for the bridge ready message. |
| destroy() | Removes listeners and rejects pending requests. |
10. Common Commands
await tour.loadScene("scene_lobby", {
blend: 1,
flags: "MERGE"
});await tour.lookAt({
hlookat: 120,
vlookat: -5,
fov: 80,
duration: 1
});await tour.setView({
hlookat: 0,
vlookat: 0,
fov: 90,
duration: 0.8
});await tour.zoomIn({
step: 10,
minFov: 30
});await tour.zoomOut({
step: 10,
maxFov: 150
});Use runAction carefully. It requires allowRawCall !== false in the tour bridge:
await tour.runAction("lookto(0, 0, 90);");Troubleshooting
The React app cannot control the iframe
Check allowedOrigins in tour.html. The value must exactly match the React app origin, including protocol and port.
allowedOrigins: ["http://localhost:5173"]Commands time out
Common causes:
- the iframe URL is wrong,
- the bridge script is not loaded in
tour.html, KrpanoIframeBridge.init(...)is not called insideembedpano.onready,allowedOriginsdoes not include the React app origin,iframeOriginis wrong when using a relative URL.
runAction fails
If the bridge is initialized with:
allowRawCall: falsethen tour.runAction(...) is blocked. Prefer safer methods such as loadScene, lookAt, setView, zoomIn, and zoomOut.
Publishing
Before publishing:
npm run typecheck
npm run build
npm pack --dry-runPublish this scoped package as public:
npm publish --access public