janusweb 1.7.0 → 1.7.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.
Files changed (32) hide show
  1. package/.github/workflows/deploy.yml +71 -0
  2. package/Dockerfile +14 -0
  3. package/README.md +210 -7
  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 +3 -0
  8. package/media/assets/webui/apps/navigation/navigation.js +1 -1
  9. package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
  10. package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
  11. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
  12. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
  13. package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
  14. package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
  15. package/media/assets/webui/default.json +1 -0
  16. package/media/images/portal.svg +130 -0
  17. package/media/index.html +48 -1
  18. package/package.json +2 -2
  19. package/scripts/client.js +24 -0
  20. package/scripts/config.js +12 -1
  21. package/scripts/janusbase.js +1 -0
  22. package/scripts/janusparagraph.js +169 -22
  23. package/scripts/janusxrplayer.js +4 -6
  24. package/scripts/room.js +59 -36
  25. package/scripts/translators/paragraph/html-xml-rss.js +47 -0
  26. package/scripts/translators/peertube.js +89 -0
  27. package/scripts/translators/xrfragments.js +5 -5
  28. package/tests/room/paragraph.xml +47 -0
  29. package/utils/build.sh +1 -0
  30. package/utils/dev-link.sh +75 -0
  31. package/utils/release.binary.sh +36 -0
  32. package/utils/release.docker.sh +11 -0
@@ -1,9 +1,43 @@
1
- elation.require(['janusweb.janusbase'], function() {
1
+ /*
2
+ * <paragraph> text-to-texture translator. The builtin XMLtranslator allows:
3
+ *
4
+ * 1. inline html : <paragraph><![CDATA[ <b>hello world</b> ]]></paragraph>
5
+ * 2. inline html (partial) : <paragraph selector="span"><![CDATA[ <b>hello <span>world</span></b> ]]></paragraph>
6
+ * 3. external url : <paragraph url="https://janusxr.org"/>
7
+ * 4. external url (partials) : <paragraph url="https://janusxr.org" selector=".infoText"/>
8
+ * 5. janusroom container html: <paragraph selector=".someclass"/>
9
+ * 6. janusroom container xml : <paragraph selector="tag subtag title"/>
10
+ * 7. RSS : <paragraph url="https://my.org/foo.rss" selector="item description"/>
11
+ * 8. XML : <paragraph url="https://my.org/bar.xml" selector="title"/>
12
+ * 9. any data- or selector-format via 'paragraph_translator' event
13
+ *
14
+ * EXAMPLE TRANSLATOR:
15
+ *
16
+ * room.addEventListener("paragraph_translator", function(e){
17
+ * const {translator,paragraph} = e.detail
18
+ * // now you can override the default translator
19
+ * translator.fetch = async (url) => this.text = "a response" // my custom fetch function
20
+ * translator.translate = async ( ) => ["<h1>head</h1>",this.text] // my x2html translator
21
+ * }
22
+ *
23
+ * NOTES:
24
+ * 1. By default, selectors are done via `document.querySelectorAll`
25
+ * 2. in case of multiple results:
26
+ * 2.1 clicking the paragraph with cycle through them
27
+ * 2.2 setting cycle="3000" allows for (2sec per item) slideshows
28
+ * 2.3 setting `rooms.objects.myparagraph.index++` cycles to next item
29
+ * 3. width/height-attributes offers finer control of texture size
30
+ * 4. ttl-attributes allows finer control of webrequest-cache (default 2 mins)
31
+ * 5. setting `rooms.object.myparagraph.selector = "#section2"' allows for statemanagement
32
+ * 6. separation of `this.text` (for JML) and `this.html` (for texture) is necessary (to not break JML/glb exports)
33
+ */
34
+
35
+ elation.require(['janusweb.janusbase','janusweb.translators.paragraph.html-xml-rss'], function() {
2
36
  elation.component.add('engine.things.janusparagraph', function() {
3
37
  this.postinit = function() {
4
38
  elation.engine.things.janusparagraph.extendclass.postinit.call(this);
5
39
  this.defineProperties({
6
- text: {type: 'string', default: '', set: this.toInlineHTML },
40
+ text: {type: 'string', default: '', set: this.textToHTML },
7
41
  font_size: {type: 'integer', default: 16, set: this.updateTexture},
8
42
  text_col: {type: 'color', default: 0x000000, set: this.updateTexture},
9
43
  back_col: {type: 'color', default: 0xffffff, set: this.updateTexture},
@@ -18,8 +52,33 @@ elation.require(['janusweb.janusbase'], function() {
18
52
  shadow: { type: 'boolean', default: true, set: this.updateMaterial },
19
53
  shadow_receive: { type: 'boolean', default: true, set: this.updateMaterial, comment: 'Receive shadows from self and other objects' },
20
54
  shadow_cast: { type: 'boolean', default: true, set: this.updateMaterial, comment: 'Cast shadows onto self and other objects' },
55
+ url: { type: 'string', default: '', set: this.fetchSource },
56
+ selector: { type: 'string', default: '', set: this.fetchSource },
57
+ index: { type: 'integer', default: 0, set: this.updateHTML },
58
+ cycle: {type: 'integer', default: 0, set: this.updateHTML },
59
+ width: { type: 'integer', default: 1024 },
60
+ height: { type: 'integer', default: 1024 },
61
+ ttl: { type: 'integer', default: 120000, comment: 'expire texture/html after ms (default:2 mins)' },
21
62
  });
22
63
  }
64
+
65
+ this.paragraphs = [] // for cycle / partial purposes
66
+
67
+ this.textToHTML = function(){
68
+ // IMPORTANT: skip infinite loop
69
+ if( this.html == this.text ) return
70
+ if( this.text ){
71
+ if( this.html ){ // reset stuff
72
+ this.url = this.selector = ''
73
+ this.cycle = this.indexes = 0;
74
+ }
75
+ this.fetchSource() // expect 'selector' not being set here
76
+ }else{
77
+ this.html = ''
78
+ this.updateTexture()
79
+ }
80
+ }
81
+
23
82
  this.createObject3D = function() {
24
83
  var material = this.createMaterial();
25
84
  var geo = new THREE.PlaneGeometry(2,2);
@@ -27,6 +86,14 @@ elation.require(['janusweb.janusbase'], function() {
27
86
  var mesh = new THREE.Mesh(geo, material);
28
87
  mesh.castShadow = this.shadow && this.shadow_cast;
29
88
  mesh.receiveShadow = this.shadow && this.shadow_receive;
89
+
90
+ elation.events.add(this, 'click', () => {
91
+ if( this.indexes ){
92
+ this.index++
93
+ this.cycle = 0 // disable
94
+ }
95
+ })
96
+
30
97
  return mesh;
31
98
  }
32
99
  this.createForces = function() {
@@ -37,9 +104,9 @@ elation.require(['janusweb.janusbase'], function() {
37
104
  this.createMaterial = function() {
38
105
  var texture = this.createTexture();
39
106
  var sidemap = {
40
- 'back': THREE.FrontSide,
107
+ 'back': THREE.FrontSide,
41
108
  'front': THREE.BackSide,
42
- 'none': THREE.DoubleSide
109
+ 'none': THREE.DoubleSide
43
110
  };
44
111
  var matargs = {
45
112
  color: 0xffffff,
@@ -58,26 +125,28 @@ elation.require(['janusweb.janusbase'], function() {
58
125
  }
59
126
  this.createTexture = function() {
60
127
  this.canvas = document.createElement('canvas');
61
- this.canvas.width = 1024;
62
- this.canvas.height = 1024;
128
+ this.canvas.width = this.width;
129
+ this.canvas.height = this.height;
63
130
  this.texture = new THREE.Texture(this.canvas);
64
131
  this.texture.encoding = THREE.sRGBEncoding;
65
- this.updateTexture();
132
+ if( this.text ) this.updateTexture();
66
133
  return this.texture;
67
134
  }
68
135
  this.updateTexture = function() {
69
136
  if (!this.canvas || !this.texture) return;
70
137
  var ctx = this.canvas.getContext('2d'),
71
138
  texture = this.texture;
72
- this.canvas.width = 1024;
73
- this.canvas.height = 1024;
139
+ this.canvas.width = this.width;
140
+ this.canvas.height = this.height;
141
+
74
142
  var text_col = '#' + this.text_col.getHexString(),
75
143
  back_col = 'rgba(' + (this.back_col.r * 255) + ', ' + (this.back_col.g * 255) + ', ' + (this.back_col.b * 255) + ', ' + this.back_alpha + ')';
76
144
  var basestyle = 'font-family: sans-serif;' +
77
145
  'font-size: ' + this.font_size + 'px;' +
146
+ 'box-sizing: border-box;' +
78
147
  'color: ' + text_col + ';' +
79
148
  'background: ' + (this.transparent ? 'transparent' : back_col) + ';' +
80
- 'max-width: 1014px;' +
149
+ `max-width: ${this.width}px;` +
81
150
  'padding: 5px;';
82
151
 
83
152
  // We need to sanitize our HTML in case someone provides us with malformed markup.
@@ -92,6 +161,19 @@ elation.require(['janusweb.janusbase'], function() {
92
161
  content = content.replace(/<img(.*?)>/g, "<img$1 />");
93
162
 
94
163
  var styletag = '<style>.paragraphcontainer { ' + basestyle + '} .br { height: 1em; } .hr { margin: .5em 0; border: 1px inset #ccc; height: 0px; }';
164
+ styletag += 'a { color:unset; text-decoration: none; }' // dont confuse users with nonclickable links
165
+
166
+
167
+
168
+ // hybrid 2D/3D styling: apply styles from container HTML if any
169
+ styletag += [ ...(new DOMParser)
170
+ .parseFromString(room.fullsource,"text/html")
171
+ .querySelectorAll("style[type=\"text/css\"]")
172
+ ]
173
+ .map( (el) => el.innerText )
174
+ .join("\n")
175
+
176
+
95
177
  if (this.css) {
96
178
  styletag += this.css;
97
179
  }
@@ -147,13 +229,19 @@ elation.require(['janusweb.janusbase'], function() {
147
229
  shadow: [ 'property', 'shadow' ],
148
230
  shadow_receive: [ 'property', 'shadow_receive' ],
149
231
  shadow_cast: [ 'property', 'shadow_cast' ],
232
+ url: [ 'property', 'url' ],
233
+ selector: [ 'property', 'selector' ],
234
+ index: ['property','index'],
235
+ cycle: ['property','cycle'],
236
+ width: ['property','width'],
237
+ height: ['property','height'],
238
+ ttl: ['property','ttl'],
150
239
  };
151
240
  }
152
241
  return this._proxyobject;
153
242
  }
154
243
 
155
244
  this.toInlineHTML = async function(){
156
- this.html = this.text
157
245
  try{
158
246
  // find all img tags and capture src attributes
159
247
  const imgRegex = /<img[^>]*src=["'](?!data:)([^"']+)["']/gi;
@@ -171,19 +259,78 @@ elation.require(['janusweb.janusbase'], function() {
171
259
  });
172
260
  this.html = updatedHtml
173
261
  }catch(e){ console.error(e) } // continue when inlining failed
174
- this.updateTexture()
175
262
  };
176
263
 
177
- this.toDataURL = function(url){
178
- const finalUrl = `${elation.engine.assets.corsproxy||''}${url}`
179
- return fetch(finalUrl)
180
- .then(response => response.blob())
181
- .then(blob => new Promise((resolve, reject) => {
182
- const reader = new FileReader()
183
- reader.onloadend = () => resolve(reader.result)
184
- reader.onerror = reject
185
- reader.readAsDataURL(blob)
186
- }))
264
+ this.toDataURL = async function(url){
265
+ const fullUrl = this.room.getFullRoomURL(url)
266
+ const finalUrl = this.room.isLocal( fullUrl) ? url : `${elation.engine.assets.corsproxy||''}${url}`
267
+ return await this.useCache(url, async () =>
268
+ await fetch(finalUrl)
269
+ .then(response => response.blob())
270
+ .then(blob => new Promise((resolve, reject) => {
271
+ const reader = new FileReader()
272
+ reader.onloadend = () => resolve(reader.result)
273
+ reader.onerror = reject
274
+ reader.readAsDataURL(blob)
275
+ }))
276
+ )
277
+ }
278
+
279
+ this.fetchSource = async function(){
280
+ if( this.url && this.fetchSource.last == this.url ) return // avoid fake updates
281
+ this.fetchSource.last = this.url || this.fetchSource.last
282
+
283
+ let translator = { // fallback translator for non-url content
284
+ translate: async () => [this.html],
285
+ fetch: async (uri) => {
286
+ if( String(this.text).match(/^data:text/) ){ // support text data-uri's
287
+ return await window.fetch(this.text).then( (r) => r.text() )
288
+ }
289
+ return this.text || room.fullsource
290
+ }
291
+ }
292
+ // allow (via event) other scripts to select different translator based on url
293
+ room.dispatchEvent({type:'paragraph_translator', detail:{ paragraph:this, translator }})
294
+ this.html = await translator.fetch.call(this, this.url || ' ')
295
+ await this.toInlineHTML()
296
+ this.paragraphs = await translator.translate.apply(this)
297
+ await this.updateHTML()
298
+ }
299
+
300
+ this.useCache = async function(url, myFetch) {
301
+ const cache = elation.janusweb.urlcache = elation.janusweb.urlcache || {}
302
+ if (cache[url]) return cache[url];
303
+ const data = cache[url] = await myFetch();
304
+ setTimeout(() => delete cache[url], this.ttl); // default 2 mins TTL cleanup
305
+ return data;
306
+ },
307
+
308
+ this.autoRotateIndex = function(){
309
+ if( this.paragraphs.length > 1 ){
310
+ if( this.cycle == 0 ){
311
+ clearInterval(this.intervalId )
312
+ this.intervalId = false
313
+ }
314
+ if( !this.intervalId && this.cycle ){
315
+ this.intervalId = setInterval( () => {
316
+ this.index = (this.index+1) % this.indexes
317
+ this.html = this.paragraphs[ this.index ]
318
+ }, this.cycle )
319
+ }
320
+ }
321
+ }
322
+
323
+ this.updateHTML = async function(html){
324
+ // skip uninited url content
325
+ if( !this.html && this.url ) return
326
+ if( this.html ){
327
+ this.indexes = this.paragraphs.length > 1 ? this.paragraphs.length : 1
328
+ this.html = this.paragraphs[ this.index % this.indexes ]
329
+ if( !this.html || this.htmlLast == this.html ) return // skip fake updates
330
+ this.htmlLast = this.html
331
+ this.autoRotateIndex()
332
+ this.updateTexture()
333
+ }
187
334
  }
188
335
 
189
336
  }, elation.engine.things.janusbase);
@@ -412,7 +412,7 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
412
412
  this.buttonwaspressed = false;
413
413
  if (!this.menu.parent || this.menu.parent === this) {
414
414
  player.appendChild(this.menu);
415
- player.worldToLocal(this.pointer.localToWorld(this.menu.pos.set(0.1,-0.07,-.2)));
415
+ player.worldToLocal(this.pointer.localToWorld(this.menu.pos.set(0,0,-.25)));
416
416
  this.menu.billboard = 'y';
417
417
  } else {
418
418
  //this.appendChild(this.menu);
@@ -656,9 +656,8 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
656
656
  }
657
657
  },
658
658
  handleRaycastEnter(ev) {
659
- let proxyobj = ev.data.object
660
- if( !proxyobj || !proxyobj?._target ) return
661
- obj = proxyobj._target;
659
+ let proxyobj = ev.data.object,
660
+ obj = proxyobj._target;
662
661
  let oldactiveobject = this.activeobject;
663
662
  if (this.activeobject) {
664
663
  this.engine.client.view.proxyEvent({
@@ -724,7 +723,6 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
724
723
  this.engine.client.view.proxyEvent(evdata, evdata.element);
725
724
  },
726
725
  handleRaycastMove(ev) {
727
- if( ! ev?.data?.intersection?.face?.normal ) return // skip those!
728
726
  this.updateLaserEndpoint(ev.data.intersection.point, ev.data.intersection.face.normal);
729
727
  if (this.activeobject) {
730
728
  let obj = this.activeobject.objects['3d'];
@@ -781,7 +779,7 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
781
779
  }
782
780
  },
783
781
  handleSelect(ev) {
784
- if (this.activeobject && this.laser.visible ) {
782
+ if (this.activeobject) {
785
783
  let obj = this.activeobject.objects['3d'];
786
784
  console.log('CLICK IT', this.activeobject);
787
785
  this.engine.client.view.proxyEvent({
package/scripts/room.js CHANGED
@@ -3,11 +3,16 @@ elation.require([
3
3
  'engine.things.generic', 'engine.things.label', 'engine.things.skybox',
4
4
  'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.janusparagraph',
5
5
  'janusweb.sound', 'janusweb.januslight', 'janusweb.janusparticle', 'janusweb.janusghost',
6
- 'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs', 'janusweb.translators.xrfragments'
6
+ 'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs', 'janusweb.translators.xrfragments', 'janusweb.translators.peertube'
7
7
  ], function() {
8
8
  let roomTranslators = false;
9
9
  function initRoomTranslators(room) {
10
- roomTranslators = {
10
+
11
+ room.translators = roomTranslators = {
12
+
13
+
14
+ '^peertube$': elation.janusweb.translators.peertube({janus: janus}),
15
+
11
16
  '^janus-vfs:': elation.janusweb.translators.janusvfs({janus: janus}),
12
17
  '^about:blank$': elation.janusweb.translators.blank({janus: janus}),
13
18
  '^bookmarks$': elation.janusweb.translators.bookmarks({janus: janus}),
@@ -15,7 +20,7 @@ elation.require([
15
20
  '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: janus}),
16
21
  '^error$': elation.janusweb.translators.error({janus: janus}),
17
22
  '.*\.(gltf|glb|dae)$': elation.janusweb.translators.xrfragments({janus: janus}),
18
- '^default$': elation.janusweb.translators.default({janus: janus})
23
+ '^default$': elation.janusweb.translators.default({janus: janus}),
19
24
  }
20
25
  }
21
26
  elation.component.add('engine.things.janusroom', function() {
@@ -247,7 +252,8 @@ elation.require([
247
252
  orientation = spawnpoint.orientation;
248
253
  }
249
254
  var player = this.engine.client.player;
250
- if (player.parent !== this) {
255
+ if( pos.equals(player.position) ) return // ignore
256
+ if (player.parent.id !== this.id) {
251
257
  // Reparent player to the room if necessary
252
258
  this.appendChild(player.getProxyObject());
253
259
  }
@@ -298,16 +304,25 @@ elation.require([
298
304
  spawnpoint.orientation.multiply(node.orientation);
299
305
  }
300
306
  spawnpoint.orientation.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0))); // Flip 180 degrees from portal orientation
301
- break;
307
+ return spawnpoint;
302
308
  }
303
309
  }
304
- } else if (this.urlhash) {
310
+ }
311
+ if (this.urlhash) {
305
312
  // XR Fragments deeplink spec (Level1: URL) https://xrfragment.org/#teleport%20camera
306
- let obj = this.getObjectById(this.urlhash) || this.getObjectByDeepName(this.urlhash)
307
- if (obj) {
308
- obj.localToWorld(spawnpoint.position.set(0,0,0));
309
- spawnpoint.orientation.setFromRotationMatrix(obj.objects['3d'].matrixWorld.lookAt(spawnpoint.position, obj.localToWorld(V(0,0,-1)), obj.localToWorld(V(0,1,0).sub(spawnpoint.position))));
310
- }
313
+ // backwards-compat: pos-names are deprecated
314
+ let obj
315
+ new URLSearchParams( this.urlhash.replace(/pos=/,'') ).forEach( (v,name) => {
316
+ let obj = this.getObjectById(name) || this.getObjectByDeepName(name)
317
+ if (obj) {
318
+ obj.localToWorld(spawnpoint.position.set(0,0,0));
319
+ if( obj.type == 'PerspectiveCamera' ){
320
+ spawnpoint.position.y -= 1.6 // https://xrfragment.org/#teleport%20camera%20spawnpoint
321
+ }
322
+ spawnpoint.orientation.setFromRotationMatrix(obj.objects['3d'].matrixWorld.lookAt(spawnpoint.position, obj.localToWorld(V(0,0,-1)), obj.localToWorld(V(0,1,0).sub(spawnpoint.position))));
323
+ }
324
+ })
325
+ if( obj ) elation.events.fire({element: this, type: 'href', data: {href,opts}});
311
326
  }
312
327
  return spawnpoint;
313
328
  }
@@ -561,23 +576,28 @@ elation.require([
561
576
  this.debugwindow.show();
562
577
  this.debugwindow.center();
563
578
  }
564
- this.load = function(url, baseurloverride) {
565
- if (!url) {
566
- url = this.properties.url;
567
- } else {
568
- this.properties.url = url;
569
- }
570
- var baseurl = baseurloverride;
579
+
580
+ this.getBaseUrl = function(url, baseurloverride){
581
+ let baseurl = baseurloverride;
571
582
  if (!baseurl) {
572
583
  if (url && url.match("://") && !this.baseurl) {
573
584
  baseurl = url.split('/');
574
585
  if (baseurl.length > 3) baseurl.pop();
575
586
  baseurl = baseurl.join('/') + '/';
576
587
  }
577
- this.baseurl = baseurl || "";
588
+ return baseurl || document.location.origin + document.location.pathname;
578
589
  } else {
579
- this.baseurl = baseurl;
590
+ return baseurl;
580
591
  }
592
+ }
593
+
594
+ this.load = function(url, baseurloverride) {
595
+ if (!url) {
596
+ url = this.properties.url;
597
+ } else {
598
+ this.properties.url = url;
599
+ }
600
+ this.baseurl = this.getBaseUrl(url,baseurloverride)
581
601
 
582
602
  this.jsobjects = {};
583
603
  this.cookies = {};
@@ -588,14 +608,7 @@ elation.require([
588
608
  this.setTitle('loading...');
589
609
 
590
610
  var proxyurl = this.corsproxy || '';
591
-
592
- var fullurl = url;
593
- if (fullurl[0] == '/' && fullurl[1] != '/') fullurl = this.baseurl + fullurl;
594
- if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) {
595
- fullurl = self.location.origin + (fullurl[0] == '/' ? '' :'/') + fullurl;
596
- } else if (!fullurl.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) && fullurl.indexOf(document.location.origin) != 0) {
597
- fullurl = proxyurl + fullurl;
598
- }
611
+ var fullurl = this.getFullRoomURL(url, proxyurl )
599
612
 
600
613
  elation.events.fire({element: this, type: 'room_load_queued'});
601
614
 
@@ -1655,6 +1668,8 @@ elation.require([
1655
1668
  assetlist.push({
1656
1669
  assettype: 'shader',
1657
1670
  name: args.id,
1671
+ shadertype: args.shadertype || 'default',
1672
+ hasalpha: args.hasalpha,
1658
1673
  fragment_src: args.src,
1659
1674
  vertex_src: args.vertex_src,
1660
1675
  uniforms: args.uniforms,
@@ -2103,7 +2118,7 @@ elation.require([
2103
2118
  }
2104
2119
  var urls = [this.url];
2105
2120
  for (var i = 0; i < assets.length; i++) {
2106
- var url = assets[i].getFullURL();
2121
+ var url = assets[i].getFullRoomURL();
2107
2122
  urls.push(url);
2108
2123
  }
2109
2124
  return urls;
@@ -2828,14 +2843,22 @@ console.log('dispatch to parent', event, this, event.target);
2828
2843
  this.handleDelayedSound = function(ev) {
2829
2844
  // TODO - implement some visual indicator that this room is trying to play sound but was temporarily blocked
2830
2845
  }
2831
- this.getFullRoomURL = function(url) {
2832
- if (!url) url = this.url;
2833
- if (url[0] == '/') {
2834
- url = this.baseurl.replace(/^(https?:\/\/[^\/]+)\/.*$/, '$1') + url;
2835
- } else if (!url.match(/:\/\//i)) {
2836
- url = this.baseurl + url;
2846
+ this.getFullRoomURL = function(url, proxyurl) {
2847
+ let fullurl = url;
2848
+ if (fullurl[0] == '/' && fullurl[1] != '/'){
2849
+ fullurl = this.baseurl.replace(/^(https?:\/\/[^\/]+)\/.*$/, '$1') + fullurl;
2850
+ }else if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) {
2851
+ fullurl = this.baseurl + (
2852
+ fullurl.match(/^\.\//) ? fullurl.replace(/^\.\//,'') // './index.html' e.g.
2853
+ : fullurl
2854
+ )
2855
+ } else if (proxyurl && !this.isLocal(fullurl) ){
2856
+ fullurl = proxyurl + fullurl;
2837
2857
  }
2838
- return url;
2858
+ return fullurl
2859
+ }
2860
+ this.isLocal = function(url){
2861
+ return url.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) || url.indexOf(document.location.origin) == 0
2839
2862
  }
2840
2863
  this.dispose = function() {
2841
2864
  if (this.assetpack) {
@@ -0,0 +1,47 @@
1
+ // this translator converts the following url-output to HTML:
2
+ // * HTML
3
+ // * XML
4
+ // * RSS
5
+ //
6
+ // NOTE1: to not bloat the core-codebase please define custom translators
7
+ // in assetscripts or webui apps. Thank you.
8
+ //
9
+ // NOTE2: 'this' is basically a paragraph janus object
10
+
11
+ (function(){
12
+
13
+ let XMLTranslator = {
14
+ fetch: async function(uri){
15
+ const uriExFragment = String(uri).replace(/#.*/,'')
16
+ const finalUrl = `${elation.engine.assets.corsproxy||''}${uriExFragment||''}`
17
+ return await this.useCache( finalUrl, async () =>
18
+ await fetch( finalUrl )
19
+ .then( (res) => res.text() )
20
+ )
21
+ },
22
+ translate: async function(){ // generic XML/RSS/HTML preprocessor [this.text to this.html]
23
+ if( !this.html ) return [""]
24
+ if( this.selector && typeof this.index != 'undefined' ){
25
+ const type = this.html.match(/<(rss|feed)/) ? "text/xml" : "text/html"
26
+ const parser = new DOMParser()
27
+ const xmlDoc = parser.parseFromString( this.html, type)
28
+ let paragraphs = [ ...xmlDoc.querySelectorAll(this.selector) ] // KiSS JML: CSS selectors level 1
29
+ if( !paragraphs.length ){
30
+ console.error(`paragraph: level1 css selector '${this.selector}' not matching anything`)
31
+ }
32
+ return paragraphs.map( (p) => type == 'text/xml' ? p.textContent : p.outerHTML )
33
+ }
34
+ return [this.html]
35
+ }
36
+ }
37
+
38
+ elation.events.add(null, 'paragraph_translator', function(e){
39
+ const {translator,paragraph} = e.detail
40
+ // now you can override the default translator
41
+ if( String(paragraph.url).match(/^http/) ){
42
+ translator.fetch = XMLTranslator.fetch
43
+ }
44
+ translator.translate = XMLTranslator.translate // boldly always set ourselfs as default
45
+ })
46
+
47
+ })();
@@ -0,0 +1,89 @@
1
+ elation.require([], function() {
2
+ elation.component.add('janusweb.translators.peertube', function() {
3
+ this.init = function() {
4
+ this.description = "peertube website to 3D translator"
5
+ }
6
+ this.exec = function(args) {
7
+ return new Promise(elation.bind(this, function(resolve, reject) {
8
+
9
+ var room = this.room = args.room;
10
+
11
+ reject() // for now
12
+
13
+ this.setupEvents()
14
+ var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
15
+
16
+ var roomdata = {
17
+ assets: {
18
+ assetlist: [
19
+ // {assettype: 'model', name: 'scene', src: args.url},
20
+ ]
21
+ },
22
+ room: {
23
+ pos: [0,0,0],
24
+ xdir: "1 0 0",
25
+ zdir: "0 0 1",
26
+ },
27
+ object: [
28
+ // {id: 'scene', js_id: 0, pos: "0 0 0", xdir: "-1 0 0", zdir: "0 0 -1", lighting: "false"}
29
+ ],
30
+ link: []
31
+ };
32
+ resolve(roomdata);
33
+ }));
34
+ }
35
+
36
+ // translate XR Fragments microformat into JML
37
+ this.parseSource = async function(sourcecode, room){
38
+ this.room = room
39
+ const isJML = /<fireboxroom>[\s\S]*?<\/fireboxroom>/si;
40
+ const isPeertube = /<meta\s+[^>]*property=['"]og:platform['"]\s+[^>]*content=['"]peertube['"][^>]*\/?>/si;
41
+ const isPeertubeVideo = /<meta\s+[^>]*property=['"]og:type['"]\s+[^>]*content=['"]video['"][^>]*\/?>/si;
42
+ const isPeertubeInstance = /<meta\s+[^>]*property=['"]og:type['"]\s+[^>]*content=['"]website['"][^>]*\/?>/si;
43
+ const is = {
44
+ JML: sourcecode.match(isJML),
45
+ peertube: sourcecode.match(isPeertube),
46
+ peertubeVideo: sourcecode.match(isPeertubeVideo),
47
+ peertubeInstance: sourcecode.match(isPeertubeInstance),
48
+ }
49
+ if( is.JML || !is.peertube ) return // JML takes precedence over microformats
50
+ if( !is.peertubeVideo && !is.peertubeInstance ) return
51
+
52
+ // ok..peertube time: get domtree
53
+ let el = document.createElement("div")
54
+ el.innerHTML = sourcecode
55
+
56
+ // try to fetch title + cover
57
+ const title = el.querySelector("title")
58
+ let logo = el.querySelector("meta[property='og:image']")?.getAttribute("content")
59
+ logo = logo ? logo : el.querySelector("meta[property='og:image:url']")?.getAttribute("content")
60
+
61
+ let url = new URL( String(room.url) )
62
+ let instance = url?.origin
63
+ let search = url?.search ? new URLSearchParams(url.search).get("search") : ""
64
+
65
+ // return JML
66
+ return room.parseSource(`
67
+ <title>${ title ? title.innerText.replace(/\n.*/g,'') : room.baseurl.split("/").pop() }</title>
68
+ <FireBoxRoom>
69
+ <Assets>
70
+ ${ logo ? `<assetimage src="${logo}" id="logo"/>` : ''}
71
+ <assetscript src="https://codeberg.org/coderofsalvation/janus-script-peertube/raw/branch/master/build/janus-script-peertube.js"/>
72
+ <assetobject src="https://codeberg.org/coderofsalvation/janus-script-peertube/raw/branch/master/build/asset/button.glb" id="button"/>
73
+ <assetobject src="https://codeberg.org/coderofsalvation/janus-script-peertube/raw/branch/master/build/asset/button.icon.glb" id="icon"/>
74
+ <assetsound src="https://codeberg.org/coderofsalvation/janus-script-peertube/raw/branch/master/build/asset/button.click.mp3" id="button-click"/>
75
+ <assetsound src="https://codeberg.org/coderofsalvation/janus-script-peertube/raw/branch/master/build/asset/button.hover.mp3" id="button-hover"/>
76
+ </Assets>
77
+ <Room use_local_asset="room_2pedestal" pos="1.15 0 -5" rotation="0 1.5708 0" fwd="1 0 0">
78
+ ${ is.peertubeInstance ? `<peertube-app pos="4.55 2.71 -4.7" scale="2 2 1" rotation="0 90 0" js_id="mypeertube-app" instance="${instance}" ${search?`search="${search}"`:``} autoload="true"/>`
79
+ : `<peertube pos="4.55 2.71 -4.7" scale="2 2 1" rotation="0 90 0" js_id="peertube" src="${room.url}" auto_play="true"/>`
80
+ }
81
+ ${ logo ? `<image id="logo"/>` : ''}
82
+ </Room>
83
+ </FireBoxRoom>
84
+ `)
85
+ }
86
+ // peertube microformat heuristic: <meta property="og:platform" content="PeerTube">
87
+ this.parseSource.regex = /<meta\s+[^>]*property=['"]og:platform['"]\s+[^>]*content=['"]peertube['"][^>]*\/?>/si;
88
+ });
89
+ });
@@ -18,12 +18,13 @@ elation.require([], function() {
18
18
  ]
19
19
  },
20
20
  room: {
21
+ gravity: 0,
21
22
  pos: [0,0,0],
22
23
  xdir: "1 0 0",
23
24
  zdir: "0 0 1",
24
25
  },
25
26
  object: [
26
- {id: 'scene', js_id: 0, pos: "0 0 0", xdir: "-1 0 0", zdir: "0 0 -1", lighting: "false"}
27
+ {id: 'scene', js_id: "scene", pos: "0 0 0", xdir: "-1 0 0", zdir: "0 0 -1"}
27
28
  ],
28
29
  link: []
29
30
  };
@@ -34,8 +35,8 @@ elation.require([], function() {
34
35
  this.spawnUserAtFragment = function(source) {
35
36
  // XR Fragments deeplink spec: explicit or default spawn
36
37
  // https://xrfragment.org/#teleport%20camera
37
- if( ! this.room.urlhash ) this.room.urlhash = 'spawn'
38
- this.room.setPlayerPosition.apply(this.room)
38
+ if( ! room.urlhash ) room.urlhash = 'spawn'
39
+ room.setPlayerPosition.apply(room)
39
40
  console.log("[xrfragment] camera teleport")
40
41
  }
41
42
 
@@ -78,7 +79,7 @@ elation.require([], function() {
78
79
  <Assets>
79
80
  <assetobject id="scene" src="${hrefNoHash}"/>
80
81
  </Assets>
81
- <Room>
82
+ <Room gravity="0">
82
83
  <object pos="0 0 0" collision_id="scene" id="scene" />
83
84
  </Room>
84
85
  </FireBoxRoom>
@@ -90,4 +91,3 @@ elation.require([], function() {
90
91
 
91
92
  });
92
93
  });
93
-