update: close to release
This commit is contained in:
parent
ead6a93171
commit
9df7685253
|
@ -1,4 +1,6 @@
|
|||
dist/*.pyc
|
||||
src/spec/tmp.json
|
||||
tags
|
||||
example/assets/*.blend
|
||||
example/assets/*.blend*
|
||||
2wa.gitlab.io/*
|
||||
|
||||
|
|
|
@ -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
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -554,10 +554,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
|||
})({});
|
||||
var xrfragment = $hx_exports["xrfragment"];
|
||||
// the core project uses #vanillajs #proxies #clean #noframework
|
||||
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
|
||||
$el = (html,tag) => {
|
||||
var $el = (html,tag) => {
|
||||
let el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
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.addEventListener = function(eventName, callback, scene) {
|
||||
xrf.addEventListener = function(eventName, callback, opts) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
callback.opts = opts || {weight: this._listeners.length}
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
if( scene ) callback.scene = scene
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
// sort
|
||||
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
|
||||
return () => {
|
||||
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
|
||||
}
|
||||
|
@ -653,9 +655,6 @@ xrf.emit = function(eventName, data){
|
|||
console.groupEnd(label)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -907,15 +906,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
|||
xrf.parseModel = function(model,url){
|
||||
let file = xrf.getFile(url)
|
||||
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})
|
||||
}
|
||||
|
@ -947,6 +937,8 @@ xrf.reset = () => {
|
|||
// remove mixers
|
||||
xrf.mixers.map( (m) => m.stop())
|
||||
xrf.mixers = []
|
||||
// set the player to position 0,0,0
|
||||
xrf.camera.position.set(0,0,0)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
|
@ -981,47 +973,81 @@ xrf.navigator = {}
|
|||
|
||||
xrf.navigator.to = (url,flags,loader,data) => {
|
||||
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
|
||||
xrf.emit('navigate', {url,loader,data})
|
||||
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash)
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
xrf
|
||||
.emit('navigate', {url,loader,data})
|
||||
.then( () => {
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
if( !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
if( ext && !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) return resolve()
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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})
|
||||
if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
|
||||
|
||||
xrf.add( model.scene )
|
||||
xrf.navigator.updateHash(hash)
|
||||
xrf.emit('navigateLoaded',{url,model})
|
||||
resolve(model)
|
||||
}
|
||||
if( hashChange && !hasPos ){
|
||||
hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash) // which don't require
|
||||
return resolve(xrf.model) // positional navigation
|
||||
}
|
||||
|
||||
if( data ) loader.parse(data, "", onLoad )
|
||||
else loader.load(url, onLoad )
|
||||
xrf
|
||||
.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
|
||||
|
||||
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){
|
||||
xrf.emit('hash', {hash: document.location.hash })
|
||||
})
|
||||
|
||||
xrf.navigator.setupNavigateFallbacks()
|
||||
|
||||
// this allows selectionlines to be updated according to the camera (renderloop)
|
||||
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})
|
||||
|
@ -1048,10 +1078,36 @@ xrf.navigator.init = () => {
|
|||
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) => {
|
||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||
xrf.navigator.updateHash.active = true // important to prevent recursion
|
||||
document.location.hash = hash
|
||||
xrf.navigator.updateHash.active = false
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
|
@ -1070,7 +1126,7 @@ xrf.addEventListener('env', (opts) => {
|
|||
//scene.texture = env.material.map
|
||||
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
// 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
|
||||
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
|
||||
.then( () => {
|
||||
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
|
||||
//if( !file.match(/\./) || file.match(/\.html/) ){
|
||||
// debugger
|
||||
// let inIframe
|
||||
// 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
|
||||
const isLocal = v.string[0] == '#'
|
||||
const hasPos = isLocal && v.string.match(/pos=/)
|
||||
const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
|
||||
|
||||
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)
|
||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||
//if( isLocal && !hasPos ){
|
||||
// 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 )
|
||||
}
|
||||
|
@ -1185,20 +1241,24 @@ xrf.frag.href = function(v, opts){
|
|||
xrf.frag.pos = function(v, opts){
|
||||
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
let pos = v
|
||||
|
||||
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
|
||||
if( v.x == undefined ){
|
||||
if( pos.x == undefined ){
|
||||
let obj = scene.getObjectByName(v.string)
|
||||
if( !obj ) return
|
||||
let pos = obj.position.clone()
|
||||
pos = obj.position.clone()
|
||||
obj.getWorldPosition(pos)
|
||||
camera.position.copy(pos)
|
||||
}else{
|
||||
// spec: direct coordinate: https://xrfragment.org/#navigating%203D
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
camera.position.x = pos.x
|
||||
camera.position.y = pos.y
|
||||
camera.position.z = pos.z
|
||||
}
|
||||
|
||||
xrf.frag.pos.last = pos // remember
|
||||
|
||||
camera.updateMatrixWorld()
|
||||
}
|
||||
xrf.frag.rot = function(v, opts){
|
||||
|
@ -1242,7 +1302,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
|
|||
let {mesh} = opts
|
||||
let scene = model.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)
|
||||
if( xrf.frag.src.renderAsPortal(mesh) ){
|
||||
// only add remote objects, because
|
||||
|
@ -1822,16 +1883,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
scene.traverse( (n) => {
|
||||
if( n.userData && 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) => {
|
||||
let {scene,match,v} = opts
|
||||
let objname = v.fragment
|
||||
|
@ -1866,6 +1925,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
|
|||
let {scene,id,match,v} = opts
|
||||
if( !scene ) return
|
||||
let remove = []
|
||||
|
||||
// erase previous lines
|
||||
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
|
||||
xrf.focusLine.points = []
|
||||
|
@ -2057,7 +2117,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
|
||||
mesh.material = new xrf.THREE.MeshBasicMaterial({
|
||||
map: null,
|
||||
transparent: url.match(/(png|gif)/) ? true : false,
|
||||
transparent: url.match(/\.(png|gif)/) ? true : false,
|
||||
side: THREE.DoubleSide,
|
||||
color: 0xFFFFFF,
|
||||
opacity:1
|
||||
|
@ -2078,6 +2138,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
}
|
||||
}
|
||||
mesh.material.map = texture
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.needsUpdate = true
|
||||
}
|
||||
|
||||
|
@ -2374,8 +2435,16 @@ window.AFRAME.registerComponent('xrf', {
|
|||
if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) )
|
||||
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)
|
||||
let isLocal = opts.url.match(/^#/)
|
||||
if( isLocal ) return
|
||||
|
||||
// *TODO* this does not really belong here perhaps
|
||||
let blinkControls = document.querySelector('[blink-controls]')
|
||||
|
@ -2393,34 +2462,34 @@ window.AFRAME.registerComponent('xrf', {
|
|||
el.setAttribute("class","floor")
|
||||
$('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) => {
|
||||
if( opts.click){
|
||||
let p = opts.promise()
|
||||
let url = opts.xrf.string
|
||||
let isLocal = url.match(/^#/)
|
||||
let hasPos = url.match(/pos=/)
|
||||
if( !isLocal && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
|
||||
if( isLocal && hasPos ){
|
||||
xrf.addEventListener('navigateLoading', (opts) => {
|
||||
let p = opts.promise()
|
||||
let url = opts.url
|
||||
let isLocal = url.match(/^#/)
|
||||
let hasPos = url.match(/pos=/)
|
||||
let fastFadeMs = 200
|
||||
|
||||
if( isLocal ){
|
||||
if( hasPos ){
|
||||
// local teleports only
|
||||
let fastFadeMs = 200
|
||||
AFRAME.fade.in(fastFadeMs)
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
AFRAME.fade.out(fastFadeMs)
|
||||
}, fastFadeMs)
|
||||
}else if( !isLocal ){
|
||||
AFRAME.fade.in()
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g.
|
||||
}, AFRAME.fade.data.fadetime )
|
||||
}else p.resolve()
|
||||
}
|
||||
}else{
|
||||
AFRAME.fade.in(fastFadeMs)
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
}, AFRAME.fade.data.fadetime )
|
||||
}
|
||||
})
|
||||
},{weight:-1000})
|
||||
|
||||
// convert href's to a-entity's so AFRAME
|
||||
// raycaster can find & execute it
|
||||
|
|
|
@ -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
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
|||
})({});
|
||||
var xrfragment = $hx_exports["xrfragment"];
|
||||
// the core project uses #vanillajs #proxies #clean #noframework
|
||||
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
|
||||
$el = (html,tag) => {
|
||||
var $el = (html,tag) => {
|
||||
let el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
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.addEventListener = function(eventName, callback, scene) {
|
||||
xrf.addEventListener = function(eventName, callback, opts) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
callback.opts = opts || {weight: this._listeners.length}
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
if( scene ) callback.scene = scene
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
// sort
|
||||
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
|
||||
return () => {
|
||||
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
|
||||
}
|
||||
|
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
|
|||
console.groupEnd(label)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
|||
xrf.parseModel = function(model,url){
|
||||
let file = xrf.getFile(url)
|
||||
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})
|
||||
}
|
||||
|
@ -945,6 +935,8 @@ xrf.reset = () => {
|
|||
// remove mixers
|
||||
xrf.mixers.map( (m) => m.stop())
|
||||
xrf.mixers = []
|
||||
// set the player to position 0,0,0
|
||||
xrf.camera.position.set(0,0,0)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
|
@ -979,47 +971,81 @@ xrf.navigator = {}
|
|||
|
||||
xrf.navigator.to = (url,flags,loader,data) => {
|
||||
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
|
||||
xrf.emit('navigate', {url,loader,data})
|
||||
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash)
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
xrf
|
||||
.emit('navigate', {url,loader,data})
|
||||
.then( () => {
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
if( !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
if( ext && !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) return resolve()
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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})
|
||||
if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
|
||||
|
||||
xrf.add( model.scene )
|
||||
xrf.navigator.updateHash(hash)
|
||||
xrf.emit('navigateLoaded',{url,model})
|
||||
resolve(model)
|
||||
}
|
||||
if( hashChange && !hasPos ){
|
||||
hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash) // which don't require
|
||||
return resolve(xrf.model) // positional navigation
|
||||
}
|
||||
|
||||
if( data ) loader.parse(data, "", onLoad )
|
||||
else loader.load(url, onLoad )
|
||||
xrf
|
||||
.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
|
||||
|
||||
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){
|
||||
xrf.emit('hash', {hash: document.location.hash })
|
||||
})
|
||||
|
||||
xrf.navigator.setupNavigateFallbacks()
|
||||
|
||||
// this allows selectionlines to be updated according to the camera (renderloop)
|
||||
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})
|
||||
|
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
|
|||
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) => {
|
||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||
xrf.navigator.updateHash.active = true // important to prevent recursion
|
||||
document.location.hash = hash
|
||||
xrf.navigator.updateHash.active = false
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
|
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
|
|||
//scene.texture = env.material.map
|
||||
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
// 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
|
||||
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
|
||||
.then( () => {
|
||||
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
|
||||
//if( !file.match(/\./) || file.match(/\.html/) ){
|
||||
// debugger
|
||||
// let inIframe
|
||||
// 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
|
||||
const isLocal = v.string[0] == '#'
|
||||
const hasPos = isLocal && v.string.match(/pos=/)
|
||||
const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
|
||||
|
||||
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)
|
||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||
//if( isLocal && !hasPos ){
|
||||
// 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 )
|
||||
}
|
||||
|
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
|
|||
xrf.frag.pos = function(v, opts){
|
||||
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
let pos = v
|
||||
|
||||
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
|
||||
if( v.x == undefined ){
|
||||
if( pos.x == undefined ){
|
||||
let obj = scene.getObjectByName(v.string)
|
||||
if( !obj ) return
|
||||
let pos = obj.position.clone()
|
||||
pos = obj.position.clone()
|
||||
obj.getWorldPosition(pos)
|
||||
camera.position.copy(pos)
|
||||
}else{
|
||||
// spec: direct coordinate: https://xrfragment.org/#navigating%203D
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
camera.position.x = pos.x
|
||||
camera.position.y = pos.y
|
||||
camera.position.z = pos.z
|
||||
}
|
||||
|
||||
xrf.frag.pos.last = pos // remember
|
||||
|
||||
camera.updateMatrixWorld()
|
||||
}
|
||||
xrf.frag.rot = function(v, opts){
|
||||
|
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
|
|||
let {mesh} = opts
|
||||
let scene = model.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)
|
||||
if( xrf.frag.src.renderAsPortal(mesh) ){
|
||||
// only add remote objects, because
|
||||
|
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
scene.traverse( (n) => {
|
||||
if( n.userData && 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) => {
|
||||
let {scene,match,v} = opts
|
||||
let objname = v.fragment
|
||||
|
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
|
|||
let {scene,id,match,v} = opts
|
||||
if( !scene ) return
|
||||
let remove = []
|
||||
|
||||
// erase previous lines
|
||||
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
|
||||
xrf.focusLine.points = []
|
||||
|
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
|
||||
mesh.material = new xrf.THREE.MeshBasicMaterial({
|
||||
map: null,
|
||||
transparent: url.match(/(png|gif)/) ? true : false,
|
||||
transparent: url.match(/\.(png|gif)/) ? true : false,
|
||||
side: THREE.DoubleSide,
|
||||
color: 0xFFFFFF,
|
||||
opacity:1
|
||||
|
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
}
|
||||
}
|
||||
mesh.material.map = texture
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.needsUpdate = true
|
||||
}
|
||||
|
||||
|
@ -2372,8 +2433,16 @@ window.AFRAME.registerComponent('xrf', {
|
|||
if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) )
|
||||
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)
|
||||
let isLocal = opts.url.match(/^#/)
|
||||
if( isLocal ) return
|
||||
|
||||
// *TODO* this does not really belong here perhaps
|
||||
let blinkControls = document.querySelector('[blink-controls]')
|
||||
|
@ -2391,34 +2460,34 @@ window.AFRAME.registerComponent('xrf', {
|
|||
el.setAttribute("class","floor")
|
||||
$('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) => {
|
||||
if( opts.click){
|
||||
let p = opts.promise()
|
||||
let url = opts.xrf.string
|
||||
let isLocal = url.match(/^#/)
|
||||
let hasPos = url.match(/pos=/)
|
||||
if( !isLocal && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
|
||||
if( isLocal && hasPos ){
|
||||
xrf.addEventListener('navigateLoading', (opts) => {
|
||||
let p = opts.promise()
|
||||
let url = opts.url
|
||||
let isLocal = url.match(/^#/)
|
||||
let hasPos = url.match(/pos=/)
|
||||
let fastFadeMs = 200
|
||||
|
||||
if( isLocal ){
|
||||
if( hasPos ){
|
||||
// local teleports only
|
||||
let fastFadeMs = 200
|
||||
AFRAME.fade.in(fastFadeMs)
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
AFRAME.fade.out(fastFadeMs)
|
||||
}, fastFadeMs)
|
||||
}else if( !isLocal ){
|
||||
AFRAME.fade.in()
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g.
|
||||
}, AFRAME.fade.data.fadetime )
|
||||
}else p.resolve()
|
||||
}
|
||||
}else{
|
||||
AFRAME.fade.in(fastFadeMs)
|
||||
setTimeout( () => {
|
||||
p.resolve()
|
||||
}, AFRAME.fade.data.fadetime )
|
||||
}
|
||||
})
|
||||
},{weight:-1000})
|
||||
|
||||
// convert href's to a-entity's so AFRAME
|
||||
// raycaster can find & execute it
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,11 +2,12 @@
|
|||
chatComponent = {
|
||||
|
||||
html: `
|
||||
<div id="chat">
|
||||
<div id="videos" style="pointer-events:none"></div>
|
||||
<div id="messages" aria-live="assertive" aria-relevant></div>
|
||||
<div id="chatfooter">
|
||||
<div id="chatbar">
|
||||
<input id="chatline" type="text" placeholder="type here"></input>
|
||||
<input id="chatline" type="text" placeholder="chat here"></input>
|
||||
</div>
|
||||
<button id="showchat" class="btn">show chat</button>
|
||||
</div>
|
||||
|
@ -15,10 +16,12 @@ chatComponent = {
|
|||
|
||||
init: (el) => new Proxy({
|
||||
|
||||
scene: null,
|
||||
visible: true,
|
||||
visibleChatbar: false,
|
||||
messages: [],
|
||||
scene: null,
|
||||
visible: true,
|
||||
messages: [],
|
||||
oneMessagePerUser: false,
|
||||
|
||||
username: '', // configured by 'network.connected' event
|
||||
|
||||
$videos: el.querySelector("#videos"),
|
||||
$messages: el.querySelector("#messages"),
|
||||
|
@ -28,16 +31,19 @@ chatComponent = {
|
|||
install(opts){
|
||||
this.opts = opts
|
||||
this.scene = opts.scene
|
||||
this.$chatbar.style.display = 'none'
|
||||
el.className = "xrf"
|
||||
el.style.display = 'none' // start hidden
|
||||
document.body.appendChild( el )
|
||||
this.visibleChatbar = false
|
||||
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"] })
|
||||
},
|
||||
|
||||
initListeners(){
|
||||
let {$chatline} = this
|
||||
|
||||
$chatline.addEventListener('click', (e) => this.inform() )
|
||||
|
||||
$chatline.addEventListener('keydown', (e) => {
|
||||
if (e.key == 'Enter' ){
|
||||
if( $chatline.value[0] != '/' ){
|
||||
|
@ -45,9 +51,25 @@ chatComponent = {
|
|||
}
|
||||
this.send({message: $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(){
|
||||
|
@ -55,10 +77,20 @@ chatComponent = {
|
|||
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){
|
||||
let {$messages} = this
|
||||
opts = { linebreak:true, message:"", class:[], ...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 msg = document.createElement('div')
|
||||
let br = document.createElement('br')
|
||||
|
@ -74,7 +106,12 @@ chatComponent = {
|
|||
br.classList.add.apply(br.classList, opts.class)
|
||||
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 ){
|
||||
nick.className = "user"
|
||||
nick.innerText = opts.from+' '
|
||||
|
@ -86,6 +123,15 @@ chatComponent = {
|
|||
}
|
||||
}
|
||||
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)
|
||||
if( opts.linebreak ) div.appendChild(br)
|
||||
$messages.scrollTop = $messages.scrollHeight // scroll down
|
||||
|
@ -110,9 +156,6 @@ chatComponent = {
|
|||
if( !el.inited && (el.inited = true) ) me.initListeners()
|
||||
break;
|
||||
}
|
||||
case "visibleChatbar": {
|
||||
me.$chatbar.style.display = v ? 'block' : 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,46 +239,63 @@ chatComponent.css = `
|
|||
max-width:unset;
|
||||
}
|
||||
#messages{
|
||||
position: absolute;
|
||||
transition:1s;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
bottom: 130px;
|
||||
padding: 15px;
|
||||
overflow:hidden;
|
||||
pointer-events:none;
|
||||
transition:1s;
|
||||
/*
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 91%;
|
||||
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;
|
||||
-webkit-user-select:none;
|
||||
-moz-user-select:-moz-none;
|
||||
-ms-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
body.menu #messages{
|
||||
top:50px;
|
||||
}
|
||||
#messages *{
|
||||
#messages:hover {
|
||||
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{
|
||||
transition:all 1s ease;
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
padding: 1px 17px;
|
||||
border-radius: 20px 0px 20px 20px;
|
||||
border-radius: 20px;
|
||||
color: #000c;
|
||||
margin-bottom: 10px;
|
||||
line-height:23px;
|
||||
pointer-events:visible;
|
||||
line-height:33px;
|
||||
cursor:grabbing;
|
||||
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{
|
||||
border-radius: 0px 20px 20px 20px;
|
||||
background:var(--xrf-primary);
|
||||
border-radius: 20px;
|
||||
background:var(--xrf-box-shadow);
|
||||
}
|
||||
#messages .msg.self,
|
||||
#messages .msg.self div{
|
||||
|
@ -254,12 +314,16 @@ chatComponent.css = `
|
|||
}
|
||||
#messages .msg a {
|
||||
text-decoration:underline;
|
||||
color: #EEE;
|
||||
color: var(--xrf-primary);
|
||||
font-weight:bold;
|
||||
transition:1s;
|
||||
transition:0.3s;
|
||||
}
|
||||
#messages .msg.info a,
|
||||
#messages a.ruler{
|
||||
color:#FFF;
|
||||
}
|
||||
#messages .msg a:hover{
|
||||
color:#FFF;
|
||||
color:#000;
|
||||
}
|
||||
#messages .msg.ui,
|
||||
#messages .msg.ui div{
|
||||
|
@ -327,10 +391,19 @@ chatComponent.css = `
|
|||
margin:0;
|
||||
}
|
||||
|
||||
.envelope{
|
||||
height:unset;
|
||||
.envelope,
|
||||
.envelope * {
|
||||
overflow:hidden;
|
||||
transition:1s;
|
||||
pointer-events:none;
|
||||
}
|
||||
.envelope a,
|
||||
.envelope button,
|
||||
.envelope input,
|
||||
.envelope textarea,
|
||||
.envelope msg,
|
||||
.envelope msg * {
|
||||
pointer-events:all;
|
||||
}
|
||||
|
||||
.user{
|
||||
|
@ -346,70 +419,92 @@ connectionsComponent = {
|
|||
|
||||
html: `
|
||||
<div id="connections">
|
||||
<i class="gg-close-o" id="close" onclick="$connections.toggle()"></i>
|
||||
<div id="networking">
|
||||
<h2>Network channels:</h2>
|
||||
<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>
|
||||
<i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
|
||||
<br>
|
||||
<div class="tab-frame">
|
||||
<input type="radio" name="tab" id="login" checked>
|
||||
<label for="login">login</label>
|
||||
|
||||
<input type="radio" name="tab" id="io">
|
||||
<label for="io">devices</label>
|
||||
|
||||
<input type="radio" name="tab" id="networks">
|
||||
<label for="networks">advanced</label>
|
||||
|
||||
<div class="tab">
|
||||
<div id="settings"></div>
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
|
||||
</td>
|
||||
</tr>
|
||||
</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 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>
|
||||
`,
|
||||
|
||||
init: (el) => new Proxy({
|
||||
|
||||
webcam: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
chatnetwork: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
scene: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
visible: true,
|
||||
|
||||
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: '',
|
||||
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> connect</a><br>`
|
||||
]).concat($menu.buttons)
|
||||
|
||||
// hide networking settings if entering thru meetinglink
|
||||
if( document.location.href.match(/meet=/) ) this.show()
|
||||
|
||||
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
|
||||
},
|
||||
|
||||
toggle(){
|
||||
let parent = el.closest('.envelope')
|
||||
parent.style.display = parent.style.display == 'none' ? parent.style.display = '' : 'none'
|
||||
$chat.visible = !$chat.visible
|
||||
},
|
||||
|
||||
change(id,e){
|
||||
|
@ -452,20 +545,28 @@ connectionsComponent = {
|
|||
}
|
||||
},
|
||||
|
||||
show(){
|
||||
$chat.visible = true
|
||||
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
|
||||
if( !network.connected ){
|
||||
if( el.parentElement ) el.parentElement.parentElement.remove()
|
||||
$chat.send({message:"", el, class:['ui']})
|
||||
if( !network.meetinglink ){ // set default
|
||||
$webcam.value = 'Peer2Peer'
|
||||
$chatnetwork.value = 'Peer2Peer'
|
||||
$scene.value = 'Peer2Peer'
|
||||
}
|
||||
this.renderSettings()
|
||||
show(opts){
|
||||
opts = opts || {}
|
||||
if( opts.hide ){
|
||||
if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
|
||||
if( !opts.showChat ) $chat.visible = false
|
||||
}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){
|
||||
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
|
||||
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.chatnetwork.find( select(this.selectedChatnetwork) )
|
||||
this.scene.find( select(this.selectedScene) )
|
||||
|
@ -491,12 +592,12 @@ connectionsComponent = {
|
|||
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
||||
this.update()
|
||||
$settings.innerHTML = ''
|
||||
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) )
|
||||
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
|
||||
this.renderInputs()
|
||||
},
|
||||
|
||||
renderInputs(){
|
||||
if( this.selectedWebcam == 'No thanks' ){
|
||||
if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
|
||||
return this.$devices.style.display = 'none'
|
||||
}else this.$devices.style.display = ''
|
||||
|
||||
|
@ -548,25 +649,15 @@ connectionsComponent = {
|
|||
})
|
||||
},
|
||||
|
||||
reactToNetwork(){
|
||||
document.addEventListener('network.connected', () => {
|
||||
console.log("network.connected")
|
||||
window.notify("🪐 connected to awesomeness..")
|
||||
$chat.visibleChatbar = true
|
||||
$chat.send({message:`🎉 connected!`,class:['info']})
|
||||
})
|
||||
reactToNetwork(){ // *TODO* move to network?
|
||||
|
||||
document.addEventListener('network.connect', () => {
|
||||
console.log("network.connect")
|
||||
el.parentElement.classList.add('connecthide')
|
||||
window.notify("🪐 connecting to awesomeness..")
|
||||
$connect.innerText = 'connecting..'
|
||||
this.show({hide:true, showChat: true})
|
||||
})
|
||||
document.addEventListener('network.disconnect', () => {
|
||||
window.notify("🪐 disconnecting..")
|
||||
$connect.innerText = 'disconnecting..'
|
||||
setTimeout( () => $connect.innerText = 'connect', 1000)
|
||||
if( !window.accessibility.enabled ) $chat.visibleChatbar = false
|
||||
document.addEventListener('network.disconnect', () => {
|
||||
this.connected = false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
},{
|
||||
|
@ -575,9 +666,10 @@ connectionsComponent = {
|
|||
set(data,k,v){
|
||||
data[k] = v
|
||||
switch( k ){
|
||||
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
||||
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
||||
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
||||
case "visible": el.style.display = v ? '' : 'none'; break;
|
||||
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.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 "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
||||
case "selectedWebcam": {
|
||||
|
@ -620,11 +712,11 @@ connectionsComponent.css = `
|
|||
}
|
||||
#close{
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-bottom: 7px;
|
||||
top: 16px;
|
||||
}
|
||||
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
|
||||
</style>`
|
||||
// reactive component for displaying the menu
|
||||
menuComponent = (el) => new Proxy({
|
||||
|
@ -644,8 +736,8 @@ menuComponent = (el) => new Proxy({
|
|||
$buttons: $buttons = el.querySelector('#buttons'),
|
||||
$btnMore: $btnMore = el.querySelector('#more'),
|
||||
|
||||
toggle(){
|
||||
this.collapsed = !this.collapsed
|
||||
toggle(state){
|
||||
this.collapsed = state !== undefined ? state : !this.collapsed
|
||||
el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
|
||||
document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
|
||||
},
|
||||
|
@ -777,16 +869,19 @@ window.accessibility = (opts) => new Proxy({
|
|||
document.addEventListener('network.send', (e) => {
|
||||
let opts = e.detail
|
||||
opts.message = opts.message || ''
|
||||
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
|
||||
this.speak(opts.message)
|
||||
})
|
||||
|
||||
opts.xrf.addEventListener('pos', (opts) => {
|
||||
if( this.enabled ){
|
||||
$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;
|
||||
min-width: 45px;
|
||||
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.xrf button:hover,
|
||||
|
@ -997,7 +1093,7 @@ document.head.innerHTML += `
|
|||
|
||||
|
||||
|
||||
.menu .btn{
|
||||
.footer > .menu .btn{
|
||||
display:inline-block;
|
||||
background: var(--xrf-primary);
|
||||
border-radius: 25px;
|
||||
|
@ -1136,7 +1232,8 @@ document.head.innerHTML += `
|
|||
text-align:right;
|
||||
}
|
||||
|
||||
.badge {
|
||||
.badge,
|
||||
#messages .msg.ui div.badge{
|
||||
display:inline-block;
|
||||
color: var(--xrf-white);
|
||||
font-weight: bold;
|
||||
|
@ -1205,6 +1302,22 @@ document.head.innerHTML += `
|
|||
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
|
||||
*/
|
||||
|
@ -1366,7 +1479,7 @@ document.head.innerHTML += `
|
|||
position: relative;
|
||||
display: inline-block;
|
||||
-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;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
|
@ -1532,14 +1645,14 @@ document.head.innerHTML += `
|
|||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1)) translate(3px,2px);
|
||||
transform: scale(var(--ggs,1)) translate(3px,9px);
|
||||
width: 16px;
|
||||
height: 6px;
|
||||
border: 2px solid;
|
||||
border-top: 0;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
margin-top: 8px
|
||||
line-height:15px;
|
||||
}
|
||||
.gg-software-download::after {
|
||||
content: "";
|
||||
|
@ -1643,7 +1756,9 @@ window.frontend = (opts) => new Proxy({
|
|||
.setupIframeUrlHandler()
|
||||
.setupCapture()
|
||||
.setupUserHints()
|
||||
.setupNetworkListeners()
|
||||
.hidetopbarWhenMenuCollapse()
|
||||
.hideUIWhenNavigating()
|
||||
|
||||
window.notify = this.notify
|
||||
setTimeout( () => {
|
||||
|
@ -1694,12 +1809,66 @@ window.frontend = (opts) => new Proxy({
|
|||
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(){
|
||||
// hide topbar when menu collapse button is pressed
|
||||
document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none')
|
||||
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){
|
||||
return () => {
|
||||
window.notify("if you're on Meta browser, file-uploads might be disabled")
|
||||
|
@ -1787,24 +1956,28 @@ window.frontend = (opts) => new Proxy({
|
|||
},
|
||||
|
||||
share(opts){
|
||||
opts = opts || {notify:true,qr:true,share:true}
|
||||
if( network.connected && !document.location.hash.match(/meet=/) ){
|
||||
let p = $connections.chatnetwork.find( (p) => p.plugin.name == $connections.selectedChatnetwork )
|
||||
if( p.link ) document.location.hash += `&meet=${p.link}`
|
||||
opts = opts || {notify:true,qr:true,share:true,linkonly:false}
|
||||
if( network.meetingLink && !document.location.hash.match(/meet=/) ){
|
||||
document.location.hash += `&meet=${network.meetingLink}`
|
||||
}
|
||||
if( !document.location.hash.match(/pos=/) ){
|
||||
document.location.hash += `&pos=${ network.posName || network.pos }`
|
||||
}
|
||||
let url = window.location.href
|
||||
if( opts.linkonly ) return url
|
||||
this.copyToClipboard( url )
|
||||
// End of *TODO*
|
||||
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>
|
||||
<button onclick="frontend.download()"><i class="gg-software-download"></i> download scene file</button> <br>
|
||||
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i> download 360 screenshot</button> <br>
|
||||
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i> clone & selfhost this experience</a><br>
|
||||
<br>
|
||||
To embed this experience in your blog,<br>
|
||||
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'></iframe>" id="share"/>
|
||||
<br>
|
||||
<br>
|
||||
`,{timeout:false})
|
||||
}
|
||||
// draw QR code
|
||||
|
@ -1846,6 +2019,7 @@ window.network = (opts) => new Proxy({
|
|||
|
||||
connected: false,
|
||||
pos: '',
|
||||
posName: '',
|
||||
meetinglink: "",
|
||||
peers: {},
|
||||
plugin: {},
|
||||
|
@ -1901,8 +2075,7 @@ window.network = (opts) => new Proxy({
|
|||
for ( var i in String.prototype ) add(i)
|
||||
var a = 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}-${c}`).toLowerCase()
|
||||
return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
|
||||
}
|
||||
|
||||
},
|
||||
|
@ -2097,7 +2270,7 @@ document.head.innerHTML += `
|
|||
height: auto;
|
||||
margin: 5px 0;
|
||||
transition: all ease .5s;
|
||||
border-radius: 3px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
|
||||
right: 20px;
|
||||
position: fixed;
|
||||
|
@ -2157,8 +2330,8 @@ document.head.innerHTML += `
|
|||
.js-snackbar__close {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
align-items: top;
|
||||
padding: 8px 13px 0px 0px;
|
||||
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
|
@ -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
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
|||
})({});
|
||||
var xrfragment = $hx_exports["xrfragment"];
|
||||
// the core project uses #vanillajs #proxies #clean #noframework
|
||||
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
|
||||
$el = (html,tag) => {
|
||||
var $el = (html,tag) => {
|
||||
let el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
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.addEventListener = function(eventName, callback, scene) {
|
||||
xrf.addEventListener = function(eventName, callback, opts) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
callback.opts = opts || {weight: this._listeners.length}
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
if( scene ) callback.scene = scene
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
// sort
|
||||
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
|
||||
return () => {
|
||||
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
|
||||
}
|
||||
|
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
|
|||
console.groupEnd(label)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
|||
xrf.parseModel = function(model,url){
|
||||
let file = xrf.getFile(url)
|
||||
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})
|
||||
}
|
||||
|
@ -945,6 +935,8 @@ xrf.reset = () => {
|
|||
// remove mixers
|
||||
xrf.mixers.map( (m) => m.stop())
|
||||
xrf.mixers = []
|
||||
// set the player to position 0,0,0
|
||||
xrf.camera.position.set(0,0,0)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
|
@ -979,47 +971,81 @@ xrf.navigator = {}
|
|||
|
||||
xrf.navigator.to = (url,flags,loader,data) => {
|
||||
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
|
||||
xrf.emit('navigate', {url,loader,data})
|
||||
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash)
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
xrf
|
||||
.emit('navigate', {url,loader,data})
|
||||
.then( () => {
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
if( !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
if( ext && !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) return resolve()
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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})
|
||||
if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
|
||||
|
||||
xrf.add( model.scene )
|
||||
xrf.navigator.updateHash(hash)
|
||||
xrf.emit('navigateLoaded',{url,model})
|
||||
resolve(model)
|
||||
}
|
||||
if( hashChange && !hasPos ){
|
||||
hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash) // which don't require
|
||||
return resolve(xrf.model) // positional navigation
|
||||
}
|
||||
|
||||
if( data ) loader.parse(data, "", onLoad )
|
||||
else loader.load(url, onLoad )
|
||||
xrf
|
||||
.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
|
||||
|
||||
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){
|
||||
xrf.emit('hash', {hash: document.location.hash })
|
||||
})
|
||||
|
||||
xrf.navigator.setupNavigateFallbacks()
|
||||
|
||||
// this allows selectionlines to be updated according to the camera (renderloop)
|
||||
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})
|
||||
|
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
|
|||
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) => {
|
||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||
xrf.navigator.updateHash.active = true // important to prevent recursion
|
||||
document.location.hash = hash
|
||||
xrf.navigator.updateHash.active = false
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
|
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
|
|||
//scene.texture = env.material.map
|
||||
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
// 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
|
||||
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
|
||||
.then( () => {
|
||||
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
|
||||
//if( !file.match(/\./) || file.match(/\.html/) ){
|
||||
// debugger
|
||||
// let inIframe
|
||||
// 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
|
||||
const isLocal = v.string[0] == '#'
|
||||
const hasPos = isLocal && v.string.match(/pos=/)
|
||||
const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
|
||||
|
||||
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)
|
||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||
//if( isLocal && !hasPos ){
|
||||
// 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 )
|
||||
}
|
||||
|
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
|
|||
xrf.frag.pos = function(v, opts){
|
||||
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
let pos = v
|
||||
|
||||
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
|
||||
if( v.x == undefined ){
|
||||
if( pos.x == undefined ){
|
||||
let obj = scene.getObjectByName(v.string)
|
||||
if( !obj ) return
|
||||
let pos = obj.position.clone()
|
||||
pos = obj.position.clone()
|
||||
obj.getWorldPosition(pos)
|
||||
camera.position.copy(pos)
|
||||
}else{
|
||||
// spec: direct coordinate: https://xrfragment.org/#navigating%203D
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
camera.position.x = pos.x
|
||||
camera.position.y = pos.y
|
||||
camera.position.z = pos.z
|
||||
}
|
||||
|
||||
xrf.frag.pos.last = pos // remember
|
||||
|
||||
camera.updateMatrixWorld()
|
||||
}
|
||||
xrf.frag.rot = function(v, opts){
|
||||
|
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
|
|||
let {mesh} = opts
|
||||
let scene = model.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)
|
||||
if( xrf.frag.src.renderAsPortal(mesh) ){
|
||||
// only add remote objects, because
|
||||
|
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
scene.traverse( (n) => {
|
||||
if( n.userData && 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) => {
|
||||
let {scene,match,v} = opts
|
||||
let objname = v.fragment
|
||||
|
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
|
|||
let {scene,id,match,v} = opts
|
||||
if( !scene ) return
|
||||
let remove = []
|
||||
|
||||
// erase previous lines
|
||||
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
|
||||
xrf.focusLine.points = []
|
||||
|
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
|
||||
mesh.material = new xrf.THREE.MeshBasicMaterial({
|
||||
map: null,
|
||||
transparent: url.match(/(png|gif)/) ? true : false,
|
||||
transparent: url.match(/\.(png|gif)/) ? true : false,
|
||||
side: THREE.DoubleSide,
|
||||
color: 0xFFFFFF,
|
||||
opacity:1
|
||||
|
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
}
|
||||
}
|
||||
mesh.material.map = texture
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.needsUpdate = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
|||
})({});
|
||||
var xrfragment = $hx_exports["xrfragment"];
|
||||
// the core project uses #vanillajs #proxies #clean #noframework
|
||||
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
|
||||
$el = (html,tag) => {
|
||||
var $el = (html,tag) => {
|
||||
let el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
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.addEventListener = function(eventName, callback, scene) {
|
||||
xrf.addEventListener = function(eventName, callback, opts) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
callback.opts = opts || {weight: this._listeners.length}
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
if( scene ) callback.scene = scene
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
// sort
|
||||
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
|
||||
return () => {
|
||||
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
|
||||
}
|
||||
|
@ -651,9 +653,6 @@ xrf.emit = function(eventName, data){
|
|||
console.groupEnd(label)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
|||
xrf.parseModel = function(model,url){
|
||||
let file = xrf.getFile(url)
|
||||
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})
|
||||
}
|
||||
|
@ -945,6 +935,8 @@ xrf.reset = () => {
|
|||
// remove mixers
|
||||
xrf.mixers.map( (m) => m.stop())
|
||||
xrf.mixers = []
|
||||
// set the player to position 0,0,0
|
||||
xrf.camera.position.set(0,0,0)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
|
@ -979,47 +971,81 @@ xrf.navigator = {}
|
|||
|
||||
xrf.navigator.to = (url,flags,loader,data) => {
|
||||
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
|
||||
xrf.emit('navigate', {url,loader,data})
|
||||
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash)
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
xrf
|
||||
.emit('navigate', {url,loader,data})
|
||||
.then( () => {
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
if( !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
if( ext && !loader ){
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) return resolve()
|
||||
loader = loader || new Loader().setPath( dir )
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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})
|
||||
if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
|
||||
|
||||
xrf.add( model.scene )
|
||||
xrf.navigator.updateHash(hash)
|
||||
xrf.emit('navigateLoaded',{url,model})
|
||||
resolve(model)
|
||||
}
|
||||
if( hashChange && !hasPos ){
|
||||
hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
|
||||
xrf.navigator.updateHash(hash) // which don't require
|
||||
return resolve(xrf.model) // positional navigation
|
||||
}
|
||||
|
||||
if( data ) loader.parse(data, "", onLoad )
|
||||
else loader.load(url, onLoad )
|
||||
xrf
|
||||
.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
|
||||
|
||||
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){
|
||||
xrf.emit('hash', {hash: document.location.hash })
|
||||
})
|
||||
|
||||
xrf.navigator.setupNavigateFallbacks()
|
||||
|
||||
// this allows selectionlines to be updated according to the camera (renderloop)
|
||||
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})
|
||||
|
@ -1046,10 +1076,36 @@ xrf.navigator.init = () => {
|
|||
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) => {
|
||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||
xrf.navigator.updateHash.active = true // important to prevent recursion
|
||||
document.location.hash = hash
|
||||
xrf.navigator.updateHash.active = false
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
|
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
|
|||
//scene.texture = env.material.map
|
||||
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
// 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
|
||||
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
|
||||
.then( () => {
|
||||
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
|
||||
//if( !file.match(/\./) || file.match(/\.html/) ){
|
||||
// debugger
|
||||
// let inIframe
|
||||
// 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
|
||||
const isLocal = v.string[0] == '#'
|
||||
const hasPos = isLocal && v.string.match(/pos=/)
|
||||
const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
|
||||
|
||||
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)
|
||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||
//if( isLocal && !hasPos ){
|
||||
// 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 )
|
||||
}
|
||||
|
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
|
|||
xrf.frag.pos = function(v, opts){
|
||||
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
let pos = v
|
||||
|
||||
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
|
||||
if( v.x == undefined ){
|
||||
if( pos.x == undefined ){
|
||||
let obj = scene.getObjectByName(v.string)
|
||||
if( !obj ) return
|
||||
let pos = obj.position.clone()
|
||||
pos = obj.position.clone()
|
||||
obj.getWorldPosition(pos)
|
||||
camera.position.copy(pos)
|
||||
}else{
|
||||
// spec: direct coordinate: https://xrfragment.org/#navigating%203D
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
camera.position.x = pos.x
|
||||
camera.position.y = pos.y
|
||||
camera.position.z = pos.z
|
||||
}
|
||||
|
||||
xrf.frag.pos.last = pos // remember
|
||||
|
||||
camera.updateMatrixWorld()
|
||||
}
|
||||
xrf.frag.rot = function(v, opts){
|
||||
|
@ -1240,7 +1300,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
|
|||
let {mesh} = opts
|
||||
let scene = model.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)
|
||||
if( xrf.frag.src.renderAsPortal(mesh) ){
|
||||
// only add remote objects, because
|
||||
|
@ -1820,16 +1881,14 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
scene.traverse( (n) => {
|
||||
if( n.userData && 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) => {
|
||||
let {scene,match,v} = opts
|
||||
let objname = v.fragment
|
||||
|
@ -1864,6 +1923,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
|
|||
let {scene,id,match,v} = opts
|
||||
if( !scene ) return
|
||||
let remove = []
|
||||
|
||||
// erase previous lines
|
||||
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
|
||||
xrf.focusLine.points = []
|
||||
|
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
|
||||
mesh.material = new xrf.THREE.MeshBasicMaterial({
|
||||
map: null,
|
||||
transparent: url.match(/(png|gif)/) ? true : false,
|
||||
transparent: url.match(/\.(png|gif)/) ? true : false,
|
||||
side: THREE.DoubleSide,
|
||||
color: 0xFFFFFF,
|
||||
opacity:1
|
||||
|
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
}
|
||||
}
|
||||
mesh.material.map = texture
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.needsUpdate = true
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,10 @@
|
|||
`,{timeout:false})
|
||||
})
|
||||
</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="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 -->
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
../../assets/query.gltf
|
|
@ -0,0 +1 @@
|
|||
../../assets/index.glb
|
|
@ -1 +0,0 @@
|
|||
assets/index.gltf
|
|
@ -4,24 +4,14 @@
|
|||
<title>THREE.js - xrfragment sandbox</title>
|
||||
<meta charset="utf-8">
|
||||
<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"/>
|
||||
<body style="margin:0">
|
||||
<div id="overlay" style="text-align:right;position:absolute;width:100%;height:100%;top:0;left:0;bottom:0;right:0">
|
||||
<b>NOTE:</b> this is a THREE barebones example (AFRAME viewer has more features)<br><Br>
|
||||
<input type="submit" value="load 3D asset"/>
|
||||
</div>
|
||||
|
||||
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">➕ clone project</a>
|
||||
<a class="btn-foot" id="embed" target="_blank" onclick="embed()">🔗 share</a>
|
||||
<a class="btn-foot" id="model" target="_blank" href="index.glb">⬇️ scene</a>
|
||||
<a class="btn-foot" id="more" target="_blank" >XRF</a>
|
||||
<textarea style="display:none"></textarea>
|
||||
<canvas id="qrcode" style="display:none" width="300" height="300"></canvas>
|
||||
|
||||
<!-- Import maps polyfill -->
|
||||
<!-- Remove this when import maps will be widely supported -->
|
||||
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
|
||||
|
@ -39,7 +29,6 @@
|
|||
|
||||
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 { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
|
||||
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
|
||||
|
@ -66,7 +55,6 @@
|
|||
};
|
||||
|
||||
init();
|
||||
notify("NOTE: only AFRAME demo has immersive back-button (for now)")
|
||||
|
||||
function init() {
|
||||
|
||||
|
@ -174,10 +162,6 @@
|
|||
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
|
||||
cameraRig.add( controllerGrip2 );
|
||||
|
||||
|
||||
setupConsole()
|
||||
setupUrlBar( $('input#uri'), XRF )
|
||||
|
||||
// Add stats.js
|
||||
stats = new Stats();
|
||||
stats.dom.style.width = '80px';
|
||||
|
@ -198,11 +182,33 @@
|
|||
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({
|
||||
".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) )
|
||||
".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) )
|
||||
})
|
||||
$("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
|
||||
$("input[type=submit]").addEventListener("click", fileLoaders )
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// the core project uses #vanillajs #proxies #clean #noframework
|
||||
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||
|
||||
$el = (html,tag) => {
|
||||
var $el = (html,tag) => {
|
||||
let el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
return el.children[0]
|
||||
|
|
|
@ -72,6 +72,7 @@ xrf.navigator.to = (url,flags,loader,data) => {
|
|||
}
|
||||
|
||||
if( data ){ // file upload
|
||||
console.dir(loader)
|
||||
loader.parse(data, "", onLoad )
|
||||
}else loader.load(url, onLoad )
|
||||
})
|
||||
|
|
|
@ -5,10 +5,8 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
let frag = xrf.URI.parse( n.userData['#'] )
|
||||
if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
|
||||
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
|
||||
console.log("to")
|
||||
}else{
|
||||
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
|
||||
console.log("pub")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
|
||||
mesh.material = new xrf.THREE.MeshBasicMaterial({
|
||||
map: null,
|
||||
transparent: url.match(/(png|gif)/) ? true : false,
|
||||
transparent: url.match(/\.(png|gif)/) ? true : false,
|
||||
side: THREE.DoubleSide,
|
||||
color: 0xFFFFFF,
|
||||
opacity:1
|
||||
|
@ -31,6 +31,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
|
|||
}
|
||||
}
|
||||
mesh.material.map = texture
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.needsUpdate = true
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue