janusweb
Version:
Web client for JanusVR worlds
292 lines (289 loc) • 10.9 kB
JavaScript
/*
janus.registerElement('avatar_simple', {
headsrc: 'media/assets/hoverscreen.obj',
bodysrc: '',
handsrc: '',
create() {
if (this.headsrc) {
this.head = this.createObject('object', {id: this.headsrc});
}
if (this.bodysrc) {
this.body = this.createObject('object', {id: this.bodysrc});
}
if (this.handsrc) {
this.hands = this.createObject('object', {id: this.handsrc});
}
//this.attachToPlayer();
},
attachToPlayer(player) {
// THIS IS WHERE I STOPPED
// I need to attach this avatar object to the player so it can do whatever it needs to position its sub-objects
// In this case this is the simple head-and-hands avatar style, but it should be done in a way that can work
// with more advanced types, like rigged models with IK
player.addEventListener('xrframe', (ev) => this.handleXRFrame(ev));
},
//update(dt) {
//},
handleXRFrame(ev) {
console.log('update it', ev.target, ev.data);
}
});
janus.registerElement('avatar_rigged', {
create() {
},
update(dt) {
},
updateXR(pose, referenceSpace) {
}
});
*/
elation.elements.define('janus-avatar-picker', class extends elation.elements.base {
init() {
this.defineAttribute('src', { type: 'string' });
this.defineAttribute('sources', { type: 'object' });
this.defineAttribute('previewpos', { type: 'vector3', default: V(-2, 0, -3) });
this.defineAttribute('hideconfirm', { type: 'boolean', default: true });
this.defineAttribute('hidereset', { type: 'boolean', default: true });
this.selected = false;
}
create() {
let tpl = '';
if (this.sources) {
let sources = (elation.utils.isString(this.sources) ? JSON.parse(this.sources) : this.sources);
if (sources.length > 1) {
tpl += '<ui-tabs>';
sources.forEach(source => {
let sourcestr = this.getSourceString(source);
tpl += `<ui-tab label="${source.name}">${sourcestr}</ui-tab>`;
});
} else {
tpl = this.getSourceString(sources[0]);
}
} else {
tpl = `
<ui-tabs>
<ui-tab label="Chibis">
<collection-jsonapi id="avatarlist" endpoint="${this.src}"></collection-jsonapi>
<ui-list name="avatar" selectable="1" collection="avatarlist" itemcomponent="janus-avatar-picker-item"></ui-list>
</ui-tab>
<ui-tab label="Ready Player Me">
<iframe name="rpmselector" data-src="https://demo.readyplayer.me/avatar?frameApi" allow="camera"></iframe>
</ui-tab>
</ui-tabs>
`;
}
if (!this.hideconfirm) {
tpl += `<ui-button name="confirm" disabled="1">Confirm</ui-button>`;
}
if (!this.hidereset) {
tpl += `<ui-button name="reset">Reset</ui-button>`;
}
this.elements = elation.elements.fromString(tpl, this);
if (this.elements['avatar']) {
elation.events.add(this.elements['avatar'], 'select', (ev) => this.handleAvatarSelect(ev));
}
if (this.elements['confirm']) {
elation.events.add(this.elements['confirm'], 'click', (ev) => this.handleAvatarConfirm(ev));
}
if (this.elements['reset']) {
elation.events.add(this.elements['reset'], 'click', (ev) => this.handleAvatarReset(ev));
}
// Observer watches for visibility events and hides the 3d avatar preview when the 2d ui disappears
let threshold = [];
for (let i = 0; i < 100; i += 10) threshold.push(i / 100);
let observer = new IntersectionObserver(ev => this.handleIntersectionChange(ev), { root: document, rootMargin: '0px', threshold: threshold });
observer.observe(this.elements.rpmselector);
document.addEventListener('scroll', ev => this.handleScroll(ev));
window.addEventListener('message', ev => this.handleWindowMessage(ev));
}
getSourceString(source) {
let tpl = '';
if (source.type == 'list') {
tpl = `
<collection-jsonapi id="avatarlist" endpoint="${source.src}"></collection-jsonapi>
<ui-list name="avatar" selectable="1" collection="avatarlist" itemcomponent="janus-avatar-picker-item"></ui-list>
`;
} else if (source.type == 'iframe') {
tpl = `<iframe data-src="${source.src}" allow="camera"></iframe>`;
}
return tpl;
}
selectAvatar(avatar) {
let items = this.elements.avatarlist.items,
list = this.elements.avatar;
console.log('avatar?', items, list, avatar);
}
hidePreview(hidelight=false) {
if (this.avatarpreview) {
this.avatarpreview.die();
this.avatarpreview = false;
}
if (this.avatarlight && hidelight) {
this.avatarlight.die();
this.avatarlight = false;
}
}
showPreview(src) {
/*
if (!src) {
src = 'data:text/plain,' + escape(player.getCurrentAvatarData());
}
this.hidePreview();
this.avatarpreview = player.createObject('ghost', { avatar_src: src, pos: V(this.previewpos), rotate_deg_per_sec: 10, });
if (!this.avatarlight) {
this.avatarlight = player.createObject('light', {
light_target: this.avatarpreview,
light_cone_angle: .5,
light_range: 4,
light_decay: 1,
col: 'white',
pos: V(0, 3, 1)
});
}
this.updatePreviewPosition();
*/
/*
let preview = this.previewwindow;
if (!preview) {
console.log('append preview', this);
let avatarPreviewRoom = `
<fireboxroom>
<assets>
</assets>
<room skybox_left_id="black" skybox_right_id="black" skybox_up_id="black" skybox_down_id="black" skybox_front_id="black" skybox_back_id="black" use_local_asset="room_plane" zdir="0 0 -1">
<ghost id="{userid}" avatar_src="{src}" lighting="true" pos="0 0 4" rotate_deg_per_sec="20" />
</room>
</fireboxroom>`;
this.previewwindow = elation.elements.create('janus-viewer-avatar', {
src: 'data:text/plain,' + avatarPreviewRoom, //escape(player.getCurrentAvatarData()),
shownavigation: false,
uiconfig: 'https://baicoianu.com/~bai/janusweb/build/media/assets/webui/none.json',
});
console.log('bleh', this.previewwindow);
this.appendChild(this.previewwindow);
}
*/
}
updatePreviewPosition() {
if (this.avatarpreview) {
let rect = this.getBoundingClientRect();
let middle = (rect.top + rect.bottom) / 2;
let pos = middle / window.innerHeight;
this.avatarpreview.pos.y = 0.5 - pos;
}
}
handleAvatarSelect(ev) {
console.log('selected an avatar', ev.data);
this.selected = ev.data;
if (this.elements.confirm) {
this.showPreview(ev.data.url);
this.elements.confirm.disabled = false;
} else {
this.handleAvatarConfirm();
}
this.dispatchEvent(new CustomEvent("select", { detail: this.selected }));
}
handleAvatarConfirm(ev) {
this.hidePreview(true);
if (this.selected) {
fetch(this.selected.url)
.then(r => r.text())
.then(t => {
player.setAvatar(t);
this.dispatchEvent(new CustomEvent('select', { detail: this.selected }));
});
}
}
handleAvatarReset(ev) {
player.setAvatar(player.defaultavatar);
}
handleIntersectionChange(intersections) {
if (this.avatarpreview && !intersections[0].isIntersecting) {
this.hidePreview(true);
} else if (!this.avatarpreview && intersections[0].isIntersecting) {
this.showPreview();
}
// If our iframe hasn't been loaded yet, set a timer to load it. Clear the time out if the element becomes hidden before it fires.
if (intersections[0].intersectionRatio > 0) {
if (this.iframevistimer) {
clearTimeout(this.iframevistimer);
}
this.iframevistimer = setTimeout(() => {
let iframe = this.querySelector('iframe[data-src]');
if (iframe && iframe.src != iframe.dataset.src) {
iframe.src = iframe.dataset.src;
delete iframe.dataset.src;
}
}, 50);
} else if (this.iframevistimer) {
clearTimeout(this.iframevistimer);
}
}
handleScroll(ev) {
this.updatePreviewPosition();
}
handleWindowMessage(ev) {
if (ev.origin.match(/https:\/\/.*\.readyplayer\.me/)) {
if (ev.data.indexOf('https:') == 0) {
// raw URL
let avatarurl = ev.data;
let avatarstr = `
<FireBoxRoom>
<assets>
<assetobject id="body" src="${avatarurl}" />
<assetobject id="avatar_animations" src="https://assets.metacade.com/james/readyplayerme/animations.glb" />
</assets>
<room>
<ghost body_id="body" bone_head="Head" morphtarget_mouth="mouthOpen" morphtarget_eyes="eyesClosed" />
</room>
</FireBoxRoom>
`;
player.setAvatar(avatarstr);
this.handleAvatarSelect({data: { url: 'data:text/plain,' + avatarstr } });
} else if (ev.data[0] == '{') {
// frame API
let apimsg = JSON.parse(ev.data);
let frame = this.querySelector('iframe'); // FIXME - hacky
if (apimsg.eventName == 'v1.frame.ready' && frame && frame.contentWindow) {
frame.contentWindow.postMessage(JSON.stringify({
target: 'readyplayerme',
type: 'subscribe',
eventName: 'v2.**'
}), '*');
} else if (apimsg.eventName == 'v2.avatar.exported') {
let avatarurl = apimsg.data.url;
let animurl = 'https://assets.metacade.com/james/readyplayerme/animations.glb';
if (apimsg.data.metadata.gender == 'female') {
animurl = 'https://assets.metacade.com/james/readyplayerme/animations-rpm-female.glb';
}
// TODO - select avatar pack based on metadata.gender
let avatarstr = `
<FireBoxRoom>
<assets>
<assetobject id="body" src="${avatarurl}" />
<assetobject id="avatar_animations" src="${animurl}" />
<assetobject id="avatar_animations_sit" src="https://assets.metacade.com/james/readyplayerme/animation-pack-sit.glb" />
</assets>
<room>
<ghost body_id="body" bone_head="Head" morphtarget_mouth="mouthOpen" morphtarget_eyes="eyesClosed" animation_extras="avatar_animations_sit" />
</room>
</FireBoxRoom>
`;
player.setAvatar(avatarstr);
this.handleAvatarSelect({data: { url: 'data:text/plain,' + avatarstr } });
}
}
}
}
});
elation.elements.define('janus-avatar-picker-item', class extends elation.elements.ui.item {
create() {
super.create();
//elation.events.add(this, 'click', (ev) => { console.log('duh', this); this.click(ev) });
let item = this.value;
let defaulticon = janus.ui.apps.default.apps.avatar.resolveFullURL('./images/default-avatar.jpg');
this.elements = elation.elements.fromString(`
<img src="${item.thumb || defaulticon}" alt="${item.name || 'Untitled Avatar'}">
`, this);
}
});