diff --git a/src/janusweb/index.html b/src/janusweb/index.html
new file mode 100644
index 0000000..0280133
--- /dev/null
+++ b/src/janusweb/index.html
@@ -0,0 +1,77 @@
+
+
+
+ JanusWeb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/janusweb/media/assets.json b/src/janusweb/media/assets.json
new file mode 100644
index 0000000..ddb3d0e
--- /dev/null
+++ b/src/janusweb/media/assets.json
@@ -0,0 +1,133 @@
+[
+ { "assettype":"model", "name":"capsule", "src":"assets/primitives/capsule.obj" },
+ { "assettype":"model", "name":"cone", "src":"assets/primitives/cone.obj" },
+ { "assettype":"model", "name":"cube", "src":"assets/primitives/cube.obj" },
+ { "assettype":"model", "name":"cylinder", "src":"assets/primitives/cylinder.obj" },
+ { "assettype":"model", "name":"pipe", "src":"assets/primitives/pipe.obj" },
+ { "assettype":"model", "name":"plane", "src":"assets/primitives/plane.obj" },
+ { "assettype":"model", "name":"pyramid", "src":"assets/primitives/pyramid.obj" },
+ { "assettype":"model", "name":"sphere", "src":"assets/primitives/sphere.obj" },
+ { "assettype":"model", "name":"torus", "src":"assets/primitives/torus.obj" },
+
+ { "assettype":"image", "name":"room1_tex0", "src":"assets/rooms/room1.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room2_tex0", "src":"assets/rooms/room2.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room3_tex0", "src":"assets/rooms/room3.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room4_tex0", "src":"assets/rooms/room4.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room5_tex0", "src":"assets/rooms/room5.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room6_tex0", "src":"assets/rooms/room6.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_1pedestal_tex0", "src":"assets/rooms/room_1pedestal.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_2pedestal_tex0", "src":"assets/rooms/room_2pedestal.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_3_narrow_tex0", "src":"assets/rooms/room_3_narrow.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_3_wide_tex0", "src":"assets/rooms/room_3_wide.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_4_narrow_tex0", "src":"assets/rooms/room_4_narrow.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_4_wide_tex0", "src":"assets/rooms/room_4_wide.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_box_small_tex0", "src":"assets/rooms/room_box_small.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_box_medium_tex0", "src":"assets/rooms/room_box_medium.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room1_new_tex0", "src":"assets/rooms/room1_new.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_home_tex0", "src":"assets/rooms/room_home.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_plane_tex0", "src":"assets/rooms/tile_d.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"room_plane2_tex0", "src":"assets/rooms/room_plane2.png", "hasalpha": false, "preload": false },
+
+ { "assettype":"model", "name":"room1", "src":"assets/rooms/room1.obj", "tex0":"room1_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room2", "src":"assets/rooms/room2.obj", "tex0":"room2_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room3", "src":"assets/rooms/room3.obj", "tex0":"room3_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room4", "src":"assets/rooms/room4.obj", "tex0":"room4_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room5", "src":"assets/rooms/room5.obj", "tex0":"room5_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room6", "src":"assets/rooms/room6.obj", "tex0":"room6_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_1pedestal", "src":"assets/rooms/room_1pedestal.obj", "tex0":"room_1pedestal_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_2pedestal", "src":"assets/rooms/room_2pedestal.obj", "tex0":"room_2pedestal_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_3_narrow", "src":"assets/rooms/room_3_narrow.obj", "tex0":"room_3_narrow_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_3_wide", "src":"assets/rooms/room_3_wide.obj", "tex0":"room_3_wide_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_4_narrow", "src":"assets/rooms/room_4_narrow.obj", "tex0":"room_4_narrow_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_4_wide", "src":"assets/rooms/room_4_wide.obj", "tex0":"room_4_wide_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_box_small", "src":"assets/rooms/room_box_small.obj", "tex0":"room_box_small_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_box_medium", "src":"assets/rooms/room_box_medium.obj", "tex0":"room_box_medium_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room1_new", "src":"assets/rooms/room1_new.obj", "tex0":"room1_new_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_home", "src":"assets/rooms/room_home.obj", "tex0":"room_home_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_plane", "src":"assets/rooms/room_plane.obj", "tex0":"room_plane_tex0", "hasalpha": false },
+ { "assettype":"model", "name":"room_plane2", "src":"assets/rooms/room_plane2.obj", "tex0":"room_plane2_tex0", "hasalpha": false },
+
+ { "assettype":"model", "name":"room1_collision", "src":"assets/rooms/room1_collision.obj" },
+ { "assettype":"model", "name":"room2_collision", "src":"assets/rooms/room2_collision.obj" },
+ { "assettype":"model", "name":"room3_collision", "src":"assets/rooms/room3_collision.obj" },
+ { "assettype":"model", "name":"room4_collision", "src":"assets/rooms/room4_collision.obj" },
+ { "assettype":"model", "name":"room5_collision", "src":"assets/rooms/room5_collision.obj" },
+ { "assettype":"model", "name":"room6_collision", "src":"assets/rooms/room6_collision.obj" },
+ { "assettype":"model", "name":"room_1pedestal_collision", "src":"assets/rooms/room_1pedestal_collision.obj" },
+ { "assettype":"model", "name":"room_2pedestal_collision", "src":"assets/rooms/room_2pedestal_collision.obj" },
+ { "assettype":"model", "name":"room_3_narrow_collision", "src":"assets/rooms/room_3_narrow_collision.obj" },
+ { "assettype":"model", "name":"room_3_wide_collision", "src":"assets/rooms/room_3_wide_collision.obj" },
+ { "assettype":"model", "name":"room_4_narrow_collision", "src":"assets/rooms/room_4_narrow_collision.obj" },
+ { "assettype":"model", "name":"room_4_wide_collision", "src":"assets/rooms/room_4_wide_collision.obj" },
+ { "assettype":"model", "name":"room_box_small_collision", "src":"assets/rooms/room_box_small_collision.obj" },
+ { "assettype":"model", "name":"room_box_medium_collision", "src":"assets/rooms/room_box_medium_collision.obj" },
+ { "assettype":"model", "name":"room1_new_collision", "src":"assets/rooms/room1_new_collision.obj" },
+ { "assettype":"model", "name":"room_home_collision", "src":"assets/rooms/room_home_collision.obj" },
+ { "assettype":"model", "name":"room_plane_collision", "src":"assets/rooms/room_plane_collision.obj" },
+ { "assettype":"model", "name":"room_plane2_collision", "src":"assets/rooms/room_plane2_collision.obj" },
+
+ { "assettype":"image", "name":"black", "src":"assets/black.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"clear", "src":"assets/clear.png", "hasalpha": true, "preload": false },
+ { "assettype":"image", "name":"dark_back", "src":"assets/skybox/dark_back.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dark_down", "src":"assets/skybox/dark_down.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dark_front", "src":"assets/skybox/dark_front.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dark_left", "src":"assets/skybox/dark_left.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dark_right", "src":"assets/skybox/dark_right.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dark_up", "src":"assets/skybox/dark_up.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_back", "src":"assets/skybox/miramar_back.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_down", "src":"assets/skybox/miramar_down.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_front", "src":"assets/skybox/miramar_front.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_left", "src":"assets/skybox/miramar_left.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_right", "src":"assets/skybox/miramar_right.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"miramar_up", "src":"assets/skybox/miramar_up.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_back", "src":"assets/skybox/skyrender_back.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_down", "src":"assets/skybox/skyrender_down.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_front", "src":"assets/skybox/skyrender_front.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_left", "src":"assets/skybox/skyrender_left.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_right", "src":"assets/skybox/skyrender_right.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"skyrender_up", "src":"assets/skybox/skyrender_up.png", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_back", "src":"assets/skybox/Day/Dayback.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_down", "src":"assets/skybox/Day/Daybottom.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_front", "src":"assets/skybox/Day/Dayfront.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_left", "src":"assets/skybox/Day/Dayleft.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_right", "src":"assets/skybox/Day/Dayright.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"dayskybox_up", "src":"assets/skybox/Day/Daytop.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_back", "src":"assets/skybox/Night/Nightback.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_down", "src":"assets/skybox/Night/Nightbottom.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_front", "src":"assets/skybox/Night/Nightfront.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_left", "src":"assets/skybox/Night/Nightleft.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_right", "src":"assets/skybox/Night/Nightright.jpg", "hasalpha": false, "preload": false },
+ { "assettype":"image", "name":"nightskybox_up", "src":"assets/skybox/Night/Nighttop.jpg", "hasalpha": false, "preload": false },
+
+ { "assettype":"model", "name":"avatar", "src":"assets/avatar.obj", "tex0": "avatar_tex0" },
+ { "assettype":"model", "name":"avatar_head", "src":"assets/avatar_head.obj", "tex0": "avatar_head_tex0" },
+ { "assettype":"model", "name":"avatar_gltf", "src":"assets/janus-avatar-base.glb" },
+ { "assettype":"model", "name":"avatar_animations", "src":"assets/janus-avatar-animations.glb" },
+
+ { "assettype":"image", "name":"avatar_tex0", "src":"assets/avatar.png", "preload": false },
+ { "assettype":"image", "name":"avatar_head_tex0", "src":"assets/avatar_head.png", "preload": false },
+
+ { "assettype":"model", "name":"avatar_janusweb", "src":"assets/hoverscreen.obj" },
+
+ { "assettype":"model", "name":"loading", "src":"assets/loading.obj" },
+
+ { "assettype":"image", "name":"cursor_arrow", "src":"assets/cursor/arrow.png", "hasalpha": true },
+ { "assettype":"image", "name":"cursor_crosshair", "src":"assets/cursor/crosshair.png", "hasalpha": true },
+ { "assettype":"image", "name":"cursor_hand", "src":"assets/cursor/hand.png", "hasalpha": true },
+ { "assettype":"image", "name":"cursor_dot_inactive", "src":"assets/cursor/cursor-dot-white.png", "hasalpha": true },
+ { "assettype":"image", "name":"cursor_dot_active", "src":"assets/cursor/cursor-dot-green.png", "hasalpha": true },
+
+
+ { "assettype":"model", "name":"controller_daydream", "src":"assets/controllers/daydream/vr_controller_daydream.obj", "mtl":"vr_controller_daydream.mtl" },
+ { "assettype":"model", "name":"controller_vive", "src":"assets/controllers/vive/ViveController.dae.gz" },
+ { "assettype":"model", "name":"controller_touch_left", "src":"assets/controllers/oculus/TouchLeftModified.dae.gz" },
+ { "assettype":"model", "name":"controller_touch_right", "src":"assets/controllers/oculus/TouchRight.dae.gz" },
+ { "assettype":"font", "name":"helvetiker", "src":"assets/fonts/helvetiker_regular.typeface.json" },
+ { "assettype":"shader", "name":"defaultportal", "shadertype":"shadertoy","fragment_src":"assets/shaders/greenportal2.shadertoy.frag", "preload":true },
+
+ { "assettype":"model", "name":"button", "src":"component/janus-script-peertube/asset/button.glb", "preload":true},
+ { "assettype":"model", "name":"icon", "src":"component/janus-script-peertube/asset/button.icon.glb", "preload":true},
+ { "assettype":"sound", "name":"button-click", "src":"component/janus-script-peertube/asset/button.click.mp3", "preload":true},
+ { "assettype":"sound", "name":"button-hover", "src":"component/janus-script-peertube/asset/button.hover.mp3", "preload":true}
+
+]
diff --git a/src/janusweb/media/component/janus-script-peertube/asset/button.click.mp3 b/src/janusweb/media/component/janus-script-peertube/asset/button.click.mp3
new file mode 100644
index 0000000..852f5fd
Binary files /dev/null and b/src/janusweb/media/component/janus-script-peertube/asset/button.click.mp3 differ
diff --git a/src/janusweb/media/component/janus-script-peertube/asset/button.glb b/src/janusweb/media/component/janus-script-peertube/asset/button.glb
new file mode 100644
index 0000000..0c20185
Binary files /dev/null and b/src/janusweb/media/component/janus-script-peertube/asset/button.glb differ
diff --git a/src/janusweb/media/component/janus-script-peertube/asset/button.hover.mp3 b/src/janusweb/media/component/janus-script-peertube/asset/button.hover.mp3
new file mode 100644
index 0000000..949ee29
Binary files /dev/null and b/src/janusweb/media/component/janus-script-peertube/asset/button.hover.mp3 differ
diff --git a/src/janusweb/media/component/janus-script-peertube/asset/button.icon.glb b/src/janusweb/media/component/janus-script-peertube/asset/button.icon.glb
new file mode 100644
index 0000000..13818e2
Binary files /dev/null and b/src/janusweb/media/component/janus-script-peertube/asset/button.icon.glb differ
diff --git a/src/janusweb/media/component/janus-script-peertube/janus-script-peertube.js b/src/janusweb/media/component/janus-script-peertube/janus-script-peertube.js
new file mode 100644
index 0000000..48edb38
--- /dev/null
+++ b/src/janusweb/media/component/janus-script-peertube/janus-script-peertube.js
@@ -0,0 +1,1752 @@
+room.registerElement('pushbutton', {
+ width: .5,
+ length: .5,
+ height: .5,
+ clicking: false,
+ state: 'off',
+ accesskey: false,
+
+ onbuttondown: new CustomEvent('buttondown'),
+ onbuttonpress: new CustomEvent('buttonpress'),
+ onbuttonup: new CustomEvent('buttonup'),
+ onclick: new CustomEvent('click'),
+
+ createChildren: function() {
+ this.base = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: V(.6,.6,.6),
+ scale: V(this.length,this.height / 2,this.width),
+ pos: V(0,this.height/4,0)
+ }, this);
+ this.button = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: this.col,
+ scale: V(this.length * .8, this.height / 4, this.width * .8),
+ }, this);
+ this.sounds = {
+ clickin: room.createObject('Sound', { id: 'pushbutton-click-in', }, this),
+ clickon: room.createObject('Sound', { id: 'pushbutton-click-on', }, this),
+ clickoff: room.createObject('Sound', { id: 'pushbutton-click-off', }, this),
+ };
+
+ this.button.addEventListener('mousedown', this.onMouseDown);
+ window.addEventListener('mouseup', this.onMouseUp);
+ window.addEventListener('touchend', this.onMouseUp);
+ this.button.addEventListener('click', this.onClick);
+
+ if (this.accesskey) {
+ room.onKeyDown = this.onKeyDown;
+ room.onKeyUp = this.onKeyUp;
+ }
+
+ this.setButtonPos();
+ },
+ press: function() {
+ this.executeCallback(this.onbuttonpress);
+ },
+ onClick: function(ev) {
+ if (this.onclick) {
+ this.executeCallback(this.onclick);
+ }
+ },
+ onMouseDown: function(ev) {
+ this.button.sync = true;
+ this.clicking = true;
+ this.activate();
+
+ this.setState('in');
+ this.executeCallback(this.onbuttondown);
+
+ if (ev) {
+ ev.stopPropagation();
+ }
+ },
+ onMouseUp: function(ev) {
+ if (this.clicking) {
+ this.setState('off');
+ this.button.sync = true;
+ this.clicking = false;
+ this.executeCallback(this.onbuttonup);
+
+ if (ev) {
+ ev.stopPropagation();
+ }
+ }
+ },
+ onKeyDown: function(ev) {
+ if (ev.keyCode == this.accesskey) {
+ this.onMouseDown(ev);
+ }
+ },
+ onKeyUp: function(ev) {
+ if (ev.keyCode == this.accesskey) {
+ this.onMouseUp(ev);
+ }
+ },
+ setActive: function(active) {
+ this.setState(active ? 'on' : 'off');
+ },
+ setButtonPos: function(pos) {
+ if (typeof pos == 'undefined') pos = (this.active ? .6 : 0);
+ var percent = Math.min(1, Math.max(0, pos));
+ var defaultpos = this.height / 2;
+ this.button.pos = V(0, defaultpos - ((this.height / 8) * percent) + (this.height / 16), 0);
+ },
+ setState: function(state) {
+ if (state != this.state) {
+ this.state = state;
+ if (state == 'in') {
+ this.setButtonPos(1);
+ this.sounds.clickin.play();
+ } else if (state == 'on') {
+ this.setButtonPos(.6);
+ this.sounds.clickon.play();
+ } else if (state == 'off') {
+ this.setButtonPos(0);
+ this.sounds.clickoff.play();
+ }
+ }
+ }
+
+});
+
+room.registerElement('slider', {
+ grabbing: false,
+ value: 1,
+ min: 0,
+ max: 1,
+ length: 1,
+ width: .25,
+ height: .2,
+
+ ongrabstart: new CustomEvent('grabstart'),
+ onchange: new CustomEvent('change'),
+ ongrabend: new CustomEvent('grabend'),
+
+ createChildren: function() {
+ this.base = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: V(.6,.6,.6),
+ scale: V(this.length,this.height/2,this.width),
+ pos: V(0,this.height/4,0)
+ }, this);
+ this.track = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: V(.02,.02,.02),
+ scale: V(this.length * .9, this.height / 4, this.width / 5),
+ pos: V(0, this.base.pos.y + this.height / 4,0)
+ }, this);
+ this.handle = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: this.col,
+ scale: V(this.width / 4, this.height / 4, this.width * .75),
+ pos: V(0,this.base.pos.y,0)
+ }, this);
+ this.sounds = {
+ clickin: room.createObject('Sound', { id: 'pushbutton-click-in', }, this),
+ clickon: room.createObject('Sound', { id: 'pushbutton-click-on', }, this),
+ clickoff: room.createObject('Sound', { id: 'pushbutton-click-off', }, this),
+ };
+
+ this.handle.addEventListener('mousedown', this.onMouseDown);
+ this.track.addEventListener('mousedown', this.onMouseDown);
+ // FIXME - bind these in mousedown, for maximum efficiency
+ window.addEventListener('mouseup', this.onMouseUp);
+ window.addEventListener('touchend', this.onMouseUp);
+ window.addEventListener('mousemove', this.onMouseMove);
+ window.addEventListener('touchmove', this.onMouseMove);
+ this.handle.addEventListener('click', this.onClick);
+
+ this.setValue(this.value);
+ },
+ setValue: function(value, skipchange) {
+ var realvalue = Math.min(this.max, Math.max(this.min, value));
+ var percent = realvalue / (this.max - this.min);
+ var pos = (percent - .5) * this.length * .9;
+ this.handle.pos = V(pos, this.base.pos.y + this.height * 3 / 8 , 0);
+ this.handle.sync = true;
+ this.value = realvalue;
+
+ if (this.onchange && !skipchange) {
+ this.executeCallback(this.onchange);
+ }
+ },
+ updateValueFromCursorPos: function() {
+ // FIXME - vector proxies are currently broken
+ var sliderpos = this.parent.localToWorld(this.pos._target.clone());
+ var dist = player.head_pos.distanceTo(sliderpos);
+
+ // Cast a ray from my head to the cursor position
+ var dir = V(player.cursor_pos.x - player.head_pos.x, player.cursor_pos.y - player.head_pos.y, player.cursor_pos.z - player.head_pos.z);
+
+ // Then project the ray into the same plane as the slider I'm manipulating
+ var pos = translate(player.head_pos, scalarMultiply(normalized(dir), dist));
+ this.worldToLocal(pos);
+
+ // The slider position is now determined based on the x position (left/right) in the slider's own coordinate system, and the length of the slider
+ var foo = pos.x / (this.length * .9) + .5;
+
+ this.setValue(foo * (this.max - this.min));
+ },
+ onMouseDown: function(ev) {
+ this.grabbing = true;
+ this.updateValueFromCursorPos();
+ this.sounds.clickin.play();
+ if (this.ongrabstart) this.executeCallback(this.ongrabstart);
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+ onMouseMove: function(ev) {
+ if (this.grabbing) {
+ this.updateValueFromCursorPos();
+ ev.stopPropagation();
+ }
+ },
+ onMouseUp: function(ev) {
+ if (this.grabbing) {
+ this.grabbing = false;
+ this.sounds.clickoff.play();
+ if (this.ongrabend) this.executeCallback(this.ongrabend);
+ ev.stopPropagation();
+ }
+ },
+ onClick: function(ev) {
+ }
+
+});
+
+room.registerElement('togglebutton', {
+ width: .5,
+ length: .5,
+ height: .5,
+ clicking: false,
+ active: false,
+
+ onbuttondown: new CustomEvent('buttondown'),
+ onbuttonpress: new CustomEvent('buttonpress'),
+ onbuttonup: new CustomEvent('buttonup'),
+ onactivate: new CustomEvent('activate'),
+ ondeactivate: new CustomEvent('deactivate'),
+
+ createChildren: function() {
+ this.base = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: V(.6,.6,.6),
+ scale: V(this.length,this.height / 2,this.width),
+ pos: V(0,this.height/4,0)
+ }, this);
+ this.button = room.createObject('Object', {
+ id: 'cube',
+ collision_id: 'cube',
+ col: this.col,
+ scale: V(this.length * .8, this.height / 4, this.width * .8),
+ }, this);
+ this.sounds = {
+ clickin: this.createObject('Sound', { id: 'pushbutton-click-in', }),
+ clickon: this.createObject('Sound', { id: 'pushbutton-click-on', }),
+ clickoff: this.createObject('Sound', { id: 'pushbutton-click-off', }),
+ };
+
+ this.button.addEventListener('mousedown', this.onMouseDown);
+ window.addEventListener('mouseup', this.onMouseUp);
+ window.addEventListener('touchend', this.onMouseUp);
+ this.button.addEventListener('click', this.onClick);
+
+ this.setButtonPos();
+ },
+ press: function() {
+ this.active = !this.active;
+
+ if (this.active && this.onactivate) {
+ this.executeCallback(this.onactivate);
+ } else if (!this.active && this.ondeactivate) {
+ this.executeCallback(this.ondeactivate);
+ }
+ },
+ onClick: function(ev) {
+ if (this.onclick) {
+ this.executeCallback(this.onclick);
+ }
+ },
+ onMouseDown: function(ev) {
+ this.button.sync = true;
+ this.clicking = true;
+ this.press();
+
+ this.setState('in');
+
+ ev.stopPropagation();
+ },
+ onMouseUp: function(ev) {
+ if (this.clicking) {
+ if (this.active) {
+ this.setState('on');
+ } else {
+ this.setState('off');
+ }
+ this.button.sync = true;
+ this.clicking = false;
+
+ ev.stopPropagation();
+ }
+ },
+ setActive: function(active) {
+ this.active = active;
+ this.setState(active ? 'on' : 'off');
+ },
+ setButtonPos: function(pos) {
+ if (typeof pos == 'undefined') pos = (this.active ? .6 : 0);
+ var percent = Math.min(1, Math.max(0, pos));
+ var defaultpos = this.height / 2;
+ this.button.pos = V(0, defaultpos - ((this.height / 8) * percent) + (this.height / 16), 0);
+ },
+ setState: function(state) {
+ if (state != this.state) {
+ this.state = state;
+ if (state == 'in') {
+ this.setButtonPos(1);
+ this.sounds.clickin.play();
+ } else if (state == 'on') {
+ this.setButtonPos(.6);
+ this.sounds.clickon.play();
+ } else if (state == 'off') {
+ this.setButtonPos(0);
+ this.sounds.clickoff.play();
+ }
+ }
+ }
+
+});
+
+/*
+ * ## JJQuery
+ * see https://codeberg.org/coderofsalvation/janus-script-jjq
+ */
+
+$ = function(sel,opts){
+ out = $$(sel,opts)[0]
+ return out || false
+}
+
+$$ = function(sel, opts){
+ opts = opts || {}
+ if( !opts.in ) opts.in =['tag','js_id','name','id']
+ out = []
+ db = {children: room.objects}
+ if( typeof sel !== 'string' && !(sel instanceof RegExp) ){
+ if( sel?.length ) db.children = sel
+ if( sel?.children ) db.children = sel.children
+ sel = /.*/
+ }
+ if( opts?.from ) db.children = opts.from
+ $$.traverse( function(child){
+ for( let prop in opts.in ){
+ found = child[ opts.in[prop] ]
+ if( found ){
+ let match = ( typeof sel == 'string' ? String(found).toLowerCase() == sel.toLowerCase()
+ : String(found).match(sel) )
+ if( match && typeof child != 'function' ){
+ out.push( child )
+ break;
+ }
+ }
+ }
+ },db)
+ return $$.decorate(out)
+}
+
+$$.decorate = function(obj){
+ for( let i in $$ ) obj[i] = $$[i].bind(obj)
+ return obj
+}
+
+$$.each = function(cb){
+ this.map( cb )
+ return this
+}
+
+$$.traverse = function(cb,me){
+ me = me || room
+ if( !me.children ) return
+ for( let i in me.children ){
+ proceed = cb( me.children[i] )
+ if( proceed === false ) return
+ $$.traverse(cb, me.children[i] )
+ }
+ return this
+}
+
+$$.get = function(k, fallback){
+ if( !k ){ // return first object
+ return this[0] || false
+ }
+ res = []
+ this.each( function(el){
+ res.push(
+ $.get( el, k )
+ )
+ })
+ //res = res.filter( v => v != undefined) // delete empties
+
+ return $$.decorate(res)
+}
+
+$.get = function(el,path){
+ return new Function('el',`return el.${path.replace(/\./,'?.')}`)(el)
+}
+
+$.set = function set(obj, path, value) {
+ var last
+ var o = obj
+ path = String(path)
+ var vars = path.split(".")
+ var lastVar = vars[vars.length - 1]
+ vars.map(function(v) {
+ if (lastVar == v) return
+ o = (new Function("o","return o." + v)(o) || new Function("o","return o."+v+" = {}")(o))
+ last = v
+ })
+ new Function("o","v","o." + lastVar + " = v")(o, value)
+}
+
+$$.set = function(k,v,opts){
+
+ this.each( function(el){ $.set( el, k, v ) })
+ return this
+}
+
+$$.wrap = function wrap(obj, method, handler, context) {
+ var org = obj[method];
+ // Unpatch first if already patched.
+ if (org.unwrap) {
+ org = org.unwrap();
+ }
+ // Patch the function.
+ obj[method] = function() {
+ var ctx = context || obj;
+ var args = [].slice.call(arguments);
+ args.unshift(org.bind(ctx));
+ return handler.apply(ctx, args);
+ };
+ // Provide "unpatch" function.
+ obj[method].unwrap = function() {
+ obj[method] = org;
+ return org;
+ };
+ // Return the original.
+ return org;
+}
+
+$$.dispatchEvent = function(event){
+ this.each( (o) => {
+ if( o.dispatchEvent ){
+ o.dispatchEvent(event)
+ }
+ })
+ return this
+}
+
+$$.addEventListener = function(eventname, cb){
+ this.each( (o) => {
+ if( o.addEventListener ){
+ o.addEventListener(eventname, cb)
+ }
+ })
+ return this
+}
+
+$$.when = function( condition ){
+ res = this.filter( condition )
+ return $$.decorate(res)
+}
+
+$$.then = function( cb ){
+ this.each( cb )
+ return this
+}
+
+$$.proxy = function(extra){
+ let newtargets = []
+ this.each( (el) => newtargets.push( $.proxy(extra,el) ) )
+ return $$.decorate( newtargets )
+}
+
+$.proxy = (extra,el) => new Proxy(el,{
+ get(me,k){ return me[k]; },
+ set(me,k,v){
+ if( extra[k] ){
+ return extra[k].apply(me,[() => me[k] = v])
+ }else me[k] = v
+ return true
+ },
+})
+
+if( typeof room != 'undefined' ) room.registerElement("$$",{}) // just to notify dependencies
+/* tiny (recursive) tween function for Janus objects with auto-garbage collection
+ *
+ * usage: const speed = 0.05 // closer to zero == smoother/slower
+ * logo.addEventListener("tween_done", alert )
+ * logo.addEventListener("tween_prop_done", alert )
+ * tween( logo, {"pos.y": [-0.1,speed], "pos.x": [-0.05,speed]})
+ * room.update = $.tween.update // call tween.update() every frame
+ *
+ * dependency: https://codeberg.org/coderofsalvation/janus-script-jjq
+ */
+
+mountTween = function(){
+
+ $.tween = function(el, opts){
+ $.tween.remove = (el) => {
+ delete el.tween
+ el.tween = undefined
+ $.tween.items = $.tween.items.filter( (el) => el.tween ? el : false )
+ }
+ // This is our Linear Interpolation method. It takes 3 parameters:
+ // a: The starting value
+ // b: The destination value
+ // n: The normal value (between 0 and 1) to control the Linear Interpolation
+ $.tween.lerp = function(a, b, n) {
+ return (1 - n) * a + n * b;
+ }
+ $.tween.update = () => {
+ $.tween.items.map( (el) => {
+ for( let prop in el.tween ){
+ let diff = 0
+ const to = el.tween[prop][0]
+ const speed = el.tween[prop][1]
+ let children = 0
+ const lerp = (child) => {
+ if( $.get(child,prop) == undefined ) return
+ const v = $.tween.lerp( $.get(child,prop), to, speed )
+ $.set(child, prop, v)
+ diff += Math.abs(to-v)
+ children++
+ }
+ lerp(el)
+ $$(el).each( lerp )
+ // garbage collect
+ if( diff < $.tween.items.resolution ){
+ if( el.dispatchEvent ){
+ el.dispatchEvent({type:"tween_prop_done",prop,tween:el.tween})
+ }
+ delete el.tween[prop]
+ }
+ }
+ if( Object.keys(el.tween).length == 0 ){
+ $.tween.remove(el)
+ if( el.dispatchEvent ){
+ setTimeout( () => {
+ el.dispatchEvent({type:"tween_done",tween:el.tween})
+ },50)
+ }
+ }
+ })
+ }
+ $.tween.items = $.tween.items || []
+ $.tween.items.resolution = 0.01 // decides when to flush tweens
+
+ if( el.tween ) $.tween.remove(el) // overwrite previous
+ el.tween = opts
+ $.tween.items.push(el)
+ }
+
+}
+
+// be direct or patient
+if( typeof $$ != 'undefined' ) mountTween()
+else{
+ room.addEventListener("registerelement", function(e){
+ if( e.data == "$$") mountTween()
+ })
+}
+function register(){
+
+ if( register.triggered ) return // ignore on-the-fly-loaded assetscripts
+ register.triggered = true
+
+ room.registerElement('button', {
+ use:"button",
+ lighting: "false",
+ text: "",
+ locked:true,
+ wireframe: false,
+ scale: "1 1 1",
+ scale_back: "1 1 1",
+ text_scale: 0.6,
+ font_scale: false,
+ font_size: 0.08,
+ col: "0 0 0",
+ col_text: "1 1 1",
+
+ createChildren: function(){
+ if( this.billboard ){
+ console.warn("billboarding on multiple transparent dialogs will tank the framerate :/")
+ }
+ this.sounds = {
+ click: room.createObject('Sound', { id: 'button-click', }, this),
+ hover: room.createObject('Sound', { id: 'button-hover', }, this),
+ };
+ if( this.text ){
+ this.label = this.createObject('text',{
+ xdir: "-1 0 -0.000002",
+ locked:true,
+ pos: "0 -0.022 0.01",
+ scale: `${this.scale.x*this.text_scale} ${this.scale.y*this.text_scale} 0.0001`, // dereference
+ font_scale: this.font_scale,
+ font_size: this.font_size,
+ text: this.text,
+ col: `{this.col_text.r} ${this.col_text.g} ${this.col_text.b}` // dereference
+ })
+ }
+ this.button = this.createObject('object',{
+ id: this.use,
+ collision_id: 'button',
+ wireframe: this.wireframe,
+ col: `{this.col.r} ${this.col.g} ${this.col.b}`, // dereference
+ locked:true,
+ lighting: this.lighting ? "true" : "false",
+ scale: this.scale_back //`${this.scale_back.x} ${this.scale_back.y} ${this.scale_back.z}`
+ })
+
+ const onClick = () => {
+ this.sounds.click.play()
+ setTimeout( () => { this.button.visible = false }, 1 ) // wait for theme-change
+ setTimeout( () => { this.button.visible = true; }, 70 )
+ room.dispatchEvent({type:"dialog_button_click", button:this})
+ }
+ const onHover = () => {
+ this.sounds.hover.play()
+ room.dispatchEvent({type:"dialog_button_hover", button:this})
+ }
+ this.button.addEventListener('activate', onClick)
+ this.button.addEventListener('click', onClick)
+ this.button.addEventListener('gazeenter', onHover)
+ this.button.addEventListener('mouseover', onHover)
+ }
+
+ })
+
+ room.registerElement('dialog', {
+ "content_id":"",
+ "backportal": false,
+ "debug": false,
+ "theme_default": "light",
+ "reroute": true, // reroute window.alert e.g.
+ "billboard": false,
+ watch: "",
+
+ theme:{
+ "current":"dark",
+ "dark":{ /* generated from "light" via lightToDarkTheme() */ },
+ "light":{
+ css: (opts) => `
+ .paragraphcontainer{
+ max-width:unset;
+ min-height:unset;
+ text-align: center;
+ }
+ .br { height: 1em; }
+ .hr { margin: .5em 0; border: 1px inset #ccc; height: 0px; }
+ .paragraphcontent{
+ overflow:hidden;
+ text-align:left;
+ height: ${opts.height} !important; /* important for crossbrowser compatibility */
+ max-height: ${opts.height} !important; /* important for crossbrowser compatibility */
+ width: ${opts.width} !important; /* important for crossbrowser compatibility */
+ max-width: ${opts.width} !important; /* important for crossbrowser compatibility */
+ background:#FFFFFFCC;
+ border-radius:10px;
+ padding:7px 15px;
+ display:inline-block;
+ }
+ blockquote {
+ margin-left: 0;
+ padding: 5px 15px;
+ border-left: 4px solid #000;
+ }
+ pre {
+ font-face: monospace;
+ padding:15px;
+ margin:0;
+ background:#00000044;
+ border-radius:7px;
+ display:inline-block;
+ }
+ `,
+ jml: {
+ "*": {
+ },
+ object: {
+ back_alpha: 0
+ },
+ paragraph: {
+ scale: "2 2 2",
+ back_alpha: "0",
+ lighting: "false"
+ }
+ },
+ custom: (opts) => {
+ $$('text', opts.db).set('col', opts.invert ? '1 1 1' : '0 0 0')
+ $$('paragraph', opts.db).set('col', opts.invert ? '1 1 1' : '0 0 0')
+ $$('object', opts.db).set('col', opts.invert ? '0 0 0' : '1 1 1')
+ $$('object', opts.db).set('lighting', 'false')
+ $$('object', opts.db).set('transparent','false')
+ }
+ }
+ },
+
+ //onbuttondown: new CustomEvent('buttondown'),
+
+ getTheme: function(){
+ if( !room.theme ){ // setup proxy so we can listen to global themechanges
+ room.theme = new Proxy( this.theme,{
+ get(me,k){ return me[k] },
+ set(me,k,v){
+ me[k] = v
+ if( k == 'current' ){
+ $$('dialog').each( (dialog) => dialog.render() )
+ }
+ return true
+ }
+ })
+ room.dispatchEvent({type:"dialog_theme_init"})
+ }
+ return room.theme[ room.theme.current ]
+ },
+
+ createChildren: function() {
+ this.lightToDarkTheme() // generate extra dark theme
+ // attach theme to room.theme
+ this._theme = this.getTheme()
+ // set default theme
+ room.theme.current = this.theme_default
+
+ // adjust for billboard
+ this.xdir = this.billboard ? "1 0 0" : "-1 0 -0.000002"
+ this.zdir = this.billboard ? "0 0 1" : "0.000002 0 -1"
+
+ this.getContent()
+ this.setupWatchers()
+ this.render()
+ this.rerouteWindow()
+ this.optimizeAR()
+
+ // fix janus.navigateBack() janus.navigateForward() userposition
+ room.addEventListener('room_active', () => {
+ this.optimizeAR(true)
+ })
+ },
+
+ removeContent: function(js_id){
+ let content = this.children[0]
+ if( content ){
+ this.removeChild(content)
+ room.objects[ content.js_id ] = content
+ room.appendChild(content)
+ room.dispatchEvent({type:"dialog_content_remove", content, dialog:this})
+ }
+ },
+
+ getContent: function(js_id){
+ if( js_id ){
+ this.removeContent()
+ this.content_id = js_id
+ }
+ let content = this.children[0] || $(this.content_id)
+ if( !content ) return
+ content.visible = true
+ content.pos = "0 0 0"
+ content.parentBackup = content.parent
+ content.parent.removeChild(content)
+ this.appendChild( content )
+ },
+
+ render: function(js_id){
+ if( this.rendering ) return // prevent recursion
+ this.rendering = true
+ this._theme = this.getTheme()
+ room.dispatchEvent({type:"dialog_content_render", dialog:this})
+ try{
+ if( js_id ) this.getContent(js_id)
+ room.dispatchEvent({type:"dialog_theme_apply", dialog:this})
+ this.processParagraphs()
+ $$(this.children).set('collidable', this.collidable ? "true" : "false")
+ $$(this.children).set('pickable', this.collidable ? "true" : "false")
+ this.applyTheme()
+ }catch(e){
+ console.error(e)
+ }
+ this.rendering = false
+ },
+
+ applyTheme: function(){
+ let theme = this._theme = this.getTheme()
+ $$( this.children )
+ .traverse( (child) => {
+ if( !child.tag ) return
+ let tag = String(child.tag).toLowerCase()
+ for( let i in theme.jml['*'] ){ // default
+ child[i] = theme.jml['*'][i]
+ if( this.debug ) console.log(`child.${tag} = theme.jml['*'][${i}] = ${theme.jml['*'][i]}`)
+ }
+ if( theme.jml[tag] ){ // tag-specific
+ for( let i in theme.jml[tag] ){
+ if( this.debug ) console.log(`child.${tag} = theme.jml[${tag}][${i}]`)
+ child[i] = theme.jml[tag][i]
+ }
+ }
+ })
+
+ if( !this.children[0]?.children ) return
+ const db = {in:['tag'], from: this.children[0].children }
+ theme.custom({db}) // apply only to children of dialog
+ // invert colors of objects which have
+ $$(/.*/, db).each( (el) => {
+ if( el.invert || el?.args?.properties?.invert ){
+ el.col.r = 1.0 - el.col.r
+ el.col.g = 1.0 - el.col.g
+ el.col.b = 1.0 - el.col.b
+ }
+ })
+ return this
+ },
+
+ processParagraphs(){
+ // get all elements
+ $$('paragraph', {in:['tag'], from:this.children})
+ .each( el => {
+ // *FIXME* is el.args a janusweb thing?
+ const height = el.args.properties.height || '200px' // annoying default to urge dev to set it explicitly
+ const width = el.args.properties.width || '300px' // annoying default to urge dev to set it explicitly
+ const cssClass = el.cssClass
+ let tag = el.tag
+ el.textBackup = el.textBackup || el.text
+ let text = el.textBackup // reset to original template
+ text = this.processTemplateVars(text)
+ text = this.processMarkdown(text)
+ text = this.wrapCSS(text, this._theme.css({height,width}), cssClass )
+ el.text = text
+ room.dispatchEvent({type:"dialog_paragraph_render", paragraph: el, dialog:this})
+ if( this.debug ) console.log(el.text)
+ })
+ return this
+ },
+
+ processMarkdown: function(text){
+ return text
+ .replace(/(\n|^|\s+)#(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)##(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)###(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)####(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)#####(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)######(?!#) *(.*)/g, "$2
")
+ .replace(/(\n|^|\s+)> *(.*)/g, "$2
")
+ .replace(/\*\*(.*)\*\*/g, "$1")
+ .replace(/```(\S*)((?:[^`]|`(?!``))*)```/g, "$2
")
+ .replace(/(<\/h[1-6]>)\n/g, "$1")
+ .replace(/\n/g, "
")
+ .replace(/\[([^\]]+)\]\(([^\)]*)\)/g, "$1")
+ .replace(/(?$1"); //` closing backtick for syntax highlighting
+ },
+
+ processTemplateVars: function(text){
+ return (new Function('text','return `'+text.replace(/`/g,'\\`')+'`')(text))
+ },
+
+ wrapCSS: function(text,css,classname){
+ return `\n${text}
`
+ },
+
+ lightToDarkTheme: function(){
+ // we just invert the light theme
+ this.theme.dark = {
+ css: (opts) => {
+ let css = this.theme.light.css(opts)
+ css = css.replace('.paragraphcontainer{', '.paragraphcontainer{ filter: invert(1);')
+ return css
+ },
+ jml: this.theme.light.jml,
+ custom: (opts) => this.theme.light.custom({...opts,invert:true})
+ }
+ },
+
+ setupWatchers: function(){
+ const clearProxies = () => {
+ for( let i in room ){
+ if( room[i]?.unwatch ) room[i] = room[i].unwatch()
+ }
+ }
+ clearProxies()
+ const watch = this.children[0]?.args?.properties?.watch
+ if( !watch ) return
+ let objs = watch.split(",")
+ objs.map( (name) => {
+ name = name.trim()
+ room[ name ] = room[ name ] || {}
+ if( room[ name ]?.watching ) return
+ room[ name ] = new Proxy( room[ name ],{
+ dialog: this,
+ get(me,k){ return me[k]; },
+ set(me,k,v){
+ me[k] = v
+ this.dialog.render()
+ return true
+ }
+ })
+
+ })
+ },
+
+ positionBeforeCamera: function(obj){
+
+ },
+
+ rerouteWindow: function(){
+ if( !this.reroute ) return
+
+ const resetPopup = () => {
+ if( room.popup ) room.removeChild(room.popup)
+ room.popup = false
+ }
+
+ window.alert = (msg,opts) => {
+ opts = opts || {}
+ opts = {timeout:2000, ...opts}
+ resetPopup()
+
+ if( !room.objects.dialog_alert ){
+ const container = room.createObject('object',{
+ id: "dialog_alert",
+ back_alpha: "0"
+ })
+ container.createObject('paragraph',{
+ width: "200px",
+ height: "120px",
+ lighting: "false",
+ collission_id: "false",
+ text: `\n${ opts.header ? `# ${opts.header}` : ''}\n${msg}
`
+ })
+ }
+ //tmpVec is the coords of -0.3 meter in front of your face
+ let tmpVec = translate(scalarMultiply(player.view_dir,-0.3),player.head_pos);
+ let pos = translate( tmpVec, V(0,-0.3,0))
+ room.popup = room.createObject('dialog',{
+ billboard:"y",
+ content_id: "dialog_alert",
+ pos: pos
+ })
+ this.positionBeforeCamera( room.popup )
+ if( opts.timeout ) setTimeout( resetPopup, opts.timeout)
+ }
+ },
+
+ optimizeAR: async function(resetHead){
+ // hide or reposition the reciprocal-link in AR
+ const exitLink = room.objects['reciprocal-link']
+ if( exitLink ){
+ if( this.backportal === false ) exitLink.visible = false
+ else exitLink.pos.z -= 1 // move a bit backward
+ }
+ // reposition user exactly at room.pos for seamless AR-menus
+ // because:
+ // * nearby exitLink (collision) can push avatar/player away
+ // * janus.navigateBack() does not reset camera orientation
+ console.log("position player")
+ player.pos = room.pos || "0 0 0"
+ if( resetHead ) player.reset_position()
+ }
+
+ });
+
+}
+
+room.addEventListener('janus_room_scriptload', register )
+room.addEventListener('janus_room_scriptload', function(){
+
+ room.registerElement('peertube', {
+ src: "",
+ loop: false,
+ seeking: false,
+ seekrate: 1000,
+ profile: '720p',
+ object_id: "plane",
+ auto_play: false,
+ loop: true,
+ show_playbutton: true,
+ show_progress: true,
+ aspectRatio: true,
+ playbutton_col: "0.5 0.5 0.5",
+ buttoncolor_play: "0 1 0",
+ buttoncolor_pause: "1 0 0",
+ slider_pos: "",
+
+ createChildren: function() {
+ this.createControls();
+ this.xdir = "-1 0 -0.000002"
+ this.zdir = "0.000002 0 -1"
+ setTimeout( this.loadVideo, 1000 ) // give controls time to settle
+ },
+
+ createVideo: function(){
+ this.object = this.createObject('object', {
+ id: this.object_id,
+ collision_id: this.object_id,
+ length: 1.5,
+ width: 1,
+ height: 1,
+ pos: "0 0 0",
+ cull_face:"none",
+ lighting: false
+ });
+ },
+
+ createControls: function() {
+ this.createVideo()
+ if( this.show_progress ){
+ this.slider = this.createObject('Slider', {
+ length: 1.5,
+ width: .1,
+ height: .01,
+ ydir: V(0,0,1),
+ zdir: V(0,-1,0),
+ visible: false,
+ });
+ }
+
+ // playbutton: Create three vertices and connects them into a triangle.
+ const arrayOfVertices = [ -1,1,0, 0.5,0,0, -1,-1,0 ];
+ const arrayOfFaces = [0, 1, 2];
+ room.loadNewAsset("Object", {id:"playbutton", mesh_verts:arrayOfVertices, mesh_faces:arrayOfFaces});
+ this.playbutton = room.createObject('object', {
+ id: "playbutton",
+ js_id: "playbutton",
+ collision_id: "playbutton",
+ cull_face:"none",
+ col: this.playbutton_col,
+ pickable: false,
+ collidable: false,
+ lighting: false,
+ visible: false,
+ }, this);
+
+ // we do this here because createObject does not properly set these props in time
+ setTimeout( () => {
+ this.playbutton.scale = "0.25 0.25 0.25"
+ this.playbutton.pos = "0.09 0.02 0.1"
+ this.addListeners()
+ },1000)
+
+ this.addEventListener("update", this.update )
+ },
+
+ update: function(){
+ const video = this.object?.video
+ if( video ){
+ if ( !this.object.firstframe && video && video.readyState >= video.HAVE_CURRENT_DATA ) {
+ this.object.firstframe = true
+ this.dispatchEvent({type:"peertube_video_firstframe"})
+ setTimeout( () => this.resize(), 200)
+ this.addListeners(this.object.video)
+ }
+ if( this.playbutton ){
+ this.playbutton.visible = video.paused
+ }
+ }
+ },
+
+ setSliderMax: function(){
+ if( this.slider && this._data || this.object?.video?.duration ){
+ this.slider.max = (this._data ? this._data.duration : this.object.video.duration)
+ }
+ },
+
+ updateTimeSlider: function(ev) {
+ if (!this.seeking && this.slider) {
+ this.setSliderMax()
+ this.slider.setValue(this.object.video.currentTime, true);
+ }
+ },
+ seekstart: function() {
+ this.seeking = true;
+ },
+ seekend: function() {
+ this.seeking = false;
+ },
+ seek: function(ev) {
+ var now = performance.now(),
+ lastseek = this.lastseek || 0;
+ if (!this.lastseek || now - lastseek > this.seekrate) {
+ this.lastseek = now;
+ this.seekInstantly();
+ } else {
+ if (!this.seektimer) {
+ var nextseek = Math.min(this.seekrate, now - lastseek);
+ this.seektimer = setTimeout(this.seekInstantly, nextseek);
+ }
+ }
+ },
+ seekInstantly: function() {
+ this.setSliderMax()
+ if( this.slider ){
+ this.object.video.currentTime = this.slider.value;
+ }
+ this.seektimer = false;
+ },
+
+ loadVideo: function(){
+ this.reset()
+ if( !this.src ) return console.warn(" no src-attribute given")
+ // get HLS stream from peertube
+ let parts = this.src.split("/w/")
+ videoId = parts.pop().replace(/(\?|#).*/,"")
+ this.host = parts[0]
+ console.log(`peertube: API request ${this.host}/api/v1/videos/${videoId}`)
+ fetch(`${this.host}/api/v1/videos/${videoId}`)
+ .then(res => res.json())
+ .then( data => {
+ this._data = data
+ const files = data.streamingPlaylists && data.streamingPlaylists.length ? data.streamingPlaylists[0].files : data.files
+ this._stream = files[0]
+ if( this.aspectRatio && this.slider ){
+ this.object.scale = `${this.slider.length*1.015} ${this.slider.length/data.aspectRatio} 0.01`
+ }
+ // pick '720p' profile by default (to not let a 4k video tank our performance)
+ files.map( (stream) => {
+ if( stream.label == this.profile ){
+ this._stream = stream
+ }
+ })
+ this.dispatchEvent({type: "loaded", data})
+ })
+ .catch( (e) => {
+ console.error(e)
+ })
+ .finally( () => this.loadAssets() )
+ },
+
+ pollVideo: function(){
+ if( this.object.video && !this.object.video?.detected ){
+ this.object.video.detected = true
+ }else{
+ console.warn("peertube: waiting for new video texture")
+ clearTimeout( this.pollVideo.tid )
+ this.pollVideo.tid = setTimeout( () => this.pollVideo(), 300 )
+ }
+ },
+
+ reset: function(){
+ if( this.object.isPlaying() ) this.object.pause()
+ this.object.firstframe = false
+ },
+
+ resize: function(){
+ const videoBBox = this.object.getBoundingBox()
+ if( !this.slider ) return
+ const sliderBBox = this.slider.getBoundingBox()
+ const videoHeight = videoBBox.max.y - videoBBox.min.y;
+ if( this.slider ){
+ if( this.slider_pos ) this.slider.pos = this.slider_pos
+ else{
+ this.slider.pos.y = -(this.slider.length / this._data.aspectRatio) / 2
+ this.slider.visible = this.show_progress
+ }
+ this.slider.pos.z = 0.05
+ }
+ this.dispatchEvent({type:"peertube_video_resize"})
+ },
+
+ loadAssets: function(){
+ if( !this._stream || !this._stream.fileUrl ) return console.error("could not get peertube stream link")
+ const hash = btoa(this.src)
+ this._peertubeThumb = this.host+"/"+this._data.thumbnailPath
+ this._peertubeSrc = this._stream.fileUrl
+ //room.loadNewAsset("image",{id:`${hash}_thumb`,src: this._peertubeThumb, proxy:false})
+ room.loadNewAsset("video",{id:`${hash}_video`,src: this._peertubeSrc, auto_play: this.auto_play, loop: this.loop, proxy:false })
+
+ this.object.video_id = `${hash}_video`
+ const asset = this.getAsset('video', this.object.video_id)
+ this.object.video = asset.getInstance().image;
+ return true
+ },
+
+ playPause: function(ev){
+ // not sure why this works better inside setTimeouts
+ setTimeout( () => {
+ const fn = this.object.isPlaying() ? 'pause' : 'play'
+ this.object[ fn ]()
+ this.dispatchEvent({type: `peertube_video_${fn}`})
+ },100)
+ },
+
+ haltClick: function(ev){
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+
+ addListeners: function(video){
+ if( !this.addListeners.done ){ // once
+ this.addListeners.done = true
+ if( this.slider ){
+ this.slider.ongrabstart = this.seekstart
+ this.slider.onchange = this.seek
+ this.slider.ongrabend = this.seekend
+ this.slider.base.addEventListener('mouseup', this.haltClick )
+ this.slider.base.addEventListener('mousedown', this.haltClick )
+ this.slider.base.addEventListener('click', this.haltClick )
+ }
+ this.object.addEventListener("click", () => this.playPause() )
+ }
+
+ if( video ){
+ //video.addEventListener('play', () => this.playPause() )
+ //video.addEventListener('pause', () => this.playPause() )
+ //this.object.video.addEventListener("click", this.playPause )
+ video.addEventListener('timeupdate', (ev) => this.updateTimeSlider(ev) );
+ // forwarders
+ video.addEventListener('playing', this.dispatchEvent )
+ video.addEventListener('play', this.dispatchEvent )
+ video.addEventListener('pause', this.dispatchEvent )
+ video.addEventListener('ended', this.dispatchEvent )
+ video.addEventListener('timeupdate', this.dispatchEvent )
+ }
+ }
+
+ })
+
+})
+var init = async function init(){
+ if( this.instance ) this.instance = String(this.instance).replace(/\/$/,'') // remove trailing slash
+ if( this.message ) this.state.status = this.message
+ init.interface.call(this)
+ init.stateMachine.call(this)
+ init.animations.call(this)
+ this.state.loading(true)
+
+ if( this.autoload ) init.data.call(this)
+
+}
+
+init.data = function(){
+ setTimeout( async () => {
+ try{
+ //room.categories = await this.getApi('categories')
+ let vids = await this.getApi( (this.channel ? 'channel' : 'videos'),{count: this.maxcolumns * this.maxrows})
+
+ $('videos').grid.load(vids).render()
+ this.state.interface()
+
+ }catch(e){
+ console.error(e) // *TODO* handle error (error img?)
+ }
+ }, this.delay)
+}
+
+init.interface = function(){
+
+ this.loader = this.createObject("object",{
+ js_id:"loader",
+ pos: "0.15 0.2 0"
+ })
+ this.loader.anim = this.loader.createObject("object",{
+ id:"plane",
+ js_id: "anim",
+ wireframe: true,
+ col: "0.33 0.33 0.33",
+ scale: "0.1 0.1 0.1",
+ pos: "0 0 0",
+ transparent: true,
+ opacity: 1.0,
+ rotate_axis: "0 0 1",
+ rotate_deg_per_sec: 280.0
+ })
+ this.loader.text = this.loader.createObject("text",{
+ locked:true,
+ pos: "0 -0.15 0.01",
+ rotation: "-180 0 180",
+ font_scale: false,
+ font_size: 0.035,
+ scale: `1 1 0.001`,
+ text: this.state.status,
+ col: `0.33 0.33 0.33`
+ })
+
+ // generate peertube triangle logo
+ const arrayOfVertices = [ -1,1,0, 1,0,0, -1,-1,0 ];
+ const arrayOfFaces = [0, 1, 2];
+ room.loadNewAsset("Object", {id:"triangle", mesh_verts:arrayOfVertices, mesh_faces:arrayOfFaces});
+
+ this.loader.logo = this.loader.createObject("object",{js_id:"logo", pos: "0 0.1 0"})
+ const triangleScale = "0.082 0.088 1"
+ const onClickLogo = () => {
+ console.log("click")
+ if( !this.autoload ){
+ console.log("dataload")
+ init.data.call(this)
+ this.state.loading.unifyLogo()
+ this.autoload = false
+ this.loader.anim.visible = true
+ }
+ }
+ this.loader.logo.createObject('object',{
+ id:"triangle",
+ opacity:0.0,
+ transparent:true,
+ collision_id:"triangle",
+ col: "0.9 0.3 0.01",
+ lighting: false,
+ scale: triangleScale,
+ pos: "-0.1 0 0",
+ rotation: "0 0 180",
+ onclick: onClickLogo
+ })
+ this.loader.logo.createObject('object',{
+ id:"triangle",
+ collision_id:"triangle",
+ opacity:0.0,
+ transparent:true,
+ col: "0 0 0",
+ lighting: false,
+ scale: triangleScale,
+ pos: "0.073 0.088 0",
+ rotation: "0 0 180",
+ onclick: onClickLogo
+ })
+ this.loader.logo.createObject('object',{
+ id:"triangle",
+ collision_id:"triangle",
+ opacity:0.0,
+ transparent:true,
+ col: "0.33 0.33 0.33",
+ lighting: false,
+ scale: triangleScale,
+ pos: "0.073 -0.088 0",
+ rotation: "0 0 180",
+ onclick: onClickLogo
+ })
+
+ const videos = room.createObject('object',{
+ js_id: "videos",
+ col: "1 0 0",
+ alpha: 0.5,
+ watch: "peertube",
+ })
+
+ videos.appendChild(
+ videos.grid = room.createObject('peertube-grid',{
+ js_id: "grid",
+ spaceX: 0.70,
+ spaceY: 0.55,
+ pos: "-1.2 0.7 0", // reposition thumbnail grid
+ columns: this.maxcolumns,
+ visible: false
+ })
+ )
+ videos.grid.room = room // not sure why this is needed?
+
+ videos.grid.load = (vids) => {
+
+ // reset children
+ $$( $('videos').grid ).each( (c) => {
+ if( c.parent ) c.parent.removeChild(c)
+ c.die()
+ })
+
+ // load thumbs
+ this.state.status = "loading thumbnails"
+ const maxItems = this.maxrows * this.maxcolumns
+ for( let i = 0; i < vids.data.length && i < maxItems; i++ ){
+ const js_id = `thumb${i}`
+ init.interface.thumbnail.call( this, vids, js_id, i, this.delay_asset )
+ this.state.status = "loading thumbnail "+i
+ }
+ return videos.grid
+ }
+
+ // close button for hiding the grid
+ videos.grid.close = () => {
+ this.state.interface.show(false)
+ }
+ videos.grid.close.button = this.createIcon(videos, {
+ js_id: "infoicon",
+ text: "x",
+ visible: false,
+ onclick: videos.grid.close
+ })
+
+ this.video = this.createObject("object",{js_id:"video"})
+
+ /* TODO: info button to show dialog with info */
+ // info icon on video
+ //this.video.info = this.createIcon( this.video, {
+ // text:"i",
+ // pos: "1.06 0.75 -0.5",
+ // rotation: "0 0 0",
+ // text_pos: "0 -0.041 0.01",
+ // visible: false,
+ // onclick: () => alert("hello")
+ //})
+
+ videos.title = videos.createObject("text",{
+ visible: false,
+ collidable: false,
+ pickable: false,
+ opacity: 0.0,
+ js_id:"title",
+ transparent:true,
+ align:"left",
+ text: "hello world",
+ pos: "-1.49 1 0.0000001",
+ font_size: .07,
+ font_scale: false,
+ lighting: false
+ })
+
+ // wrap all in dialog to trigger theming
+ this.dialog = {
+
+ videos: this.createObject('dialog',{
+ content_id: "videos",
+ pos: this.videos_pos,
+ rotation: "0 0 0",
+ js_id: "dialog_videos"
+ }),
+
+ video: this.createObject('dialog',{
+ content_id: "video",
+ rotation: "0 0 0",
+ js_id: "dialog_video"
+ }),
+ }
+
+ // init themes
+ for( let i in this.dialog ){
+ this.dialog[i].addEventListener("dialog_theme_init", () => {
+ room.theme.current = "light"
+ })
+ }
+
+}
+
+init.interface.thumbnail = function(vids, js_id, i, delay){
+ const maxtitle = 34
+ const truncate = String(vids.data[i].name).length > maxtitle ? '..' : ''
+ const btn = $('videos').grid.createObject('button',{
+ js_id,
+ scale_back: "1 1.9 1",
+ pos: "0 0 0.001",
+ data: vids.data[i],
+ src: `${this.instance}/w/${vids.data[i].embedPath.replace('/videos/embed/','')}`,
+ onclick: ((data) => () => this.playVideo(data))(vids.data[i])
+ })
+ btn.createObject('text',{
+ js_id: "title_"+i,
+ text: String(vids.data[i].name).substr(0,maxtitle) + truncate,
+ font_scale: false,
+ font_size: 0.02,
+ align: "center",
+ pos: "0 -0.16 0.02"
+ })
+ btn.createObject('text',{
+ js_id: "name"+i,
+ text: String(vids.data[i].channel.name).substr(0,maxtitle) ,
+ font_scale: false,
+ font_size: 0.02,
+ col: "0.9 0.3 0.01",
+ align: "center",
+ pos: "0 -0.19 0.02"
+ })
+ // apply images without choking cpu/network
+ console.log(this.instance + vids.data[i].previewPath )
+ setTimeout( () => {
+ room.loadNewAsset("image",{ id: `asset_${js_id}`, src: this.instance + vids.data[i].previewPath })
+ btn.createObject("image",{
+ id: `asset_${js_id}`,
+ lighting: false,
+ scale: "0.28 0.28 0.0005",
+ pos: "0.003 0.04 -0.09",
+ cull_face: "front",
+ collidable: false,
+ pickable: false
+ })
+ }, 500 + (i*delay) )
+}
+
+init.stateMachine = function(){
+
+ this.state = new Proxy(this.state,{
+ get(me,k){ return me[k] },
+ set: (me,k,v) => {
+ me[k] = v
+ switch(k){
+ case 'status': this.loader?.text && (this.loader.text.text = v); break;
+ }
+ return true
+ }
+ })
+
+ this.state.loading = (state) => {
+ if( !state ) this.state.status = "enjoy"
+ $.tween( this.loader.anim, {"opacity": [ state ? 1.0 : 0.0, 0.05]})
+ $.tween( this.loader.text, {"opacity": [ state ? 1.0 : 0.0, 0.05]})
+
+ // show logo once
+ if( state && !this.loader.logo.shown){
+ this.loader.logo.shown = true
+ $.tween( this.loader.logo, { "opacity":[ 1.0, 0.05] })
+ if( this.autoload ){
+ setTimeout( () => this.state.loading.unifyLogo(), 1000)
+ } else this.loader.anim.visible = false
+ }
+ }
+
+ this.state.loading.unifyLogo = () => {
+ // unify logo triangles to center
+ const speed = 0.08
+ $.tween( this.loader.logo, { "pos.y": [ 0, speed],
+ "pos.x": [ 0.01, speed],
+ "scale.x": [ 0.2, speed],
+ "scale.y": [ 0.2, speed],
+ "scale.z": [ 0.2, speed],
+ "rotation.z": [ 360, speed],
+ "opacity": [ 0, speed] })
+ }
+
+ this.state.interface = (state) => {
+ this.state.loading(false)
+ this.state.interface.show(true)
+ }
+
+ this.state.interface.show = (state) => {
+ if( state ) this.state.status = "enter navigator"
+ $$( $('videos') ).set('visible', state)
+ .set('pickable', state)
+ $('videos').grid.close.button.visible = $('peertubevideo') && state ? true : false
+ }
+
+ this.state.watchvideo = () => {
+ this.state.status = "almost ready"
+ $$( $('videos') ).set('visible', false)
+ .set('pickable', false)
+ $$( $('video') ).when( (el) => el.js_id != "playbutton" )
+ .set('visible', true)
+ .set('pickable',true)
+ }
+
+}
+
+room.registerElement('peertube-grid', {
+ offset: V(0,0,0),
+ spaceX: 1,
+ spaceY: 1,
+ columns: 0,
+ centerX: true,
+ centerY: true,
+
+ create: function() {
+ this.render()
+ },
+
+ render: function(){
+ let children = this.children;
+ let i = 0;
+ let offset = V( this.offset.x, this.offset.y, this.offset.z )
+ for (var k in children) {
+ children[k].pos.x = offset.x
+ children[k].pos.y = offset.y
+ offset.x += this.spaceX
+ i++
+ if( this.columns && ((i % this.columns) == 0) ){
+ offset.x = this.offset.x
+ offset.y -= this.spaceY
+ }
+ }
+ }
+});
+
+init.animations = function(){
+ room.update = function(dt){
+ if( $.tween.update ) $.tween.update()
+ }.bind(this)
+}
+
+registerPeerTubeApp = () => room.registerElement('peertube-app', {
+ instance: "https://makertube.net",
+ channel: "",
+ search:"",
+ message: "hello peertube",
+ autoload: true,
+ maxrows: 3,
+ maxcolumns: 4,
+ delay: 2000, // scene-load delay to save fps
+ delay_asset: 300, // load asset delay to save fps
+ videos_pos: "0 0 0", // reposition thumbnail grid
+
+ state: { status: "" },
+ _config: {
+ dialog: {
+ rooturl: window.dialogPath || "https://codeberg.org/coderofsalvation/janus-script-dialog/raw/branch/master",
+ corsproxy: true
+ }
+ },
+ _api: {
+ videos: function(opts){ return this.instance + `/api/v1/videos?${this.search?`search=${this.search}&`:''}start=${opts.start||0}&count=${opts.count||9}` },
+ channel: function(opts){ return this.instance + `/api/v1/video-channels/${this.search?`search=${this.search}&`:''}${this.channel}/videos?start=${opts.start||0}&count=${opts.count||9}` },
+ categories: function(opts){ return this.instance + `/api/v1/videos/categories` }
+ },
+
+ createChildren: init,
+
+ playVideo: function(video){
+ this.state.status = "loading peertube video"
+ this.state.interface.show(false)
+ this.state.loading(true)
+
+ let src = `${this.instance}/w/${video.embedPath.replace('/videos/embed/','')}`
+ if( !this.video.peertube ){
+ this.video.peertube = this.createObject("peertube",{
+ js_id: "peertubevideo",
+ src,
+ auto_play:true,
+ pos: V(0.2, 0.1, 0.6),
+ scale: "1.9 1.9 1",
+ visible: false
+ })
+
+ const watch = () => {
+ this.video.peertube.visible = true
+ this.state.watchvideo()
+ }
+ this.video.peertube.addEventListener("peertube_video_firstframe", () => {
+ this.state.loading(false)
+ // style the player
+ //this.video.peertube.slider.children[0].col = "0 0 0"
+ //this.video.peertube.slider.children[1].col = "0.1 0.1 0.1"
+ })
+ this.video.peertube.addEventListener("peertube_video_resize", watch )
+ this.video.peertube.addEventListener("peertube_video_play", watch )
+ this.video.peertube.addEventListener("peertube_video_pause", () => {
+ this.state.interface.show(true)
+ })
+ }else{
+ this.video.peertube.src = src
+ this.video.peertube.auto_play = true
+ this.video.peertube.loadVideo()
+ }
+ },
+
+
+ /* tiny (recursive) tween function for Janus objects with auto-garbage collection
+ *
+ * usage: const speed = 0.05 // closer to zero == smoother/slower
+ * logo.addEventListener("tween_done", alert )
+ * logo.addEventListener("tween_prop_done", alert )
+ * $.tween( logo, {"pos.y": [-0.1,speed], "pos.x": [-0.05,speed]})
+ * room.update = $.tween.update // call tween.update() every frame
+ *
+ * dependency: https://codeberg.org/coderofsalvation/janus-script-jjq
+ */
+ tween: function(el, opts){
+ $.tween.remove = (el) => {
+ delete el.tween
+ el.tween = undefined
+ $.tween.items = $.tween.items.filter( (el) => el.tween ? el : false )
+ }
+ // This is our Linear Interpolation method. It takes 3 parameters:
+ // a: The starting value
+ // b: The destination value
+ // n: The normal value (between 0 and 1) to control the Linear Interpolation
+ $.tween.lerp = function(a, b, n) {
+ return (1 - n) * a + n * b;
+ }
+ $.tween.update = () => {
+ $.tween.items.map( (el) => {
+ for( let prop in el.tween ){
+ let diff = 0
+ const to = el.tween[prop][0]
+ const speed = el.tween[prop][1]
+ let children = 0
+ const lerp = (child) => {
+ if( $.get(child,prop) == undefined ) return
+ const v = $.tween.lerp( $.get(child,prop), to, speed )
+ $.set(child, prop, v)
+ diff += Math.abs(to-v)
+ children++
+ }
+ lerp(el)
+ $$(el).each( lerp )
+ // garbage collect
+ if( diff < $.tween.items.resolution ){
+ if( el.dispatchEvent ){
+ el.dispatchEvent({type:"tween_prop_done",prop,tween:el.tween})
+ }
+ delete el.tween[prop]
+ }
+ }
+ if( Object.keys(el.tween).length == 0 ){
+ $.tween.remove(el)
+ if( el.dispatchEvent ){
+ setTimeout( () => {
+ el.dispatchEvent({type:"tween_done",tween:el.tween})
+ },50)
+ }
+ }
+ })
+ }
+ $.tween.items = $.tween.items || []
+ $.tween.items.resolution = 0.01 // decides when to flush tweens
+
+ if( el.tween ) $.tween.remove(el) // overwrite previous
+ el.tween = opts
+ $.tween.items.push(el)
+ },
+
+ getApi: async function(resource,opts){
+ this.state.status = `connecting to ${String(this.instance).replace(/.*:\/\//,'')}`
+ opts = opts || {}
+ for( let i in opts ) opts[i] = String(opts[i]) // enforce strings
+ let res = await fetch( this._api[resource].call(this,opts) )
+ return res.json()
+ },
+
+ createIcon: function(parent, opts){
+ opts = opts ||{}
+ let btn = parent.createObject('button',{
+ pos: "-0.15 -0.75 0",
+ use: "icon",
+ fontscale: 0.08,
+ ...opts,
+ })
+ if( opts.pos_text ){
+ setTimeout( ( )=> btn.label.pos = opts.pos_text, 100 ) // center text
+ }
+ return btn
+ }
+
+})
+
+room.addEventListener("registerelement", function(e){
+ room.has = room.has || {}
+ room.has[e.data] = true
+
+ if( room.has.dialog && room.has.button && room.has.peertube && !room.has.inited){
+
+ room.has.inited = true
+ registerPeerTubeApp()
+
+
+ // const isTranslator = typeof janus != 'undefined' ? true : false
+ // if( isTranslator ){
+ // room.createObject("peertube-app",{
+ // js_id: "peertube-app",
+ // pos: "0 1.3 0"
+ // })
+ // }
+
+ }
+})
diff --git a/src/janusweb/media/component/janus-script-peertube/translator.js b/src/janusweb/media/component/janus-script-peertube/translator.js
new file mode 100644
index 0000000..55570c2
--- /dev/null
+++ b/src/janusweb/media/component/janus-script-peertube/translator.js
@@ -0,0 +1,104 @@
+const PeertubeTranslatorBaseURL = document.currentScript.src.replace("/translator.js","")
+
+elation.require([], function() {
+ elation.component.add('janusweb.translators.peertube', function() {
+ this.init = function() {
+ this.description = "peertube website to 3D translator"
+ }
+ this.exec = function(args) {
+ return new Promise(elation.bind(this, function(resolve, reject) {
+
+ var room = this.room = args.room;
+
+ reject() // for now
+
+ this.setupEvents()
+ var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
+
+ var roomdata = {
+ assets: {
+ assetlist: [
+ // {assettype: 'model', name: 'scene', src: args.url},
+ ]
+ },
+ room: {
+ pos: [0,0,0],
+ xdir: "1 0 0",
+ zdir: "0 0 1",
+ },
+ object: [
+ // {id: 'scene', js_id: 0, pos: "0 0 0", xdir: "-1 0 0", zdir: "0 0 -1", lighting: "false"}
+ ],
+ link: []
+ };
+ resolve(roomdata);
+ }));
+ }
+
+ // translate XR Fragments microformat into JML
+ this.parseSource = async function(sourcecode, room){
+ this.room = room
+ const isJML = /[\s\S]*?<\/fireboxroom>/si;
+ const isPeertube = /]*property=['"]og:platform['"]\s+[^>]*content=['"]peertube['"][^>]*\/?>/si;
+ const isPeertubeVideo = /]*property=['"]og:type['"]\s+[^>]*content=['"]video['"][^>]*\/?>/si;
+ const isPeertubeInstance = /]*property=['"]og:type['"]\s+[^>]*content=['"]website['"][^>]*\/?>/si;
+ const is = {
+ JML: sourcecode.match(isJML),
+ peertube: sourcecode.match(isPeertube),
+ peertubeVideo: sourcecode.match(isPeertubeVideo),
+ peertubeInstance: sourcecode.match(isPeertubeInstance),
+ }
+ if( is.JML || !is.peertube ) return // JML takes precedence over microformats
+ if( !is.peertubeVideo && !is.peertubeInstance ) return
+
+ // ok..peertube time: get domtree
+ let el = document.createElement("div")
+ el.innerHTML = sourcecode
+
+ // try to fetch title + cover
+ const title = el.querySelector("title")
+ let logo = el.querySelector("meta[property='og:image']")?.getAttribute("content")
+ logo = logo ? logo : el.querySelector("meta[property='og:image:url']")?.getAttribute("content")
+
+ let url = new URL( String(room.url) )
+ let instance = url?.origin
+ let search = url?.search ? new URLSearchParams(url.search).get("search") : ""
+
+ room.corsproxy = ''
+ // return JML
+ let parsed = room.parseSource(`
+ ${ title ? title.innerText.replace(/\n.*/g,'') : room.baseurl.split("/").pop() }
+
+
+ ${ logo ? `` : ''}
+
+
+
+ ${ is.peertubeInstance ? ``
+ : ``
+ }
+
+
+
+ `)
+ return parsed
+ }
+ // peertube microformat heuristic:
+ this.parseSource.regex = /]*property=['"]og:platform['"]\s+[^>]*content=['"]peertube['"][^>]*\/?>/si;
+ });
+});
+
+elation.events.add(null, 'room_translator_register', function(e){
+ const room = e.element
+ const translators = e.data.translators
+ // prepend translator
+ e.data.translators = {}
+ e.data.translators['!peertube'] = elation.janusweb.translators.peertube({janus: janus})
+ for( let i in translators ){
+ e.data.translators[i] = translators[i]
+ }
+})
diff --git a/src/myverse/icosa-gallery.png b/src/myverse/tech-demos/icosa-gallery.png
similarity index 100%
rename from src/myverse/icosa-gallery.png
rename to src/myverse/tech-demos/icosa-gallery.png
diff --git a/src/myverse/icosa-gallery/README.md b/src/myverse/tech-demos/icosa-gallery/README.md
similarity index 100%
rename from src/myverse/icosa-gallery/README.md
rename to src/myverse/tech-demos/icosa-gallery/README.md
diff --git a/src/myverse/icosa-gallery/icosa-gallery.desktop b/src/myverse/tech-demos/icosa-gallery/icosa-gallery.desktop
similarity index 100%
rename from src/myverse/icosa-gallery/icosa-gallery.desktop
rename to src/myverse/tech-demos/icosa-gallery/icosa-gallery.desktop
diff --git a/src/myverse/janusxr_tutorial.png b/src/myverse/tech-demos/janusxr_tutorial.png
similarity index 100%
rename from src/myverse/janusxr_tutorial.png
rename to src/myverse/tech-demos/janusxr_tutorial.png
diff --git a/src/myverse/janusxr_tutorial/AFRAME.jpg b/src/myverse/tech-demos/janusxr_tutorial/AFRAME.jpg
similarity index 100%
rename from src/myverse/janusxr_tutorial/AFRAME.jpg
rename to src/myverse/tech-demos/janusxr_tutorial/AFRAME.jpg
diff --git a/src/myverse/janusxr_tutorial/AFRAME/README.md b/src/myverse/tech-demos/janusxr_tutorial/AFRAME/README.md
similarity index 100%
rename from src/myverse/janusxr_tutorial/AFRAME/README.md
rename to src/myverse/tech-demos/janusxr_tutorial/AFRAME/README.md
diff --git a/src/myverse/janusxr_tutorial/AFRAME/index.html b/src/myverse/tech-demos/janusxr_tutorial/AFRAME/index.html
similarity index 100%
rename from src/myverse/janusxr_tutorial/AFRAME/index.html
rename to src/myverse/tech-demos/janusxr_tutorial/AFRAME/index.html
diff --git a/src/myverse/janusxr_tutorial/AFRAME/tree2.glb b/src/myverse/tech-demos/janusxr_tutorial/AFRAME/tree2.glb
similarity index 100%
rename from src/myverse/janusxr_tutorial/AFRAME/tree2.glb
rename to src/myverse/tech-demos/janusxr_tutorial/AFRAME/tree2.glb
diff --git a/src/myverse/janusxr_tutorial/arwindow.jpg b/src/myverse/tech-demos/janusxr_tutorial/arwindow.jpg
similarity index 100%
rename from src/myverse/janusxr_tutorial/arwindow.jpg
rename to src/myverse/tech-demos/janusxr_tutorial/arwindow.jpg
diff --git a/src/myverse/janusxr_tutorial/arwindow/README.md b/src/myverse/tech-demos/janusxr_tutorial/arwindow/README.md
similarity index 100%
rename from src/myverse/janusxr_tutorial/arwindow/README.md
rename to src/myverse/tech-demos/janusxr_tutorial/arwindow/README.md
diff --git a/src/myverse/janusxr_tutorial/arwindow/Walk in the Woods by Don Carson - 38m6Q1H12DU.glb b/src/myverse/tech-demos/janusxr_tutorial/arwindow/Walk in the Woods by Don Carson - 38m6Q1H12DU.glb
similarity index 100%
rename from src/myverse/janusxr_tutorial/arwindow/Walk in the Woods by Don Carson - 38m6Q1H12DU.glb
rename to src/myverse/tech-demos/janusxr_tutorial/arwindow/Walk in the Woods by Don Carson - 38m6Q1H12DU.glb
diff --git a/src/myverse/janusxr_tutorial/arwindow/index.html b/src/myverse/tech-demos/janusxr_tutorial/arwindow/index.html
similarity index 100%
rename from src/myverse/janusxr_tutorial/arwindow/index.html
rename to src/myverse/tech-demos/janusxr_tutorial/arwindow/index.html
diff --git a/src/myverse/janusxr_tutorial/arwindow/occludebox.glb b/src/myverse/tech-demos/janusxr_tutorial/arwindow/occludebox.glb
similarity index 100%
rename from src/myverse/janusxr_tutorial/arwindow/occludebox.glb
rename to src/myverse/tech-demos/janusxr_tutorial/arwindow/occludebox.glb
diff --git a/src/myverse/janusxr_tutorial/helloworld.jpg b/src/myverse/tech-demos/janusxr_tutorial/helloworld.jpg
similarity index 100%
rename from src/myverse/janusxr_tutorial/helloworld.jpg
rename to src/myverse/tech-demos/janusxr_tutorial/helloworld.jpg
diff --git a/src/myverse/janusxr_tutorial/helloworld/README.md b/src/myverse/tech-demos/janusxr_tutorial/helloworld/README.md
similarity index 100%
rename from src/myverse/janusxr_tutorial/helloworld/README.md
rename to src/myverse/tech-demos/janusxr_tutorial/helloworld/README.md
diff --git a/src/myverse/janusxr_tutorial/helloworld/janusxr.xml b/src/myverse/tech-demos/janusxr_tutorial/helloworld/janusxr.xml
similarity index 100%
rename from src/myverse/janusxr_tutorial/helloworld/janusxr.xml
rename to src/myverse/tech-demos/janusxr_tutorial/helloworld/janusxr.xml
diff --git a/src/myverse/janusxr_tutorial/tronverse b/src/myverse/tech-demos/janusxr_tutorial/tronverse
similarity index 100%
rename from src/myverse/janusxr_tutorial/tronverse
rename to src/myverse/tech-demos/janusxr_tutorial/tronverse
diff --git a/src/myverse/janusxr_tutorial/tronverse.png b/src/myverse/tech-demos/janusxr_tutorial/tronverse.png
similarity index 100%
rename from src/myverse/janusxr_tutorial/tronverse.png
rename to src/myverse/tech-demos/janusxr_tutorial/tronverse.png
diff --git a/src/myverse/tronverse.png b/src/myverse/tech-demos/tronverse.png
similarity index 100%
rename from src/myverse/tronverse.png
rename to src/myverse/tech-demos/tronverse.png
diff --git a/src/myverse/tronverse/janusxr b/src/myverse/tech-demos/tronverse/janusxr
similarity index 100%
rename from src/myverse/tronverse/janusxr
rename to src/myverse/tech-demos/tronverse/janusxr
diff --git a/src/myverse/tronverse/tron/README.md b/src/myverse/tech-demos/tronverse/tron/README.md
similarity index 100%
rename from src/myverse/tronverse/tron/README.md
rename to src/myverse/tech-demos/tronverse/tron/README.md
diff --git a/src/myverse/tronverse/tron/tron.glb b/src/myverse/tech-demos/tronverse/tron/tron.glb
similarity index 100%
rename from src/myverse/tronverse/tron/tron.glb
rename to src/myverse/tech-demos/tronverse/tron/tron.glb
diff --git a/src/myverse/tronverse/tron/tron.mp3 b/src/myverse/tech-demos/tronverse/tron/tron.mp3
similarity index 100%
rename from src/myverse/tronverse/tron/tron.mp3
rename to src/myverse/tech-demos/tronverse/tron/tron.mp3
diff --git a/src/myverse/tronverse/tron/tron.png b/src/myverse/tech-demos/tronverse/tron/tron.png
similarity index 100%
rename from src/myverse/tronverse/tron/tron.png
rename to src/myverse/tech-demos/tronverse/tron/tron.png
diff --git a/src/myverse/tronverse/tronverse-icosagallery.jpg b/src/myverse/tech-demos/tronverse/tronverse-icosagallery.jpg
similarity index 100%
rename from src/myverse/tronverse/tronverse-icosagallery.jpg
rename to src/myverse/tech-demos/tronverse/tronverse-icosagallery.jpg
diff --git a/src/myverse/tronverse/tronverse-icosagallery/README.md b/src/myverse/tech-demos/tronverse/tronverse-icosagallery/README.md
similarity index 100%
rename from src/myverse/tronverse/tronverse-icosagallery/README.md
rename to src/myverse/tech-demos/tronverse/tronverse-icosagallery/README.md
diff --git a/src/plugin/openbrush.lua b/src/plugin/openbrush.lua
index b6320fb..0418aaa 100644
--- a/src/plugin/openbrush.lua
+++ b/src/plugin/openbrush.lua
@@ -2,7 +2,11 @@ local util = require('util')
local apiSync = require('apiSync')
app.on('init', function()
- app.plugin.openbrush.api = apiSync.init( 'openbrush' )
+ if not app.opts.plugin_diskscan_dir:match("^/zip/") then
+ app.plugin.openbrush.api = apiSync.init( 'openbrush' )
+ else
+ print("[openbrush.lua] disabled..please create a 'myverse' dir we can store files into")
+ end
end)
local function scanTiltFile(exp)
@@ -116,7 +120,7 @@ local plugin = {
key='app.opts.plugin_openbrush_sync_folder',
title='Sync to folder',
info='openbrush (icosa.gallery) assets will be saved here',
- default = "./myverse/tronverse",
+ default = "./myverse/tech-demos/tronverse",
type = "dir",
values = {} -- populated by plugin/diskscan.lua
},
diff --git a/src/util/build.sh b/src/util/build.sh
index 147f53e..62141b7 100755
--- a/src/util/build.sh
+++ b/src/util/build.sh
@@ -4,7 +4,7 @@ OUTDIR=result/bin
REDBEAN_URL="https://redbean.dev/redbean-3.0.0.com"
REDBEAN_CRC="382f1288bb96ace4bab5145e7df236846c33cc4f1be69233710682a9e71e7467 $OUTDIR/xrforge.com"
-download(){
+get(){
redbean(){
test -d $OUTDIR || mkdir -p $OUTDIR
@@ -32,18 +32,20 @@ download(){
janusweb(){
set -x
- test -d janusweb || {
- version=1.7.5
+ test -d pkg/janusweb || {
+ mkdir pkg &>/dev/null || true
test -f janusweb-${version}.tar.gz || {
wget "https://github.com/coderofsalvation/janusweb/releases/download/v${version}-rc/janusweb-${version}.tar.gz"
}
test -d janusweb-${version} || tar -xvf janusweb-${version}.tar.gz
- mv janusweb-${version} janusweb
+ mv janusweb-${version} pkg/janusweb
}
set -x
- rsync -avL janusweb src/.
+ rsync -avLu pkg/janusweb src/. # overlay but leave newer files intact
# cleanup
rm src/janusweb/janusxr.com || true
+ rm src/janusweb/test || true
+ rm src/janusweb/media/test || true
# set xrforge as defualt config
cp src/janusweb/media/assets/webui/{xrforge,default}.json
}
@@ -52,11 +54,12 @@ download(){
}
build(){
- download redbean
- download janusweb
+ get redbean
+ get janusweb
cd src
bundle(){ zip -yr $1 * .lua .init.lua .args; }
bundle ../result/bin/xrforge.com
+ unzip -l ../result/bin/xrforge.com
test -n "$ARCH" && bundle ../result/bin/xrforge-$ARCH.com
}