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. 96
      UserStories.md
  8. BIN
      UxResearch/3dMockup/Exports/01_UnOrganized.png
  9. BIN
      UxResearch/3dMockup/Exports/02_SpatialOrganization.png
  10. BIN
      UxResearch/3dMockup/Exports/03_FolderOrganization.png
  11. BIN
      UxResearch/3dMockup/Exports/04_SubFolderCreation.png
  12. BIN
      UxResearch/3dMockup/Exports/05_OpenSubFolder.png
  13. BIN
      UxResearch/3dMockup/Exports/06_SubFolderView.png
  14. BIN
      UxResearch/3dMockup/Exports/07_SubFolderOrganization.png
  15. BIN
      UxResearch/3dMockup/Textures/Graph1.png
  16. BIN
      UxResearch/3dMockup/Textures/Graph2.png
  17. BIN
      UxResearch/3dMockup/Textures/Image1.jpg
  18. BIN
      UxResearch/3dMockup/Textures/Image2.jpg
  19. BIN
      UxResearch/3dMockup/Textures/Image3.jpg
  20. BIN
      UxResearch/3dMockup/Textures/Paper1.png
  21. BIN
      UxResearch/3dMockup/Textures/Paper2.png
  22. BIN
      UxResearch/3dMockup/Textures/Paper3.png
  23. BIN
      UxResearch/3dMockup/Textures/image4.jpg
  24. BIN
      UxResearch/3dMockup/Textures/image5.jpg
  25. BIN
      UxResearch/3dMockup/Textures/image6.jpg
  26. BIN
      UxResearch/3dMockup/Textures/image7.jpg
  27. BIN
      UxResearch/3dMockup/Textures/paper4.png
  28. BIN
      UxResearch/3dMockup/ThinkSpaceConcept.skb
  29. BIN
      UxResearch/3dMockup/ThinkSpaceConcept.skp
  30. BIN
      UxResearch/ThinkRelaxMockups/01_UnOrganized.png
  31. BIN
      UxResearch/ThinkRelaxMockups/02B_SpatialOrganization.png
  32. BIN
      UxResearch/ThinkRelaxMockups/02_SpatialOrganization.png
  33. BIN
      UxResearch/ThinkRelaxMockups/03_FolderOrganization.png
  34. BIN
      UxResearch/ThinkRelaxMockups/04_SubFolderCreation.png
  35. BIN
      UxResearch/ThinkRelaxMockups/05_OpenSubFolder.png
  36. BIN
      UxResearch/ThinkRelaxMockups/06_SubFolderView.png
  37. BIN
      UxResearch/ThinkRelaxMockups/07B_SubFolderOrganization.png
  38. BIN
      UxResearch/ThinkRelaxMockups/07_SubFolderOrganization.png
  39. BIN
      UxResearch/ThinkRelaxMockups/ThinkRelaxMockups.psd
  40. 85773
      dist/aframe-master.js
  41. BIN
      dist/assets/img/enter-vr-button-background.png
  42. BIN
      dist/assets/img/favicon.png
  43. BIN
      dist/assets/img/loadingLogo.png
  44. 32
      dist/index.html
  45. 111
      flat.html
  46. 150
      index.html
  47. 42
      package.json
  48. 173
      pimvrhelpers.js
  49. 187
      setup/index.html
  50. 1411
      setup/upload/server/php/UploadHandler.php
  51. 18
      setup/upload/server/php/files/usersdb.js
  52. 17
      setup/upload/server/php/index.php
  53. 105
      src/components/aabb-collider.js
  54. 89
      src/components/grab.js
  55. 32
      src/components/watch.js
  56. 72
      src/home.html
  57. 58
      src/index.css
  58. 17
      src/index.js
  59. 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 # relax-plus-think-space
an infinite space for your big ideas
## Principle An infinite space for your big ideas
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).
## UX flow for demos and tests ## Development
### 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
### returning user on 6DoF device Have Node (< v12, recommended v11) and npm installed.
1. visit https://learnwebvr.xyz
1 ...
### new user on 6DoF device Change 192.168.0.12 to your local IP on package.json scripts/start
1. visit https://learnwebvr.xyz
1 ...
## Code ```
* frontend : mostly `index.html` but also to prepare a session `setup/index.html` and `flat.html` to review after (and before the next) npm install
* backend : `setup/server/upload.php` npm run start
```
## License Then head to `https://[YOUR LOCAL IP]:3000` in your browser.
MIT.
## Property and rights
Iterative Explorations SCS based in Belgium

@ -1,96 +0,0 @@
# User story
## Julieta, PhD student in Ghent preparing her doctorate thesis on MEMS used for optics in AR
### Before VR
Julieta collects 20 PDFs after a searche on DuckDuckGo, Google Scholar, U Ghent library web portal in 1 DropBox directory named “Optical MEMS research” as Monday she has to present her understand of the state of the art to her lab.
Typically she would physically print all articles and throw them all the floor of her bedroom.
She would like to do so in the library but does not feel comfortable doing so.
She would also like to avoid printing all those papers and solely use her laptop but somehow, it does not “feel” the same as to have physically the papers in her hand and move them around.
Once all the papers are on the floor she stands up and step back, looking at her documents, considering the larger goal of completing her PhD, what moatived her to do so, etc.
She recalls her past similar task and which ones were the most pleasant and efficient.
As she does so she starts physically moving the a paper around chronologically.
She finds that the older papers are more referenced by also more outdated.
She quickly scans through the papers titles and authors back and forth, noticing how some authors seems more prominent than others.
She also consider the length of each paper, wondering if reading the shortest paper first could open up possibilities but she restraints herself from doing so.
She gets a notification from a friend suggesting to have a drink this evening but as she is getting stressed by her deadline she replies that she is busy for now and will think about it later, apologises and put her phone in airplane mode.
She slowly brings her focus back to the task at hand, looking at the paper sorted from the oldest to the newest. She finds it very superficial and wonder what could help her better understand and summarize what she has founds so what, what order would be the best for her to read all those articles.
She takes blue post-it notes and stick them to all papers from the same author, a professor her PhD advisor knows well.
She then highlights the different keywords of each article and consider those instead.
She stands up to steps back again wondering if distinct categories based on keywords or research lab of origin could help simplify her organisation.
She takes a break to go prepare herself a ginger infusion as she keeps on looking at all articles from afar.
Finally she decides that organising by distance to implementation would be the most efficient. She then moves to one corner of her room the 4 papers focusing on the theoretical foundations in physics and chemistry of optical sensors.
On the opposite corner she pushes 3 articles focusing on the challenge of scaling production of MEMS due to error rate of specific part of the chemical edging process.
She steps back again and in the center leaves all papers, moving them around in a seemingly random fashion.
The gradient with its category seems to make more and more sense to her.
She then subdivide the space again in zones reflecting how the rate of progress over time bringing 4 papers all the way to the top based on how prolific the research lab was then 3 remaining in the center and finally the last 6 all the way down.
Gradually she roughly adjusts based on those two axis.
She looks up at her wall noticing the poster her poster on graph theory and notices that most papers with the blue post-it notes falls within the same zone.
She starts to get a clearer view but unfortunately somebody buzzes, a friend reminding her she was supposed to have go out with soon. She lets her in right before snapping a photo of her floor covered with papers.
She knows she has to fold back her entire workspace and hates the feeling of being forced to stack all those papers on a pile but she doesn’t have the space to keep it all there and does not feel good letting a friend in with such a mess.
She knows the photo will help her to jolt her memory and the categories, gradient and dimensions will be sufficient to go on.
She stacks all papers quickly after emailing herself the photo with as title “Monday presentation lab” and in the body “2 axis, theory to prod x rate of progress”.
She wishes she could keep an entire room only for this research but instead of dwelving more on it she warmly welcomes her friend who open arrived with flowers and a large smile.
### First VR session
Julieta heads back to the U Ghent library to explore a new technique from IMEC that she heard about during a visit to another lab at KUL.
She heard the library opened a new VR space allowing researcher to more efficiently organize documents.
She is skeptical about it but unlike her latest presentation this time she is not in such a research and register a 30min session to this afternoon.
After registering she is invited to connect her DropBox account.
She does it allowing access to only a new empty folder name “AR lenses IMEC new process (VR test)” being unsure how safe or efficient that VR tool truly is. She then goes back to her usual online articles discovery, going from the research lab to the bibliography section of the last paper, looking for articles of the same authors, looking for similar research of labs but also private companies like Bell Labs, Intel in Taiwan and Sony Japan. After an hour or so of searches she looks at her DropBox folder and noticed she collected 34 papers. This starts to be more than she can realistically read in the next two weeks. At it is becoming to feel overwhelming she goes have a walk outside before her session in VR starts.
She then comes to the dedicated room wondering what she will have to do. She uses her phone to confirm the appointment and that she is ready. She then puts on the black headset which greets her and asks her to confirm that her name in indeed Julieta. Once she does an environment representing a cartoonish forest appear all around her. Some subtle souns like wind, birds, water become more and more present. For 30 seconds or so nothing seem to happen beside a small “Welcome Julieta” text slightly in front of her. She notices the controlers in her hands are represented by a pair of white gloves. She brings them closer to her face a couple of times. Under the “Welcome Julieta” text more text appears, suggesting her to relax and consider the goal she set aside for this session. More text appear letting her know that her documents are being loaded in her dedicated space. She notices behind her a timer counting down the 30min she set aside for this session.
Finally after a minute her DropBox folder appear with the title she choose earlier “AR lenses IMEC new process (VR test)”. All the articles as PDF are represented as boxes with as cover the front page in the size of an A4 page. The depth of the boxes are different but she is not sure why.
![img1](UxResearch/ThinkRelaxMockups/01_UnOrganized.png)
After moving forward slightly she notices she can walk to a box, or document, to be able to read the content more conveniently. Each document appear few seconds after the precedent in no obvious order. She steps back and forth as all the documents are finally displayed as lines of 7 documents forming a 5x7 grid. She steps further back to look at all of them. She starts to wonder if the can apply the same orgnisation she used on the floor of her room few weeks ago.
Unsure if it possible she extends her arm and noticed that when her white glove intersect with a document, the document glows slightly and her controler vibrate. She does it a couple of times until texts appear next to the documents indicating “To move a document, click on button under your index finger”. She tries it, using her thumb at first but then finally moves the document just few centimeters to the left. She repeats it a couple of times then decides to try on another document. She starts to move all documents, spreading them around her randomly. After a minute or so playing around she steps back again, wondering if indeed the classification used before would work. She scans over all documents and find the most theoretical one and brings it to the top left corner, however far she can reach. She repeats the process for few more documents then decides it is already better but not apply so well this time as it is hard for her to assess the rate of progress as well as last time since the process is so recent. She steps back again and wonder what criteria would be more appropriate. She decides that because of the process being new reliability could be interesting. She then moves all papers discussing the fabrication process would go on the right side her space, moving 10 articles there. She moves all other articles on the left. While moving them she notices that quite a few mention potential health concerns and challenges of bio interfaces. She moves those 5 articles to the top.
![img1](UxResearch/ThinkRelaxMockups/02_SpatialOrganization.png)
![img1](UxResearch/ThinkRelaxMockups/02B_SpatialOrganization.png)
She notices some remaining articles are probably outdated and wants to remove them. She is not sure how so she moves then behind her back, “Out of sight, out of mind” she thinks. While looking around she notices a + box on her right. She brings her hand to it and a carton like boxes unfolds. A text prompt with a keyboard invites her to name that folder. She is not sure but writes “Outdated”.
![img1](UxResearch/ThinkRelaxMockups/03_FolderOrganization.png)
She then brings all articles that were behind her in that box and see them becoming very small but still visible inside. She looks around her feeling her space already neater. She also notices the time is running out. She wonders what she should do next but before she does a message warns her that “Only 5 minutes left, all content will remain saved in place.”. A metadata document appear and moves with an animation to a 3D logo of DropBox that sits above her, seemingly out of reach. She is not sure what it meant but she feels confident that, like last time at home, her classification is helping her to more efficiently go through her research domain. She looks around her, trying to memorize the space and wonder if it will truly be there next time. As another text “1 minute left, it is now 18:27 and the weather is cloudy in Ghent.” appear before her. A camera shutter sound resonates as a photo of her viewpoint is taken with the name “AR lenses IMEC new process (VR test) _ screenshot.jpg” and also moves back to the DropBox logo. The forest fades away as another text appear “Please remove the headset now. Have a good day Julieta”. She removes the headset and sites for am minute. The web page she used to confirm her presence now displays “Sessions saved” with the screenshot take in VR. She notices that new file in her DropBox and a new directory named “Outdated”. She is quite curious to see if her files are really saved since her DropBox folder, beside the metadata document, screenshot and new direcyory, look just like it was before. She notices the website has a link to explore her “space” on her phone. She sees that her documents are indeed same in the correct position. She feels confident they will remain there and she can’t help but wonder if it is the right place. She unfortunately has to move on but books another session for the following day.
### Second VR session
The following day she repeats the process a lot more efficiently and notices right away that all files are indeed exactly where she left them. She gets familiar much more quickly with the environment, the same forest she was inside of last time.
She moves the closest document back and forth few time as if to refresh her memory on how to interact.
She notices that the 3 new PDFs she added are in center with for each a small yellow start on the top right corner. She promptly move them to the appropriate locate, all 3 going in the right side of her space are they related to the fabrication process.
She sees the outdated box and leans closer to it to see all documents in there. She picks one in particular after the small vibration she is now familiar with an pulls it out of the box are it regains is normal size.
She skims through quickly and nods confirming that it is indeed too old to consider.
She pushes it back in the box as it shrinks.
She looks back at the implicit groups she created.
She then grabs the Outdated folder and moves it again behind her, away from her focus.
She takes a document from the top and moves it to the plus sign.
![img1](UxResearch/ThinkRelaxMockups/04_SubFolderCreation.png)
As it prompts for a name she types “Bio interface”.
As the new box includes that documents the quicky moved the 4 other documents.
![img1](UxResearch/ThinkRelaxMockups/05_OpenSubFolder.png)
She then brings that new box to the top where all 5 documents were until then.
![img1](UxResearch/ThinkRelaxMockups/06_SubFolderView.png)
She nods again then steps back.
She repeats the process for the two categories and step back again.
![img1](UxResearch/ThinkRelaxMockups/07_SubFolderOrganization.png)
The end of the session warning appears with the saving message and screenshot shutter sound letting her know all her work is again saved.
She smiles and removes the headset.
She is still unsure she will be ready for her PhD but at least she feels confident she is forming in her mind and thus in her following papers a more synthetic structure of her field of research.
She takes her phone out of her pocket, have a brief look at the screenshot preview and her better structured Dropbox folder then moves back to the other tasks she has to do for the day.
PS: illustrations are in `UxResearch/ThinkRelaxMockups` with `UxResearch/3dMockup` for Sketchup source.
Illustrations fall under CC-BY-NC-SA-4.0
CreativeCommons Attribution-NonCommercial-ShareAlike 4.0 International.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

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