senangwebs-tour 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * SceneManager - Handles scene transitions and <a-sky> management
3
+ */
4
+ export class SceneManager {
5
+ constructor(sceneEl, assetManager) {
6
+ this.sceneEl = sceneEl;
7
+ this.assetManager = assetManager;
8
+ this.skyEl = null;
9
+ this.currentSceneId = null;
10
+ this.transitionDuration = 500; // milliseconds
11
+ this.init();
12
+ }
13
+
14
+ init() {
15
+ // Find or create <a-sky> element
16
+ this.skyEl = this.sceneEl.querySelector('a-sky');
17
+ if (!this.skyEl) {
18
+ this.skyEl = document.createElement('a-sky');
19
+ this.skyEl.setAttribute('id', 'swt-sky');
20
+ this.sceneEl.appendChild(this.skyEl);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Load a scene by its ID
26
+ * @param {string} sceneId - The scene ID
27
+ * @param {Object} sceneData - The scene configuration object
28
+ * @returns {Promise} - Resolves when the scene is loaded
29
+ */
30
+ async loadScene(sceneId, sceneData) {
31
+ this.currentSceneId = sceneId;
32
+
33
+ // Preload the panorama
34
+ const panoramaUrl = sceneData.panorama;
35
+ const assetId = `scene-${sceneId}`;
36
+
37
+ const isVideo = this.assetManager.isVideo(panoramaUrl);
38
+
39
+ if (isVideo) {
40
+ await this.assetManager.preloadVideo(panoramaUrl, assetId);
41
+ } else {
42
+ await this.assetManager.preloadImage(panoramaUrl, assetId);
43
+ }
44
+
45
+ // Set the sky source
46
+ this.skyEl.setAttribute('src', `#${assetId}`);
47
+
48
+ // If it's a video, play it
49
+ if (isVideo) {
50
+ const videoEl = this.assetManager.getAsset(assetId);
51
+ if (videoEl && videoEl.play) {
52
+ videoEl.play().catch(err => {
53
+ console.warn('Video autoplay failed:', err);
54
+ });
55
+ }
56
+ }
57
+
58
+ return Promise.resolve();
59
+ }
60
+
61
+ /**
62
+ * Transition to a new scene with fade effect
63
+ * @param {string} sceneId - The target scene ID
64
+ * @param {Object} sceneData - The scene configuration object
65
+ * @returns {Promise} - Resolves when the transition is complete
66
+ */
67
+ async transitionTo(sceneId, sceneData) {
68
+ // Fade out
69
+ await this.fadeOut();
70
+
71
+ // Load new scene
72
+ await this.loadScene(sceneId, sceneData);
73
+
74
+ // Fade in
75
+ await this.fadeIn();
76
+
77
+ return Promise.resolve();
78
+ }
79
+
80
+ /**
81
+ * Fade out the scene
82
+ * @returns {Promise}
83
+ */
84
+ fadeOut() {
85
+ return new Promise((resolve) => {
86
+ // Create a fade overlay
87
+ const fadeEl = document.createElement('a-entity');
88
+ fadeEl.setAttribute('id', 'swt-fade');
89
+ fadeEl.setAttribute('geometry', 'primitive: sphere; radius: 1');
90
+ fadeEl.setAttribute('material', 'color: black; opacity: 0; shader: flat; side: back');
91
+ fadeEl.setAttribute('scale', '0.1 0.1 0.1');
92
+
93
+ // Position it at the camera
94
+ const cameraEl = this.sceneEl.querySelector('[camera]');
95
+ if (cameraEl && cameraEl.tagName) {
96
+ fadeEl.setAttribute('position', '0 0 0');
97
+ cameraEl.appendChild(fadeEl);
98
+ } else {
99
+ // Fallback: append to scene
100
+ fadeEl.setAttribute('position', '0 1.6 0');
101
+ this.sceneEl.appendChild(fadeEl);
102
+ }
103
+
104
+ // Animate opacity
105
+ fadeEl.setAttribute('animation', {
106
+ property: 'material.opacity',
107
+ to: 1,
108
+ dur: this.transitionDuration,
109
+ easing: 'easeInQuad'
110
+ });
111
+
112
+ setTimeout(() => {
113
+ resolve(fadeEl);
114
+ }, this.transitionDuration);
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Fade in the scene
120
+ * @returns {Promise}
121
+ */
122
+ fadeIn() {
123
+ return new Promise((resolve) => {
124
+ const fadeEl = document.getElementById('swt-fade');
125
+ if (fadeEl) {
126
+ fadeEl.setAttribute('animation', {
127
+ property: 'material.opacity',
128
+ to: 0,
129
+ dur: this.transitionDuration,
130
+ easing: 'easeOutQuad'
131
+ });
132
+
133
+ setTimeout(() => {
134
+ if (fadeEl.parentNode) {
135
+ fadeEl.parentNode.removeChild(fadeEl);
136
+ }
137
+ resolve();
138
+ }, this.transitionDuration);
139
+ } else {
140
+ resolve();
141
+ }
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Get the current scene ID
147
+ * @returns {string}
148
+ */
149
+ getCurrentSceneId() {
150
+ return this.currentSceneId;
151
+ }
152
+
153
+ /**
154
+ * Clean up the scene manager
155
+ */
156
+ destroy() {
157
+ if (this.skyEl && this.skyEl.parentNode) {
158
+ this.skyEl.setAttribute('src', '');
159
+ }
160
+ this.currentSceneId = null;
161
+ }
162
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A-Frame component that listens for clicks on hotspots and emits a custom event
3
+ */
4
+ if (typeof AFRAME !== 'undefined') {
5
+ AFRAME.registerComponent('swt-hotspot-listener', {
6
+ schema: {
7
+ hotspotData: { type: 'string', default: '{}' }
8
+ },
9
+
10
+ init: function () {
11
+ this.onClick = this.onClick.bind(this);
12
+ this.onMouseEnter = this.onMouseEnter.bind(this);
13
+ this.onMouseLeave = this.onMouseLeave.bind(this);
14
+ },
15
+
16
+ play: function () {
17
+ this.el.addEventListener('click', this.onClick);
18
+ this.el.addEventListener('mouseenter', this.onMouseEnter);
19
+ this.el.addEventListener('mouseleave', this.onMouseLeave);
20
+ },
21
+
22
+ pause: function () {
23
+ this.el.removeEventListener('click', this.onClick);
24
+ this.el.removeEventListener('mouseenter', this.onMouseEnter);
25
+ this.el.removeEventListener('mouseleave', this.onMouseLeave);
26
+ },
27
+
28
+ onClick: function (evt) {
29
+ const hotspotData = JSON.parse(this.data.hotspotData);
30
+
31
+ // Emit custom event that the Tour class can listen to
32
+ this.el.sceneEl.emit('swt-hotspot-clicked', {
33
+ hotspotData: hotspotData
34
+ });
35
+ },
36
+
37
+ onMouseEnter: function (evt) {
38
+ const hotspotData = JSON.parse(this.data.hotspotData);
39
+
40
+ // Show tooltip if exists
41
+ if (hotspotData.tooltip) {
42
+ this.el.sceneEl.emit('swt-hotspot-hover', {
43
+ hotspotData: hotspotData,
44
+ isHovering: true
45
+ });
46
+ }
47
+
48
+ // Add visual feedback
49
+ this.el.setAttribute('scale', this.getScaledSize(1.2));
50
+ },
51
+
52
+ onMouseLeave: function (evt) {
53
+ const hotspotData = JSON.parse(this.data.hotspotData);
54
+
55
+ // Hide tooltip
56
+ if (hotspotData.tooltip) {
57
+ this.el.sceneEl.emit('swt-hotspot-hover', {
58
+ hotspotData: hotspotData,
59
+ isHovering: false
60
+ });
61
+ }
62
+
63
+ // Reset scale
64
+ this.el.setAttribute('scale', this.getOriginalScale());
65
+ },
66
+
67
+ getOriginalScale: function () {
68
+ const hotspotData = JSON.parse(this.data.hotspotData);
69
+ return hotspotData.appearance?.scale || '1 1 1';
70
+ },
71
+
72
+ getScaledSize: function (multiplier) {
73
+ const scale = this.getOriginalScale();
74
+ const parts = scale.split(' ').map(Number);
75
+ return `${parts[0] * multiplier} ${parts[1] * multiplier} ${parts[2] * multiplier}`;
76
+ }
77
+ });
78
+
79
+ /**
80
+ * Billboard component - Makes an entity always face the camera
81
+ */
82
+ AFRAME.registerComponent('swt-billboard', {
83
+ schema: {
84
+ enabled: { type: 'boolean', default: true }
85
+ },
86
+
87
+ init: function () {
88
+ this.camera = null;
89
+ },
90
+
91
+ tick: function () {
92
+ if (!this.data.enabled) return;
93
+
94
+ // Find camera if not cached
95
+ if (!this.camera) {
96
+ this.camera = this.el.sceneEl.camera;
97
+ if (!this.camera) return;
98
+ }
99
+
100
+ // Get camera world position
101
+ const cameraPosition = new THREE.Vector3();
102
+ this.camera.getWorldPosition(cameraPosition);
103
+
104
+ // Get this element's world position
105
+ const elementPosition = new THREE.Vector3();
106
+ this.el.object3D.getWorldPosition(elementPosition);
107
+
108
+ // Make the element look at the camera
109
+ this.el.object3D.lookAt(cameraPosition);
110
+ }
111
+ });
112
+ }
113
+
114
+ export default 'swt-hotspot-listener';