janusweb 1.5.56 → 1.7.1

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.
Files changed (42) hide show
  1. package/.github/workflows/deploy.yml +71 -0
  2. package/Dockerfile +14 -0
  3. package/README.md +258 -19
  4. package/janusxr.com/.args +2 -0
  5. package/janusxr.com/.init.lua +30 -0
  6. package/media/assets/webui/apps/comms/voip.js +27 -25
  7. package/media/assets/webui/apps/editor/editor.js +16 -0
  8. package/media/assets/webui/apps/locomotion/locomotion-assets.json +3 -0
  9. package/media/assets/webui/apps/locomotion/teleport.mp3 +0 -0
  10. package/media/assets/webui/apps/locomotion/teleporter.js +75 -43
  11. package/media/assets/webui/apps/navigation/navigation.js +6 -1
  12. package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
  13. package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
  14. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
  15. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
  16. package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
  17. package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
  18. package/media/assets/webui/apps/xrmenu/images/teleport.png +0 -0
  19. package/media/assets/webui/apps/xrmenu/xrmenu-assets.json +2 -1
  20. package/media/assets/webui/apps/xrmenu/xrmenu.js +39 -15
  21. package/media/assets/webui/default.json +1 -0
  22. package/media/images/portal.svg +130 -0
  23. package/media/index.html +48 -1
  24. package/package.json +1 -1
  25. package/scripts/client.js +24 -0
  26. package/scripts/config.js +12 -1
  27. package/scripts/janusbase.js +14 -22
  28. package/scripts/janusghost.js +2 -2
  29. package/scripts/janusparagraph.js +195 -12
  30. package/scripts/janusplayer.js +9 -6
  31. package/scripts/room.js +91 -44
  32. package/scripts/sound.js +1 -1
  33. package/scripts/text.js +2 -1
  34. package/scripts/translators/paragraph/html-xml-rss.js +47 -0
  35. package/scripts/translators/peertube.js +89 -0
  36. package/scripts/translators/xrfragments.js +93 -0
  37. package/tests/room/paragraph.xml +47 -0
  38. package/utils/build.sh +1 -0
  39. package/utils/clean-build.sh +5 -0
  40. package/utils/dev-link.sh +75 -0
  41. package/utils/release.binary.sh +36 -0
  42. package/utils/release.docker.sh +11 -0
@@ -1,8 +1,15 @@
1
+ /*
2
+ * for room-specific teleport sound use:
3
+ *
4
+ * <room teleport_sound_id="mysound" ...>
5
+ *
6
+ */
7
+
1
8
  janus.registerElement('locomotion_teleporter', {
2
9
  active: false,
3
10
  longpresstime: 500,
4
11
  deadzone: 5,
5
- xrplayer: null,
12
+ xrplayer: null, // *TODO* use janus.engine.client.xrplayer instead (and allow desktop too)
6
13
  linesegments: 100,
7
14
 
8
15
  create() {
@@ -102,7 +109,13 @@ janus.registerElement('locomotion_teleporter', {
102
109
  });
103
110
  player.head.add(this.shroud._target);
104
111
  this.particles.particle_vel = V(-.4, 0, -.4); // FIXME - particle velocity isn't being set on spawn
105
- this.sound = room.createObject('Sound', { id: 'teleport2' }, this);
112
+
113
+ let locomotion = janus.ui.apps.default.apps.locomotion;
114
+ let asseturl = locomotion.resolveFullURL('./locomotion-assets.json');
115
+ fetch(asseturl).then(res => res.json()).then(assetlist => {
116
+ this.assetpack = elation.engine.assets.loadJSON(assetlist, locomotion.resolveFullURL('./'));
117
+ this.sound = room.createObject('Sound', { id: room.teleport_sound_id || 'button-teleport' }, this);
118
+ })
106
119
 
107
120
  this.disableCursor();
108
121
  window.addEventListener('mousemove', this.handleMouseMove);
@@ -133,13 +146,15 @@ janus.registerElement('locomotion_teleporter', {
133
146
  });
134
147
  this.activateControlContext('teleporter');
135
148
 
136
- //room.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
137
- //room.addEventListener('mouseup', (ev) => this.handleMouseUp(ev));
149
+ // emulated mouse-events (via webxr handtracking e.g.)
150
+ room.addEventListener('mousemove', (ev) => this.handleMouseMove(ev));
151
+ room.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
152
+ room.addEventListener('mouseup', (ev) => this.handleMouseUp(ev));
138
153
  elation.events.add(janus._target, 'room_change', (ev) => this.handleRoomChange(ev));
139
154
  },
140
155
  update() {
141
156
  if (this.active) {
142
- let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
157
+ let hand = this.xrplayer ? this.xrplayer.trackedobjects['hand_' + this.teleporthand] : null
143
158
  let startpoint,
144
159
  segments = this.linesegments,
145
160
  duration = .25,
@@ -152,11 +167,13 @@ janus.registerElement('locomotion_teleporter', {
152
167
  if (hand) {
153
168
  startpoint = hand.pointer.localToWorld(V(0, 0, 0));
154
169
  v0 = hand.pointer.localToWorld(V(0, 0, -speed)).sub(startpoint);
170
+ hand.teleportPos = hand.teleportPos || v0
155
171
  } else {
156
172
  startpoint = player.head.localToWorld(V(0,0,0));
157
173
  v0 = player.head.localToWorld(V(0,0,-speed)).sub(startpoint);
158
174
  return; // Head-locked teleporter is awkward, do we really still support this?
159
175
  }
176
+
160
177
  let i = 0,
161
178
  p0 = startpoint.clone(),
162
179
  dir = v0.clone().normalize();
@@ -181,6 +198,8 @@ janus.registerElement('locomotion_teleporter', {
181
198
  this.pos = player.worldToLocal(hit.point);
182
199
  this.localToWorld(this.particles.emitter_pos.set(0,0,0));
183
200
  this.updateTeleportAngle();
201
+ // track movement to cancel longpresstimer
202
+ if( hand ) hand.teleportPos = hand.teleportPos || this.pos.clone()
184
203
  }
185
204
  laserpoints[i * 2].copy(p0);
186
205
  laserpoints[i * 2 + 1].copy(p1);
@@ -231,7 +250,6 @@ janus.registerElement('locomotion_teleporter', {
231
250
  if (len > .8) {
232
251
  this.updateTeleportAngle();
233
252
  } else if (len < .01) {
234
- this.showShroud();
235
253
  this.teleport();
236
254
  this.teleportactive = false;
237
255
  this.teleporthand = false;
@@ -262,16 +280,23 @@ janus.registerElement('locomotion_teleporter', {
262
280
 
263
281
  }
264
282
  },
265
- teleport() {
283
+ teleport(mouse) {
266
284
  //if (!room.teleport) return;
267
285
  let pos = player.localToWorld(this.pos.clone());
268
286
  pos.y += player.fatness; // FIXME - This accounts for wonky player sphere collider, when we switch to a capsule we won't need it
269
287
  player.pos = pos;
270
288
  player.vel = V(0,0,.01); // "wake up" physics engine
271
- player.angular.set(0,0,0);
272
- let playerangle = Math.atan2(this.pos.x, this.pos.z);
273
- console.log('Teleport player', pos, playerangle);
274
- this.xrplayer.orientation._target.setFromEuler(new THREE.Euler(0, this.teleportangle - playerangle, 0));
289
+ console.log('Teleport player', pos);
290
+ this.showShroud();
291
+ if( this.sound ){
292
+ this.sound.pos = pos;
293
+ this.sound.play();
294
+ }
295
+ if( this.xrplayer && this.teleportangle && !mouse){
296
+ player.angular.set(0,0,0);
297
+ let playerangle = Math.atan2(this.pos.x, this.pos.z);
298
+ this.xrplayer.orientation._target.setFromEuler(new THREE.Euler(0, this.teleportangle - playerangle, 0));
299
+ }
275
300
  },
276
301
  updateTeleportAngle() {
277
302
  let controls = this.activecontrols;
@@ -282,27 +307,28 @@ janus.registerElement('locomotion_teleporter', {
282
307
  handleMouseDown(ev) {
283
308
  if (!room.teleport) return;
284
309
  if (ev.button == 0 && (player.enabled || janus.hmd)) {
310
+ if( ev.inputSourceObject && this.xrplayer ){ // translate webxr emulated mousedown event
311
+ this.teleporthand = ev.inputSourceObject.device.handedness
312
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
313
+ if( hand ) hand.teleportPos = false
314
+ }
285
315
  if (!this.longpresstimer) {
286
- //this.longpresstimer = setTimeout(() => { console.log('timer fired'); this.enableCursor(); }, this.longpresstime);
316
+ this.longpresstimer = setTimeout(() => { console.log('timer fired'); this.enableCursor(); }, this.longpresstime);
287
317
  this.mousediff = [0,0];
288
- }
289
- //this.active = true;
318
+ }else this.cancelLongPress()
290
319
  }
291
320
  },
292
321
  handleMouseMove(ev) {
293
322
  if (!room.teleport) return;
294
- if (this.longpresstimer) {
295
- this.mousediff[0] += ev.movementX;
296
- this.mousediff[1] += ev.movementY
297
- var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
298
- if (distance > this.deadzone) {
299
- clearTimeout(this.longpresstimer);
300
- this.longpresstimer = false;
301
- }
302
- }
323
+ //if (this.longpresstimer) {
324
+ // this.mousediff[0] += ev.movementX;
325
+ // this.mousediff[1] += ev.movementY
326
+ // var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
327
+ // if (distance > this.deadzone) this.cancelLongPress()
328
+ //}
303
329
  },
304
330
  handleTouchMove(ev) {
305
- if (!room.teleport) return;
331
+ if (!room.teleport || !this.active) return;
306
332
  if (this.longpresstimer) {
307
333
  var touch = ev.changedTouches[0];
308
334
 
@@ -315,9 +341,7 @@ janus.registerElement('locomotion_teleporter', {
315
341
  }
316
342
  this.lasttouch = [touch.clientX, touch.clientY];
317
343
  var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
318
- if (distance > this.deadzone) {
319
- clearTimeout(this.longpresstimer);
320
- }
344
+ if (distance > this.deadzone) this.cancelLongPress()
321
345
  if (this.active) {
322
346
  ev.stopPropagation();
323
347
  ev.preventDefault();
@@ -325,42 +349,50 @@ janus.registerElement('locomotion_teleporter', {
325
349
  }
326
350
  },
327
351
  handleMouseUp(ev) {
328
- //if (!room.teleport) return;
329
- if (this.longpresstimer) {
330
- clearTimeout(this.longpresstimer);
331
- this.longpresstimer = false;
332
- }
333
- if (this.active) {
334
- /*
335
- player.pos = this.pos;
336
- this.sound.pos = this.pos;
337
- this.sound.play();
338
- */
339
- this.showShroud();
340
- this.teleport();
341
- }
342
- this.disableCursor();
352
+ if (!room.teleport) return;
353
+ if (this.teleportactive) {
354
+ this.teleport(true);
355
+ this.active = false
356
+ this.teleportactive = false;
357
+ this.teleporthand = false;
358
+ this.disableCursor();
359
+ }else if( this.longpresstimer ) this.disableCursor();
343
360
  },
344
361
  enableCursor() {
345
362
  if (!room.teleport) return;
363
+ if( this.xrplayer ){
364
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
365
+ if( hand ) hand.pointer.laser.visible = false // prevent (portal)clicks
366
+ }
367
+ this.active = true;
368
+ this.teleportactive = true;
346
369
  this.visible = true;
347
370
  this.particles.resetParticles();
348
371
  this.particles.visible = true;
349
372
  if (this.laser.room !== room._target) room.appendChild(this.laser);
350
373
  if (this.particles.room !== room._target) room.appendChild(this.particles);
351
374
  this.laser.visible = true;
352
- this.active = true;
353
375
  this.particles.start();
354
376
  },
355
377
  disableCursor() {
378
+ if( this.xrplayer ){
379
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
380
+ if( hand ) hand.pointer.laser.visible = true // restore original laser
381
+ }
382
+ this.teleportactive = false;
356
383
  this.visible = false;
357
384
  this.particles.visible = false;
358
385
  this.laser.visible = false;
359
386
  this.active = false;
360
387
  this.particles.stop();
388
+ this.cancelLongPress()
361
389
  },
362
390
  showShroud() {
363
391
  this.shroud.visible = true;
364
392
  this.shroud.opacity = 1;
393
+ },
394
+ cancelLongPress(){
395
+ clearTimeout(this.longpresstimer);
396
+ this.longpresstimer = false
365
397
  }
366
398
  });
@@ -391,7 +391,7 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
391
391
  handleAccept(ev) {
392
392
  var url = this.input.value;
393
393
  if (url.length > 0) {
394
- if (url.indexOf(':') == -1) {
394
+ if (url.indexOf(':') == -1 && !url.match(/^\.\//) ) {
395
395
  url = 'http://' + url;
396
396
  }
397
397
  if (ev.altKey) {
@@ -408,6 +408,11 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
408
408
  }
409
409
  }
410
410
  handleBlur(ev) {
411
+ // enter-key on Quest devices (onscreen keyboards) is not emitted (blur-event instead)
412
+ // https://developers.meta.com/horizon/documentation/web/webxr-keyboard/
413
+ if (navigator.userAgent.indexOf('OculusBrowser') != -1) {
414
+ this.handleAccept({altKey:true}) // get straight to it
415
+ }
411
416
  setTimeout(() => {
412
417
  this.suggestions.hide();
413
418
  }, 100);
@@ -0,0 +1,308 @@
1
+ /* XR URI Fragments spec (https://xrfragment.org)
2
+ *
3
+ * level0 sidecar files as per the XR URI Fragments spec
4
+ * https://xrfragment.org/#sidecar%20files
5
+ *
6
+ * NOTE: you can also load sidecarfiles in a roomscript manually:
7
+ *
8
+ * elation.events.add(null, 'room_load_complete', function(e){
9
+ * if( room?.sidecarfile ) room.sidecarfile.load()
10
+ * })
11
+ */
12
+
13
+ elation.require([], function() {
14
+
15
+ elation.extend('janusweb.sidecarfile', class {
16
+ constructor(object) {
17
+ this.scene = elation.engine.instances.default.systems.world.scene['world-3d']
18
+ this._object = object
19
+ this.extension = /\.(gltf|glb|dae)$/
20
+ this.extensionXRF = /\.xrf\./
21
+ if( room.url.match(this.extension) && this.isXRF(this.hideSystemFolder) ){
22
+ this.load()
23
+ }
24
+ }
25
+
26
+ load(){
27
+ this.cleanup()
28
+ this.loadWebVTT() // https://xrfragment.org/#sidecar%20files
29
+ this.loadSound() // https://xrfragment.org/#sidecar%20files
30
+ this.initStartButton()
31
+ this.initSubtitle()
32
+ }
33
+
34
+ hideSystemFolder(o){
35
+ // https://xrfragment.org/#system%20folders
36
+ if(o.name[0] == '_'){
37
+ o.visible = false
38
+ o.pickable = false
39
+ }
40
+ }
41
+
42
+ isXRF(cb){
43
+ // for XRF heuristics see https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
44
+ let heuristic = room.url.match(this.extensionXRF)
45
+ this.scene.traverse( (o) => {
46
+ if( o.userData?.href ) heuristic = true // has XRF hyperlink
47
+ if( o.name[0] == '_' ) heuristic = true // has XRF system folder
48
+ })
49
+ return heuristic
50
+ }
51
+
52
+ loadSound(){ // https://xrfragment.org/#sidecar%20files
53
+ room.removeObject('xrf_audio')
54
+ const audio = room.url.replace(this.extension,'.ogg')
55
+ room.loadNewAsset("sound", {id:"xrf_audio", src:audio,proxy:false})
56
+ this.sound = room.createObject('sound',{ id: "xrf_audio", js_id: 'xrf_audio', loop:true })
57
+ }
58
+
59
+ loadWebVTT(){ // https://xrfragment.org/#sidecar%20files
60
+ const webvtt = room.url.replace(this.extension,'.vtt')
61
+ fetch( webvtt )
62
+ .then( (res) => res.text() )
63
+ .then( (webvtt) => {
64
+ this.webvtt = this.parseWEBVTT(webvtt)
65
+ })
66
+ .catch( () => false ) // no biggy (optional)
67
+ }
68
+
69
+ initStartButton(){
70
+ this.btn = room.createObject('object',{
71
+ id: 'cube',
72
+ js_id: 'btnstart',
73
+ pos: player.localToWorld( V(0,1.8,-1) ),
74
+ scale: '0.5 0.125 0.01',
75
+ col: '0.5 0.5 0.5',
76
+ lighting:false,
77
+ sync: true,
78
+ billboard: 'y',
79
+ collision_id: 'cube'
80
+ })
81
+ const label = this.btn.createObject('text',{
82
+ text: 'Start experience',
83
+ lighting:false,
84
+ col: '0 0 0',
85
+ pos: '0 -0.15 0.5',
86
+ scale: '1.2 5 1.2',
87
+ })
88
+ this.btn.addEventListener('click', () => this.start() )
89
+ this.positionStartButton()
90
+ }
91
+
92
+ positionStartButton(){
93
+ setTimeout( () => {
94
+ this.btn.pos = player.localToWorld( V(0,1.8,-1) )
95
+ }, 500)
96
+ }
97
+
98
+ initSubtitle(){
99
+ this.subtitle = player.createObject('paragraph',{
100
+ js_id: 'subtitle',
101
+ pos: '0 1.7 -1.5',
102
+ lighting: false,
103
+ pickable: false,
104
+ css: `.paragraphcontainer{
105
+ background: transparent;
106
+ text-align:center;
107
+ height:100%;
108
+ width:100%;
109
+ display: flex;
110
+ align-items: center;
111
+ font-size:55px;
112
+ line-height:55px;
113
+ text-shadow: 0px 0px 2px #FFFF;
114
+ color:black;
115
+ }
116
+ .subtitle{
117
+ width:100%;
118
+ }
119
+ .loading{
120
+ background:#FFF;
121
+ display:inline-block;
122
+ font-size:22px;
123
+ font-family: monospace;
124
+ line-height:30px;
125
+ padding:20px;
126
+ border-radius:15px;
127
+ }
128
+ `,
129
+ text_col: '1 1 1',
130
+ back_col: '1 1 1'
131
+ })
132
+ this.subtitle.objects['3d'].depthWrite = false
133
+ this.subtitle.objects['3d'].depthTest = false
134
+ this.subtitle.objects['3d'].renderOrder = 100
135
+ this.subtitle.setHTML = (html) => {
136
+ this.subtitle.visible = true
137
+ this.subtitle.text = `<div class='subtitle'>${html}</div>`
138
+ }
139
+ }
140
+
141
+ update(){
142
+ if( this.update.id || !this.playing ) return // throttle
143
+ const advance = () => {
144
+ if( !this?.sound?.timeoffset && this?.sound?.audio?.context){
145
+ this.sound.timeoffset = this.sound.audio.context.currentTime
146
+ }
147
+ if( this?.sound?.timeoffset != undefined &&
148
+ this.webvtt?.items[ this.webvtt?.i ] &&
149
+ this?.sound?.playStarted &&
150
+ this?.sound?.audio?.context?.currentTime ){
151
+
152
+ const time = this.sound.audio.context.currentTime - this.sound.timeoffset;
153
+ let item = this.webvtt.items[ this.webvtt.i ]
154
+ if( !item.seen && time > item.start.ts ){
155
+ this.subtitle.setHTML(`
156
+ ${ item.who ? `<b>${item.who}:</b><br/>` : '' }
157
+ ${item.text.replace(/\n/g,'<br/>')}
158
+ `)
159
+ if( room.hyperlink && item.href && !item.seen ){
160
+ room.hyperlink.execute(item.href, {portalActivateDelay:6000})
161
+ }
162
+ item.seen = true
163
+ }
164
+ if( item.seen && time > item.stop.ts ){
165
+ this.subtitle.visible = false
166
+ this.webvtt.i++
167
+ }
168
+ }
169
+ this.update.id = false
170
+ }
171
+ this.update.id = setTimeout( () => advance(), 100 ) // throttle
172
+ }
173
+
174
+ // naive webvtt parser
175
+ parseWEBVTT(text) {
176
+ let i = 0;
177
+ const WEBVTT_HEADER = /^WEBVTT/i;
178
+ const TIME_LINE = /^([0-9:.]+)\s+-->\s+([0-9:.]+)(?:\s+(.+))?$/;
179
+ const WHO = /^<v ([^>]+)>/
180
+ const HREF = / href:([^ ]+)/
181
+ const result = { type: "WEBVTT", items: [], i: 0 };
182
+ const lines = text.split(/\r?\n/)
183
+ .map( (line) => line.trim() )
184
+ const vttToSeconds = (vttString) => {
185
+ const parts = vttString.split(':');
186
+ let hours = 0, minutes = 0, secondsWithMs;
187
+ if (parts.length === 3) { [hours, minutes, secondsWithMs] = parts; }
188
+ else { [minutes, secondsWithMs] = parts; } // Format is MM:SS.mmm
189
+ const [seconds, milliseconds] = secondsWithMs.split('.');
190
+ return (
191
+ (parseInt(hours) * 3600) +
192
+ (parseInt(minutes) * 60) +
193
+ (parseInt(seconds) * 1) +
194
+ parseInt(milliseconds || 0)
195
+ );
196
+ }
197
+
198
+ // Skip the WEBVTT header line
199
+ if (WEBVTT_HEADER.test(lines[i].trim())) i++
200
+ while (i < lines.length) {
201
+ if ( lines[i] === "") { i++; continue; } // Skip empty lines
202
+ // Match cue timing line
203
+ const timeMatch = lines[i].match(TIME_LINE);
204
+ if (timeMatch) {
205
+ const item = {
206
+ start: {str:timeMatch[1], ts: vttToSeconds(timeMatch[1]) },
207
+ stop: {str:timeMatch[2], ts: vttToSeconds(timeMatch[2]) },
208
+ href: lines[i].match(HREF) ? lines[i].match(HREF)[1] : false,
209
+ who: false,
210
+ text: ""
211
+ };
212
+ i++;
213
+ const speakerMatch = lines[i]?.match(WHO);
214
+ if (speakerMatch) {
215
+ item.who = speakerMatch[1];
216
+ lines[i] = lines[i].replace(WHO,'');
217
+ }
218
+ // Collect all text lines until next empty line
219
+ while (i < lines.length && lines[i].trim() !== "") {
220
+ item.text += (item.text ? "\n" : "") + lines[i] ;
221
+ i++;
222
+ }
223
+
224
+ result.items.push(item);
225
+ } else { i++; }
226
+ }
227
+ return result;
228
+ }
229
+
230
+ start(){
231
+ // reset subtitles
232
+ if( this?.webvtt?.items ){
233
+ this.webvtt.items.map( (item) => item.seen = undefined )
234
+ this.webvtt.i = 0;
235
+ }
236
+ // start sound
237
+ this.sound.pos = '0 0 0'
238
+ if( this.sound.isPlaying ) this.sound.stop()
239
+ this.sound.timeoffset = undefined
240
+ this.sound.play()
241
+ // ensure subtitle
242
+ this.subtitle.visible = false
243
+ player.add(this.subtitle)
244
+ // hide button
245
+ this.btn.visible = false
246
+ this.btn.pickable = false
247
+ // mark playing
248
+ this.playing = true
249
+ }
250
+
251
+
252
+ stop(){
253
+ this.playing = false
254
+ this.update.id = false
255
+ this.subtitle.visible = false
256
+ this.subtitle.text = ''
257
+ this.btn.visible = true
258
+ this.btn.pickable = true
259
+ }
260
+
261
+ cleanup(){
262
+ const deleteJsId = (js_id) => {
263
+ for( let i in player.children )
264
+ if( player.children[i].js_id == js_id )
265
+ player.remove( player.children[i] )
266
+ }
267
+ deleteJsId('subtitle')
268
+ }
269
+
270
+ })
271
+ });
272
+
273
+ xrf_install_sidecarfiles = function(){
274
+ if( !room.objects?.scene?.modelasset?.loaded ) {
275
+ return setTimeout( xrf_install_sidecarfiles, 300 )
276
+ }
277
+ if( !room.sidecarfile ) room.sidecarfile = new elation.janusweb.sidecarfile(room);
278
+ else{
279
+ room.sidecarfile.positionStartButton() // wait for user being spawned
280
+ room.sidecarfile.stop()
281
+ }
282
+ }
283
+
284
+ xrf_install_sidecarfiles()
285
+
286
+ elation.events.add(null, 'room_load_complete', xrf_install_sidecarfiles )
287
+ elation.events.add(null, 'room_enable', xrf_install_sidecarfiles )
288
+ elation.events.add(null, 'janusweb_script_frame', function(){
289
+ if( room?.sidecarfile ) room.sidecarfile.update()
290
+ })
291
+ elation.events.add(null, 'room_load_start', function(e){
292
+ if( !e.data ) return
293
+ if( room?.sidecarfile?.subtitle ) room.sidecarfile.subtitle.setHTML(`<div class='loading'>🔗 ${e.data.name}<br/><br/>please wait..</div>`)
294
+ })
295
+
296
+ elation.events.add(null, 'room_disable', function(){
297
+ if( room?.sidecarfile?.subtitle ) room.sidecarfile.stop()
298
+ })
299
+
300
+ // some convenience WebVTT cue settings => room function mappings
301
+ // href:#fadeAudioOut&spawnhere => room.fadeAudioOut()
302
+ // href:#myfunc=3 => room.myfunc(3)
303
+ elation.events.add(null, 'href', function(e){
304
+ if( room?.hyperlink ){
305
+ const {url,hash} = room.hyperlink.getUrlObject(e.data.href)
306
+ hash.forEach( (v,k) => { if( room[k] ) room[k](v) })
307
+ }
308
+ })