SenangWebs Tour (SWT)
A powerful, data-driven 360° virtual tour system built on A-Frame WebVR. Create immersive virtual tours with a visual editor, or integrate the lightweight viewer library into your own projects.

Table of Contents
- Features
- Quick Start
- Custom Editor Integration
- Building from Source
- API Documentation
- Editor Features
- Browser Compatibility
- Documentation
- Contributing
- License
Features
Core Capabilities
- Visual Editor - No-code tour builder with click-to-place hotspot interface
- Viewer Library - Lightweight (12KB minified) JavaScript library for embedding tours
- Standalone Viewer - Self-contained HTML viewer with drag-and-drop JSON support
- Mobile & VR Ready - Full touch, mouse, and VR headset support
- Customizable Hotspots - Custom colors, scales, and tooltip text
- Multiple Export Formats - JSON configuration or standalone HTML files
Developer-Friendly
- Two Integration Modes - Declarative (HTML attributes) or Programmatic (JavaScript API)
- Modular Architecture - Six-controller editor pattern with clear separation of concerns
- Event System - React to scene loads, hotspot clicks, and navigation events
- Data-Driven - Pure JSON configuration, no internal state management
- Built with Rollup - Modern ES6+ modules with sourcemaps for debugging
Quick Start
Using the Visual Editor (No Coding Required)
Start Local Server (required for A-Frame CORS):
npm install npm run serveOpen Editor: Navigate to
http://localhost:8080/examples/editor.htmlCreate Your Tour:
- Click "Add Scene" and upload 360° panorama images (JPG/PNG)
- Click "Add Hotspot" then click on the preview to place navigation points
- Configure hotspot properties: color, scale, tooltip text, target scene
- See changes instantly in the live A-Frame preview
Export Your Tour:
- JSON Export → Portable config file for library integration
- Viewer Export → Self-contained HTML file (no dependencies needed)
Test Standalone Viewer: Open
http://localhost:8080/examples/viewer.htmland drag-and-drop your exported JSON
Integrating with Code (Viewer Library)
Minimal Example (see examples/example-simple.html):
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/a-hakim/senangwebs-tour@latest/dist/swt.min.js"></script>
</head>
<body>
<a-scene id="tour-scene">
<a-camera><a-cursor></a-cursor></a-camera>
</a-scene>
<script>
const config = {
initialScene: "room1",
scenes: {
room1: {
name: "Living Room",
panorama: "path/to/panorama1.jpg",
hotspots: [{
position: { x: 5, y: 0, z: -5 },
action: { type: "navigateTo", target: "room2" },
appearance: { color: "#FF6B6B", scale: 1.5 },
tooltip: { text: "Go to Kitchen" }
}]
},
room2: {
name: "Kitchen",
panorama: "path/to/panorama2.jpg",
hotspots: [{
position: { x: -5, y: 0, z: 5 },
action: { type: "navigateTo", target: "room1" },
appearance: { color: "#4ECDC4", scale: 1.5 },
tooltip: { text: "Back to Living Room" }
}]
}
}
};
const scene = document.querySelector('#tour-scene');
scene.addEventListener('loaded', () => {
const tour = new SWT.Tour(scene, config);
tour.addEventListener('scene-loaded', (e) => {
console.log('Now viewing:', e.detail.sceneName);
});
tour.start();
});
</script>
</body>
</html>Custom Editor Integration
Build your own tour editor using the swt-editor.js bundle. Two initialization modes are supported:
Option 1: Declarative Mode (HTML Attributes)
Zero JavaScript - perfect for quick prototypes:
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="dist/swt.js"></script>
<script src="dist/swt-editor.js"></script>
<link rel="stylesheet" href="dist/swt-editor.css">
</head>
<body>
<!-- Auto-initializes on page load -->
<div data-swt-editor
data-swt-auto-init="true"
data-swt-project-name="My Virtual Tour">
<div data-swt-scene-list></div>
<div data-swt-preview-area></div>
<div data-swt-properties-panel></div>
</div>
</body>
</html>Supported HTML Attributes:
| Attribute | Description | Default |
|-----------|-------------|---------|
| data-swt-editor | Marks the editor container (required) | - |
| data-swt-auto-init | Auto-initialize on DOMContentLoaded | false |
| data-swt-project-name | Initial project name | "Untitled Tour" |
| data-swt-auto-save | Enable LocalStorage auto-save | false |
| data-swt-auto-save-interval | Auto-save interval (milliseconds) | 30000 |
| data-swt-scene-list | Scene list panel container | - |
| data-swt-preview-area | A-Frame preview container | - |
| data-swt-properties-panel | Hotspot properties panel | - |
Example: examples/editor-declarative.html
Option 2: Programmatic Mode (JavaScript API)
Full control - for custom workflows and advanced integrations:
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="dist/swt.js"></script>
<script src="dist/swt-editor.js"></script>
<link rel="stylesheet" href="dist/swt-editor.css">
</head>
<body>
<div id="editor-container">
<div id="scenes"></div>
<div id="preview"></div>
<div id="properties"></div>
</div>
<script>
// Create editor instance with custom config
const editor = new TourEditor({
projectName: 'My Custom Tour',
autoSave: true,
autoSaveInterval: 30000
});
// Initialize with DOM elements
editor.init({
sceneListElement: document.getElementById('scenes'),
previewElement: document.getElementById('preview'),
propertiesElement: document.getElementById('properties')
});
// Access editor programmatically
editor.addEventListener('scene-added', (scene) => {
console.log('New scene:', scene.name);
});
// Export tour configuration
const config = editor.exportJSON();
</script>
</body>
</html>Available Classes (all attached to window after loading swt-editor.js):
TourEditor- Main coordinator, orchestrates all managersSceneManagerEditor- Scene CRUD operationsHotspotEditor- Hotspot placement and editingPreviewController- A-Frame preview renderingUIController- DOM rendering and updatesProjectStorageManager- LocalStorage persistenceExportManager- JSON and HTML export
Examples:
examples/editor-declarative.html- Declarative HTML-only modeexamples/editor.html- Full-featured programmatic editor
Building from Source
Installation & Build
# Install dependencies
npm install
# Build all bundles (viewer + editor)
npm run build
# Development mode (watch for changes)
npm run dev
# Serve locally (required for A-Frame CORS)
npm run serve
# Access at http://localhost:8080Build Output
| File | Size | Format | Purpose |
|---|---|---|---|
dist/swt.js |
26KB | UMD | Viewer library (development) |
dist/swt.min.js |
12KB | UMD | Viewer library (production) |
dist/swt-editor.js |
89KB | IIFE | Editor bundle (development) |
dist/swt-editor.min.js |
38KB | IIFE | Editor bundle (production) |
dist/swt-editor.css |
39KB | CSS | Editor styles (development) |
dist/swt-editor.min.css |
25KB | CSS | Editor styles (production) |
All builds include sourcemaps for debugging.
Project Structure
src/
├── index.js # Viewer library entry (UMD export)
├── AssetManager.js # Panorama preloading
├── SceneManager.js # Sky entity, transitions, fades
├── HotspotManager.js # Hotspot creation & updates
├── components/
│ └── hotspot-listener.js # A-Frame hotspot component
└── editor/
├── editor-entry.js # Editor bundle entry (IIFE)
├── editor-entry.css # CSS bundle entry (@import)
├── css/
│ └── main.css # Editor UI styles
└── js/
├── editor.js # Main coordinator (TourEditor)
├── scene-manager.js # Scene CRUD (SceneManagerEditor)
├── hotspot-editor.js # Hotspot placement (HotspotEditor)
├── preview-controller.js # A-Frame preview (PreviewController)
├── ui-controller.js # DOM rendering (UIController)
├── storage-manager.js # LocalStorage (ProjectStorageManager)
├── export-manager.js # JSON/HTML export (ExportManager)
├── utils.js # Helper functions
└── ui-init.js # DOMContentLoaded initialization
examples/
├── example.html # Full viewer demo
├── example-simple.html # Minimal viewer demo
├── editor.html # Full editor demo
├── editor-declarative.html # Declarative editor demo
└── viewer.html # Standalone drag-and-drop viewerAPI Documentation
Viewer Library API
Constructor
new SWT.Tour(aframeSceneElement, tourConfiguration)Parameters:
aframeSceneElement(HTMLElement) - A-Frame<a-scene>DOM elementtourConfiguration(Object) - Tour config (see structure below)
Returns: Tour instance
Configuration Structure
{
initialScene: "scene-id", // Required: Starting scene ID
scenes: { // Required: Object (not array!)
"scene-id": { // Key = scene ID
name: "Scene Name", // Required: Display name
panorama: "url-or-dataurl", // Required: Image URL or base64
hotspots: [ // Optional: Array of hotspots
{
id: "hotspot-1", // Optional: Auto-generated if omitted
position: { // Required: 3D coordinates
x: 10,
y: 1.5,
z: -3
},
action: { // Required: Hotspot action
type: "navigateTo", // Required: Action type
target: "scene-id-2" // Required: Target scene ID
},
appearance: { // Optional: Visual customization
color: "#00ff00", // Default: "#00ff00"
scale: 1.5, // Default: 1.0 (number or "x y z" string)
icon: "arrow" // Default: sphere (future: custom icons)
},
tooltip: { // Optional: Hover/focus text
text: "Click to navigate" // Tooltip content
}
}
]
}
}
}Important Notes:
scenesis an object (keys are scene IDs), not an array- Hotspot
positionis in 3D space (typically on 10-unit sphere surface) - Editor stores
imageUrl, library expectspanorama(export handles conversion) - Images can be URLs or Data URLs (base64) for offline tours
Methods
tour.start()
Initialize and start the tour. Loads the initial scene and sets up event listeners.
Returns: void
Example:
const tour = new SWT.Tour(sceneElement, config);
tour.start();tour.navigateTo(sceneId)
Navigate to a specific scene by ID.
Parameters:
sceneId(String) - Target scene ID (must exist inscenesobject)
Returns: void
Example:
tour.navigateTo('bedroom'); // Loads scene with id "bedroom"tour.getCurrentSceneId()
Get the ID of the currently active scene.
Returns: String - Current scene ID
Example:
const currentScene = tour.getCurrentSceneId();
console.log('Viewing:', currentScene); // "living-room"tour.destroy()
Clean up and remove the tour. Removes all hotspots, event listeners, and resets scene.
Returns: void
Example:
tour.destroy(); // Cleanup before removing from DOMtour.addEventListener(eventName, callback)
Listen to tour events. Custom event system (not DOM events).
Parameters:
eventName(String) - Event name (see Events section)callback(Function) - Handler function receiving event object
Returns: void
Example:
tour.addEventListener('scene-loaded', (event) => {
console.log('Scene:', event.detail.sceneName);
});Events
All events include a detail object with event-specific data.
tour-started
Fired when tour.start() is called.
Detail:
{
config: Object // Full tour configuration
}scene-loading
Fired before a scene begins loading.
Detail:
{
sceneId: String // ID of scene being loaded
}scene-loaded
Fired after a scene is fully loaded and rendered.
Detail:
{
sceneId: String, // ID of loaded scene
sceneName: String // Display name of scene
}hotspot-activated
Fired when a hotspot is clicked/activated.
Detail:
{
hotspotId: String, // Hotspot ID
sceneId: String, // Current scene ID
action: Object // Hotspot action object { type, target }
}Usage Example
const scene = document.querySelector('#vr-scene');
const tour = new SWT.Tour(scene, {
initialScene: "room1",
scenes: {
room1: {
name: "Living Room",
panorama: "360-living-room.jpg",
hotspots: [{
position: { x: 5, y: 0, z: -5 },
action: { type: "navigateTo", target: "room2" },
appearance: { color: "#FF6B6B" },
tooltip: { text: "Kitchen" }
}]
},
room2: {
name: "Kitchen",
panorama: "360-kitchen.jpg",
hotspots: []
}
}
});
// Listen to events
tour.addEventListener('tour-started', (e) => {
console.log('Tour configuration:', e.detail.config);
});
tour.addEventListener('scene-loading', (e) => {
console.log('Loading scene:', e.detail.sceneId);
// Show loading indicator
});
tour.addEventListener('scene-loaded', (e) => {
console.log('Loaded:', e.detail.sceneName);
// Hide loading indicator
});
tour.addEventListener('hotspot-activated', (e) => {
console.log('Hotspot clicked:', e.detail.hotspotId);
console.log('Navigating to:', e.detail.action.target);
});
// Start the tour
tour.start();
// Programmatic navigation
setTimeout(() => {
tour.navigateTo('room2');
}, 5000);
// Cleanup
// tour.destroy();Editor Features
Visual Tour Creation
- Click-to-Place Hotspots - Raycast-based placement on panorama sphere
- Real-Time Preview - Instant A-Frame rendering as you edit
- Scene Management - Add, remove, reorder scenes with thumbnails
- Camera Control - Auto-point camera to selected hotspot with animation
- Position Validation - Hotspots clamped to 10-unit sphere radius
Hotspot Configuration
- 3D Position - Click-to-place or manual X/Y/Z coordinate input
- Navigation Target - Link to any scene in the tour
- Visual Customization - Color picker and scale slider
- Tooltips - Custom hover text for each hotspot
Data Management
- LocalStorage Persistence - Auto-save projects (configurable interval)
- Import/Export - Load and save tour JSON configurations
- Data URLs - Panoramas embedded as base64 (no external files needed)
- Thumbnail Generation - Auto-generated scene previews (100x50px)
Export Options
JSON Export - Portable configuration file for library integration
- Converts editor's
imageUrlto library'spanoramaformat - Compatible with
SWT.Tourviewer library - Use in custom integrations or standalone viewer
- Converts editor's
Viewer Export - Self-contained HTML file
- Embeds minified
swt.min.jslibrary - Includes full tour JSON configuration
- No external dependencies - works offline
- Drag-and-drop ready for distribution
- Embeds minified
Developer Tools
- ES6 Module Architecture - Six-controller pattern with clear separation
- Sourcemaps - Debug original ES6 source in browser DevTools
- Two Init Modes - Declarative (HTML) or Programmatic (JS API)
- Event System - React to scene-added, hotspot-clicked events
- Global Access - All classes attached to
windowfor console debugging
Browser Compatibility
| Browser | Version | Notes |
|---|---|---|
| Chrome | 90+ | Recommended - best performance |
| Firefox | 88+ | Full support |
| Safari | 14+ | WebGL support required |
| Edge | 90+ | Chromium-based |
| Mobile Safari | iOS 14+ | Touch and gyroscope support |
| Chrome Mobile | Android 90+ | Touch and gyroscope support |
Requirements:
- WebGL 1.0 or higher
- ES6 module support (for editor)
- LocalStorage (for editor persistence)
VR Headsets:
- Oculus Quest 1/2/3
- Meta Quest Pro
- HTC Vive
- Valve Index
- Any WebXR-compatible headset
Documentation
- EDITOR.md - Complete guide to building custom editors
- .github/copilot-instructions.md - AI agent development instructions
- examples/ - Live code examples and demos
Contributing
Contributions are welcome! Here's how you can help:
- Report Bugs - Open an issue with reproduction steps
- Suggest Features - Share your ideas in GitHub Issues
- Submit PRs - Fork, create a feature branch, and submit a pull request
- Improve Docs - Help make documentation clearer and more comprehensive
Development Workflow
# Fork and clone the repository
git clone https://github.com/your-username/senangwebs-tour.git
cd senangwebs-tour
# Install dependencies
npm install
# Start development server with watch mode
npm run dev
# In another terminal, serve the examples
npm run serve
# Make changes, test in browser at http://localhost:8080
# Build for production
npm run build
# Submit a pull requestCode Guidelines
- Use ES6+ syntax (modules, classes, arrow functions)
- Follow existing naming conventions (see copilot-instructions.md)
- No debug
console.log()- onlyconsole.error()for critical errors - All editor classes must use
export default - Debounce text inputs (300ms) using
debounce()from utils.js - Test in Chrome, Firefox, and Safari before submitting
License
MIT License - see LICENSE.md for details.