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,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.updateTexture},
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},
@@ -11,14 +45,40 @@ elation.require(['janusweb.janusbase'], function() {
11
45
  cull_face: { type: 'string', default: 'back', set: this.updateMaterial },
12
46
  css: {type: 'string', set: this.updateTexture },
13
47
  depth_write: { type: 'boolean', default: true },
48
+ transparent: {type: 'boolean', set: this.updateTexture },
14
49
  depth_test: { type: 'boolean', default: true },
15
50
  collision_id: { type: 'string', default: 'cube' },
16
51
  collision_scale: { type: 'vector3', default: V(.5, .5, .02) },
17
52
  shadow: { type: 'boolean', default: true, set: this.updateMaterial },
18
53
  shadow_receive: { type: 'boolean', default: true, set: this.updateMaterial, comment: 'Receive shadows from self and other objects' },
19
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)' },
20
62
  });
21
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
+
22
82
  this.createObject3D = function() {
23
83
  var material = this.createMaterial();
24
84
  var geo = new THREE.PlaneGeometry(2,2);
@@ -26,6 +86,14 @@ elation.require(['janusweb.janusbase'], function() {
26
86
  var mesh = new THREE.Mesh(geo, material);
27
87
  mesh.castShadow = this.shadow && this.shadow_cast;
28
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
+
29
97
  return mesh;
30
98
  }
31
99
  this.createForces = function() {
@@ -36,9 +104,9 @@ elation.require(['janusweb.janusbase'], function() {
36
104
  this.createMaterial = function() {
37
105
  var texture = this.createTexture();
38
106
  var sidemap = {
39
- 'back': THREE.FrontSide,
107
+ 'back': THREE.FrontSide,
40
108
  'front': THREE.BackSide,
41
- 'none': THREE.DoubleSide
109
+ 'none': THREE.DoubleSide
42
110
  };
43
111
  var matargs = {
44
112
  color: 0xffffff,
@@ -57,26 +125,28 @@ elation.require(['janusweb.janusbase'], function() {
57
125
  }
58
126
  this.createTexture = function() {
59
127
  this.canvas = document.createElement('canvas');
60
- this.canvas.width = 1024;
61
- this.canvas.height = 1024;
128
+ this.canvas.width = this.width;
129
+ this.canvas.height = this.height;
62
130
  this.texture = new THREE.Texture(this.canvas);
63
131
  this.texture.encoding = THREE.sRGBEncoding;
64
- this.updateTexture();
132
+ if( this.text ) this.updateTexture();
65
133
  return this.texture;
66
134
  }
67
135
  this.updateTexture = function() {
68
136
  if (!this.canvas || !this.texture) return;
69
137
  var ctx = this.canvas.getContext('2d'),
70
138
  texture = this.texture;
71
- this.canvas.width = 1024;
72
- this.canvas.height = 1024;
139
+ this.canvas.width = this.width;
140
+ this.canvas.height = this.height;
141
+
73
142
  var text_col = '#' + this.text_col.getHexString(),
74
143
  back_col = 'rgba(' + (this.back_col.r * 255) + ', ' + (this.back_col.g * 255) + ', ' + (this.back_col.b * 255) + ', ' + this.back_alpha + ')';
75
144
  var basestyle = 'font-family: sans-serif;' +
76
145
  'font-size: ' + this.font_size + 'px;' +
146
+ 'box-sizing: border-box;' +
77
147
  'color: ' + text_col + ';' +
78
- 'background: ' + back_col + ';' +
79
- 'max-width: 1014px;' +
148
+ 'background: ' + (this.transparent ? 'transparent' : back_col) + ';' +
149
+ `max-width: ${this.width}px;` +
80
150
  'padding: 5px;';
81
151
 
82
152
  // We need to sanitize our HTML in case someone provides us with malformed markup.
@@ -85,12 +155,25 @@ elation.require(['janusweb.janusbase'], function() {
85
155
  // styled divs instead.
86
156
 
87
157
  var sanitarydiv = document.createElement('div');
88
- sanitarydiv.innerHTML = this.text;
158
+ sanitarydiv.innerHTML = this.html;
89
159
  var content = sanitarydiv.innerHTML.replace(/<br\s*\/?>/g, '<div class="br"></div>');
90
160
  content = content.replace(/<hr\s*\/?>/g, '<div class="hr"></div>');
91
161
  content = content.replace(/<img(.*?)>/g, "<img$1 />");
92
162
 
93
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
+
94
177
  if (this.css) {
95
178
  styletag += this.css;
96
179
  }
@@ -146,9 +229,109 @@ elation.require(['janusweb.janusbase'], function() {
146
229
  shadow: [ 'property', 'shadow' ],
147
230
  shadow_receive: [ 'property', 'shadow_receive' ],
148
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'],
149
239
  };
150
240
  }
151
241
  return this._proxyobject;
152
242
  }
243
+
244
+ this.toInlineHTML = async function(){
245
+ try{
246
+ // find all img tags and capture src attributes
247
+ const imgRegex = /<img[^>]*src=["'](?!data:)([^"']+)["']/gi;
248
+ const matches = [];
249
+ let match;
250
+ while ((match = imgRegex.exec(this.html)) !== null) { // Collect all src values
251
+ matches.push(match[1]);
252
+ }
253
+ // fill array with base64 image-strings
254
+ const dataURLs = await Promise.all( matches.map(src => this.toDataURL(src)) );
255
+ // Replace each src in the html with the corresponding data URL
256
+ let updatedHtml = this.html;
257
+ matches.forEach((src, i) => {
258
+ updatedHtml = updatedHtml.replace( `src="${src}"`, `src="${dataURLs[i]}"`)
259
+ });
260
+ this.html = updatedHtml
261
+ }catch(e){ console.error(e) } // continue when inlining failed
262
+ };
263
+
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
+ }
334
+ }
335
+
153
336
  }, elation.engine.things.janusbase);
154
337
  });
@@ -56,7 +56,7 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
56
56
  elation.events.add(this.engine.client.view.container, 'touchcancel', elation.bind(this, this.handleTouchEnd));
57
57
 
58
58
  this.controlstate2 = this.engine.systems.controls.addContext('janusplayer', {
59
- 'toggle_view': ['keyboard_nomod_v,keyboard_shift_v', elation.bind(this, this.toggleCamera)],
59
+ 'toggle_view': ['keyboard_nomod_v,keyboard_shift_v', ev => { if (ev.value == 1) this.toggleCamera() }],
60
60
  'zoom_out': ['mouse_wheel_down', ev => this.zoomView(-1, ev)],
61
61
  'zoom_in': ['mouse_wheel_up', ev => this.zoomView(1, ev)],
62
62
  //'browse_back': ['gamepad_any_button_4', elation.bind(this, this.browseBack)],
@@ -945,9 +945,9 @@ document.body.dispatchEvent(click);
945
945
  */
946
946
  this.pickable = false;
947
947
  this.setCollider('capsule', {
948
- radius: this.collision_radius,
949
- length: 1,
950
- offset: V(0, this.collision_radius, 0)
948
+ radius: this.fatness,
949
+ length: this.height - (this.fatness * 2),
950
+ offset: V(0, this.fatness, 0)
951
951
  });
952
952
  } else {
953
953
  this.removeCollider();
@@ -1230,8 +1230,11 @@ document.body.dispatchEvent(click);
1230
1230
  this.defaultanimation = 'portal';
1231
1231
  setTimeout(() => this.defaultanimation = 'idle', 3000);
1232
1232
  }
1233
- this.toggleCamera = function(ev) {
1234
- if (ev.value == 1) {
1233
+ this.toggleCamera = function(arg) {
1234
+ if (typeof arg == 'string') {
1235
+ this.setCameraView(arg);
1236
+ } else if (!arg || arg.value) {
1237
+ // Legacy handling - if arg is a control input event, check to make sure this is a button press, not a release
1235
1238
  if (this.cameraview == 'firstperson') {
1236
1239
  this.setCameraView('thirdperson');
1237
1240
  } else {
package/scripts/room.js CHANGED
@@ -3,19 +3,25 @@ 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'
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
- function initRoomTranslators() {
10
- roomTranslators = {
9
+ function initRoomTranslators(room) {
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}),
14
19
  '^dat:': elation.janusweb.translators.dat({janus: janus}),
15
20
  '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: janus}),
16
21
  '^error$': elation.janusweb.translators.error({janus: janus}),
17
- '^default$': elation.janusweb.translators.default({janus: janus})
18
- };
22
+ '.*\.(gltf|glb|dae)$': elation.janusweb.translators.xrfragments({janus: janus}),
23
+ '^default$': elation.janusweb.translators.default({janus: janus}),
24
+ }
19
25
  }
20
26
  elation.component.add('engine.things.janusroom', function() {
21
27
  this.postinit = function() {
@@ -100,7 +106,7 @@ elation.require([
100
106
  });
101
107
 
102
108
  if (!roomTranslators) {
103
- initRoomTranslators();
109
+ initRoomTranslators(this);
104
110
  }
105
111
 
106
112
  this.spawnpoint = new THREE.Object3D();
@@ -246,7 +252,8 @@ elation.require([
246
252
  orientation = spawnpoint.orientation;
247
253
  }
248
254
  var player = this.engine.client.player;
249
- if (player.parent !== this) {
255
+ if( pos.equals(player.position) ) return // ignore
256
+ if (player.parent.id !== this.id) {
250
257
  // Reparent player to the room if necessary
251
258
  this.appendChild(player.getProxyObject());
252
259
  }
@@ -297,15 +304,25 @@ elation.require([
297
304
  spawnpoint.orientation.multiply(node.orientation);
298
305
  }
299
306
  spawnpoint.orientation.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0))); // Flip 180 degrees from portal orientation
300
- break;
307
+ return spawnpoint;
301
308
  }
302
309
  }
303
- } else if (this.urlhash) {
304
- let obj = this.getObjectById(this.urlhash);
305
- if (obj) {
306
- obj.localToWorld(spawnpoint.position.set(0,0,0));
307
- 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))));
308
- }
310
+ }
311
+ if (this.urlhash) {
312
+ // XR Fragments deeplink spec (Level1: URL) https://xrfragment.org/#teleport%20camera
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}});
309
326
  }
310
327
  return spawnpoint;
311
328
  }
@@ -559,23 +576,28 @@ elation.require([
559
576
  this.debugwindow.show();
560
577
  this.debugwindow.center();
561
578
  }
562
- this.load = function(url, baseurloverride) {
563
- if (!url) {
564
- url = this.properties.url;
565
- } else {
566
- this.properties.url = url;
567
- }
568
- var baseurl = baseurloverride;
579
+
580
+ this.getBaseUrl = function(url, baseurloverride){
581
+ let baseurl = baseurloverride;
569
582
  if (!baseurl) {
570
- if (url && !this.baseurl) {
583
+ if (url && url.match("://") && !this.baseurl) {
571
584
  baseurl = url.split('/');
572
585
  if (baseurl.length > 3) baseurl.pop();
573
586
  baseurl = baseurl.join('/') + '/';
574
- this.baseurl = baseurl;
575
587
  }
588
+ return baseurl || document.location.origin + document.location.pathname;
576
589
  } else {
577
- this.baseurl = baseurl;
590
+ return baseurl;
578
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)
579
601
 
580
602
  this.jsobjects = {};
581
603
  this.cookies = {};
@@ -586,14 +608,7 @@ elation.require([
586
608
  this.setTitle('loading...');
587
609
 
588
610
  var proxyurl = this.corsproxy || '';
589
-
590
- var fullurl = url;
591
- if (fullurl[0] == '/' && fullurl[1] != '/') fullurl = this.baseurl + fullurl;
592
- if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) {
593
- fullurl = self.location.origin + fullurl;
594
- } else if (!fullurl.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) && fullurl.indexOf(document.location.origin) != 0) {
595
- fullurl = proxyurl + fullurl;
596
- }
611
+ var fullurl = this.getFullRoomURL(url, proxyurl )
597
612
 
598
613
  elation.events.fire({element: this, type: 'room_load_queued'});
599
614
 
@@ -706,11 +721,22 @@ elation.require([
706
721
  }
707
722
  }));
708
723
  }
709
- this.loadFromSource = function(sourcecode, baseurl) {
724
+ this.loadFromSource = async function(sourcecode, baseurl) {
710
725
  if (baseurl) {
711
726
  this.baseurl = baseurl;
712
727
  }
713
- var source = this.parseSource(sourcecode);
728
+
729
+ var source
730
+ // scan sourcecode for non-JML microformats within translators
731
+ for( m in roomTranslators ){
732
+ let rt = roomTranslators[m]
733
+ if( rt.parseSource && rt.parseSource.regex ){
734
+ source = await rt.parseSource(sourcecode,this)
735
+ if( source ) break;
736
+ }
737
+ }
738
+ if( !source ) source = this.parseSource(sourcecode)
739
+
714
740
  if (source && source.source) {
715
741
  this.roomsrc = source.source;
716
742
  var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
@@ -1642,6 +1668,8 @@ elation.require([
1642
1668
  assetlist.push({
1643
1669
  assettype: 'shader',
1644
1670
  name: args.id,
1671
+ shadertype: args.shadertype || 'default',
1672
+ hasalpha: args.hasalpha,
1645
1673
  fragment_src: args.src,
1646
1674
  vertex_src: args.vertex_src,
1647
1675
  uniforms: args.uniforms,
@@ -1955,8 +1983,19 @@ elation.require([
1955
1983
  */
1956
1984
 
1957
1985
  }
1958
- this.getObjectById = function(js_id) {
1959
- return this.jsobjects[js_id];
1986
+ this.getObjectByDeepName = function(name) {
1987
+ if( !this.janus.currentroom ) return
1988
+ let obj = this.janus.currentroom.objects['3d'].getObjectByName(name)
1989
+ if( obj ){ // return polyglot THREE/janusweb object for convenience
1990
+ return new Proxy(obj,{
1991
+ set(me,k,v){ obj[k] = v; return true; },
1992
+ get(me,k){
1993
+ if( k == 'objects' ) return { '3d': me }
1994
+ return obj[k];
1995
+ }
1996
+ })
1997
+ }
1998
+ return obj
1960
1999
  }
1961
2000
  this.getObjectsByClassName = function(classname) {
1962
2001
  var objects = [];
@@ -2079,7 +2118,7 @@ elation.require([
2079
2118
  }
2080
2119
  var urls = [this.url];
2081
2120
  for (var i = 0; i < assets.length; i++) {
2082
- var url = assets[i].getFullURL();
2121
+ var url = assets[i].getFullRoomURL();
2083
2122
  urls.push(url);
2084
2123
  }
2085
2124
  return urls;
@@ -2804,14 +2843,22 @@ console.log('dispatch to parent', event, this, event.target);
2804
2843
  this.handleDelayedSound = function(ev) {
2805
2844
  // TODO - implement some visual indicator that this room is trying to play sound but was temporarily blocked
2806
2845
  }
2807
- this.getFullRoomURL = function(url) {
2808
- if (!url) url = this.url;
2809
- if (url[0] == '/') {
2810
- url = this.baseurl.replace(/^(https?:\/\/[^\/]+)\/.*$/, '$1') + url;
2811
- } else if (!url.match(/:\/\//i)) {
2812
- 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;
2813
2857
  }
2814
- 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
2815
2862
  }
2816
2863
  this.dispose = function() {
2817
2864
  if (this.assetpack) {
package/scripts/sound.js CHANGED
@@ -65,7 +65,7 @@ elation.require(['janusweb.janusbase'], function() {
65
65
  }
66
66
  //this.audio.panner.maxDistance = this.properties.distance;
67
67
  if (this.dist) {
68
- this.audio.setRefDistance(this.dist);
68
+ this.audio.setRefDistance(Math.max(0, this.dist));
69
69
  } else {
70
70
  //this.audio.panner.distanceModel = 'linear';
71
71
  }
package/scripts/text.js CHANGED
@@ -11,7 +11,7 @@ elation.require(['engine.things.label'], function() {
11
11
  'align': { type: 'string', default: 'center', refreshGeometry: true },
12
12
  'verticalalign': { type: 'string', default: 'bottom', refreshGeometry: true },
13
13
  'zalign': { type: 'string', default: 'back', refreshGeometry: true },
14
- 'emissive': { type: 'color', default: 0x000000 },
14
+ 'emissive': { type: 'color', default: 0x000000, set: this.updateMaterial },
15
15
  'opacity': { type: 'float', default: 1.0 },
16
16
  'depth_write': { type: 'bool', default: true },
17
17
  'depth_test': { type: 'bool', default: true },
@@ -120,6 +120,7 @@ elation.require(['engine.things.label'], function() {
120
120
  this.material.metalness = this.metalness;
121
121
  this.material.envMap = this.getEnvmap();
122
122
  }
123
+ this.material.emissive = this.emissive;
123
124
  this.material.needsUpdate = true;
124
125
  }
125
126
  }
@@ -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
+ })();