janusweb
Version:
Web client for JanusVR worlds
1,202 lines (1,140 loc) • 120 kB
JavaScript
elation.require([
'ui.textarea', 'ui.window',
'engine.things.generic', 'engine.things.label', 'engine.things.skybox',
'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.janusparagraph',
'janusweb.sound', 'janusweb.januslight', 'janusweb.janusparticle', 'janusweb.janusghost',
'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs'
], function() {
let roomTranslators = false;
function initRoomTranslators() {
roomTranslators = {
'^janus-vfs:': elation.janusweb.translators.janusvfs({janus: janus}),
'^about:blank$': elation.janusweb.translators.blank({janus: janus}),
'^bookmarks$': elation.janusweb.translators.bookmarks({janus: janus}),
'^dat:': elation.janusweb.translators.dat({janus: janus}),
'^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: janus}),
'^error$': elation.janusweb.translators.error({janus: janus}),
'^default$': elation.janusweb.translators.default({janus: janus})
};
}
elation.component.add('engine.things.janusroom', function() {
this.postinit = function() {
elation.engine.things.janusroom.extendclass.postinit.call(this);
this.toneMappingTypes = {
'none': THREE.NoToneMapping,
'linear': THREE.LinearToneMapping,
'reinhard': THREE.ReinhardToneMapping,
'uncharted2': THREE.Uncharted2ToneMapping,
'cineon': THREE.CineonToneMapping,
'acesfilmic': THREE.ACESFilmicToneMapping,
};
this.defineProperties({
'janus': { type: 'object' },
'url': { type: 'string', default: false },
'urlfragment': { type: 'string', default: false },
'referrer': { type: 'string' },
'deferload': { type: 'boolean', default: false },
'roomid': { type: 'string' },
'corsproxy': { type: 'string', default: false },
'baseurl': { type: 'string', default: false },
'source': { type: 'string' },
'skybox': { type: 'boolean', default: true, set: this.toggleSkybox },
'skybox_intensity': { type: 'float', set: this.setSkybox, default: 1.0 },
'skybox_equi': { type: 'string', set: this.setSkybox },
'skybox_left_id': { type: 'string', set: this.setSkybox },
'skybox_right_id': { type: 'string', set: this.setSkybox },
'skybox_up_id': { type: 'string', set: this.setSkybox },
'skybox_down_id': { type: 'string', set: this.setSkybox },
'skybox_front_id': { type: 'string', set: this.setSkybox },
'skybox_back_id': { type: 'string', set: this.setSkybox },
'cubemap_irradiance_id': { type: 'string' },
'cubemap_radiance_id': { type: 'string' },
'fog': { type: 'boolean', default: false, set: this.setFog },
'fog_mode': { type: 'string', default: 'exp', set: this.setFog },
'fog_density': { type: 'float', default: 1.0, set: this.setFog },
'fog_start': { type: 'float', default: 0.0, set: this.setFog },
'fog_end': { type: 'float', default: 100.0, set: this.setFog },
'fog_col': { type: 'color', default: 0x000000, set: this.setFog },
'ambient': { type: 'color', default: 0x666666, set: this.updateLights },
'near_dist': { type: 'float', default: 0.01, set: this.setNearFar },
'far_dist': { type: 'float', default: 1000.0, set: this.setNearFar },
'pbr': { type: 'boolean', default: true },
'toon': { type: 'boolean', default: false },
'bloom': { type: 'float', default: 0.05, set: this.updateBloom },
'tonemapping_type': { type: 'string', default: 'linear', set: this.updateToneMapping },
'tonemapping_exposure': { type: 'float', default: 0.8, set: this.updateToneMapping },
'defaultlights': { type: 'bool', default: true, set: this.updateLights },
'shadows': { type: 'bool', default: false, set: this.updateShadows },
'party_mode': { type: 'bool', default: true },
'walk_speed': { type: 'float', default: 1.8, set: this.updatePlayerSpeed },
'run_speed': { type: 'float', default: 5.4, set: this.updatePlayerSpeed },
'jump_velocity': { type: 'float', default: 5.0 },
'flying': { type: 'boolean', default: true, set: this.updateFlying },
'gravity': { type: 'float', default: 0, set: this.updateGravity },
'teleport': { type: 'bool', default: true, set: this.updateTeleport },
'locked': { type: 'bool', default: false },
'cursor_visible': { type: 'bool', default: true },
'cursor_opacity': { type: 'float', default: .8 },
'use_local_asset': { type: 'string', set: this.updateLocalAsset },
'col': { type: 'color', set: this.updateLocalAsset },
'private': { type: 'bool', default: false },
'server': { type: 'string' },
'port': { type: 'int' },
'rate': { type: 'int', default: 200 },
'voip': { type: 'string', default: 'janus' },
'voipid': { type: 'string' },
'voiprange': { type: 'float', default: 1 },
'voipserver': { type: 'string', default: 'voip.janusxr.org' },
'classList': { type: 'object', default: [] },
'className': { type: 'string', default: '', set: this.setClassName },
'gazetime': { type: 'float', default: 1000 },
'selfavatar': { type: 'boolean', default: false },
'requires': { type: 'string' },
'onload': { type: 'string' },
'sync': { type: 'boolean', default: false },
'pointerlock': { type: 'boolean', default: true, set: this.updatePointerLock },
'players': { type: 'object', get: this.getRoomPlayers },
'defaultview': { type: 'string' },
'showavatar': { type: 'bool', default: true, set: this.updateAvatarVisibility },
'spawnradius': { type: 'float', default: 0 },
});
if (!roomTranslators) {
initRoomTranslators();
}
this.spawnpoint = new THREE.Object3D();
this.roomsrc = '';
this.changes = {};
this.deletions = [];
this.appliedchanges = {};
this.openportals = [];
this.roomassets = {};
this.pendingassets = [];
this.jsobjects = {};
this.cookies = {};
this.websurfaces = {};
this.ghosts = {};
this.images = {};
this.sounds = {};
this.videos = {};
this.loaded = false;
this.completed = false;
this.parseerror = false;
this.roomscripts = [];
this.customElements = {};
this.unknownElements = {};
this.eventlistenerproxies = {};
this.pendingScriptMap = {};
// FIXME - binding functions to this instance so we can unbind events later. Should be done at a lower level
this.onRoomEdit = elation.bind(this, this.onRoomEdit);
this.onClick = elation.bind(this, this.onClick);
this.onKeyDown = elation.bind(this, this.onKeyDown);
this.onKeyUp = elation.bind(this, this.onKeyUp);
this.onObjectClick = elation.bind(this, this.onObjectClick);
this.onMouseDown = elation.bind(this, this.onMouseDown);
this.onMouseUp = elation.bind(this, this.onMouseUp);
this.onMouseMove = elation.bind(this, this.onMouseMove);
this.onScriptTick = elation.bind(this, this.onScriptTick);
this.roomedit = {
snap: .01,
modes: ['pos', 'rotation', 'scale', 'col'],
movespeed: new THREE.Vector3(),
modeid: 0,
object: false,
distancescale: 1
};
if (this.url) {
let hashidx = this.url.indexOf('#');
if (hashidx != -1) {
this.urlhash = this.url.substr(hashidx+1);
this.setHash(this.urlhash)
this.url = this.url.substr(0, hashidx);
}
this.roomid = md5(this.url);
if (!this.deferload) {
this.load(this.url, this.baseurl);
}
} else if (this.source) {
this.loadFromSource(this.source);
}
elation.events.add(this, 'thing_remove', elation.bind(this, this.onThingRemove));
document.addEventListener('visibilitychange', ev => this.handleVisibilityChange());
}
this.createChildren = function() {
this.collidable = false;
this.setCollider('sphere', {radius: 1e4});
/*
this.skyboxcollider = this.createObject('object', {
js_id: 'room_skybox',
collision_id: 'sphere',
collision_scale: V(10000),
collision_trigger: true,
pickable: true,
});
this.skyboxcollider.collidable = false;
*/
this.objects['3d'].add(this.spawnpoint);
this.lastthink = 0;
this.thinktime = 0;
}
this.createLights = function() {
this.roomlights = {
ambient: this.spawn('light_ambient', this.id + '_ambient', {
color: this.properties.ambient
}),
directional: this.spawn('light_directional', this.id + '_sun', {
position: [-20,50,25],
intensity: 0.1
}),
point: this.spawn('light_point', this.id + '_point', {
position: [22,19,-15],
intensity: 0.1
})
};
}
this.updateLights = function() {
if (!this.roomlights) {
this.createLights();
}
if (!this.objects['3d']) return;
this.roomlights.ambient.lightobj.color = this.ambient._target;
if (this.defaultlights) {
if (this.roomlights.directional.parent != this) {
this.add(this.roomlights.directional);
}
if (this.roomlights.point.parent != this) {
this.add(this.roomlights.point);
}
} else {
if (this.roomlights.directional.parent) {
this.roomlights.directional.parent.remove(this.roomlights.directional);
}
if (this.roomlights.point.parent) {
this.roomlights.point.parent.remove(this.roomlights.point);
}
}
}
this.setActive = function() {
this.active = true;
this.setSkybox();
this.setFog();
this.updateBloom();
this.updateToneMapping();
this.setNearFar();
this.setPlayerPosition();
if (typeof player != 'undefined' && this.defaultview && this.defaultview != player.cameraview) {
player.setCameraView(this.defaultview);
}
this.updateAvatarVisibility();
elation.events.fire({element: this, type: 'room_active', data: this});
}
this.setPlayerPosition = function(pos, orientation) {
if (!pos) {
let spawnpoint = this.getSpawnpoint(this.referrer);
pos = spawnpoint.position;
orientation = spawnpoint.orientation;
}
var player = this.engine.client.player;
if (player.parent !== this) {
// Reparent player to the room if necessary
this.appendChild(player.getProxyObject());
}
player.reset_position();
player.position.copy(pos);
player.orientation.copy(orientation);
player.properties.movestrength = 80 * this.properties.walk_speed;
player.properties.runstrength = 80 * this.properties.run_speed;
player.cursor_visible = (!janus.hmd ? elation.utils.any(this.cursor_visible, true) : false);
player.cursor_opacity = elation.utils.any(this.cursor_opacity, .8);
// FIXME - for some reason the above call sometimes orients the player backwards. Doing it on a delay fixes it...
//setTimeout(elation.bind(player, player.reset_position), 0);
}
this.setHash = function(hash) {
this.urlhash = hash;
if (document.location.origin + document.location.pathname == this.url) {
document.location.hash = hash;
}
let spawnpoint = this.getSpawnpoint();
if (typeof player != 'undefined') {
player.startposition.copy(spawnpoint.position);
player.startorientation.copy(spawnpoint.orientation);
}
}
this.getSpawnpoint = function(referrer) {
let spawnpoint = {
position: this.spawnpoint.position,
orientation: this.spawnpoint.quaternion,
};
if (this.spawnradius) {
let dist = Math.random() * this.spawnradius,
angle = Math.random() * 2 * Math.PI;
spawnpoint.position.x += Math.sin(angle) * dist;
spawnpoint.position.z += Math.cos(angle) * dist;
}
if (referrer) {
let links = this.getObjectsByTagName('link');
for (let i = 0; i < links.length; i++) {
if (links[i].url == referrer) {
//spawnpoint.position = links[i].position;
spawnpoint.position = links[i].localToWorld(V(0,0,player.fatness*2));
spawnpoint.orientation = links[i].orientation.clone();
let node = links[i];
let roomproxy = this.getProxyObject();
while (node.parent && node.parent != roomproxy) {
node = node.parent;
node.handleFrameUpdates({});
spawnpoint.orientation.multiply(node.orientation);
}
spawnpoint.orientation.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0))); // Flip 180 degrees from portal orientation
break;
}
}
} else if (this.urlhash) {
let obj = this.getObjectById(this.urlhash);
if (obj) {
obj.localToWorld(spawnpoint.position.set(0,0,0));
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))));
}
}
return spawnpoint;
}
this.setSkybox = function() {
if (!this.loaded) return;
if (!this.skybox) {
this.engine.systems.render.renderer.setClearAlpha(0);
return;
} else {
this.engine.systems.render.renderer.setClearAlpha(1);
}
if (!this.skyboxobj) {
this.skyboxobj = this.spawn('skybox', this.id + '_sky', {
position: [0,0,0],
collidable: false,
});
}
if (this.skyboxtexture) {
this.skyboxobj.setTexture(this.skyboxtexture);
//return;
}
var hasSkybox = (this.skybox_left_id || this.skybox_right_id || this.skybox_top_id || this.skybox_bottom_id || this.skybox_left_id || this.skybox_right_id) != undefined;
var assets = [];
if (this.skybox_equi) {
let equi = this.getAsset('image', this.skybox_equi);
if (equi.loaded) {
this.skyboxtexture = equi.getInstance();
this.skyboxtexture.mapping = THREE.EquirectangularReflectionMapping;
this.skyboxtexture.encoding = THREE.sRGBEncoding;
if (this.janus.currentroom === this) {
this.skyboxobj.setTexture(this.skyboxtexture);
}
elation.events.fire({element: this, type: 'skybox_update'});
} else {
elation.events.add(equi, 'asset_load', ev => {
this.skyboxtexture = ev.target._texture;
this.skyboxtexture.mapping = THREE.EquirectangularReflectionMapping;
this.skyboxtexture.encoding = THREE.sRGBEncoding;
if (this.janus.currentroom === this) {
this.skyboxobj.setTexture(this.skyboxtexture);
}
elation.events.fire({element: this, type: 'skybox_update'});
});
equi.getInstance();
}
} else if (hasSkybox) {
assets = [
this.getAsset('image', this.skybox_right_id || 'black'),
this.getAsset('image', this.skybox_left_id || 'black'),
this.getAsset('image', this.skybox_up_id || 'black'),
this.getAsset('image', this.skybox_down_id || 'black'),
this.getAsset('image', this.skybox_front_id || 'black'),
this.getAsset('image', this.skybox_back_id || 'black')
];
} else {
var skyboxname = 'dayskybox';
assets = [
this.getAsset('image', skyboxname + '_right'),
this.getAsset('image', skyboxname + '_left'),
this.getAsset('image', skyboxname + '_up'),
this.getAsset('image', skyboxname + '_down'),
this.getAsset('image', skyboxname + '_front'),
this.getAsset('image', skyboxname + '_back')
];
}
// TODO - THREE.DDSLoader only supports compressed DDS textures, but JanusVR sends uncompressed RGBA888 (I think)
/*
if (this.cubemap_irradiance_id) {
var asset = this.getAsset('image', this.cubemap_irradiance_id);
console.log('irradiance', this.cubemap_irradiance_id, asset);
}
if (this.cubemap_radiance_id) {
var asset = this.getAsset('image', this.cubemap_radiance_id);
console.log('radiance', this.cubemap_radiance_id, asset);
}
*/
var loaded = 0, errored = 0;
var texures = [];
assets.forEach(elation.bind(this, function(asset) {
if (asset) {
var n = asset.getInstance();
if (asset.loaded) {
loaded++;
if (loaded + errored == 6) {
this.processSkybox(assets);
}
} else {
elation.events.add(n, 'asset_load,asset_error', elation.bind(this, function(ev) {
if (ev.type == 'asset_load') loaded++;
else errored++;
if (loaded + errored == 6) {
this.processSkybox(assets);
}
}));
}
}
}));
return false;
}
this.processSkybox = function(assets) {
if (assets[0] && assets[1] && assets[2] && assets[3] && assets[4] && assets[5]) {
var images = [];
assets.forEach(function(asset) {
var t = asset.getInstance();
images.push(t.image);
});
// Handle skyboxes with missing textures. We need to figure out
// the skybox texture size, then create a blank canvas of that size
// Images of size 16x16 are (probably!) placeholders
var width = undefined, height = undefined;
images.forEach(function(img) {
if (img.width != 16 && img.height != 16) {
width = img.width;
height = img.height;
}
});
if (width && height) {
for (var i = 0; i < images.length; i++) {
if (images[i] instanceof HTMLCanvasElement && images[i].width != width && images[i].height != height) {
images[i].width = width;
images[i].height = height;
}
// flip skybox 180 degrees
if (images[i] instanceof HTMLCanvasElement) {
var ctx = images[i].getContext('2d');
if (i == 2 || i == 3) { // Y+ or Y-
// Y+
ctx.translate(images[i].width/2, images[i].height/2);
ctx.rotate(Math.PI);
ctx.translate(-images[i].width/2, -images[i].height/2);
}
ctx.drawImage(images[i],0,0,images[i].width,images[i].height);
}
}
}
if (images[0] && images[1] && images[2] && images[3] && images[4] && images[5]) {
// flip skybox 180 degrees
images = [images[1],images[0],images[2],images[3],images[5],images[4]];
var texture = new THREE.CubeTexture( images );
texture.encoding = THREE.sRGBEncoding;
texture.needsUpdate = true;
this.skyboxtexture = texture;
if (this.janus.currentroom === this) {
this.skyboxobj.setTexture(this.skyboxtexture);
}
elation.events.fire({element: this, type: 'skybox_update'});
return true;
}
}
}
this.toggleSkybox = function() {
if (this.skybox) {
this.engine.systems.render.renderer.setClearAlpha(1);
if (this.skyboxtexture) {
this.skyboxobj.setTexture(this.skyboxtexture);
} else {
this.setSkybox();
}
} else {
this.engine.systems.render.renderer.setClearAlpha(0);
if (this.skyboxobj) {
this.skyboxobj.setTexture(null);
}
}
}
this.setFog = function() {
if (this.fog) {
var fogcol = this.properties.fog_col || 0;
var fogcolor = new THREE.Color();
if (fogcol[0] == '#') {
fogcolor.setHex(parseInt(fogcol.substr(1), 16));
} else if (elation.utils.isString(fogcol) && fogcol.indexOf(' ') != -1) {
var rgb = fogcol.split(' ');
fogcolor.setRGB(rgb[0], rgb[1], rgb[2]);
} else if (fogcol instanceof THREE.Color) {
fogcolor.copy(fogcol);
} else {
fogcolor.setHex(fogcol);
}
if (this.properties.fog_mode == 'exp' || this.properties.fog_mode == 'exp2') {
this.engine.systems.world.setFogExp(parseFloat(this.properties.fog_density), fogcolor);
} else {
this.engine.systems.world.setFog(this.properties.fog_start, this.properties.fog_end, fogcolor);
}
} else {
this.engine.systems.world.disableFog();
}
}
this.updateBloom = function() {
// adjust bloom setting
var bloomfilter = this.engine.client.view.effects['bloom'];
if (bloomfilter) {
bloomfilter.copyUniforms.opacity.value = this.bloom;
}
var bloomfilter2 = this.engine.client.view.effects['unrealbloom'];
if (bloomfilter2) {
bloomfilter2.strength = this.bloom / 2; // unrealbloom seems to be a lot stronger than the old bloom method, so we scale it to keep things somewhat in line
}
}
this.updateToneMapping = function() {
if (this.active) {
this.engine.systems.render.renderer.toneMapping = this.toneMappingTypes[this.tonemapping_type] || 0;
this.engine.systems.render.renderer.toneMappingExposure = this.tonemapping_exposure;
this.refresh();
}
}
this.setNearFar = function() {
if (this.active) {
this.engine.client.player.camera.camera.near = this.properties.near_dist;
this.engine.client.player.camera.camera.far = this.properties.far_dist;
}
}
this.showDebug = function() {
this.engine.client.player.disable();
if (!this.debugwindow) {
var content = elation.ui.panel_vertical({classname: 'janusweb_room_debug'});
this.debugwindow = elation.ui.window({title: 'Janus Room', content: content, append: document.body, center: true});
this.debugeditor = elation.ui.textarea({append: content, value: this.getRoomSource(), classname: 'janusweb_room_source', wrap: 'off'});
this.debugwindow.settitle('Room Source: ' + this.properties.url);
var applybutton = elation.ui.button({label: 'Apply Changes'});
applybutton.disabled = true;
var refreshbutton = elation.ui.button({label: 'Refresh'});
var buttons = elation.ui.buttonbar({
classname: 'janusweb_room_debug_buttons',
append: content,
buttons: {
apply: applybutton,
refresh: refreshbutton,
}
});
this.debugwindow.setcontent(content);
this.debugbuttons = buttons.buttons;
elation.events.add(this.debugeditor, 'input', elation.bind(this, this.handleEditorInput));
elation.events.add(this.debugeditor, 'change', elation.bind(this, this.handleEditorChange));
elation.events.add(applybutton, 'click', elation.bind(this, this.handleEditorApply));
elation.events.add(refreshbutton, 'click', elation.bind(this, this.handleEditorRefresh));
elation.events.add(this, 'room_edit', (ev) => { this.debugeditor.value = this.getRoomSource(); console.log('updated source'); });
}
//elation.ui.content({append: content, content: this.properties.url, classname: 'janusweb_room_url'});
this.debugwindow.show();
this.debugwindow.center();
}
this.load = function(url, baseurloverride) {
if (!url) {
url = this.properties.url;
} else {
this.properties.url = url;
}
var baseurl = baseurloverride;
if (!baseurl) {
if (url && !this.baseurl) {
baseurl = url.split('/');
if (baseurl.length > 3) baseurl.pop();
baseurl = baseurl.join('/') + '/';
this.baseurl = baseurl;
}
} else {
this.baseurl = baseurl;
}
this.jsobjects = {};
this.cookies = {};
this.websurfaces = {};
this.images = {};
this.videos = {};
this.setTitle('loading...');
var proxyurl = this.corsproxy || '';
var fullurl = url;
if (fullurl[0] == '/' && fullurl[1] != '/') fullurl = this.baseurl + fullurl;
if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) {
fullurl = self.location.origin + fullurl;
} else if (!fullurl.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) && fullurl.indexOf(document.location.origin) != 0) {
fullurl = proxyurl + fullurl;
}
elation.events.fire({element: this, type: 'room_load_queued'});
var translator = this.getTranslator(url);
/*
// Disabled - using outerHTML to get the source is a nice way to avoid an extra network trip,
// but due to how browser parsers work it means we can't get back unmodified XML, so it's best
// to just make an XHR call and let the browser cache handle it
if (url == document.location.href) {
setTimeout(elation.bind(this, function() {
this.roomsrc = this.parseSource(document.documentElement.outerHTML);
var roomdata = this.parseFireBox(this.roomsrc.source);
this.createRoomObjects(roomdata);
this.setActive();
this.loaded = false;
elation.events.fire({type: 'janus_room_load', element: this});
}), 0);
} else
*/
if (translator) {
setTimeout(elation.bind(this, function() {
// TODO - use the new official translators here!
translator.exec({url: url, janus: this.properties.janus, room: this})
.then(elation.bind(this, function(objs) {
if (objs.room.title) {
this.setTitle(objs.room.title);
}
this.roomsrc = objs.source;
this.loadRoomAssets(objs);
this.createRoomObjects(objs);
this.loaded = true;
this.setActive();
elation.events.fire({element: this, type: 'room_load_processed'});
elation.events.fire({type: 'janus_room_load', element: this});
}));
}), 0);
} else if (url.indexOf('data:') == 0) {
var prefix = 'data:text/html,';
var data = decodeURIComponent(url.substr(prefix.length));
setTimeout(elation.bind(this, function() {
elation.events.fire({element: this, type: 'room_load_processing'});
this.loadFromSource(data);
}), 0);
} else {
var started = false;
elation.net.get(fullurl, null, {
headers: {
//'User-Agent':'FireBox 1.0'
//'X-Requested-With':'JanusWeb Client'
},
callback: elation.bind(this, function(data, xhr) {
var responseURL = xhr.getResponseHeader('X-Final-URL');
if (!responseURL) {
responseURL = xhr.responseURL.replace(proxyurl, '');
}
if (responseURL != this.properties.url) {
var url = responseURL;
if (!baseurloverride) {
baseurl = url.split('/');
baseurl.pop();
baseurl = baseurl.join('/') + '/';
this.baseurl = baseurl;
}
this.properties.url = url;
}
elation.events.fire({element: this, type: 'room_load_processing'});
this.loadFromSource(data);
}),
failurecallback: elation.bind(this, function(xhr) {
var translator = roomTranslators['^error$'];
translator.exec({janus: this.properties.janus, room: this, error: xhr.status || 404})
.then(elation.bind(this, function(objs) {
var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
var assetpath = datapath + 'assets/translator/errors/';
this.baseurl = assetpath;
this.loadRoomAssets(objs);
this.createRoomObjects(objs);
this.enable();
this.setActive();
}));
}),
onprogress: elation.bind(this, function(ev) {
if (!started) {
started = true;
elation.events.fire({element: this, type: 'room_load_start'});
}
var progress = {
src: ev.target.responseURL,
total: ev.total,
loaded: ev.loaded
};
elation.events.fire({element: this, type: 'room_load_progress', data: progress});
})
});
}
}
this.validateSource = function(sourcecode) {
return new Promise(elation.bind(this, function(resolve, reject) {
var source = this.parseSource(sourcecode);
var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
try {
var roomdata = this.janus.parser.parse(source.source, this.baseurl, datapath);
resolve(true);
} catch (e) {
reject();
}
}));
}
this.loadFromSource = function(sourcecode, baseurl) {
if (baseurl) {
this.baseurl = baseurl;
}
var source = this.parseSource(sourcecode);
if (source && source.source) {
this.roomsrc = source.source;
var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
try {
var roomdata = this.janus.parser.parse(source.source, this.baseurl, datapath);
this.loadRoomAssets(roomdata);
this.createRoomObjects(roomdata);
this.loaded = true;
if (this.active) {
this.setActive();
}
//this.parseerror = false;
elation.events.fire({element: this, type: 'room_load_processed'});
elation.events.fire({type: 'janus_room_load', element: this});
if (this.pendingassets.length == 0 && !this.completed) {
setTimeout(() => elation.events.fire({element: this, type: 'room_load_complete'}), 0);
}
} catch (e) {
console.error('Janus room parse error:', e.message);
this.parseerror = e.message;
elation.events.fire({type: 'room_load_error', element: this, data: e.message});
}
} else {
var translator = this.getTranslator('default');
setTimeout(elation.bind(this, function() {
// TODO - use the new official translators here!
translator.exec({url: this.url, janus: this.properties.janus, room: this})
.then(elation.bind(this, function(objs) {
this.roomsrc = objs.source;
this.loadRoomAssets(objs);
this.createRoomObjects(objs);
this.loaded = true;
this.setActive();
elation.events.fire({element: this, type: 'room_load_processed'});
elation.events.fire({type: 'janus_room_load', element: this});
}));
}), 0);
}
}
this.parseSource = function(data) {
this.fullsource = data;
var titlere = /<title>([\s\S]*?)<\/title>/si;
var re = /<fireboxroom>[\s\S]*?<\/fireboxroom>/si;
var re2 = /<janus-viewer[^>]*>[\s\S]*?<\/janus-viewer>/si;
var mtitle = data.match(titlere);
var parsed = {
title: 'Untitled Room',
source: false
};
if (mtitle) {
parsed.title = mtitle[1];
this.setTitle(mtitle[1]);
} else {
this.setTitle(null);
}
var m = data.match(re);
if (m) {
parsed.source = m[0];
} else {
let m2 = data.match(re2);
if (m2) {
parsed.source = m2[0].replaceAll('janus-viewer', 'fireboxroom');
}
}
return parsed;
}
this.updateSource = function(source) {
var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
try {
var roomdata = this.janus.parser.parse(source, this.baseurl, datapath);
} catch (e) {
}
console.log('updated roomdata!', roomdata);
if (roomdata && roomdata.room) {
for (let k in roomdata.room) {
let val = roomdata.room[k];
if (val !== null && room[k] != val) {
console.log('update room value', k, room[k], val);
room[k] = val;
}
}
if (roomdata.object) {
for (let i = 0; i < roomdata.object.length; i++) {
let objdata = roomdata.object[i];
let roomobj = room.objects[objdata.js_id];
if (roomobj) {
for (let k in objdata) {
let val = objdata[k];
if (roomobj[k] != objdata[k] && val !== null) {
roomobj[k] = val;
}
}
} else {
objdata.persist = true;
this.createObject('object', objdata);
}
}
}
}
}
this.loadRoomAssets = function(roomdata) {
if (roomdata && roomdata.assets && roomdata.assets.assetlist && roomdata.assets.assetlist.length > 0) {
var assetlist = roomdata.assets.assetlist;
if (roomdata.assets.websurfaces) {
elation.utils.merge(roomdata.assets.websurfaces, this.websurfaces);
if (!this.roomassets.websurface) this.roomassets.websurface = {};
elation.utils.merge(roomdata.assets.websurfaces, this.roomassets.websurface);
}
if (roomdata.assets.ghosts) {
var ghosts = roomdata.assets.ghosts;
for (var i = 0; i < ghosts.length; i++) {
var ghost = ghosts[i];
this.ghosts[ghost.id] = ghost;
assetlist.push({
assettype: 'file',
name: ghost.id,
src: ghost.src
});
}
//elation.utils.merge(roomdata.assets.ghosts, this.ghosts);
}
if (roomdata.assets.shaders) {
let shaders = roomdata.assets.shaders;
for (var i = 0; i < shaders.length; i++) {
if (shaders[i].uniforms) {
//shaders[i].uniforms = this.parseShaderUniforms(shaders[i].uniforms);
}
}
}
//this.assetpack = elation.engine.assets.loadJSON(assetlist, this.baseurl);
if (!this.assetpack) {
this.assetpack = new elation.engine.assets.pack({name: this.id + '_assets', baseurl: this.baseurl, json: assetlist});
} else {
this.assetpack.loadJSON(assetlist);
}
}
}
this.clear = function() {
for (var k in this.children) {
this.remove(this.children[k]);
}
this.createLights();
}
this.createRoomObjects = function(roomdata, parent, isinternal) {
var room = roomdata.room,
assets = roomdata.assets || [];
// Prevent our loads from echoing back to the server as edits
this.applyingEdits = true;
// Parse room objects from the XML, and create Janus objects for them
var exclude = ['assets', 'room', 'source'];
for (var k in roomdata) {
if (exclude.indexOf(k) == -1) {
roomdata[k].forEach(elation.bind(this, function(n) {
n.persist = true;
n.isinternal = isinternal;
this.createObject(k, n, parent);
}));
}
}
if (room && !parent) {
if (room.use_local_asset) {
var modelid = (room.visible !== false ? room.use_local_asset : undefined),
collisionid = room.use_local_asset + '_collision',
collisionscale = V(1,1,1),
collisionpos = V(0,0,0);
if (room.use_local_asset == 'room_plane') {
collisionid = 'cube';
collisionscale.set(100000,100,100000);
collisionpos.set(0,-50,0);
}
this.localasset = this.createObject('object', {
id: modelid,
collision_id: collisionid,
collision_scale: collisionscale,
collision_pos: collisionpos,
col: room.col,
//fwd: room.fwd,
xdir: room.xdir,
ydir: room.ydir,
zdir: room.zdir,
shadows: true
});
}
// set player spawnpoint based on room info
if (room.pos) {
this.spawnpoint.position.fromArray(room.pos);
}
if (room.xdir || room.ydir || room.zdir) {
room.orientation = this.janus.parser.getOrientation(room.xdir, room.ydir, room.zdir);
}
if (room.orientation) {
this.spawnpoint.quaternion.copy(room.orientation);
this.spawnpoint.quaternion.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0,Math.PI,0))); // Janus Native starts the player backwards
this.spawnpoint.updateMatrixWorld();
}
// If we have a referrer, check to see if a reciprocal link exists. If it does, use this as our spawn point.
if (this.referrer) {
let hasReciprocalLink = false;
let links = this.getObjectsByTagName('link');
if (links && links.length > 0) {
links.forEach(link => {
let url = this.getFullRoomURL(link.url);
if (url == this.referrer) {
this.spawnpoint.quaternion.copy(link.orientation).invert();
this.spawnpoint.position.copy(link.pos);
this.spawnpoint.position.add(this.spawnpoint.localToWorld(V(0,0,-player.fatness)));
hasReciprocalLink = true;
}
});
}
if (!hasReciprocalLink) {
// If no reciprocal link was found, spawn one so we can find our way back
let linkrot = new EulerDegrees();
linkrot.radians.copy(this.spawnpoint.rotation);
//linkrot.x *= THREE.MathUtils.RAD2DEG;
linkrot.y = linkrot.y + 180;
//linkrot.z *= THREE.MathUtils.RAD2DEG;
let linkpos = this.spawnpoint.localToWorld(V(0,0,player.fatness/2));
this.createObject('link', {
pos: linkpos,
rotation: linkrot,
url: this.referrer,
round: true,
shader_id: 'defaultportal',
js_id: 'reciprocal-link',
});
}
}
if (this.active) {
setTimeout(() => this.setPlayerPosition(), 0);
}
if (typeof room.skybox != 'undefined') this.properties.skybox = room.skybox;
if (room.skybox_equi) this.properties.skybox_equi = room.skybox_equi;
if (room.skybox_left_id) this.properties.skybox_left_id = room.skybox_left_id;
if (room.skybox_right_id) this.properties.skybox_right_id = room.skybox_right_id;
if (room.skybox_up_id) this.properties.skybox_up_id = room.skybox_up_id;
if (room.skybox_down_id) this.properties.skybox_down_id = room.skybox_down_id;
if (room.skybox_front_id) this.properties.skybox_front_id = room.skybox_front_id;
if (room.skybox_back_id) this.properties.skybox_back_id = room.skybox_back_id;
if (typeof room.skybox_intensity != 'undefined') this.skybox_intensity = room.skybox_intensity;
if (room.cubemap_radiance_id) this.properties.cubemap_radiance_id = room.cubemap_radiance_id;
if (room.cubemap_irradiance_id) this.properties.cubemap_irradiance_id = room.cubemap_irradiance_id;
this.setSkybox();
if (room.server) this.properties.server = room.server;
if (room.port) this.properties.port = room.port;
if (room.rate) this.properties.rate = room.rate;
if (room.voip) this.properties.voip = room.voip || 'janus';
if (room.voipid) this.properties.voipid = room.voipid;
if (room.voiprange) this.properties.voiprange = room.voiprange;
if (room.voipserver) this.properties.voipserver = room.voipserver;
if (room.gazetime) this.properties.gazetime = room.gazetime;
if (typeof room.pbr != 'undefined') this.properties.pbr = room.pbr;
if (typeof room.toon != 'undefined') this.properties.toon = room.toon;
if (typeof room.ambient != 'undefined') this.ambient = room.ambient;
if (typeof room.defaultlights != 'undefined') this.defaultlights = room.defaultlights;
if (typeof room.selfavatar != 'undefined') this.properties.selfavatar = (room.selfavatar == "true");
this.properties.near_dist = parseFloat(room.near_dist) || 0.01;
this.properties.far_dist = parseFloat(room.far_dist) || 1000;
this.properties.fog = room.fog;
this.properties.private = room.private;
this.properties.fog_mode = room.fog_mode || 'exp';
this.properties.fog_density = room.fog_density;
this.properties.fog_start = parseFloat(room.fog_start) || this.properties.near_dist;
this.properties.fog_end = parseFloat(room.fog_end) || this.properties.far_dist;
this.fog_col = room.fog_col || room.fog_color;
this.properties.bloom = room.bloom || 0.05;
this.properties.tonemapping_type = room.tonemapping_type || 'linear';
this.properties.tonemapping_exposure = room.tonemapping_exposure || this.tonemapping_exposure;
this.properties.shadows = elation.utils.any(room.shadows, false);
this.properties.party_mode = elation.utils.any(room.party_mode, true);
this.properties.locked = room.locked || false;
this.gravity = elation.utils.any(room.gravity, 0);
this.flying = elation.utils.any(room.flying, true);
this.teleport = elation.utils.any(room.teleport, true);
this.pointerlock = elation.utils.any(room.pointerlock, true);
this.defaultview = elation.utils.any(room.defaultview, null);
this.showavatar = elation.utils.any(room.showavatar, true);
if ('spawnradius' in room) this.spawnradius = room.spawnradius;
if (typeof player != 'undefined' && this.defaultview && this.defaultview != player.cameraview) {
player.setCameraView(this.defaultview);
}
//if (room.col) this.properties.col = room.col;
this.properties.walk_speed = room.walk_speed || 1.8;
this.properties.run_speed = room.run_speed || 5.4;
this.properties.cursor_visible = room.cursor_visible;
this.properties.cursor_opacity = room.cursor_opacity;
let cookieStorageID = 'cookies.' + this.url;
try {
if (cookieStorageID in localStorage) {
this.cookies = JSON.parse(localStorage['cookies.' + this.url]);
}
} catch (e) {
this.cookies = [];
}
if (room.onload) {
this.properties.onload = room.onload;
this.addEventListenerProxy('room_load_complete', (ev) => { let func = new Function(room.onload); func();});
}
if (assets.scripts) {
this.pendingScripts = 0;
this.loadScripts(assets.scripts);
}
let requires = room.requires || room.require;
if (requires) {
let roomproxy = this.getProxyObject();
roomproxy.require(requires);
this.requires = requires;
}
}
this.applyingEdits = false;
//if (!this.active) {
// this.setActive();
//}
//this.showDebug();
}
this.loadScripts = function(scripts) {
scripts.forEach(s => {
this.loadNewAsset('script', { src: s.src, override: { room: this.getProxyObject() } });
let scriptasset = this.getAsset('script', s.src);
let script = scriptasset.getInstance();
if (scriptasset.loaded) {
// If the script is already part of the document, remove it and readd it so it's reevaluated
if (script.parentNode) {
script.parentNode.removeChild(script);
}
let oldscript = script;
script = document.createElement('script');
script.src = oldscript.src;
document.head.appendChild(script);
} else {
if (!this.pendingScriptMap[s.src]) {
this.pendingScripts++;
this.pendingScriptMap[s.src] = true;;
elation.events.add(scriptasset, 'asset_error', (ev) => {
console.error('Failed to load Janus room script', scriptasset);
this.doScriptOnload();
});
elation.events.add(script, 'asset_load', () => {
script.onload = () => this.doScriptOnload();
document.head.appendChild(script);
});
}
}
this.roomscripts.push(script);
});
}
this.getTranslator = function(url) {
var keys = Object.keys(roomTranslators);
for (var i = 0; i < keys.length; i++) {
var re = new RegExp(keys[i]);
if (url.match(re)) {
return roomTranslators[keys[i]];
}
}
// TODO - implement default page handling as translator
return false;
}
this.enable = function() {
var keys = Object.keys(this.children);
for (var i = 0; i < keys.length; i++) {
var obj = this.children[keys[i]];
if (obj.start) {
obj.start();
}
}
if (!this.enabled) {
this.enabled = true;
this.engine.systems.ai.add(this);
elation.events.add(window, 'click', this.onClick);
elation.events.add(window, 'keydown', this.onKeyDown);
elation.events.add(window, 'keyup', this.onKeyUp);
//elation.events.add(this, 'mousedown,touchstart', this.onMouseDown);
//elation.events.add(this, 'mouseup,touchend', this.onMouseUp);
//elation.events.add(this, 'mousemove', this.onMouseMove);
elation.events.add(this, 'click', this.onObjectClick);
/*
elation.events.add(this, 'dragenter', this.handleDragOver);
elation.events.add(this, 'dragover', this.handleDragOver);
elation.events.add(this, 'drop', this.handleDrop);
*/
elation.events.add(this, 'thing_think', this.onScriptTick);
elation.events.fire({type: 'room_enable', data: this});
if (this.audionodes) {
this.audionodes.gain.connect(this.audionodes.listener.getInput());
//console.log('connect room audio to graph', this.audionodes.gain, this.audionodes.listener.getInput(), this);
this.fadeAudioIn(2);
}
this.engine.systems.controls.pointerLockEnabled = this.pointerlock;
if (this.engine.systems.admin) {
elation.events.add(this.engine.systems.admin, 'admin_edit_change', elation.bind(this, this.onRoomEdit));
}
}
//this.showDebug();
}
this.disable = function() {
var keys = Object.keys(this.children);
for (var i = 0; i < keys.length; i++) {
var obj = this.children[keys[i]];
if (obj.stop) {
obj.stop();
}
}
if (this.enabled) {
this.engine.systems.ai.remove(this);
elation.events.fire({type: 'room_disable', data: this});
this.enabled = false;
elation.events.remove(this, 'thing_think', this.onScriptTick);
elation.events.remove(window, 'click', this.onClick);
elation.events.remove(window, 'keydown', this.onKeyDown);
elation.events.remove(window, 'keyup', this.onKeyUp);
elation.events.remove(this.engine.client.container, 'mousedown,touchstart', this.onMouseDown);
elation.events.remove(this.engine.client.container, 'mouseup,touchend', this.onMouseUp);
elation.events.remove(this, 'click', this.onObjectClick);
elation.events.remove(this, 'dragover', this.handleDragOver);
elation.events.remove(this, 'dragenter', this.handleDragOver);
elation.events.remove(this, 'drop', this.handleDrop);
if (this.intervals) {
console.log('stop intervals:', this.intervals);
while (this.intervals.length > 0) {
clearInterval(this.intervals.pop());
}
}
if (this.timers) {
console.log('stop timers:', this.timers);
while (this.timers.length > 0) {
clearTimeout(this.timers.pop());
}
}
if (this.audionodes) {
this.fadeAudioOut(1);
setTimeout(() => {
this.audionodes.gain.disconnect(this.audionodes.listener.getInput());
console.log('disconnect room audio from graph', this.audionodes.gain, this.audionodes.listener.getInput(), this);
}, 1500);
}
//this.engine.systems.controls.pointerLockEnabled = this.pointerlock;
}
}
this.setTitle = function(title) {
if (!title) title = 'Untitled Page';
this.title = title;
document.title = this.title;
}
this.applyEditXML = function(editxml) {
var xml = elation.utils.parseXML('<edit>' + editxml + '</edit>');
var edit = xml.edit._children;
if (!edit) return;
var keys = Object.keys(edit);
var hasNew = false;
var waslocked = this.locked;
//this.locked = true;
this.applyingEdits = true;
var skip = ['sync', 'autosync'];
keys.forEach(elation.bind(this, function(k) {
var newobjs = edit[k];
if (!elation.utils.isArray(newobjs)) newobjs = [newobjs];
var diff = {
assets: {
objects: [],
image: [],