Compare commits

...

7 Commits
master ... dev

  1. 3
      .babelrc
  2. 2
      .gitignore
  3. 2
      .vscode/settings.json
  4. 21
      LICENSE
  5. 2
      LICENSE.md
  6. 37
      README.md
  7. 85773
      dist/aframe-master.js
  8. BIN
      dist/assets/img/enter-vr-button-background.png
  9. BIN
      dist/assets/img/favicon.png
  10. BIN
      dist/assets/img/loadingLogo.png
  11. 32
      dist/index.html
  12. 111
      flat.html
  13. 150
      index.html
  14. 42
      package.json
  15. 173
      pimvrhelpers.js
  16. 187
      setup/index.html
  17. 1411
      setup/upload/server/php/UploadHandler.php
  18. 18
      setup/upload/server/php/files/usersdb.js
  19. 17
      setup/upload/server/php/index.php
  20. 105
      src/components/aabb-collider.js
  21. 89
      src/components/grab.js
  22. 32
      src/components/watch.js
  23. 72
      src/home.html
  24. 58
      src/index.css
  25. 17
      src/index.js
  26. 66
      webpack.config.js

@ -0,0 +1,3 @@
{
"presets": ["babel-preset-env"]
}

2
.gitignore vendored

@ -0,0 +1,2 @@
node_modules
package-lock.json

@ -0,0 +1,2 @@
{
}

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Fabien Benetou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,2 @@
CreativeCommons Attribution-NonCommercial-ShareAlike 4.0 International.
http://creativecommons.org/licenses/by-nc-sa/4.0/

@ -1,35 +1,16 @@
# relax-plus-think-space
an infinite space for your big ideas
## Principle
Natural interaction (6DoF controllers)
in an immersive environment (relaxing setup to induce state of flow)
with work related content (e.g. Github issues, photos of work posters and post-it notes)
to be freely organised in visual categories (e.g. kanban).
An infinite space for your big ideas
## UX flow for demos and tests
### new user on phone without own photos :
1. visit https://learnwebvr.xyz and get redirected to https://learnwebvr.xyz/setup/
1. upload photos
1. generate a personalised link
1. share that link to target device (e.g. Firefox Send Tab to Devices on Oculus Quest)
1. experience on device
1. remove device and visit 2D links e.g. https://learnwebvr.xyz/flat.html?email=fabien@benetou.fr
## Development
### returning user on 6DoF device
1. visit https://learnwebvr.xyz
1 ...
Have Node (< v12, recommended v11) and npm installed.
### new user on 6DoF device
1. visit https://learnwebvr.xyz
1 ...
Change 192.168.0.12 to your local IP on package.json scripts/start
## Code
* frontend : `index.html` and `setup/index.html`
* backend : `setup/server/upload.php`
```
npm install
npm run start
```
## License
MIT.
## Property and rights
Iterative Explorations SCS based in Belgium
Then head to `https://[YOUR LOCAL IP]:3000` in your browser.

85773
dist/aframe-master.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

32
dist/index.html vendored

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>relax-plus-think-space</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="theme-color" content="#353449">
<style>
body {
margin: 0;
}
canvas {
display: block;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
}
</style>
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/x-icon">
<script src="aframe-master.js"></script>
<script src="bundle.js"></script>
</head>
<body>
<div id="app"></div>
<a id="vrButton" href="#" title="Enter VR / Fullscreen"></a>
</body>
</html>

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet">
<script src="setup/upload/server/php/files/usersdb.js"></script>
<script src="https://aframe.io/releases/1.0.0/aframe.min.js"></script>
</head>
<style>
body { background-color: transparent; }
#email, #sms {
font-size: xx-large;
}
</style>
<body>
<img src="setup/productlogo.png">
<h3>flat viewer by category sorted by position</h3>
<div id="spacesholder">Your spaces:
<ul id="spaces"></ul>
</div>
<div>Already have an account? <span onclick="login()" style="text-decoration: underline;">Log-in</span>
<form>
<div id="login" style="display:none"><input id="useremail"/>
<button style="margin-top:2px;" type="button" onclick="loginViaEmail()" id="loginemail" class="btn btn-lg btn-primary btn-block">Login</button>
</div>
</form>
<div style="display:none" id="nouser">User not found. Double check your email address then contact fabien@iterative-explorations.com</a></div>
</div>
<script>
const urlParams = new URLSearchParams(window.location.search);
const email = urlParams.get('email');
if (email) loginViaEmail()
var uploadedURL = "setup/upload/server/php/files/";
var images = []
var url = "/"
var urlParameters = "?customimages="
var userspace
function login(){
document.querySelector("#login").style.display = "block"
}
function findCategory(image){
if (!userspace) return
console.log(image.filename)
var imagePos = new THREE.Vector3();
imagePos.copy ( AFRAME.utils.coordinates.parse(image.position) )
var closest
var smallestDistance = 1000
for (var category of userspace.categories){
var categoryPos = new THREE.Vector3();
categoryPos.copy ( AFRAME.utils.coordinates.parse(category.position) )
var distance = categoryPos.distanceTo( imagePos )
if (distance < smallestDistance){
smallestDistance = distance
closest = category.label
}
console.log(distance, category.label)
}
return closest
}
function loginViaEmail(){
var path = "setup/"
if (!email)
email = document.querySelector("#useremail").value
userspace = database[email]
if (!userspace){
document.querySelector("#nouser").style.display = "block"
return
}
document.querySelector("#spacesholder").style.display = "block"
var spaces = document.querySelector("#spaces")
var space = document.createElement("li")
var spacelink = document.createElement("a")
var images = userspace.files
spacelink.href = url + urlParameters
spacelink.target = "_blank"
spacelink.innerHTML = userspace.last_login
space.appendChild(spacelink)
for (var image of images){
urlParameters += image.filename + ","
space.innerHTML += path + image.filename + " " + findCategory( image )
}
console.log(space)
spaces.appendChild(space)
console.log("userspace", userspace)
}
</script>
<h3 style="position:absolute; bottom:0px; right:0px;">A product by <a href="https://iterative-explorations.com"><img width="200px" src="https://iterative-explorations.com/logo.svg"></a>.</h3>
</body>
</html>

@ -1,150 +0,0 @@
<head>
<meta charset="UTF-8">
<title>Think + Relax space by Iterative Explorations</title>
<script src="https://aframe.io/releases/1.0.0/aframe.min.js"></script>
<script src="https://rawgit.com/feiss/aframe-environment-component/master/dist/aframe-environment-component.min.js"></script>
<script src="https://cdn.rawgit.com/donmccurdy/aframe-extras/v4.1.2/dist/aframe-extras.min.js"></script>
<script src="https://unpkg.com/super-hands@3.0.0/dist/super-hands.min.js"></script>
<script src="pimvrhelpers.js"></script>
</head>
<script>
// could add an in VR edit mode for the categories, to position them, name them, etc
// should be mostly for adjustments
var timer = AFRAME.utils.getUrlParameter('timer')
if (!timer) timer = 300 //5min
function saveNewItemsPosition(){
var selector = ".notes";
var pos = ""
document.querySelectorAll(selector).forEach(
e => pos += "\n" + AFRAME.utils.coordinates.stringify( e.getAttribute("position") )
)
pimvrSaveRemote("WeWorkTest", pos) // from pimvrhelpers.js
// should be saved by username by space
// could be pointed at from the database file (thus having multiple storages)
}
AFRAME.registerComponent("watch", {
init: function() {
document.querySelector("#timer").setAttribute("text","value:"+timer)
this.tick = AFRAME.utils.throttleTick(this.tick, 1000, this);
// details https://aframe.io/docs/1.0.0/core/utils.html#aframe-utils-throttle-function-minimuminterval-optionalcontext
},
tick: function (t, dt) {
var time = Number( document.querySelector("#timer").getAttribute("text").value )
time--
document.querySelector("#timer").setAttribute("text","value:"+(time))
},
})
AFRAME.registerComponent("relaxing-introduction", {
init: function() {
// get in flow state
// for now controlled just with AFrame animation
// cf https://aframe.io/docs/1.0.0/components/animation.html
setTimeout(function(){
for (var note of document.querySelectorAll(".notes")){ note.emit("fadein") }
}, 10 * 1000) // arbitrary 10s, should be instead after the introduction is finished
}
})
AFRAME.registerComponent("decompression-conclusion", {
init: function() {
// cf PoC at 2017 W3C web authoring workshop with Roland Dubois
setTimeout(function(){
var intructions = document.querySelector("#instructions")
instructions.setAttribute("position", "-1 1.5 -1") // assumes the user is roughtly centered... could force on camera instead
instructions.setAttribute("text", "value", "Your session will end soon\n\nVisit https://learnWebVR.xyz/flat.html\n\nto work outside of VR.")
instructions.setAttribute("opacity", "1") // could be animated instead
console.log("X min voice over and notes fade-out with contextual information (time, weather, etc)")
document.querySelector("[environment]").setAttribute("environment", "preset:checkerboard")
}, timer * 1000);
}
})
AFRAME.registerComponent("environment-by-url", {
schema: {},
init: function() {
var environment = AFRAME.utils.getUrlParameter('environment')
if (environment) document.querySelector("[environment]").setAttribute("environment", "preset:" + environment)
}
})
AFRAME.registerComponent("photo-importer", {
schema: {},
init: function() {
var baseURL = "setup/"
var images = AFRAME.utils.getUrlParameter('customimages').split(',');
if (images.length == 1) window.location = baseURL; // no images? must be configured first
// could also display a tutorial instead
var i=1
for (var image of images){
if (image){
var id = "placeholder"+i
var placeholder = document.querySelector("#"+id)
if (!placeholder){
// TODO somehow not interactable anymore?!
// had a similar silly issue before but can't recall how I fixed it :(
// something basic about entities or hierarchy...
placeholder = document.createElement("a-box")
placeholder.id = id
placeholder.setAttribute("material", "shader:flat")
placeholder.setAttribute("hoverable", "")
placeholder.setAttribute("grabbable", "")
placeholder.setAttribute("draggable", "")
placeholder.setAttribute("dropppable", "")
// somehow does appear visually correct but are NOT interactable!
placeholder.setAttribute("position", "" + i/1.5-1.5 + " 1.5 -0.7")
placeholder.setAttribute("scale", "0.5 0.3 0.01")
// assume 1 * 1.5 photo ratio (landscape)
AFRAME.scenes[0].appendChild(placeholder)
}
placeholder.className += "notes"
placeholder.setAttribute("opacity", "0")
placeholder.setAttribute("animation", "property: components.material.material.opacity; to: 1; dur: 1500; easing: linear; startEvents: fadein;")
placeholder.setAttribute("src", baseURL+image)
i++
}
}
}
})
</script>
<body>
<a-scene photo-importer environment-by-url relaxing-introduction decompression-conclusion>
<a-entity environment="preset: forest;"></a-entity>
<a-entity sphere-collider="objects: a-box" super-hands hand-controls="left"></a-entity>
<a-entity sphere-collider="objects: a-box" super-hands hand-controls="right"></a-entity>
<!-- could be wrist watch as I've done via Twitter years ago -->
<a-text watch id="timer" rotation="180 0 180" position="0 1 2" value="time"></a-text>
<a-text id="instructions" position="-1 1.5 -0.65" value="welcome to your\n\nthink+relax space" opacity="0"
animation__fadein="property: components.text.material.uniforms.opacity.value; to: 0.99; dur: 1500; easing: linear"
animation__fadeout="property: components.text.material.uniforms.opacity.value; to: 0.01; dur: 1500; easing: linear; startEvents: fadeout;"
animation__leave="property: object3D.position.z; to: -100; dur: 15000; easing: linear; delay: 2500;"
></a-text>
<a-text rotation="-20 20 0" position="-2 0.2 -2" scale="1 1 1" value="waiting"></a-text>
<a-box material="wireframe:true" color="orange" position="-2 1 -2" scale="2 2 2"></a-box>
<a-text rotation="-20 0 0" position=" 0 0.2 -2" scale="1 1 1" value="in progress"></a-text>
<a-box material="wireframe:true" color="red" position="0 1 -2" scale="2 2 2"></a-box>
<a-text rotation="-20 -20 0" position=" 2 0.2 -2" scale="1 1 1" value="completed"></a-text>
<a-box material="wireframe:true" color="green" position="2 1 -2" scale="2 2 2"></a-box>
<a-box material="shader:flat" id="placeholder1" hoverable grabbable stretchable draggable dropppable position="0.5 1.5 -0.5" scale="0.5 0.3 0.01"></a-box>
<a-box material="shader:flat" id="placeholder2" hoverable grabbable stretchable draggable dropppable position="0 1.9 -0.7" scale="0.5 0.3 0.01"></a-box>
<a-box material="shader:flat" id="placeholder3" hoverable grabbable stretchable draggable dropppable position="-0.5 1.5 -0.7" scale="0.5 0.3 0.01"></a-box>
<!-- somehow creating dynamically fails... even though it did (!) work at some point (but wasn't saved via git)-->
<!-- until then will rely on a pool of objects -->
</a-scene>
</body>

@ -0,0 +1,42 @@
{
"name": "relax-plus-think-space",
"version": "0.0.1",
"license": "CC-BY-NC-SA-4.0",
"description": "An infinite space for your big ideas",
"repository": {
"type": "git",
"url": ""
},
"main": "index.js",
"dependencies": {},
"scripts": {
"start": "webpack-dev-server --mode development --host 0.0.0.0",
"build": "webpack --mode production --config webpack.config.js"
},
"keywords": [],
"author": "Arturo Paracuellos",
"devDependencies": {
"aframe-environment-component": "^2.0.0",
"aframe-event-set-component": "^5.0.0",
"aframe-super-hot-loader": "^1.7.0",
"aframe-super-hot-html-loader": "^2.1.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.7.0",
"css-loader": "^3.4.1",
"html-require-loader": "^1.0.1",
"json-loader": "^0.5.7",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.8.0",
"webpack-glsl-loader": "^1.0.1"
},
"standard": {
"globals": [
"AFRAME",
"THREE"
]
}
}

@ -1,173 +0,0 @@
//---------PIM helper functions-----------------------------------------------------------
/*
currently PmWiki backend
could use http://fabien.benetou.fr/Site/AllRecentChanges?action=source to check for update
heavy but nearly no processing required
enough if done once per minute or so
if update, request serverrender on modified page
IFF it's being displayed
planned Evernote backend
https://github.com/evernote/evernote-sdk-js
https://github.com/wanasit/everest-js
https://stackoverflow.com/questions/24580588/how-to-list-all-the-notes-from-an-evernote-notebook-javascript-node-js
https://dev.evernote.com/doc/articles/polling_notification.php
or other popular PIMs
https://developers.trello.com/
http://www.xmind.net/developer/
ideally with webhooks on a backend abstraction with coherent API
*/
function pimvrSaveItemsStates(callback) {
function updateItemsStates(globalStates){
let pageStates = {};
let [group, page] = getPageGroup();
let elements = document.body.querySelectorAll('.pimvr-item');
for (let item of elements) {
let id = item.getAttribute("id");
let position = item.getComputedAttribute("position");
pageStates[id] = {"position": position};
}
globalStates[group+"_"+page] = pageStates;
pimvrSaveRemote("ItemsStates", JSON.stringify(globalStates));
return "Items states saved";
}
pimvrLoadRemote("ItemsStates", updateItemsStates);
}
function pimvrSaveConfiguration(callback) {
let configuration = {};
let elements = document.body.querySelectorAll('.pimvr-configuration');
for (let item of elements) {
let id = item.getAttribute("id");
let position = item.getComputedAttribute("position");
configuration[id] = {"position": position};
}
pimvrSaveRemote("Configuration", JSON.stringify(configuration));
return "Configuration saved";
}
function pimvrLoadIoTData(callback) {
// should give min/max ranges, here seems to be 0-1010
// to use (once normalized) as an attribute value
// used on http://jsbin.com/nucanat/edit?html,output
// warning HTTPS on tick is really hammering
const readURL = "https://fabien.benetou.fr/PIMVRdata/IoTData?action=source";
var myRequest = new XMLHttpRequest();
myRequest.open('GET', readURL);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
callback(myRequest.responseText);
}
};
myRequest.send();
}
function pimvrServerRender(group, page, callback) {
const readURL = "https://fabien.benetou.fr/"+group+"/"+page+"?action=serverrender";
var myRequest = new XMLHttpRequest();
myRequest.open('GET', readURL);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
callback(JSON.parse(myRequest.responseText).res);
}
};
myRequest.send();
}
function pimvrLoadRemoteSmarthWatchConfiguration(callback) {
const readURL = "https://fabien.benetou.fr/PIMVRdata/SmartWatchConfiguration?action=source";
var myRequest = new XMLHttpRequest();
myRequest.open('GET', readURL);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
callback(JSON.parse(myRequest.responseText));
}
};
myRequest.send();
}
//pimvrLoadRemoteSmarthWatchConfiguration(console.log);
// usage unclear, can be used as
// haptic feedback on interactible items e.g. vibrate on gaze
// controller backup e.g. gaze+click
// controller locator e.g. making 2 bright columns
// heart rate monitor (sadly not with PebbleTime) to reshape experience
function pimvrLoadRemoteMetadata(group, page, callback, query) {
const readURL = "https://fabien.benetou.fr/"+group+"/"+page+"?action=metajson";
var myRequest = new XMLHttpRequest();
myRequest.open('GET', readURL+"&query="+query, true);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
callback(JSON.parse(myRequest.responseText));
}
};
myRequest.send();
}
function pimvrLoadRemote(page, callback) {
const readURL = "https://fabien.benetou.fr/PIMVRdata/"+page+"?action=source";
// assumes JSON
var myRequest = new XMLHttpRequest();
myRequest.open('GET', readURL);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
callback(JSON.parse(myRequest.responseText));
}
};
myRequest.send();
}
function pimvrSaveRemote(page, data) {
const writeURL = "https://fabien.benetou.fr/PIMVRdata/"+page+"?action=edit";
var myWriteRequest = new XMLHttpRequest();
myWriteRequest.open('POST', writeURL, true);
myWriteRequest.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
myWriteRequest.onreadystatechange = function () {
if (myWriteRequest.readyState === 4) {
//console.log(myWriteRequest.responseText);
console.log("Save on "+page+" sucessful");
}
};
console.log("trying to open "+writeURL+"post=1&author=PIMVR&authpw=edit_password&text="+data)
myWriteRequest.send("post=1&author=PIMVR&authpw=edit_password&text="+data);
// cf http://www.pmwiki.org/wiki/PmWiki/EditingAPI
}
function loadRemoteGraph(callback, params){
const myDataURL = "https://vatelier.net/MyDemo/newtooling/wiki_graph.json";
// not that as agressive as it gets cached
var myRequest = new XMLHttpRequest();
myRequest.open('GET', myDataURL);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === 4) {
//window.PIMgraph = JSON.parse(myRequest.responseText).Nodes;
callback(JSON.parse(myRequest.responseText).Nodes, params);
}
};
myRequest.send();
}
function getMyNeighbours(nodes, page){
console.log(nodes[page].Targets);
}
function getPageGroup(){
let group = QueryString.group || "Main";
let page = QueryString.page || "HomePage";
return [group, page];
}

@ -1,187 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet">
<script src="clipboard.min.js"></script>
<script src="upload/server/php/files/usersdb.js"></script>
</head>
<style>
body { background-color: transparent; }
#email, #sms {
font-size: xx-large;
}
</style>
<body>
<img src="productlogo.png">
<script>
var tickMark = "&#10004;";
var uploadedURL = "upload/server/php/files/";
var images = []
var url = "/"
var urlParameters = "?customimages="
function generateURL(){
//var url = "https://learnwebvr.xyz/importer.html"
for (var image of images){
urlParameters += image + ","
}
console.log ("URL", urlParameters );
var encodedURL = url + urlParameters
encodedURL += "&environment=" + document.querySelector("#environment").value
document.querySelector("#link").href = encodedURL
document.querySelector("#copyBtn").setAttribute("data-clipboard-text", encodedURL)
document.querySelector("#generateURL").style.display = "block"
document.querySelector("#email").href = "mailto:?&subject=session&body=" + encodeURIComponent( encodedURL )
// tested on iPhone 6S and Pixel2 and desktop
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
}
else {
document.querySelector("#copydiv").className = "cold-md-6"
document.querySelector("#emaildiv").className = "cold-md-6"
}
}
</script>
<div class="container">
<form class="form-signin">
Environment:
<select id="environment">
<option name="contact">contact</option>
<option name="egypt">egypt</option>
<option name="checkerboard">checkerboard</option>
<option name="forest" selected="selected">forest</option>
<option name="goaland">goaland</option>
<option name="yavapai">yavapai</option>
<option name="goldmine">goldmine</option>
<option name="threetowers">threetowers</option>
<option name="poison">poison</option>
<option name="arches">arches</option>
<option name="tron">tron</option>
<option name="japan">japan</option>
<option name="dream">dream</option>
<option name="volcano">volcano</option>
<option name="starry">starry</option>
<option name="osiris">osiris</option>
</select>
<button style="margin-top:2px;" type="button" onclick="unhide('#fileupload2');" class="btn btn-primary btn-block">Upload your office photos<br/>(post-its, white board, etc)</button>
No content available? Try <a target="_blank" href="https://learnwebvr.xyz/?customimages=upload/server/php/files/photo6035074301452988871.jpg,upload/server/php/files/photo6035074301452988870.jpg,upload/server/php/files/IMG_6496 (1).jpg"> this example instead</a>!
<input style="display:none;" id="fileupload2" type="file" name="files[]" data-url="upload/server/php/" multiple>
<div style="display:none;" id="fileuploaded2">Files uploaded: </div>
<div style="opacity:0.7;" id="uploadrate"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="upload/js/vendor/jquery.ui.widget.js"></script>
<script src="upload/js/jquery.iframe-transport.js"></script>
<script src="upload/js/jquery.fileupload.js"></script>
<script>
function unhide( element ){
document.querySelector(element).style.display = "block";
}
$(function () {
$('#fileupload2').fileupload({
dataType: 'json',
done: function (e, data) {
$.each(data.result.files, function (index, file) {
var imageURL = uploadedURL + (file.name)
images.push(imageURL)
var uploaded = document.querySelector("#fileuploaded2");
uploaded.style.display = "block";
uploaded.innerHTML += tickMark;
});
}
});
});
</script>
<button style="margin-top:2px;" type="button" onclick="generateURL()" id="generate-simulation" class="btn btn-lg btn-primary btn-block">Generate your<br/>relax+think space</button>
</form>
</div>
<div id="generateURL" style="display:none; text-align:center;" class="container">
<div class="container">
<a id="link" target="_blank" href=""><button type="button" class="btn btn-lg btn-primary btn-block">Launch relax+think space</button></a>
</div>
<br/>
<div class="container">
<div class="row">
<div class="col-md-4" id="copydiv">
<button class="btn" data-clipboard-text="" id="copyBtn">Copy to clipboard</button>
<script>
new Clipboard('.btn');
</script>
</div>
<div class="col-md-4" id="emaildiv">
<a id="email" href="" target="_blank">email link</a>
</div>
</div>
</div>
</div>
<script>
function login(){
document.querySelector("#login").style.display = "block"
}
function loginViaEmail(){
var email = document.querySelector("#useremail").value
var userspace = database[email]
if (!userspace){
document.querySelector("#nouser").style.display = "block"
return
}
document.querySelector("#spacesholder").style.display = "block"
var spaces = document.querySelector("#spaces")
var space = document.createElement("li")
var spacelink = document.createElement("a")
var images = userspace.files
for (var image of images){
urlParameters += image.filename + ","
}
spacelink.href = url + urlParameters
spacelink.target = "_blank"
spacelink.innerHTML = userspace.last_login
space.appendChild(spacelink)
console.log(space)
spaces.appendChild(space)
console.log("userspace", userspace)
}
</script>
<div>Already have an account? <span onclick="login()" style="text-decoration: underline;">Log-in</span>
<form>
<div id="login" style="display:none"><input id="useremail"/>
<button style="margin-top:2px;" type="button" onclick="loginViaEmail()" id="loginemail" class="btn btn-lg btn-primary btn-block">Login</button>
</div>
</form>
<div style="display:none" id="nouser">User not found. Double check your email address then contact fabien@iterative-explorations.com</a></div>
<div style="display:none" id="spacesholder">Your spaces:
<ul id="spaces"></ul>
</div>
</div>
<h3 style="position:absolute; bottom:0px; right:0px;">A product by <a href="https://iterative-explorations.com"><img width="200px" src="https://iterative-explorations.com/logo.svg"></a>.</h3>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -1,18 +0,0 @@
var database = {
"fabien@benetou.fr" : {
"username": "fabien",
"password": "test",
"last_login": 1576749381516,
"environment": "forest",
"files" : [
{ "position": "-2 1 -2", "filename" : "upload/server/php/files/C1C4E2F7-865A-4DB4-9274-1EA69B6C4A7E.jpeg" },
{ "position": "0 1 -2", "filename" : "upload/server/php/files/4A21A76C-F375-4884-9040-BC51A24057A6.jpeg" },
{ "position": "2 1 -2", "filename" : "upload/server/php/files/2CD9932F-891C-4BA5-BDDA-A3B39B39CF09.jpeg" }
],
"categories" : [
{ "position": "-2 1 -2", "color" : "orange", "label" : "waiting" },
{ "position": "0 1 -2", "color" : "red", "label" : "in progress" },
{ "position": "2 1 -2", "color" : "green", "label" : "completed" }
]
}
}

@ -1,17 +0,0 @@
<?php
/*
* jQuery File Upload Plugin PHP Example
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*/
error_reporting(E_ALL | E_STRICT);
require('UploadHandler.php');
$upload_handler = new UploadHandler(array(
'accept_file_types' => '/\.(png|jpe?g|mp4)$/i'
));

@ -0,0 +1,105 @@
/* global AFRAME, THREE */
/**
* Implement AABB collision detection for entities with a mesh.
* (https://en.wikipedia.org/wiki/Minimum_bounding_box#Axis-aligned_minimum_bounding_box)
* It sets the specified state on the intersected entities.
*
* @property {string} objects - Selector of the entities to test for collision.
* @property {string} state - State to set on collided entities.
*
*/
AFRAME.registerComponent('aabb-collider', {
schema: {
objects: {default: ''},
state: {default: 'collided'}
},
init: function () {
this.els = [];
this.collisions = [];
this.elMax = new THREE.Vector3();
this.elMin = new THREE.Vector3();
},
/**
* Update list of entities to test for collision.
*/
update: function () {
var data = this.data;
var objectEls;
// Push entities into list of els to intersect.
if (data.objects) {
objectEls = this.el.sceneEl.querySelectorAll(data.objects);
} else {
// If objects not defined, intersect with everything.
objectEls = this.el.sceneEl.children;
}
// Convert from NodeList to Array
this.els = Array.prototype.slice.call(objectEls);
},
tick: (function () {
var boundingBox = new THREE.Box3();
return function () {
var collisions = [];
var el = this.el;
var mesh = el.getObject3D('mesh');
var self = this;
// No mesh, no collisions
if (!mesh) { return; }
// Update the bounding box to account for rotations and
// position changes.
updateBoundingBox();
// Update collisions.
this.els.forEach(intersect);
// Emit events.
collisions.forEach(handleHit);
// No collisions.
if (collisions.length === 0) { self.el.emit('hit', {el: null}); }
// Updated the state of the elements that are not intersected anymore.
this.collisions.filter(function (el) {
return collisions.indexOf(el) === -1;
}).forEach(function removeState (el) {
el.removeState(self.data.state);
el.emit('hitend');
});
// Store new collisions
this.collisions = collisions;
// AABB collision detection
function intersect (el) {
var intersected;
var mesh = el.getObject3D('mesh');
var elMin;
var elMax;
if (!mesh) { return; }
boundingBox.setFromObject(mesh);
elMin = boundingBox.min;
elMax = boundingBox.max;
// Bounding boxes are always aligned with the world coordinate system.
// The collision test checks for the conditions where cubes intersect.
// It's an extension to 3 dimensions of this approach (with the condition negated)
// https://www.youtube.com/watch?v=ghqD3e37R7E
intersected = (self.elMin.x <= elMax.x && self.elMax.x >= elMin.x) &&
(self.elMin.y <= elMax.y && self.elMax.y >= elMin.y) &&
(self.elMin.z <= elMax.z && self.elMax.z >= elMin.z);
if (!intersected) { return; }
collisions.push(el);
}
function handleHit (hitEl) {
hitEl.emit('hit');
hitEl.addState(self.data.state);
self.el.emit('hit', {el: hitEl});
}
function updateBoundingBox () {
boundingBox.setFromObject(mesh);
self.elMin.copy(boundingBox.min);
self.elMax.copy(boundingBox.max);
}
};
})()
});

@ -0,0 +1,89 @@
/* global AFRAME, THREE */
/**
* Handles events coming from the hand-controls.
* Determines if the entity is grabbed or released.
* Updates its position to move along the controller.
*/
AFRAME.registerComponent('grab', {
init: function () {
this.GRABBED_STATE = 'grabbed';
// Bind event handlers
this.onHit = this.onHit.bind(this);
this.onGripOpen = this.onGripOpen.bind(this);
this.onGripClose = this.onGripClose.bind(this);
this.currentPosition = new THREE.Vector3();
},
play: function () {
var el = this.el;
el.addEventListener('hit', this.onHit);
el.addEventListener('buttondown', this.onGripClose);
el.addEventListener('buttonup', this.onGripOpen);
},
pause: function () {
var el = this.el;
el.removeEventListener('hit', this.onHit);
el.addEventListener('buttondown', this.onGripClose);
el.addEventListener('buttonup', this.onGripOpen);
},
onGripClose: function (evt) {
if (this.grabbing) { return; }
this.grabbing = true;
this.pressedButtonId = evt.detail.id;
delete this.previousPosition;
},
onGripOpen: function (evt) {
var hitEl = this.hitEl;
if (this.pressedButtonId !== evt.detail.id) { return; }
this.grabbing = false;
if (!hitEl) { return; }
hitEl.removeState(this.GRABBED_STATE);
hitEl.emit('grabend');
this.hitEl = undefined;
},
onHit: function (evt) {
var hitEl = evt.detail.el;
// If the element is already grabbed (it could be grabbed by another controller).
// If the hand is not grabbing the element does not stick.
// If we're already grabbing something you can't grab again.
if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }
hitEl.addState(this.GRABBED_STATE);
this.hitEl = hitEl;
},
tick: function () {
var hitEl = this.hitEl;
var position;
if (!hitEl) { return; }
this.updateDelta();
position = hitEl.getAttribute('position');
hitEl.setAttribute('position', {
x: position.x + this.deltaPosition.x,
y: position.y + this.deltaPosition.y,
z: position.z + this.deltaPosition.z
});
},
updateDelta: function () {
var currentPosition = this.currentPosition;
this.el.object3D.updateMatrixWorld();
currentPosition.setFromMatrixPosition(this.el.object3D.matrixWorld);
if (!this.previousPosition) {
this.previousPosition = new THREE.Vector3();
this.previousPosition.copy(currentPosition);
}
var previousPosition = this.previousPosition;
var deltaPosition = {
x: currentPosition.x - previousPosition.x,
y: currentPosition.y - previousPosition.y,
z: currentPosition.z - previousPosition.z
};
this.previousPosition.copy(currentPosition);
this.deltaPosition = deltaPosition;
}
});

@ -0,0 +1,32 @@
AFRAME.registerComponent("watch", {
init: function() {
this.timer = AFRAME.utils.getUrlParameter('timer')
if (!this.timer) this.timer = 300 //5min
document.querySelector("#watch").setAttribute("text","value:" + this.formatSeconds(this.timer))
this.tick = AFRAME.utils.throttleTick(this.tick, 1000, this);
// details https://aframe.io/docs/1.0.0/core/utils.html#aframe-utils-throttle-function-minimuminterval-optionalcontext
},
formatSeconds: function(secs) {
function pad(n) {
return (n < 10 ? "0" + n : n)
}
var m = Math.floor(secs / 60);
var s = Math.floor(secs - m * 60);
return pad(m) + ":" + pad(s);
},
tick: function (t, dt) {
var time = Number( this.timer )
console.log(this.timer);
if(this.timer > 0){
this.timer--
} else {
this.timer = 0
document.querySelector("#endMessage").setAttribute("visible","true")
}
document.querySelector("#watch").setAttribute("text","value:"+ this.formatSeconds(time))
},
});

@ -0,0 +1,72 @@
<a-scene background="color: #FAFAFA" vr-mode-ui="enterVRButton: #vrButton">
<a-assets>
<a-mixin id="sticker"
event-set__grab="material.color: #FFEF4F"
event-set__grabend="material.color: #F2E646"
event-set__hit="material.color: #F2E646"
event-set__hitend="material.color: #FEFEFE"
geometry="primitive: box; height: 0.2; width: 0.2; depth: 0.01"
material="color: #FEFEFE;">
</a-mixin>
</a-assets>
<!-- Extra lights -->
<a-light type="directional" color="#ffffff" intensity="0.3" position="0.5 2.2 2.866"></a-light>
<!-- Board -->
<a-entity position="0 0 -0.5">
<a-entity
position="-1 2 0"
text="color: white; align: center; width: 2; font: exo2semibold;
value: To Do"
></a-entity>
<a-plane position="-0.5 1.25 0" width="0.01" height="1.6" material="shader: flat; color: #ffffff"></a-plane>
<a-entity
position="0 2 0"
text="color: white; align: center; width: 2; font: exo2semibold;
value: In Progress"
></a-entity>
<a-plane position="0.5 1.25 0" width="0.01" height="1.6" material="shader: flat; color: #ffffff"></a-plane>
<a-entity
position="1 2 0"
text="color: white; align: center; width: 2; font: exo2semibold;
value: Done"
></a-entity>
</a-entity>
<!-- End message-->
<a-entity id="endMessage"
position="0 2.2 -1"
text="color: #FFCC00; align: center; width: 4; font: exo2semibold;
value: SESSION FINISHED" visible="false"
></a-entity>
<!-- Stickers -->
<a-entity position="0 1.5 -0.2">
<a-entity class="sticker" mixin="sticker" position="-0.30 0 0">
<a-entity
position="0 0 0.005"
text="color: black; align: center; width: 1; font: exo2semibold;
value: Task #1"
></a-entity>
</a-entity>
<a-entity class="sticker" mixin="sticker" position="0 0 0">
<a-entity
position="0 0 0.005"
text="color: black; align: center; width: 1; font: exo2semibold;
value: Task #2"
></a-entity>
</a-entity>
<a-entity class="sticker" mixin="sticker" position="0.30 0 0">
<a-entity
position="0 0 0.005"
text="color: black; align: center; width: 1; font: exo2semibold;
value: Task #3"
></a-entity>
</a-entity>
</a-entity>
<a-entity environment="preset: forest;"></a-entity>
<a-entity hand-controls="left" aabb-collider="objects: .sticker;" grab>
<a-entity watch id="watch" position="-0.02 -0.005 0.16" rotation="0 -90 180" text="color: white; align: center; width: .5; font: exo2semibold;
value: 00:59"></a-entity>
</a-entity>
<a-entity hand-controls="right" aabb-collider="objects: .sticker;" grab></a-entity>
</a-scene>

@ -0,0 +1,58 @@
html {
background: #000;
}
#vrButton {
position: absolute;
background: url('../dist/assets/img/enter-vr-button-background.png') no-repeat;
background-size: cover;
border: 0;
cursor: pointer;
right: 20px;
bottom: 20px;
text-decoration: none;
z-index: 9999999;
width: 64px;
height: 64px;
}
#vrButton.a-hidden {
visibility: hidden;
}
#vrButton:active {
border: 0;
}
#vrButton:hover {
background-position: 0 -64.5px;
}
#vrButton p {
bottom: 45px;
color: #FFF;
font-size: 12px;
font-family: monospace;
font-weight: bold;
text-align: center;
text-transform: uppercase;
position: relative;
}
.a-loader-title {
animation: loaderTitle 1s infinite alternate;
color: rgba(0,0,0,0);
background: none;
background-image: url('../dist/assets/img/loadingLogo.png');
height: 36.2vh;
background-repeat: no-repeat;
background-position: center;
margin-top: 0;
background-size: contain;
}
@keyframes loaderTitle {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}

@ -0,0 +1,17 @@
function requireAll (req) { req.keys().forEach(req); }
console.time = () => {};
console.timeEnd = () => {};
require('aframe-environment-component');
require('aframe-event-set-component');
require('./components/aabb-collider');
require('./components/grab');
require('./components/watch');
require('./index.css')
require('./home.html')
if (module.hot) { module.hot.accept(); }

@ -0,0 +1,66 @@
const path = require('path')
module.exports = {
entry: {
index: './src/index.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
devServer: {
https: true,
port: 3000,
contentBase: './dist'
},
devtool: 'source-map',
plugins: [
],
optimization: {
},
module: {
rules: [
{
test: /\.js/,
exclude: /(node_modules)/,
use: ['babel-loader', 'aframe-super-hot-loader']
},
{
test: /\.json/,
exclude: /(node_modules)/,
type: 'javascript/auto',
loader: ['json-loader']
},
{
test: /\.html/,
exclude: /(node_modules)/,
use: [
'aframe-super-hot-html-loader',
{
loader: 'html-require-loader',
options: {
root: path.resolve(__dirname, 'src')
}
}
]
},
{
test: /\.glsl/,
exclude: /(node_modules)/,
loader: 'webpack-glsl-loader'
},
{
test: /\.css$/,
exclude: /(node_modules)/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg)/,
loader: 'url-loader'
}
]
},
resolve: {
modules: [path.join(__dirname, 'node_modules')]
}
}
Loading…
Cancel
Save