added docs + compiled sources

This commit is contained in:
Leon van Kammen 2023-05-22 14:10:44 +02:00
parent 955baeb2f4
commit a428150b0a
17 changed files with 4138 additions and 546 deletions

View file

@ -852,6 +852,30 @@ xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object) xrf.scene.add(object)
} }
/*
* EVENTS
*/
xrf.addEventListener = function(eventName, callback) {
if( !this._listeners ) this._listeners = []
if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = [];
}
// add the callback to the listeners array for this event name
this._listeners[eventName].push(callback);
};
xrf.emit = function(eventName, data) {
if( !this._listeners ) this._listeners = []
var callbacks = this._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
}
};
xrf.navigator = {} xrf.navigator = {}
xrf.navigator.to = (url,event) => { xrf.navigator.to = (url,event) => {
@ -934,62 +958,67 @@ xrf.frag.env = function(v, opts){
xrf.frag.href = function(v, opts){ xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() } const world = {
pos: new THREE.Vector3(),
scale: new THREE.Vector3(),
quat: new THREE.Quaternion()
}
mesh.getWorldPosition(world.pos) mesh.getWorldPosition(world.pos)
mesh.getWorldScale(world.scale) mesh.getWorldScale(world.scale)
mesh.getWorldQuaternion(world.quat);
mesh.position.copy(world.pos) mesh.position.copy(world.pos)
mesh.scale.copy(world.scale) mesh.scale.copy(world.scale)
mesh.setRotationFromQuaternion(world.quat);
// convert texture if needed // detect equirectangular image
let texture = mesh.material.map let texture = mesh.material.map
if( texture && texture.source.data.height == texture.source.data.width/2 ){ if( texture && texture.source.data.height == texture.source.data.width/2 ){
// assume equirectangular image
texture.mapping = THREE.ClampToEdgeWrapping texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true texture.needsUpdate = true
}
// poor man's equi-portal // poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( { mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide, side: THREE.DoubleSide,
uniforms: { uniforms: {
pano: { value: texture }, pano: { value: texture },
highlight: { value: false }, selected: { value: false },
}, },
vertexShader: ` vertexShader: `
vec3 portalPosition; vec3 portalPosition;
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
varying float vDistanceToCenter; varying float vDistanceToCenter;
varying float vDistance; varying float vDistance;
void main() { void main() {
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0); vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vDistance = length(portalPosition - cameraPosition); vDistance = length(portalPosition - cameraPosition);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
} }
`, `,
fragmentShader: ` fragmentShader: `
#define RECIPROCAL_PI2 0.15915494 #define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano; uniform sampler2D pano;
uniform bool highlight; uniform bool selected;
varying float vDistanceToCenter; varying float vDistanceToCenter;
varying float vDistance; varying float vDistance;
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
void main() { void main() {
vec3 direction = normalize(vWorldPosition - cameraPosition); vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV; vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0); sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2; sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture() sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture()
vec4 color = texture2D(pano, sampleUV); vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js) // Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
vec4 grayscale_color = highlight ? color : vec4(vec3(luminance) + vec3(0.33), color.a); vec4 grayscale_color = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color; gl_FragColor = grayscale_color;
} }
`, `,
}); });
mesh.material.needsUpdate = true mesh.material.needsUpdate = true
}
let teleport = mesh.userData.XRF.href.exec = (e) => { let teleport = mesh.userData.XRF.href.exec = (e) => {
if( mesh.clicked ) return if( mesh.clicked ) return
@ -1005,31 +1034,31 @@ xrf.frag.href = function(v, opts){
cameraDirection.multiplyScalar(portalArea); // move away from portal cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection); const newPos = meshWorldPosition.clone().add(cameraDirection);
const positionInFrontOfPortal = () => {
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
}
const distance = camera.position.distanceTo(newPos); const distance = camera.position.distanceTo(newPos);
if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal() if( renderer.xr.isPresenting && distance > portalArea ) return // too far away
else xrf.navigator.to(v.string) // ok let's surf to HREF!
xrf.navigator.to(v.string) // ok let's surf to HREF!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
xrf.emit('href',{click:true,mesh,xrf:v})
} }
if( !opts.frag.q ){ let selected = (state) => () => {
if( mesh.selected == state ) return // nothing changed
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
// update mouse cursor
if( !renderer.domElement.lastCursor )
renderer.domElement.lastCursor = renderer.domElement.style.cursor
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
xrf.emit('href',{selected:state,mesh,xrf:v})
mesh.selected = state
}
if( !opts.frag.q ){ // query means an action
mesh.addEventListener('click', teleport ) mesh.addEventListener('click', teleport )
mesh.addEventListener('mousemove', () => mesh.material.uniforms.highlight.value = true ) mesh.addEventListener('mousemove', selected(true) )
mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false ) mesh.addEventListener('nocollide', selected(false) )
} }
// lazy remove mesh (because we're inside a traverse) // lazy remove mesh (because we're inside a traverse)
@ -1060,9 +1089,12 @@ xrf.frag.q = function(v, opts){
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; console.log(" └ setting camera rotation to "+v.string)
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.set(
camera.rotation.z = v.z * Math.PI / 180; v.x * Math.PI / 180,
v.y * Math.PI / 180,
v.z * Math.PI / 180
)
} }
// *TODO* use webgl instancing // *TODO* use webgl instancing
@ -1135,7 +1167,7 @@ window.AFRAME.registerComponent('xrf', {
// override the camera-related XR Fragments so the camera-rig is affected // override the camera-related XR Fragments so the camera-rig is affected
let camOverride = (xrf,v,opts) => { let camOverride = (xrf,v,opts) => {
opts.camera = $('[camera]').object3D //parentElement.object3D opts.camera = $('[camera]').object3D.parent
xrf(v,opts) xrf(v,opts)
} }

View file

@ -852,6 +852,30 @@ xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object) xrf.scene.add(object)
} }
/*
* EVENTS
*/
xrf.addEventListener = function(eventName, callback) {
if( !this._listeners ) this._listeners = []
if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = [];
}
// add the callback to the listeners array for this event name
this._listeners[eventName].push(callback);
};
xrf.emit = function(eventName, data) {
if( !this._listeners ) this._listeners = []
var callbacks = this._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
}
};
xrf.navigator = {} xrf.navigator = {}
xrf.navigator.to = (url,event) => { xrf.navigator.to = (url,event) => {
@ -934,62 +958,67 @@ xrf.frag.env = function(v, opts){
xrf.frag.href = function(v, opts){ xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() } const world = {
pos: new THREE.Vector3(),
scale: new THREE.Vector3(),
quat: new THREE.Quaternion()
}
mesh.getWorldPosition(world.pos) mesh.getWorldPosition(world.pos)
mesh.getWorldScale(world.scale) mesh.getWorldScale(world.scale)
mesh.getWorldQuaternion(world.quat);
mesh.position.copy(world.pos) mesh.position.copy(world.pos)
mesh.scale.copy(world.scale) mesh.scale.copy(world.scale)
mesh.setRotationFromQuaternion(world.quat);
// convert texture if needed // detect equirectangular image
let texture = mesh.material.map let texture = mesh.material.map
if( texture && texture.source.data.height == texture.source.data.width/2 ){ if( texture && texture.source.data.height == texture.source.data.width/2 ){
// assume equirectangular image
texture.mapping = THREE.ClampToEdgeWrapping texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true texture.needsUpdate = true
}
// poor man's equi-portal // poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( { mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide, side: THREE.DoubleSide,
uniforms: { uniforms: {
pano: { value: texture }, pano: { value: texture },
highlight: { value: false }, selected: { value: false },
}, },
vertexShader: ` vertexShader: `
vec3 portalPosition; vec3 portalPosition;
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
varying float vDistanceToCenter; varying float vDistanceToCenter;
varying float vDistance; varying float vDistance;
void main() { void main() {
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0); vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vDistance = length(portalPosition - cameraPosition); vDistance = length(portalPosition - cameraPosition);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
} }
`, `,
fragmentShader: ` fragmentShader: `
#define RECIPROCAL_PI2 0.15915494 #define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano; uniform sampler2D pano;
uniform bool highlight; uniform bool selected;
varying float vDistanceToCenter; varying float vDistanceToCenter;
varying float vDistance; varying float vDistance;
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
void main() { void main() {
vec3 direction = normalize(vWorldPosition - cameraPosition); vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV; vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0); sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2; sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture() sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture()
vec4 color = texture2D(pano, sampleUV); vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js) // Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
vec4 grayscale_color = highlight ? color : vec4(vec3(luminance) + vec3(0.33), color.a); vec4 grayscale_color = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color; gl_FragColor = grayscale_color;
} }
`, `,
}); });
mesh.material.needsUpdate = true mesh.material.needsUpdate = true
}
let teleport = mesh.userData.XRF.href.exec = (e) => { let teleport = mesh.userData.XRF.href.exec = (e) => {
if( mesh.clicked ) return if( mesh.clicked ) return
@ -1005,31 +1034,31 @@ xrf.frag.href = function(v, opts){
cameraDirection.multiplyScalar(portalArea); // move away from portal cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection); const newPos = meshWorldPosition.clone().add(cameraDirection);
const positionInFrontOfPortal = () => {
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
}
const distance = camera.position.distanceTo(newPos); const distance = camera.position.distanceTo(newPos);
if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal() if( renderer.xr.isPresenting && distance > portalArea ) return // too far away
else xrf.navigator.to(v.string) // ok let's surf to HREF!
xrf.navigator.to(v.string) // ok let's surf to HREF!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
xrf.emit('href',{click:true,mesh,xrf:v})
} }
if( !opts.frag.q ){ let selected = (state) => () => {
if( mesh.selected == state ) return // nothing changed
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
// update mouse cursor
if( !renderer.domElement.lastCursor )
renderer.domElement.lastCursor = renderer.domElement.style.cursor
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
xrf.emit('href',{selected:state,mesh,xrf:v})
mesh.selected = state
}
if( !opts.frag.q ){ // query means an action
mesh.addEventListener('click', teleport ) mesh.addEventListener('click', teleport )
mesh.addEventListener('mousemove', () => mesh.material.uniforms.highlight.value = true ) mesh.addEventListener('mousemove', selected(true) )
mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false ) mesh.addEventListener('nocollide', selected(false) )
} }
// lazy remove mesh (because we're inside a traverse) // lazy remove mesh (because we're inside a traverse)
@ -1060,9 +1089,12 @@ xrf.frag.q = function(v, opts){
} }
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; console.log(" └ setting camera rotation to "+v.string)
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.set(
camera.rotation.z = v.z * Math.PI / 180; v.x * Math.PI / 180,
v.y * Math.PI / 180,
v.z * Math.PI / 180
)
} }
// *TODO* use webgl instancing // *TODO* use webgl instancing

View file

@ -4,15 +4,15 @@
<title>AFRAME - xrfragment sandbox</title> <title>AFRAME - xrfragment sandbox</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" /> <link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/> <link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
<script async src="./../../assets/alpine.min.js" defer></script> <script async src="./../../assets/js/alpine.min.js" defer></script>
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script> <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<script src="./../../../dist/xrfragment.aframe.js"></script> <script src="./../../../dist/xrfragment.aframe.js"></script>
</head> </head>
<body> <body>
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15&rot=0,360,0'] }"> <div id="overlay">
<img src="./../../assets/logo.png" class="logo"/> <img src="./../../assets/logo.png" class="logo"/>
<input type="submit" value="load 3D asset"></input> <input type="submit" value="load 3D asset"></input>
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/> <input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/>
@ -22,18 +22,22 @@
<textarea style="display:none"></textarea> <textarea style="display:none"></textarea>
<a-scene light="defaultLightsEnabled: false"> <a-scene light="defaultLightsEnabled: false">
<a-entity id="player" > <a-entity id="player" >
<a-entity camera position="0 1.6 15" wasd-controls id="camera"></a-entity> <a-entity camera="fov:90" position="0 1.6 0" wasd-controls id="camera"></a-entity>
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity> <a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
<a-entity id="right-hand" laser-controls="hand: right" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity> <a-entity id="right-hand" laser-controls="hand: right" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
</a-entity> </a-entity>
<a-entity id="home" xrf="example3.gltf#pos=1,2,3"></a-entity> <a-entity id="home" xrf="example3.gltf#pos=0,0,0"></a-entity>
<a-entity id="floor" xrf-get="floor"></a-entity> <a-entity id="floor" xrf-get="floor"></a-entity>
</a-scene> </a-scene>
<script type="module"> <script type="module">
import { loadFile, setupConsole, setupUrlBar } from './../../assets/utils.js'; import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
window.$ = (s) => document.querySelector(s) window.$ = (s) => document.querySelector(s)
window.notify = notify(window)
console.log = ( (log) => function(str){
if( String(str).match(/(camera)/) ) window.notify(str)
log(str)
})(console.log)
if( document.location.search.length > 2 ) if( document.location.search.length > 2 )
$('#home').setAttribute('xrf', document.location.search.substr(1)+document.location.hash ) $('#home').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
@ -47,6 +51,10 @@
// add screenshot component with camera to capture proper equirects // add screenshot component with camera to capture proper equirects
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2}) $('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
setTimeout( () => window.notify("use arrow-keys and mouse-drag to move around",{timeout:4000}),2000 )
window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
}) })
</script> </script>
</body> </body>

View file

@ -132,3 +132,105 @@ html.a-fullscreen a#source{
left:auto !important; left:auto !important;
} }
} }
/* notifications */
.js-snackbar-container {
position: absolute;
top: 10px;
left: 0px;
display: flex;
align-items: center;
width:100%;
max-width: 100%;
padding: 10px;
z-index:1001;
justify-content: center;
overflow: hidden;
}
.js-snackbar-container * {
box-sizing: border-box;
}
.js-snackbar__wrapper {
--color-c: #555;
--color-a: #EEE;
}
.js-snackbar__wrapper {
overflow: hidden;
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
box-shadow: 0 0 4px 0 #0007;
left: 20px;
position: fixed;
bottom: 20px;
}
.js-snackbar {
display: inline-flex;
box-sizing: border-box;
border-radius: 3px;
color: var(--color-c);
font-size: 16px;
background-color: var(--color-a);
vertical-align: bottom;
}
.js-snackbar__close,
.js-snackbar__status,
.js-snackbar__message {
position: relative;
}
.js-snackbar__message {
padding: 12px;
}
.js-snackbar__status {
display: none;
width: 15px;
margin-right: 5px;
border-radius: 3px 0 0 3px;
background-color: transparent;
}
.js-snackbar__status.js-snackbar--success,
.js-snackbar__status.js-snackbar--warning,
.js-snackbar__status.js-snackbar--danger,
.js-snackbar__status.js-snackbar--info {
display: block;
}
.js-snackbar__status.js-snackbar--success {
background-color: #4caf50;
}
.js-snackbar__status.js-snackbar--warning {
background-color: #ff9800;
}
.js-snackbar__status.js-snackbar--danger {
background-color: #ff6060;
}
.js-snackbar__status.js-snackbar--info {
background-color: #CCC;
}
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
user-select: none;
}
.js-snackbar__close:hover {
background-color: #4443;
}

File diff suppressed because one or more lines are too long

267
example/assets/js/utils.js Normal file
View file

@ -0,0 +1,267 @@
// contentLoaders = {".gltf" : () => .....} and so on
export function loadFile(contentLoaders, multiple){
return () => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = () => {
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();
}
}
export function setupConsole(el){
if( !el ) return setTimeout( () => setupConsole( $('.lil-gui') ),200 )
let $console = document.createElement('textarea')
$console.style.position = 'absolute'
$console.style.display = 'block'
$console.style.zIndex = 2000;
$console.style.background = "transparent !important"
$console.style.pointerEvents = 'none'
$console.style.top = '70px'
$console.style.padding = '10px'
$console.style.margin = '10px'
$console.style.background = '#000'
$console.style.left = $console.style.right = $console.style.bottom = 0;
$console.style.color = '#A6F';
$console.style.fontSize = '10px';
$console.style.fontFamily = 'Courier'
$console.style.border = '0'
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
el.appendChild($console)
console.log = ( (log) => function(){
let str = ([...arguments]).join(" ")
let s = new Date().toISOString().substr(11).substr(0,8) + " " + str.replace(/.*[0-9]: /,"")
log(s)
let lines = String($console.innerHTML + "\n"+s).split("\n")
while( lines.length > 200 ) lines.shift()
$console.innerHTML = lines.join("\n")
$console.scrollTop = $console.scrollHeight;
})(console.log.bind(console))
}
export function setupUrlBar(el){
var isIframe = (window === window.parent || window.opener) ? false : true;
if( isIframe ){
// show internal URL bar to test XR fragments interactively
el.style.display = 'block'
let nav = window.AFRAME.XRF.navigator
AFRAME.XRF.navigator.to = ((to) => (url,e) => {
to(url,e)
reflectUrl(url)
})(AFRAME.XRF.navigator.to)
const reflectUrl = (url) => el.value = url || document.location.search.substr(1) + document.location.hash
reflectUrl()
}
}
function SnackBar(userOptions) {
var snackbar = this || (window.snackbar = {});
var _Interval;
var _Message;
var _Element;
var _Container;
var _OptionDefaults = {
message: "Operation performed successfully.",
dismissible: true,
timeout: 5000,
status: ""
}
var _Options = _OptionDefaults;
function _Create() {
_Container = document.getElementsByClassName("js-snackbar-container")[0];
if (!_Container) {
// need to create a new container for notifications
_Container = document.createElement("div");
_Container.classList.add("js-snackbar-container");
document.body.appendChild(_Container);
}
_Element = document.createElement("div");
_Element.classList.add("js-snackbar__wrapper");
let innerSnack = document.createElement("div");
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
if (_Options.status) {
_Options.status = _Options.status.toLowerCase().trim();
let status = document.createElement("span");
status.classList.add("js-snackbar__status");
if (_Options.status === "success" || _Options.status === "green") {
status.classList.add("js-snackbar--success");
}
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
status.classList.add("js-snackbar--warning");
}
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
status.classList.add("js-snackbar--danger");
}
else {
status.classList.add("js-snackbar--info");
}
innerSnack.appendChild(status);
}
_Message = document.createElement("span");
_Message.classList.add("js-snackbar__message");
_Message.textContent = _Options.message;
innerSnack.appendChild(_Message);
if (_Options.dismissible) {
let closeBtn = document.createElement("span");
closeBtn.classList.add("js-snackbar__close");
closeBtn.innerText = "\u00D7";
closeBtn.onclick = snackbar.Close;
innerSnack.appendChild(closeBtn);
}
_Element.style.height = "0px";
_Element.style.opacity = "0";
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.appendChild(innerSnack);
_Container.appendChild(_Element);
if (_Options.timeout !== false) {
_Interval = setTimeout(snackbar.Close, _Options.timeout);
}
}
var _ConfigureDefaults = function() {
// if no options given, revert to default
if (userOptions === undefined) {
return;
}
if (userOptions.message !== undefined) {
_Options.message = userOptions.message;
}
if (userOptions.dismissible !== undefined) {
if (typeof (userOptions.dismissible) === "string") {
_Options.dismissible = (userOptions.dismissible === "true");
}
else if (typeof (userOptions.dismissible) === "boolean") {
_Options.dismissible = userOptions.dismissible;
}
else {
console.debug("Invalid option provided for 'dismissable' [" + userOptions.dismissible + "] is of type " + (typeof userOptions.dismissible));
}
}
if (userOptions.timeout !== undefined) {
if (typeof (userOptions.timeout) === "boolean" && userOptions.timeout === false) {
_Options.timeout = false;
}
else if (typeof (userOptions.timeout) === "string") {
_Options.timeout = parseInt(userOptions.timeout);
}
if (typeof (userOptions.timeout) === "number") {
if (userOptions.timeout === Infinity) {
_Options.timeout = false;
}
else if (userOptions.timeout >= 0) {
_Options.timeout = userOptions.timeout;
}
else {
console.debug("Invalid timeout entered. Must be greater than or equal to 0.");
}
_Options.timeout = userOptions.timeout;
}
}
if (userOptions.status !== undefined) {
_Options.status = userOptions.status;
}
}
snackbar.Open = function() {
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
_Element.style.height = contentHeight + "px";
_Element.style.opacity = 1;
_Element.style.marginTop = "5px";
_Element.style.marginBottom = "5px";
_Element.addEventListener("transitioned", function() {
_Element.removeEventListener("transitioned", arguments.callee);
_Element.style.height = null;
})
}
snackbar.Close = function () {
if (_Interval)
clearInterval(_Interval);
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
let snackbarTransitions = _Element.style.transition;
_Element.style.transition = "";
requestAnimationFrame(function() {
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
_Element.style.opacity = 1;
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.style.transition = snackbarTransitions
requestAnimationFrame(function() {
_Element.style.height = "0px";
_Element.style.opacity = 0;
})
});
setTimeout(function() {
_Container.removeChild(_Element);
}, 1000);
};
_ConfigureDefaults();
_Create();
snackbar.Open();
}
export function notify(scope){
return function notify(str,opts){
str = String(str)
opts = opts || {}
if( !opts.status ){
opts.status = "info"
if( str.match(/error/g) ) opts.status = "danger"
if( str.match(/warning/g) ) opts.status = "warning"
}
opts = Object.assign({ message: str , status, timeout:2000 },opts)
SnackBar( opts )
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,70 +0,0 @@
// contentLoaders = {".gltf" : () => .....} and so on
export function loadFile(contentLoaders, multiple){
return () => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = () => {
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();
}
}
export function setupConsole(el){
if( !el ) return setTimeout( () => setupConsole( $('.lil-gui') ),200 )
let $console = document.createElement('textarea')
$console.style.position = 'absolute'
$console.style.display = 'block'
$console.style.zIndex = 2000;
$console.style.background = "transparent !important"
$console.style.pointerEvents = 'none'
$console.style.top = '70px'
$console.style.padding = '10px'
$console.style.margin = '10px'
$console.style.background = '#000'
$console.style.left = $console.style.right = $console.style.bottom = 0;
$console.style.color = '#A6F';
$console.style.fontSize = '10px';
$console.style.fontFamily = 'Courier'
$console.style.border = '0'
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
el.appendChild($console)
console.log = ( (log) => function(){
let s = new Date().toISOString().substr(11).substr(0,8) + " " + ([...arguments]).join(" ").replace(/.*[0-9]: /,"")
log(s)
let lines = String($console.innerHTML + "\n"+s).split("\n")
while( lines.length > 200 ) lines.shift()
$console.innerHTML = lines.join("\n")
$console.scrollTop = $console.scrollHeight;
})(console.log.bind(console))
}
export function setupUrlBar(el){
var isIframe = (window === window.parent || window.opener) ? false : true;
if( isIframe ){
// show internal URL bar to test XR fragments interactively
el.style.display = 'block'
let nav = window.AFRAME.XRF.navigator
AFRAME.XRF.navigator.to = ((to) => (url,e) => {
to(url,e)
reflectUrl(url)
})(AFRAME.XRF.navigator.to)
const reflectUrl = (url) => el.value = url || document.location.search.substr(1) + document.location.hash
reflectUrl()
}
}

View file

@ -1,7 +1,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="./assets/axist.min.css" /> <link rel="stylesheet" href="./assets/css/axist.min.css" />
<link rel="stylesheet" href="./assets/style.css" /> <link rel="stylesheet" href="./assets/css/style.css" />
</head> </head>
<body> <body>
<script src="./../dist/xrfragment.js"></script> <script src="./../dist/xrfragment.js"></script>

View file

@ -4,8 +4,8 @@
<title>THREE.js - xrfragment sandbox</title> <title>THREE.js - xrfragment sandbox</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" /> <link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/> <link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
</head> </head>
<body> <body>
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }"> <div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }">
@ -35,7 +35,7 @@
import xrfragment from './../../../dist/xrfragment.three.js'; import xrfragment from './../../../dist/xrfragment.three.js';
import { loadFile, setupConsole, setupUrlBar } from './../../assets/utils.js'; import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js'; import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js'; import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';

File diff suppressed because one or more lines are too long

View file

@ -31,7 +31,7 @@ window.AFRAME.registerComponent('xrf', {
// override the camera-related XR Fragments so the camera-rig is affected // override the camera-related XR Fragments so the camera-rig is affected
let camOverride = (xrf,v,opts) => { let camOverride = (xrf,v,opts) => {
opts.camera = $('[camera]').object3D //parentElement.object3D opts.camera = $('[camera]').object3D.parent
xrf(v,opts) xrf(v,opts)
} }

View file

@ -125,3 +125,27 @@ xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object) xrf.scene.add(object)
} }
/*
* EVENTS
*/
xrf.addEventListener = function(eventName, callback) {
if( !this._listeners ) this._listeners = []
if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = [];
}
// add the callback to the listeners array for this event name
this._listeners[eventName].push(callback);
};
xrf.emit = function(eventName, data) {
if( !this._listeners ) this._listeners = []
var callbacks = this._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
}
};

View file

@ -2,14 +2,17 @@
* navigation, portals & mutations * navigation, portals & mutations
* *
* | fragment | type | scope | example value | * | fragment | type | scope | example value |
* |-|-|-|-| * |`href`| string (uri or [predefined view](#predefined_view )) | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<br>`://somefile.gltf#pos=1,1,0`<br> |
* |`href`| (uri) string | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<Br>`://somefile.gltf#pos=1,1,0`<br> |
* *
* ### spec 1.0 * [img[xrfragment.jpg]]
*
* !!!spec 1.0
*
* 1. a ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
* *
* 1. a **external**- or **file URI** fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
* 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead. * 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead.
* 3. navigation should not happen immediately when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.) *
* 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.)
*/ */
xrf.frag.href = function(v, opts){ xrf.frag.href = function(v, opts){
@ -122,8 +125,8 @@ xrf.frag.href = function(v, opts){
} }
/** /**
* > above was abducted from [this](https://i.imgur.com/E3En0gJ.png) and [this](https://i.imgur.com/lpnTz3A.png) survey result * > above was abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
* *
* [» source example](https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js)<br> * [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
* [» discussion](https://github.com/coderofsalvation/xrfragment/issues/1) * [[» implementation example|https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js]]<br>
*/ */

View file

@ -1,6 +1,9 @@
xrf.frag.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; console.log(" └ setting camera rotation to "+v.string)
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.set(
camera.rotation.z = v.z * Math.PI / 180; v.x * Math.PI / 180,
v.y * Math.PI / 180,
v.z * Math.PI / 180
)
} }