update: close to release

This commit is contained in:
Leon van Kammen 2024-01-29 20:19:04 +00:00
parent ead6a93171
commit 9df7685253
21 changed files with 359139 additions and 588 deletions

4
.gitignore vendored
View file

@ -1,4 +1,6 @@
dist/*.pyc dist/*.pyc
src/spec/tmp.json src/spec/tmp.json
tags tags
example/assets/*.blend example/assets/*.blend*
2wa.gitlab.io/*

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024 * v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -554,10 +554,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework // the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => { var $el = (html,tag) => {
let el = document.createElement('div') let el = document.createElement('div')
el.innerHTML = html el.innerHTML = html
return el.children[0] return el.children[0]
@ -629,15 +629,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* xrf.emit('foo',123).then(...).catch(...).finally(...) * xrf.emit('foo',123).then(...).catch(...).finally(...)
*/ */
xrf.addEventListener = function(eventName, callback, scene) { xrf.addEventListener = function(eventName, callback, opts) {
if( !this._listeners ) this._listeners = [] if( !this._listeners ) this._listeners = []
callback.opts = opts || {weight: this._listeners.length}
if (!this._listeners[eventName]) { if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet // create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = []; this._listeners[eventName] = [];
} }
if( scene ) callback.scene = scene
// add the callback to the listeners array for this event name // add the callback to the listeners array for this event name
this._listeners[eventName].push(callback); this._listeners[eventName].push(callback);
// sort
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
return () => { return () => {
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
} }
@ -653,9 +655,6 @@ xrf.emit = function(eventName, data){
console.groupEnd(label) console.groupEnd(label)
if( xrf.debug > 1 ) debugger if( xrf.debug > 1 ) debugger
} }
// forward to THREEjs eventbus if any
if( data.scene ) data.scene.dispatchEvent( eventName, data )
if( data.mesh ) data.mesh.dispatchEvent( eventName, data )
return xrf.emit.promise(eventName,data) return xrf.emit.promise(eventName,data)
} }
@ -907,15 +906,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrf.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file}) xrf.emit('parseModel',{model,url,file})
} }
@ -947,6 +937,8 @@ xrf.reset = () => {
// remove mixers // remove mixers
xrf.mixers.map( (m) => m.stop()) xrf.mixers.map( (m) => m.stop())
xrf.mixers = [] xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
@ -981,47 +973,81 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => { xrf.navigator.to = (url,flags,loader,data) => {
if( !url ) throw 'xrf.navigator.to(..) no url given' if( !url ) throw 'xrf.navigator.to(..) no url given'
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let hashChange = (!file && hash) || !data && xrf.model.file == file
let hasPos = String(hash).match(/pos=/)
let hashbus = xrf.hashbus let hashbus = xrf.hashbus
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) xrf
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded .emit('navigate', {url,loader,data})
if( hash == document.location.hash.substr(1) ) return // block duplicate calls .then( () => {
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false if( ext && !loader ){
if( !loader ){ const Loader = xrf.loaders[ext]
const Loader = xrf.loaders[ext] if( !Loader ) return resolve()
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext loader = loader || new Loader().setPath( dir )
loader = loader || new Loader().setPath( dir ) }
}
// force relative path for files which dont include protocol or relative path if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
xrf.add( model.scene ) if( hashChange && !hasPos ){
xrf.navigator.updateHash(hash) hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.emit('navigateLoaded',{url,model}) xrf.navigator.updateHash(hash) // which don't require
resolve(model) return resolve(xrf.model) // positional navigation
} }
if( data ) loader.parse(data, "", onLoad ) xrf
else loader.load(url, onLoad ) .emit('navigateLoading', {url,loader,data})
.then( () => {
if( hashChange && hasPos ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url})
return resolve(xrf.model)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// force relative path for files which dont include protocol or relative path
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
}) })
} }
@ -1029,13 +1055,17 @@ xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){ window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
}
}) })
window.addEventListener('hashchange', function (e){ window.addEventListener('hashchange', function (e){
xrf.emit('hash', {hash: document.location.hash }) xrf.emit('hash', {hash: document.location.hash })
}) })
xrf.navigator.setupNavigateFallbacks()
// this allows selectionlines to be updated according to the camera (renderloop) // this allows selectionlines to be updated according to the camera (renderloop)
xrf.focusLine = new xrf.THREE.Group() xrf.focusLine = new xrf.THREE.Group()
xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true})
@ -1048,10 +1078,36 @@ xrf.navigator.init = () => {
xrf.navigator.init.inited = true xrf.navigator.init.inited = true
} }
xrf.navigator.setupNavigateFallbacks = () => {
xrf.addEventListener('navigate', (opts) => {
let {url} = opts
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
// handle http links
if( url.match(/^http/) && !xrf.loaders[ext] ){
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
return inIframe ? window.parent.postMessage({ url }, '*') : window.open( url, '_blank')
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
}
})
}
xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash document.location.hash = hash
xrf.navigator.updateHash.active = false
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1070,7 +1126,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map //scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping; // renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2; // renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`) // console.log(` └ applied image '${frag.env.string}' as environment map`)
} }
}) })
@ -1115,20 +1171,20 @@ xrf.frag.href = function(v, opts){
xrf xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => { .then( () => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
//if( !file.match(/\./) || file.match(/\.html/) ){ const isLocal = v.string[0] == '#'
// debugger const hasPos = isLocal && v.string.match(/pos=/)
// let inIframe const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
// try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
// return inIframe ? window.parent.postMessage({ url: v.string }, '*') : window.open( v.string, '_blank')
//}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) //if( isLocal && !hasPos ){
xrf.navigator.to(v.string) // let's surf to HREF! // xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
//}else{
//if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf
//}
}) })
.catch( console.error ) .catch( console.error )
} }
@ -1185,20 +1241,24 @@ xrf.frag.href = function(v, opts){
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
let pos = v
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){ if( pos.x == undefined ){
let obj = scene.getObjectByName(v.string) let obj = scene.getObjectByName(v.string)
if( !obj ) return if( !obj ) return
let pos = obj.position.clone() pos = obj.position.clone()
obj.getWorldPosition(pos) obj.getWorldPosition(pos)
camera.position.copy(pos) camera.position.copy(pos)
}else{ }else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D // spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x camera.position.x = pos.x
camera.position.y = v.y camera.position.y = pos.y
camera.position.z = v.z camera.position.z = pos.z
} }
xrf.frag.pos.last = pos // remember
camera.updateMatrixWorld() camera.updateMatrixWorld()
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
@ -1242,7 +1302,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts let {mesh} = opts
let scene = model.scene let scene = model.scene
scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene
if( mesh.material && !mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because // only add remote objects, because
@ -1822,16 +1883,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => { scene.traverse( (n) => {
if( n.userData && n.userData['#'] ){ if( n.userData && n.userData['#'] ){
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
}
} }
}) })
} }
// clicking href url with predefined view
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
xrf.hashbus.pub( opts.xrf.string )
})
xrf.addEventListener('dynamicKeyValue', (opts) => { xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
@ -1866,6 +1925,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
let {scene,id,match,v} = opts let {scene,id,match,v} = opts
if( !scene ) return if( !scene ) return
let remove = [] let remove = []
// erase previous lines // erase previous lines
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
xrf.focusLine.points = [] xrf.focusLine.points = []
@ -2057,7 +2117,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({ mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null, map: null,
transparent: url.match(/(png|gif)/) ? true : false, transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide, side: THREE.DoubleSide,
color: 0xFFFFFF, color: 0xFFFFFF,
opacity:1 opacity:1
@ -2078,6 +2138,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
} }
} }
mesh.material.map = texture mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true mesh.needsUpdate = true
} }
@ -2374,8 +2435,16 @@ window.AFRAME.registerComponent('xrf', {
if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) ) if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) )
if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) ) if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) )
xrf.addEventListener('navigateLoaded', () => { aScene.addEventListener('enter-vr', () => {
// sometimes AFRAME resets the user position to 0,0,0 when entering VR (not sure why)
let pos = xrf.frag.pos.last
if( pos ){ AFRAME.XRF.camera.position.set(pos.x, pos.y, pos.z) }
})
xrf.addEventListener('navigateLoaded', (opts) => {
setTimeout( () => AFRAME.fade.out(),500) setTimeout( () => AFRAME.fade.out(),500)
let isLocal = opts.url.match(/^#/)
if( isLocal ) return
// *TODO* this does not really belong here perhaps // *TODO* this does not really belong here perhaps
let blinkControls = document.querySelector('[blink-controls]') let blinkControls = document.querySelector('[blink-controls]')
@ -2393,34 +2462,34 @@ window.AFRAME.registerComponent('xrf', {
el.setAttribute("class","floor") el.setAttribute("class","floor")
$('a-scene').appendChild(el) $('a-scene').appendChild(el)
}) })
blinkControls.components['blink-controls'].update({collisionEntities:true}) let com = blinkControls.components['blink-controls']
if( com ) com.update({collisionEntities:true})
else console.warn("xrfragments: blink-controls is not mounted, please run manually: $('[blink-controls]).components['blink-controls'].update({collisionEntities:true})")
} }
}) })
xrf.addEventListener('href', (opts) => { xrf.addEventListener('navigateLoading', (opts) => {
if( opts.click){ let p = opts.promise()
let p = opts.promise() let url = opts.url
let url = opts.xrf.string let isLocal = url.match(/^#/)
let isLocal = url.match(/^#/) let hasPos = url.match(/pos=/)
let hasPos = url.match(/pos=/) let fastFadeMs = 200
if( !isLocal && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
if( isLocal && hasPos ){ if( isLocal ){
if( hasPos ){
// local teleports only // local teleports only
let fastFadeMs = 200
AFRAME.fade.in(fastFadeMs) AFRAME.fade.in(fastFadeMs)
setTimeout( () => { setTimeout( () => {
p.resolve() p.resolve()
AFRAME.fade.out(fastFadeMs)
}, fastFadeMs) }, fastFadeMs)
}else if( !isLocal ){ }
AFRAME.fade.in() }else{
setTimeout( () => { AFRAME.fade.in(fastFadeMs)
p.resolve() setTimeout( () => {
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g. p.resolve()
}, AFRAME.fade.data.fadetime ) }, AFRAME.fade.data.fadetime )
}else p.resolve()
} }
}) },{weight:-1000})
// convert href's to a-entity's so AFRAME // convert href's to a-entity's so AFRAME
// raycaster can find & execute it // raycaster can find & execute it

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024 * v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework // the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => { var $el = (html,tag) => {
let el = document.createElement('div') let el = document.createElement('div')
el.innerHTML = html el.innerHTML = html
return el.children[0] return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* xrf.emit('foo',123).then(...).catch(...).finally(...) * xrf.emit('foo',123).then(...).catch(...).finally(...)
*/ */
xrf.addEventListener = function(eventName, callback, scene) { xrf.addEventListener = function(eventName, callback, opts) {
if( !this._listeners ) this._listeners = [] if( !this._listeners ) this._listeners = []
callback.opts = opts || {weight: this._listeners.length}
if (!this._listeners[eventName]) { if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet // create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = []; this._listeners[eventName] = [];
} }
if( scene ) callback.scene = scene
// add the callback to the listeners array for this event name // add the callback to the listeners array for this event name
this._listeners[eventName].push(callback); this._listeners[eventName].push(callback);
// sort
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
return () => { return () => {
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
} }
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
console.groupEnd(label) console.groupEnd(label)
if( xrf.debug > 1 ) debugger if( xrf.debug > 1 ) debugger
} }
// forward to THREEjs eventbus if any
if( data.scene ) data.scene.dispatchEvent( eventName, data )
if( data.mesh ) data.mesh.dispatchEvent( eventName, data )
return xrf.emit.promise(eventName,data) return xrf.emit.promise(eventName,data)
} }
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrf.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file}) xrf.emit('parseModel',{model,url,file})
} }
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers // remove mixers
xrf.mixers.map( (m) => m.stop()) xrf.mixers.map( (m) => m.stop())
xrf.mixers = [] xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => { xrf.navigator.to = (url,flags,loader,data) => {
if( !url ) throw 'xrf.navigator.to(..) no url given' if( !url ) throw 'xrf.navigator.to(..) no url given'
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let hashChange = (!file && hash) || !data && xrf.model.file == file
let hasPos = String(hash).match(/pos=/)
let hashbus = xrf.hashbus let hashbus = xrf.hashbus
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) xrf
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded .emit('navigate', {url,loader,data})
if( hash == document.location.hash.substr(1) ) return // block duplicate calls .then( () => {
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false if( ext && !loader ){
if( !loader ){ const Loader = xrf.loaders[ext]
const Loader = xrf.loaders[ext] if( !Loader ) return resolve()
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext loader = loader || new Loader().setPath( dir )
loader = loader || new Loader().setPath( dir ) }
}
// force relative path for files which dont include protocol or relative path if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
xrf.add( model.scene ) if( hashChange && !hasPos ){
xrf.navigator.updateHash(hash) hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.emit('navigateLoaded',{url,model}) xrf.navigator.updateHash(hash) // which don't require
resolve(model) return resolve(xrf.model) // positional navigation
} }
if( data ) loader.parse(data, "", onLoad ) xrf
else loader.load(url, onLoad ) .emit('navigateLoading', {url,loader,data})
.then( () => {
if( hashChange && hasPos ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url})
return resolve(xrf.model)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// force relative path for files which dont include protocol or relative path
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
}) })
} }
@ -1027,13 +1053,17 @@ xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){ window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
}
}) })
window.addEventListener('hashchange', function (e){ window.addEventListener('hashchange', function (e){
xrf.emit('hash', {hash: document.location.hash }) xrf.emit('hash', {hash: document.location.hash })
}) })
xrf.navigator.setupNavigateFallbacks()
// this allows selectionlines to be updated according to the camera (renderloop) // this allows selectionlines to be updated according to the camera (renderloop)
xrf.focusLine = new xrf.THREE.Group() xrf.focusLine = new xrf.THREE.Group()
xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true})
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
xrf.navigator.init.inited = true xrf.navigator.init.inited = true
} }
xrf.navigator.setupNavigateFallbacks = () => {
xrf.addEventListener('navigate', (opts) => {
let {url} = opts
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
// handle http links
if( url.match(/^http/) && !xrf.loaders[ext] ){
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
return inIframe ? window.parent.postMessage({ url }, '*') : window.open( url, '_blank')
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
}
})
}
xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash document.location.hash = hash
xrf.navigator.updateHash.active = false
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map //scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping; // renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2; // renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`) // console.log(` └ applied image '${frag.env.string}' as environment map`)
} }
}) })
@ -1113,20 +1169,20 @@ xrf.frag.href = function(v, opts){
xrf xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => { .then( () => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
//if( !file.match(/\./) || file.match(/\.html/) ){ const isLocal = v.string[0] == '#'
// debugger const hasPos = isLocal && v.string.match(/pos=/)
// let inIframe const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
// try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
// return inIframe ? window.parent.postMessage({ url: v.string }, '*') : window.open( v.string, '_blank')
//}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) //if( isLocal && !hasPos ){
xrf.navigator.to(v.string) // let's surf to HREF! // xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
//}else{
//if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf
//}
}) })
.catch( console.error ) .catch( console.error )
} }
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
let pos = v
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){ if( pos.x == undefined ){
let obj = scene.getObjectByName(v.string) let obj = scene.getObjectByName(v.string)
if( !obj ) return if( !obj ) return
let pos = obj.position.clone() pos = obj.position.clone()
obj.getWorldPosition(pos) obj.getWorldPosition(pos)
camera.position.copy(pos) camera.position.copy(pos)
}else{ }else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D // spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x camera.position.x = pos.x
camera.position.y = v.y camera.position.y = pos.y
camera.position.z = v.z camera.position.z = pos.z
} }
xrf.frag.pos.last = pos // remember
camera.updateMatrixWorld() camera.updateMatrixWorld()
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts let {mesh} = opts
let scene = model.scene let scene = model.scene
scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene
if( mesh.material && !mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because // only add remote objects, because
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => { scene.traverse( (n) => {
if( n.userData && n.userData['#'] ){ if( n.userData && n.userData['#'] ){
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
}
} }
}) })
} }
// clicking href url with predefined view
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
xrf.hashbus.pub( opts.xrf.string )
})
xrf.addEventListener('dynamicKeyValue', (opts) => { xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
let {scene,id,match,v} = opts let {scene,id,match,v} = opts
if( !scene ) return if( !scene ) return
let remove = [] let remove = []
// erase previous lines // erase previous lines
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
xrf.focusLine.points = [] xrf.focusLine.points = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({ mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null, map: null,
transparent: url.match(/(png|gif)/) ? true : false, transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide, side: THREE.DoubleSide,
color: 0xFFFFFF, color: 0xFFFFFF,
opacity:1 opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
} }
} }
mesh.material.map = texture mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true mesh.needsUpdate = true
} }
@ -2372,8 +2433,16 @@ window.AFRAME.registerComponent('xrf', {
if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) ) if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) )
if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) ) if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) )
xrf.addEventListener('navigateLoaded', () => { aScene.addEventListener('enter-vr', () => {
// sometimes AFRAME resets the user position to 0,0,0 when entering VR (not sure why)
let pos = xrf.frag.pos.last
if( pos ){ AFRAME.XRF.camera.position.set(pos.x, pos.y, pos.z) }
})
xrf.addEventListener('navigateLoaded', (opts) => {
setTimeout( () => AFRAME.fade.out(),500) setTimeout( () => AFRAME.fade.out(),500)
let isLocal = opts.url.match(/^#/)
if( isLocal ) return
// *TODO* this does not really belong here perhaps // *TODO* this does not really belong here perhaps
let blinkControls = document.querySelector('[blink-controls]') let blinkControls = document.querySelector('[blink-controls]')
@ -2391,34 +2460,34 @@ window.AFRAME.registerComponent('xrf', {
el.setAttribute("class","floor") el.setAttribute("class","floor")
$('a-scene').appendChild(el) $('a-scene').appendChild(el)
}) })
blinkControls.components['blink-controls'].update({collisionEntities:true}) let com = blinkControls.components['blink-controls']
if( com ) com.update({collisionEntities:true})
else console.warn("xrfragments: blink-controls is not mounted, please run manually: $('[blink-controls]).components['blink-controls'].update({collisionEntities:true})")
} }
}) })
xrf.addEventListener('href', (opts) => { xrf.addEventListener('navigateLoading', (opts) => {
if( opts.click){ let p = opts.promise()
let p = opts.promise() let url = opts.url
let url = opts.xrf.string let isLocal = url.match(/^#/)
let isLocal = url.match(/^#/) let hasPos = url.match(/pos=/)
let hasPos = url.match(/pos=/) let fastFadeMs = 200
if( !isLocal && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
if( isLocal && hasPos ){ if( isLocal ){
if( hasPos ){
// local teleports only // local teleports only
let fastFadeMs = 200
AFRAME.fade.in(fastFadeMs) AFRAME.fade.in(fastFadeMs)
setTimeout( () => { setTimeout( () => {
p.resolve() p.resolve()
AFRAME.fade.out(fastFadeMs)
}, fastFadeMs) }, fastFadeMs)
}else if( !isLocal ){ }
AFRAME.fade.in() }else{
setTimeout( () => { AFRAME.fade.in(fastFadeMs)
p.resolve() setTimeout( () => {
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g. p.resolve()
}, AFRAME.fade.data.fadetime ) }, AFRAME.fade.data.fadetime )
}else p.resolve()
} }
}) },{weight:-1000})
// convert href's to a-entity's so AFRAME // convert href's to a-entity's so AFRAME
// raycaster can find & execute it // raycaster can find & execute it

357884
dist/xrfragment.module.js vendored

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,12 @@
chatComponent = { chatComponent = {
html: ` html: `
<div id="chat">
<div id="videos" style="pointer-events:none"></div> <div id="videos" style="pointer-events:none"></div>
<div id="messages" aria-live="assertive" aria-relevant></div> <div id="messages" aria-live="assertive" aria-relevant></div>
<div id="chatfooter"> <div id="chatfooter">
<div id="chatbar"> <div id="chatbar">
<input id="chatline" type="text" placeholder="type here"></input> <input id="chatline" type="text" placeholder="chat here"></input>
</div> </div>
<button id="showchat" class="btn">show chat</button> <button id="showchat" class="btn">show chat</button>
</div> </div>
@ -15,10 +16,12 @@ chatComponent = {
init: (el) => new Proxy({ init: (el) => new Proxy({
scene: null, scene: null,
visible: true, visible: true,
visibleChatbar: false, messages: [],
messages: [], oneMessagePerUser: false,
username: '', // configured by 'network.connected' event
$videos: el.querySelector("#videos"), $videos: el.querySelector("#videos"),
$messages: el.querySelector("#messages"), $messages: el.querySelector("#messages"),
@ -28,16 +31,19 @@ chatComponent = {
install(opts){ install(opts){
this.opts = opts this.opts = opts
this.scene = opts.scene this.scene = opts.scene
this.$chatbar.style.display = 'none'
el.className = "xrf" el.className = "xrf"
el.style.display = 'none' // start hidden el.style.display = 'none' // start hidden
document.body.appendChild( el ) document.body.appendChild( el )
this.visibleChatbar = false
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) ) document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] }) this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
}, },
initListeners(){ initListeners(){
let {$chatline} = this let {$chatline} = this
$chatline.addEventListener('click', (e) => this.inform() )
$chatline.addEventListener('keydown', (e) => { $chatline.addEventListener('keydown', (e) => {
if (e.key == 'Enter' ){ if (e.key == 'Enter' ){
if( $chatline.value[0] != '/' ){ if( $chatline.value[0] != '/' ){
@ -45,9 +51,25 @@ chatComponent = {
} }
this.send({message: $chatline.value }) this.send({message: $chatline.value })
$chatline.value = '' $chatline.value = ''
if( window.innerHeight < 600 ) $chatline.blur()
} }
}) })
console.dir(this.scene)
document.addEventListener('network.connect', (e) => {
this.visible = true
this.$chatbar.style.display = '' // show
})
document.addEventListener('network.connected', (e) => {
if( e.detail.username ) this.username = e.detail.username
})
},
inform(){
if( !this.inform.informed && (this.inform.informed = true) ){
window.notify("Connected via P2P. You can now type message which will be visible to others.")
}
}, },
toggle(){ toggle(){
@ -55,10 +77,20 @@ chatComponent = {
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts) if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
}, },
hyphenate(str){
return String(str).replace(/[^a-zA-Z0-9]/g,'-')
},
// sending messages to the #messages div
// every user can post maximum one msg at a time
// it's more like a 'status' which is more friendly
// for accessibility reasons
// for a fullfledged chat/transcript see matrix clients
send(opts){ send(opts){
let {$messages} = this let {$messages} = this
opts = { linebreak:true, message:"", class:[], ...opts } opts = { linebreak:true, message:"", class:[], ...opts }
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts ) if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
opts.pos = opts.pos || network.posName || network.pos
let div = document.createElement('div') let div = document.createElement('div')
let msg = document.createElement('div') let msg = document.createElement('div')
let br = document.createElement('br') let br = document.createElement('br')
@ -74,7 +106,12 @@ chatComponent = {
br.classList.add.apply(br.classList, opts.class) br.classList.add.apply(br.classList, opts.class)
div.classList.add.apply(div.classList, opts.class.concat(["envelope"])) div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
} }
if( !opts.from && !msg.className.match(/(info|guide)/) ) msg.classList.add('self') if( !msg.className.match(/(info|guide|ui)/) ){
let frag = xrf.URI.parse(document.location.hash)
opts.from = 'you'
if( frag.pos ) opts.pos = frag.pos.string
msg.classList.add('self')
}
if( opts.from ){ if( opts.from ){
nick.className = "user" nick.className = "user"
nick.innerText = opts.from+' ' nick.innerText = opts.from+' '
@ -86,6 +123,15 @@ chatComponent = {
} }
} }
div.appendChild(msg) div.appendChild(msg)
// force one message per user
if( this.oneMessagePerUser && opts.from ){
div.id = this.hyphenate(opts.from)
let oldMsg = $messages.querySelector(`#${div.id}`)
if( oldMsg ) oldMsg.remove()
}
// remove after timeout
if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
// finally add the message on top
$messages.appendChild(div) $messages.appendChild(div)
if( opts.linebreak ) div.appendChild(br) if( opts.linebreak ) div.appendChild(br)
$messages.scrollTop = $messages.scrollHeight // scroll down $messages.scrollTop = $messages.scrollHeight // scroll down
@ -110,9 +156,6 @@ chatComponent = {
if( !el.inited && (el.inited = true) ) me.initListeners() if( !el.inited && (el.inited = true) ) me.initListeners()
break; break;
} }
case "visibleChatbar": {
me.$chatbar.style.display = v ? 'block' : 'none'
}
} }
} }
@ -196,46 +239,63 @@ chatComponent.css = `
max-width:unset; max-width:unset;
} }
#messages{ #messages{
position: absolute; /*
transition:1s; display: flex;
top: 0px; flex-direction: column;
left: 0;
bottom: 130px;
padding: 15px;
overflow:hidden;
pointer-events:none;
transition:1s;
width: 91%; width: 91%;
max-width: 500px; max-width: 500px;
*/
width:100%;
align-items: flex-start;
position: absolute;
transition:1s;
top: 77px;
left: 0;
bottom: 49px;
padding: 20px;
overflow:hidden;
overflow-y: scroll;
pointer-events:none;
transition:1s;
z-index: 100; z-index: 100;
-webkit-user-select:none;
-moz-user-select:-moz-none;
-ms-user-select:none;
user-select:none;
} }
body.menu #messages{ body.menu #messages{
top:50px; top:50px;
} }
#messages *{ #messages:hover {
pointer-events:all; pointer-events:all;
} }
#messages *{
pointer-events:none;
-webkit-user-select:none;
-moz-user-select:-moz-none;
-ms-user-select:none;
user-select:none;
}
#messages .msg{ #messages .msg{
transition:all 1s ease; transition:all 1s ease;
background: #fff; background: #fff;
display: inline-block; display: inline-block;
padding: 1px 17px; padding: 1px 17px;
border-radius: 20px 0px 20px 20px; border-radius: 20px;
color: #000c; color: #000c;
margin-bottom: 10px; margin-bottom: 10px;
line-height:23px; line-height:23px;
pointer-events:visible;
line-height:33px; line-height:33px;
cursor:grabbing; cursor:grabbing;
border: 1px solid #0002; border: 1px solid #0002;
} }
#messages .msg *{
pointer-events:all;
-webkit-user-select:text;
-moz-user-select:-moz-text;
-ms-user-select:text;
user-select:text;
}
#messages .msg.self{ #messages .msg.self{
border-radius: 0px 20px 20px 20px; border-radius: 20px;
background:var(--xrf-primary); background:var(--xrf-box-shadow);
} }
#messages .msg.self, #messages .msg.self,
#messages .msg.self div{ #messages .msg.self div{
@ -254,12 +314,16 @@ chatComponent.css = `
} }
#messages .msg a { #messages .msg a {
text-decoration:underline; text-decoration:underline;
color: #EEE; color: var(--xrf-primary);
font-weight:bold; font-weight:bold;
transition:1s; transition:0.3s;
}
#messages .msg.info a,
#messages a.ruler{
color:#FFF;
} }
#messages .msg a:hover{ #messages .msg a:hover{
color:#FFF; color:#000;
} }
#messages .msg.ui, #messages .msg.ui,
#messages .msg.ui div{ #messages .msg.ui div{
@ -327,10 +391,19 @@ chatComponent.css = `
margin:0; margin:0;
} }
.envelope{ .envelope,
height:unset; .envelope * {
overflow:hidden; overflow:hidden;
transition:1s; transition:1s;
pointer-events:none;
}
.envelope a,
.envelope button,
.envelope input,
.envelope textarea,
.envelope msg,
.envelope msg * {
pointer-events:all;
} }
.user{ .user{
@ -346,70 +419,92 @@ connectionsComponent = {
html: ` html: `
<div id="connections"> <div id="connections">
<i class="gg-close-o" id="close" onclick="$connections.toggle()"></i> <i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
<div id="networking"> <br>
<h2>Network channels:</h2> <div class="tab-frame">
<table> <input type="radio" name="tab" id="login" checked>
<tr> <label for="login">login</label>
<td>Webcam</td>
<td> <input type="radio" name="tab" id="io">
<select id="webcam"></select> <label for="io">devices</label>
</td>
</tr> <input type="radio" name="tab" id="networks">
<tr> <label for="networks">advanced</label>
<td>Chat</td>
<td> <div class="tab">
<select id="chatnetwork"></select> <div id="settings"></div>
</td> <table>
</tr> <tr>
<tr> <td></td>
<td>World sync</td> <td>
<td> <button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
<select id="scene"></select> </td>
</td> </tr>
</tr>
</table> </table>
</div>
<div class="tab">
<div id="devices">
<a class="badge ruler">Webcam and/or Audio</a>
<table>
<tr>
<td>Video</td>
<td>
<select id="videoInput"></select>
</td>
</tr>
<tr>
<td>Mic</td>
<td>
<select id="audioInput"></select>
</td>
</tr>
<tr style="display:none"> <!-- not used (for now) -->
<td>Audio</td>
<td>
<select id="audioOutput"></select>
</td>
</tr>
</table>
</div>
</div>
<div class="tab">
<div id="networking">
Networking a la carte:<br>
<table>
<tr>
<td>Webcam</td>
<td>
<select id="webcam"></select>
</td>
</tr>
<tr>
<td>Chat</td>
<td>
<select id="chatnetwork"></select>
</td>
</tr>
<tr>
<td>World sync</td>
<td>
<select id="scene"></select>
</td>
</tr>
</table>
</div>
</div>
</div> </div>
<div id="devices">
<a class="badge ruler">Webcam</a>
<table>
<tr>
<td>Video</td>
<td>
<select id="videoInput"></select>
</td>
</tr>
<tr>
<td>Mic</td>
<td>
<select id="audioInput"></select>
</td>
</tr>
<tr style="display:none"> <!-- not used (for now) -->
<td>Audio</td>
<td>
<select id="audioOutput"></select>
</td>
</tr>
</table>
</div>
<div id="settings"></div>
<table>
<tr>
<td></td>
<td>
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
</td>
</tr>
</table>
</div> </div>
`, `,
init: (el) => new Proxy({ init: (el) => new Proxy({
webcam: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}], visible: true,
chatnetwork: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
scene: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}], webcam: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
chatnetwork: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
scene: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
selectedWebcam: '', selectedWebcam: '',
selectedChatnetwork:'', selectedChatnetwork:'',
@ -435,15 +530,13 @@ connectionsComponent = {
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>` `<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>`
]).concat($menu.buttons) ]).concat($menu.buttons)
// hide networking settings if entering thru meetinglink
if( document.location.href.match(/meet=/) ) this.show() if( document.location.href.match(/meet=/) ) this.show()
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 ) setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
}, },
toggle(){ toggle(){
let parent = el.closest('.envelope') $chat.visible = !$chat.visible
parent.style.display = parent.style.display == 'none' ? parent.style.display = '' : 'none'
}, },
change(id,e){ change(id,e){
@ -452,20 +545,28 @@ connectionsComponent = {
} }
}, },
show(){ show(opts){
$chat.visible = true opts = opts || {}
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block' if( opts.hide ){
if( !network.connected ){ if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
if( el.parentElement ) el.parentElement.parentElement.remove() if( !opts.showChat ) $chat.visible = false
$chat.send({message:"", el, class:['ui']})
if( !network.meetinglink ){ // set default
$webcam.value = 'Peer2Peer'
$chatnetwork.value = 'Peer2Peer'
$scene.value = 'Peer2Peer'
}
this.renderSettings()
}else{ }else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']}) $chat.visible = true
this.visible = true
// hide networking settings if entering thru meetinglink
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
if( !network.connected ){
document.querySelector('body > .xrf').appendChild(el)
$chat.send({message:"", el, class:['ui']})
if( !network.meetinglink ){ // set default
$webcam.value = opts.webcam || 'Peer2Peer'
$chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
$scene.value = opts.scene || 'Peer2Peer'
}
this.renderSettings()
}else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
}
} }
}, },
@ -478,7 +579,7 @@ connectionsComponent = {
forSelectedPluginsDo(cb){ forSelectedPluginsDo(cb){
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations // this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
let plugins = {} let plugins = {}
let select = (name) => (o) => o.plugin.name == name ? plugins[ o.plugin.name ] = o : '' let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
this.webcam.find( select(this.selectedWebcam) ) this.webcam.find( select(this.selectedWebcam) )
this.chatnetwork.find( select(this.selectedChatnetwork) ) this.chatnetwork.find( select(this.selectedChatnetwork) )
this.scene.find( select(this.selectedScene) ) this.scene.find( select(this.selectedScene) )
@ -491,12 +592,12 @@ connectionsComponent = {
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value } let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
this.update() this.update()
$settings.innerHTML = '' $settings.innerHTML = ''
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) ) this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
this.renderInputs() this.renderInputs()
}, },
renderInputs(){ renderInputs(){
if( this.selectedWebcam == 'No thanks' ){ if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
return this.$devices.style.display = 'none' return this.$devices.style.display = 'none'
}else this.$devices.style.display = '' }else this.$devices.style.display = ''
@ -548,25 +649,15 @@ connectionsComponent = {
}) })
}, },
reactToNetwork(){ reactToNetwork(){ // *TODO* move to network?
document.addEventListener('network.connected', () => {
console.log("network.connected")
window.notify("🪐 connected to awesomeness..")
$chat.visibleChatbar = true
$chat.send({message:`🎉 connected!`,class:['info']})
})
document.addEventListener('network.connect', () => { document.addEventListener('network.connect', () => {
console.log("network.connect") this.show({hide:true, showChat: true})
el.parentElement.classList.add('connecthide')
window.notify("🪐 connecting to awesomeness..")
$connect.innerText = 'connecting..'
}) })
document.addEventListener('network.disconnect', () => { document.addEventListener('network.disconnect', () => {
window.notify("🪐 disconnecting..") this.connected = false
$connect.innerText = 'disconnecting..'
setTimeout( () => $connect.innerText = 'connect', 1000)
if( !window.accessibility.enabled ) $chat.visibleChatbar = false
}) })
} }
},{ },{
@ -575,9 +666,10 @@ connectionsComponent = {
set(data,k,v){ set(data,k,v){
data[k] = v data[k] = v
switch( k ){ switch( k ){
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break; case "visible": el.style.display = v ? '' : 'none'; break;
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break; case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break; case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "selectedScene": $scene.value = v; data.renderSettings(); break; case "selectedScene": $scene.value = v; data.renderSettings(); break;
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break; case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
case "selectedWebcam": { case "selectedWebcam": {
@ -620,11 +712,11 @@ connectionsComponent.css = `
} }
#close{ #close{
display: block; display: block;
margin-top: 16px;
position: relative; position: relative;
float: right; float: right;
margin-bottom: 7px; top: 16px;
} }
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
</style>` </style>`
// reactive component for displaying the menu // reactive component for displaying the menu
menuComponent = (el) => new Proxy({ menuComponent = (el) => new Proxy({
@ -644,8 +736,8 @@ menuComponent = (el) => new Proxy({
$buttons: $buttons = el.querySelector('#buttons'), $buttons: $buttons = el.querySelector('#buttons'),
$btnMore: $btnMore = el.querySelector('#more'), $btnMore: $btnMore = el.querySelector('#more'),
toggle(){ toggle(state){
this.collapsed = !this.collapsed this.collapsed = state !== undefined ? state : !this.collapsed
el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu' el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu']) document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
}, },
@ -777,16 +869,19 @@ window.accessibility = (opts) => new Proxy({
document.addEventListener('network.send', (e) => { document.addEventListener('network.send', (e) => {
let opts = e.detail let opts = e.detail
opts.message = opts.message || '' opts.message = opts.message || ''
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
this.speak(opts.message) this.speak(opts.message)
}) })
opts.xrf.addEventListener('pos', (opts) => { opts.xrf.addEventListener('pos', (opts) => {
if( this.enabled ){ if( this.enabled ){
$chat.send({message: this.posToMessage(opts) }) $chat.send({message: this.posToMessage(opts) })
network.send({message: this.posToMessage(opts), class:["info","guide"]})
}
if( opts.frag.pos.string.match(/,/) ){
network.pos = opts.frag.pos.string
}else{
network.posName = opts.frag.pos.string
} }
network.send({message: this.posToMessage(opts), class:["info","guide"]})
network.pos = opts.frag.pos.string
}) })
}, },
@ -895,6 +990,7 @@ document.head.innerHTML += `
white-space:pre; white-space:pre;
min-width: 45px; min-width: 45px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow); box-shadow: 0px 0px 10px var(--xrf-box-shadow);
display:inline-block;
} }
.xrf button:hover, .xrf button:hover,
@ -997,7 +1093,7 @@ document.head.innerHTML += `
.menu .btn{ .footer > .menu .btn{
display:inline-block; display:inline-block;
background: var(--xrf-primary); background: var(--xrf-primary);
border-radius: 25px; border-radius: 25px;
@ -1136,7 +1232,8 @@ document.head.innerHTML += `
text-align:right; text-align:right;
} }
.badge { .badge,
#messages .msg.ui div.badge{
display:inline-block; display:inline-block;
color: var(--xrf-white); color: var(--xrf-white);
font-weight: bold; font-weight: bold;
@ -1205,6 +1302,22 @@ document.head.innerHTML += `
top: 64px; top: 64px;
} }
.right { float:right }
.left { float:left }
/*
* tabs
*/
div.tab-frame > input{ display:none;}
div.tab-frame > label{ display:block; float:left;padding:5px 10px; cursor:pointer; }
div.tab-frame > input:checked + label{ cursor:default; border-bottom:1px solid #888; font-weight:bold; }
div.tab-frame > div.tab{ display:none; padding:15px 10px 5px 10px;clear:left}
div.tab-frame > input:nth-of-type(1):checked ~ .tab:nth-of-type(1),
div.tab-frame > input:nth-of-type(2):checked ~ .tab:nth-of-type(2),
div.tab-frame > input:nth-of-type(3):checked ~ .tab:nth-of-type(3){ display:block;}
/* /*
* css icons from https://css.gg * css icons from https://css.gg
*/ */
@ -1366,7 +1479,7 @@ document.head.innerHTML += `
position: relative; position: relative;
display: inline-block; display: inline-block;
-moz-transform: rotate(-45deg) scale(var(--ggs,1)); -moz-transform: rotate(-45deg) scale(var(--ggs,1));
transform: translate(4px,-5px) rotate(-45deg) scale(var(--ggs,1)); transform: translate(4px,1px) rotate(-45deg) scale(var(--ggs,1));
width: 8px; width: 8px;
height: 2px; height: 2px;
background: currentColor; background: currentColor;
@ -1532,14 +1645,14 @@ document.head.innerHTML += `
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: inline-block; display: inline-block;
transform: scale(var(--ggs,1)) translate(3px,2px); transform: scale(var(--ggs,1)) translate(3px,9px);
width: 16px; width: 16px;
height: 6px; height: 6px;
border: 2px solid; border: 2px solid;
border-top: 0; border-top: 0;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
margin-top: 8px line-height:15px;
} }
.gg-software-download::after { .gg-software-download::after {
content: ""; content: "";
@ -1643,7 +1756,9 @@ window.frontend = (opts) => new Proxy({
.setupIframeUrlHandler() .setupIframeUrlHandler()
.setupCapture() .setupCapture()
.setupUserHints() .setupUserHints()
.setupNetworkListeners()
.hidetopbarWhenMenuCollapse() .hidetopbarWhenMenuCollapse()
.hideUIWhenNavigating()
window.notify = this.notify window.notify = this.notify
setTimeout( () => { setTimeout( () => {
@ -1694,12 +1809,66 @@ window.frontend = (opts) => new Proxy({
return this return this
}, },
setupNetworkListeners(){
document.addEventListener('network.connect', (e) => {
console.log("network.connect")
window.notify("🪐 connecting to awesomeness..")
$chat.send({message:`🪐 connecting to awesomeness..`,class:['info'], timeout:5000})
})
document.addEventListener('network.connected', (e) => {
window.notify("🪐 connected to awesomeness..")
$chat.visibleChatbar = true
$chat.send({message:`🎉 ${e.detail.plugin.profile.name||''} connected!`,class:['info'], timeout:5000})
})
document.addEventListener('network.disconnect', () => {
window.notify("🪐 disconnecting..")
})
document.addEventListener('network.info', (e) => {
window.notify(e.detail.message)
$chat.send({...e.detail, class:['info'], timeout:5000})
})
document.addEventListener('network.error', (e) => {
window.notify(e.detail.message)
$chat.send({...e.detail, class:['info'], timeout:5000})
})
return this
},
hidetopbarWhenMenuCollapse(){ hidetopbarWhenMenuCollapse(){
// hide topbar when menu collapse button is pressed // hide topbar when menu collapse button is pressed
document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none') document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none')
return this return this
}, },
hideUIWhenNavigating(){
// hide ui when user is navigating the scene using mouse/touch
let showUI = (show) => (e) => {
let isChatMsg = e.target.closest('.msg')
let isChatLine = e.target.id == 'chatline'
let isChatEmptySpace = e.target.id == 'messages'
let isUI = e.target.closest('.ui')
//console.dir({class: e.target.className, id: e.target.id, isChatMsg,isChatLine,isChatEmptySpace,isUI, tagName: e.target.tagName})
if( isUI || e.target.tagName.match(/^(BUTTON|TEXTAREA|INPUT|A)/) || e.target.className.match(/(btn)/) ) return
if( show ){
$chat.visible = true
}else{
$chat.visible = false
$menu.toggle(false)
}
return true
}
document.addEventListener('mousedown', showUI(false) )
document.addEventListener('mouseup', showUI(true) )
document.addEventListener('touchstart', showUI(false) )
document.addEventListener('touchend', showUI(true) )
},
loadFile(contentLoaders, multiple){ loadFile(contentLoaders, multiple){
return () => { return () => {
window.notify("if you're on Meta browser, file-uploads might be disabled") window.notify("if you're on Meta browser, file-uploads might be disabled")
@ -1787,24 +1956,28 @@ window.frontend = (opts) => new Proxy({
}, },
share(opts){ share(opts){
opts = opts || {notify:true,qr:true,share:true} opts = opts || {notify:true,qr:true,share:true,linkonly:false}
if( network.connected && !document.location.hash.match(/meet=/) ){ if( network.meetingLink && !document.location.hash.match(/meet=/) ){
let p = $connections.chatnetwork.find( (p) => p.plugin.name == $connections.selectedChatnetwork ) document.location.hash += `&meet=${network.meetingLink}`
if( p.link ) document.location.hash += `&meet=${p.link}` }
if( !document.location.hash.match(/pos=/) ){
document.location.hash += `&pos=${ network.posName || network.pos }`
} }
let url = window.location.href let url = window.location.href
if( opts.linkonly ) return url
this.copyToClipboard( url ) this.copyToClipboard( url )
// End of *TODO* // End of *TODO*
if( opts.notify ){ if( opts.notify ){
window.notify(`<h2>${ network.connected ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br> window.notify(`<h2>${ network.connected ? 'Meeting link ' : 'Link'} copied to clipboard!</h2>
Now share it with your friends <br>
<canvas id="qrcode" width="121" height="121"></canvas><br> <canvas id="qrcode" width="121" height="121"></canvas><br>
<button onclick="frontend.download()"><i class="gg-software-download"></i>&nbsp;&nbsp;&nbsp;download scene file</button> <br> <button onclick="frontend.download()"><i class="gg-software-download"></i>&nbsp;&nbsp;&nbsp;download scene file</button> <br>
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i>&nbsp;&nbsp;download 360 screenshot</button> <br> <button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i>&nbsp;&nbsp;download 360 screenshot</button> <br>
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i>&nbsp;&nbsp;&nbsp;clone & selfhost this experience</a><br> <a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i>&nbsp;&nbsp;&nbsp;clone & selfhost this experience</a><br>
<br>
To embed this experience in your blog,<br> To embed this experience in your blog,<br>
copy/paste the following into your HTML:<br><input type="text" value="&lt;iframe src='${document.location.href}'&gt;&lt;/iframe&gt;" id="share"/> copy/paste the following into your HTML:<br><input type="text" value="&lt;iframe src='${document.location.href}'&gt;&lt;/iframe&gt;" id="share"/>
<br> <br>
<br>
`,{timeout:false}) `,{timeout:false})
} }
// draw QR code // draw QR code
@ -1846,6 +2019,7 @@ window.network = (opts) => new Proxy({
connected: false, connected: false,
pos: '', pos: '',
posName: '',
meetinglink: "", meetinglink: "",
peers: {}, peers: {},
plugin: {}, plugin: {},
@ -1901,8 +2075,7 @@ window.network = (opts) => new Proxy({
for ( var i in String.prototype ) add(i) for ( var i in String.prototype ) add(i)
var a = names[Math.floor(Math.random() * names.length)]; var a = names[Math.floor(Math.random() * names.length)];
var b = names[Math.floor(Math.random() * names.length)]; var b = names[Math.floor(Math.random() * names.length)];
var c = names[Math.floor(Math.random() * names.length)]; return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
return String(`${a}-${b}-${c}`).toLowerCase()
} }
}, },
@ -2097,7 +2270,7 @@ document.head.innerHTML += `
height: auto; height: auto;
margin: 5px 0; margin: 5px 0;
transition: all ease .5s; transition: all ease .5s;
border-radius: 3px; border-radius: 15px;
box-shadow: 0 0 4px 0 var(--xrf-box-shadow); box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
right: 20px; right: 20px;
position: fixed; position: fixed;
@ -2157,8 +2330,8 @@ document.head.innerHTML += `
.js-snackbar__close { .js-snackbar__close {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: top;
padding: 0 10px; padding: 8px 13px 0px 0px;
user-select: none; user-select: none;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024 * v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework // the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => { var $el = (html,tag) => {
let el = document.createElement('div') let el = document.createElement('div')
el.innerHTML = html el.innerHTML = html
return el.children[0] return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* xrf.emit('foo',123).then(...).catch(...).finally(...) * xrf.emit('foo',123).then(...).catch(...).finally(...)
*/ */
xrf.addEventListener = function(eventName, callback, scene) { xrf.addEventListener = function(eventName, callback, opts) {
if( !this._listeners ) this._listeners = [] if( !this._listeners ) this._listeners = []
callback.opts = opts || {weight: this._listeners.length}
if (!this._listeners[eventName]) { if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet // create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = []; this._listeners[eventName] = [];
} }
if( scene ) callback.scene = scene
// add the callback to the listeners array for this event name // add the callback to the listeners array for this event name
this._listeners[eventName].push(callback); this._listeners[eventName].push(callback);
// sort
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
return () => { return () => {
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
} }
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
console.groupEnd(label) console.groupEnd(label)
if( xrf.debug > 1 ) debugger if( xrf.debug > 1 ) debugger
} }
// forward to THREEjs eventbus if any
if( data.scene ) data.scene.dispatchEvent( eventName, data )
if( data.mesh ) data.mesh.dispatchEvent( eventName, data )
return xrf.emit.promise(eventName,data) return xrf.emit.promise(eventName,data)
} }
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrf.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file}) xrf.emit('parseModel',{model,url,file})
} }
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers // remove mixers
xrf.mixers.map( (m) => m.stop()) xrf.mixers.map( (m) => m.stop())
xrf.mixers = [] xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => { xrf.navigator.to = (url,flags,loader,data) => {
if( !url ) throw 'xrf.navigator.to(..) no url given' if( !url ) throw 'xrf.navigator.to(..) no url given'
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let hashChange = (!file && hash) || !data && xrf.model.file == file
let hasPos = String(hash).match(/pos=/)
let hashbus = xrf.hashbus let hashbus = xrf.hashbus
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) xrf
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded .emit('navigate', {url,loader,data})
if( hash == document.location.hash.substr(1) ) return // block duplicate calls .then( () => {
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false if( ext && !loader ){
if( !loader ){ const Loader = xrf.loaders[ext]
const Loader = xrf.loaders[ext] if( !Loader ) return resolve()
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext loader = loader || new Loader().setPath( dir )
loader = loader || new Loader().setPath( dir ) }
}
// force relative path for files which dont include protocol or relative path if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
xrf.add( model.scene ) if( hashChange && !hasPos ){
xrf.navigator.updateHash(hash) hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.emit('navigateLoaded',{url,model}) xrf.navigator.updateHash(hash) // which don't require
resolve(model) return resolve(xrf.model) // positional navigation
} }
if( data ) loader.parse(data, "", onLoad ) xrf
else loader.load(url, onLoad ) .emit('navigateLoading', {url,loader,data})
.then( () => {
if( hashChange && hasPos ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url})
return resolve(xrf.model)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// force relative path for files which dont include protocol or relative path
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
}) })
} }
@ -1027,13 +1053,17 @@ xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){ window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
}
}) })
window.addEventListener('hashchange', function (e){ window.addEventListener('hashchange', function (e){
xrf.emit('hash', {hash: document.location.hash }) xrf.emit('hash', {hash: document.location.hash })
}) })
xrf.navigator.setupNavigateFallbacks()
// this allows selectionlines to be updated according to the camera (renderloop) // this allows selectionlines to be updated according to the camera (renderloop)
xrf.focusLine = new xrf.THREE.Group() xrf.focusLine = new xrf.THREE.Group()
xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true})
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
xrf.navigator.init.inited = true xrf.navigator.init.inited = true
} }
xrf.navigator.setupNavigateFallbacks = () => {
xrf.addEventListener('navigate', (opts) => {
let {url} = opts
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
// handle http links
if( url.match(/^http/) && !xrf.loaders[ext] ){
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
return inIframe ? window.parent.postMessage({ url }, '*') : window.open( url, '_blank')
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
}
})
}
xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash document.location.hash = hash
xrf.navigator.updateHash.active = false
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map //scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping; // renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2; // renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`) // console.log(` └ applied image '${frag.env.string}' as environment map`)
} }
}) })
@ -1113,20 +1169,20 @@ xrf.frag.href = function(v, opts){
xrf xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => { .then( () => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
//if( !file.match(/\./) || file.match(/\.html/) ){ const isLocal = v.string[0] == '#'
// debugger const hasPos = isLocal && v.string.match(/pos=/)
// let inIframe const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
// try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
// return inIframe ? window.parent.postMessage({ url: v.string }, '*') : window.open( v.string, '_blank')
//}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) //if( isLocal && !hasPos ){
xrf.navigator.to(v.string) // let's surf to HREF! // xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
//}else{
//if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf
//}
}) })
.catch( console.error ) .catch( console.error )
} }
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
let pos = v
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){ if( pos.x == undefined ){
let obj = scene.getObjectByName(v.string) let obj = scene.getObjectByName(v.string)
if( !obj ) return if( !obj ) return
let pos = obj.position.clone() pos = obj.position.clone()
obj.getWorldPosition(pos) obj.getWorldPosition(pos)
camera.position.copy(pos) camera.position.copy(pos)
}else{ }else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D // spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x camera.position.x = pos.x
camera.position.y = v.y camera.position.y = pos.y
camera.position.z = v.z camera.position.z = pos.z
} }
xrf.frag.pos.last = pos // remember
camera.updateMatrixWorld() camera.updateMatrixWorld()
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts let {mesh} = opts
let scene = model.scene let scene = model.scene
scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene
if( mesh.material && !mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because // only add remote objects, because
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => { scene.traverse( (n) => {
if( n.userData && n.userData['#'] ){ if( n.userData && n.userData['#'] ){
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
}
} }
}) })
} }
// clicking href url with predefined view
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
xrf.hashbus.pub( opts.xrf.string )
})
xrf.addEventListener('dynamicKeyValue', (opts) => { xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
let {scene,id,match,v} = opts let {scene,id,match,v} = opts
if( !scene ) return if( !scene ) return
let remove = [] let remove = []
// erase previous lines // erase previous lines
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
xrf.focusLine.points = [] xrf.focusLine.points = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({ mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null, map: null,
transparent: url.match(/(png|gif)/) ? true : false, transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide, side: THREE.DoubleSide,
color: 0xFFFFFF, color: 0xFFFFFF,
opacity:1 opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
} }
} }
mesh.material.map = texture mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true mesh.needsUpdate = true
} }

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024 * v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework // the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => { var $el = (html,tag) => {
let el = document.createElement('div') let el = document.createElement('div')
el.innerHTML = html el.innerHTML = html
return el.children[0] return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* xrf.emit('foo',123).then(...).catch(...).finally(...) * xrf.emit('foo',123).then(...).catch(...).finally(...)
*/ */
xrf.addEventListener = function(eventName, callback, scene) { xrf.addEventListener = function(eventName, callback, opts) {
if( !this._listeners ) this._listeners = [] if( !this._listeners ) this._listeners = []
callback.opts = opts || {weight: this._listeners.length}
if (!this._listeners[eventName]) { if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet // create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = []; this._listeners[eventName] = [];
} }
if( scene ) callback.scene = scene
// add the callback to the listeners array for this event name // add the callback to the listeners array for this event name
this._listeners[eventName].push(callback); this._listeners[eventName].push(callback);
// sort
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
return () => { return () => {
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
} }
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
console.groupEnd(label) console.groupEnd(label)
if( xrf.debug > 1 ) debugger if( xrf.debug > 1 ) debugger
} }
// forward to THREEjs eventbus if any
if( data.scene ) data.scene.dispatchEvent( eventName, data )
if( data.mesh ) data.mesh.dispatchEvent( eventName, data )
return xrf.emit.promise(eventName,data) return xrf.emit.promise(eventName,data)
} }
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrf.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file}) xrf.emit('parseModel',{model,url,file})
} }
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers // remove mixers
xrf.mixers.map( (m) => m.stop()) xrf.mixers.map( (m) => m.stop())
xrf.mixers = [] xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => { xrf.navigator.to = (url,flags,loader,data) => {
if( !url ) throw 'xrf.navigator.to(..) no url given' if( !url ) throw 'xrf.navigator.to(..) no url given'
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let hashChange = (!file && hash) || !data && xrf.model.file == file
let hasPos = String(hash).match(/pos=/)
let hashbus = xrf.hashbus let hashbus = xrf.hashbus
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) xrf
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded .emit('navigate', {url,loader,data})
if( hash == document.location.hash.substr(1) ) return // block duplicate calls .then( () => {
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false if( ext && !loader ){
if( !loader ){ const Loader = xrf.loaders[ext]
const Loader = xrf.loaders[ext] if( !Loader ) return resolve()
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext loader = loader || new Loader().setPath( dir )
loader = loader || new Loader().setPath( dir ) }
}
// force relative path for files which dont include protocol or relative path if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
xrf.add( model.scene ) if( hashChange && !hasPos ){
xrf.navigator.updateHash(hash) hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.emit('navigateLoaded',{url,model}) xrf.navigator.updateHash(hash) // which don't require
resolve(model) return resolve(xrf.model) // positional navigation
} }
if( data ) loader.parse(data, "", onLoad ) xrf
else loader.load(url, onLoad ) .emit('navigateLoading', {url,loader,data})
.then( () => {
if( hashChange && hasPos ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url})
return resolve(xrf.model)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// force relative path for files which dont include protocol or relative path
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. generate the XRWG
xrf.XRWG.generate({model,scene:model.scene})
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
}) })
} }
@ -1027,13 +1053,17 @@ xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){ window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
}
}) })
window.addEventListener('hashchange', function (e){ window.addEventListener('hashchange', function (e){
xrf.emit('hash', {hash: document.location.hash }) xrf.emit('hash', {hash: document.location.hash })
}) })
xrf.navigator.setupNavigateFallbacks()
// this allows selectionlines to be updated according to the camera (renderloop) // this allows selectionlines to be updated according to the camera (renderloop)
xrf.focusLine = new xrf.THREE.Group() xrf.focusLine = new xrf.THREE.Group()
xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true})
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
xrf.navigator.init.inited = true xrf.navigator.init.inited = true
} }
xrf.navigator.setupNavigateFallbacks = () => {
xrf.addEventListener('navigate', (opts) => {
let {url} = opts
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
// handle http links
if( url.match(/^http/) && !xrf.loaders[ext] ){
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
return inIframe ? window.parent.postMessage({ url }, '*') : window.open( url, '_blank')
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
}
})
}
xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash document.location.hash = hash
xrf.navigator.updateHash.active = false
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map //scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping; // renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2; // renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`) // console.log(` └ applied image '${frag.env.string}' as environment map`)
} }
}) })
@ -1113,20 +1169,20 @@ xrf.frag.href = function(v, opts){
xrf xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => { .then( () => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
//if( !file.match(/\./) || file.match(/\.html/) ){ const isLocal = v.string[0] == '#'
// debugger const hasPos = isLocal && v.string.match(/pos=/)
// let inIframe const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
// try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
// return inIframe ? window.parent.postMessage({ url: v.string }, '*') : window.open( v.string, '_blank')
//}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) //if( isLocal && !hasPos ){
xrf.navigator.to(v.string) // let's surf to HREF! // xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
//}else{
//if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf
//}
}) })
.catch( console.error ) .catch( console.error )
} }
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
let pos = v
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){ if( pos.x == undefined ){
let obj = scene.getObjectByName(v.string) let obj = scene.getObjectByName(v.string)
if( !obj ) return if( !obj ) return
let pos = obj.position.clone() pos = obj.position.clone()
obj.getWorldPosition(pos) obj.getWorldPosition(pos)
camera.position.copy(pos) camera.position.copy(pos)
}else{ }else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D // spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x camera.position.x = pos.x
camera.position.y = v.y camera.position.y = pos.y
camera.position.z = v.z camera.position.z = pos.z
} }
xrf.frag.pos.last = pos // remember
camera.updateMatrixWorld() camera.updateMatrixWorld()
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts let {mesh} = opts
let scene = model.scene let scene = model.scene
scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene
if( mesh.material && !mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because // only add remote objects, because
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => { scene.traverse( (n) => {
if( n.userData && n.userData['#'] ){ if( n.userData && n.userData['#'] ){
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
}
} }
}) })
} }
// clicking href url with predefined view
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
xrf.hashbus.pub( opts.xrf.string )
})
xrf.addEventListener('dynamicKeyValue', (opts) => { xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
let {scene,id,match,v} = opts let {scene,id,match,v} = opts
if( !scene ) return if( !scene ) return
let remove = [] let remove = []
// erase previous lines // erase previous lines
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
xrf.focusLine.points = [] xrf.focusLine.points = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({ mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null, map: null,
transparent: url.match(/(png|gif)/) ? true : false, transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide, side: THREE.DoubleSide,
color: 0xFFFFFF, color: 0xFFFFFF,
opacity:1 opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
} }
} }
mesh.material.map = texture mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true mesh.needsUpdate = true
} }

View file

@ -64,6 +64,10 @@
`,{timeout:false}) `,{timeout:false})
}) })
</script> </script>
<!-- everything below is completely optional and not part of the spec -->
<script src="./../../../dist/aframe-blink-controls.min.js"></script> <!-- teleporting using controllers --> <script src="./../../../dist/aframe-blink-controls.min.js"></script> <!-- teleporting using controllers -->
<script src="https://cdn.jsdelivr.net/npm/handy-work@3.1.9/build/handy-controls.min.js"></script> <!-- hand controllers --> <script src="https://cdn.jsdelivr.net/npm/handy-work@3.1.9/build/handy-controls.min.js"></script> <!-- hand controllers -->
<script src="./../../../dist/xrfragment.plugin.p2p.js"></script> <!-- serverless p2p connectivity --> <script src="./../../../dist/xrfragment.plugin.p2p.js"></script> <!-- serverless p2p connectivity -->

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +0,0 @@
../../assets/query.gltf

View file

@ -0,0 +1 @@
../../assets/index.glb

View file

@ -1 +0,0 @@
assets/index.gltf

View file

@ -4,23 +4,13 @@
<title>THREE.js - xrfragment sandbox</title> <title>THREE.js - xrfragment sandbox</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
<script src="./../../assets/js/qr.js"></script>
</head>
<body>
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }">
<img src="./../../assets/logo.png" class="logo"/>
<input type="submit" value="load 3D file"></input>
<input type="text" id="uri" value="" onchange="XRF.navigator.to( $('#uri').value )" style="display:none"/>
</div>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"> clone project</a> </head>
<a class="btn-foot" id="embed" target="_blank" onclick="embed()">🔗 share</a> <body style="margin:0">
<a class="btn-foot" id="model" target="_blank" href="index.glb">⬇️ scene</a> <div id="overlay" style="text-align:right;position:absolute;width:100%;height:100%;top:0;left:0;bottom:0;right:0">
<a class="btn-foot" id="more" target="_blank" >XRF</a> <b>NOTE:</b> this is a THREE barebones example (AFRAME viewer has more features)<br><Br>
<textarea style="display:none"></textarea> <input type="submit" value="load 3D asset"/>
<canvas id="qrcode" style="display:none" width="300" height="300"></canvas> </div>
<!-- Import maps polyfill --> <!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported --> <!-- Remove this when import maps will be widely supported -->
@ -39,7 +29,6 @@
import xrf from "./../../../dist/xrfragment.three.module.js"; import xrf from "./../../../dist/xrfragment.three.module.js";
import { loadFile, setupConsole, setupUrlBar, notify } from "./../../assets/js/utils.js";
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js'; import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js'; import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
@ -66,7 +55,6 @@
}; };
init(); init();
notify("NOTE: only AFRAME demo has immersive back-button (for now)")
function init() { function init() {
@ -174,10 +162,6 @@
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) ); controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
cameraRig.add( controllerGrip2 ); cameraRig.add( controllerGrip2 );
setupConsole()
setupUrlBar( $('input#uri'), XRF )
// Add stats.js // Add stats.js
stats = new Stats(); stats = new Stats();
stats.dom.style.width = '80px'; stats.dom.style.width = '80px';
@ -198,11 +182,33 @@
XRF.interactive.add( statsMesh ); XRF.interactive.add( statsMesh );
}) })
// util func to bind uploaded files to a loader
let loadFile = function(contentLoaders, multiple){
return function(){
alert("if you're on Meta browser, file-uploads might be disabled")
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = function(){
let files = Array.from(input.files);
let file = files.slice ? files[0] : files
for( var i in contentLoaders ){
let r = new RegExp('\\'+i+'$')
if( file.name.match(r) ) return contentLoaders[i](file)
}
alert(file.name+" is not supported")
};
input.click();
}
}
let fileLoaders = loadFile({ let fileLoaders = loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null,xrf.loaders.gltf,data) ), ".gltf": (file) => file.arrayBuffer().then( (data) => XRF.navigator.to(file.name,null,XRF.loaders.gltf,data) ),
".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null,xrf.loaders.gltf,data) ) ".glb": (file) => file.arrayBuffer().then( (data) => XRF.navigator.to(file.name,null, XRF.loaders.gltf,data) )
}) })
$("#overlay > input[type=submit]").addEventListener("click", fileLoaders ) $("input[type=submit]").addEventListener("click", fileLoaders )
} }

View file

@ -1,8 +1,8 @@
// the core project uses #vanillajs #proxies #clean #noframework // the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => { var $el = (html,tag) => {
let el = document.createElement('div') let el = document.createElement('div')
el.innerHTML = html el.innerHTML = html
return el.children[0] return el.children[0]

View file

@ -72,6 +72,7 @@ xrf.navigator.to = (url,flags,loader,data) => {
} }
if( data ){ // file upload if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad ) loader.parse(data, "", onLoad )
}else loader.load(url, onLoad ) }else loader.load(url, onLoad )
}) })

View file

@ -5,10 +5,8 @@ xrf.frag.defaultPredefinedViews = (opts) => {
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){ if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
console.log("to")
}else{ }else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
console.log("pub")
} }
} }
}) })

View file

@ -10,7 +10,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({ mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null, map: null,
transparent: url.match(/(png|gif)/) ? true : false, transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide, side: THREE.DoubleSide,
color: 0xFFFFFF, color: 0xFFFFFF,
opacity:1 opacity:1
@ -31,6 +31,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
} }
} }
mesh.material.map = texture mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true mesh.needsUpdate = true
} }