diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..4c799231c65b4508158d3231d58184e66b33e75a --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,20 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/node:10-browsers + working_directory: ~/repo + steps: + - checkout + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package-lock.json" }} + - v1-dependencies- + - run: npm ci + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package-lock.json" }} + - run: npm test + - store_artifacts: + path: dist diff --git a/.env.defaults b/.env.defaults index 01516302b442854410acdbf4067d42d5341034b2..6694552f2272da32a624f3d3d3a5f576b477158a 100644 --- a/.env.defaults +++ b/.env.defaults @@ -8,6 +8,10 @@ JANUS_SERVER="wss://dev-janus.reticulum.io" # See here for the server code: https://github.com/mozilla/reticulum RETICULUM_SERVER="dev.reticulum.io" +# The Farspark backend to connect to. Used as a CORS proxy and transformer for in-world media objects. +# See here for the server code: https://github.com/MozillaReality/farspark +FARSPARK_SERVER="farspark-dev.reticulum.io" + # The root URL under which Hubs expects environment GLTF bundles to be served. ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 92e3786c6ae68cfd8306ab619906411d65602716..0000000000000000000000000000000000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: node_js -node_js: "10" -cache: - directories: - - "$HOME/.npm" -install: - - npm ci -script: - - npm run lint - - npm run build diff --git a/Jenkinsfile b/Jenkinsfile index 6bc1e0d9b7d4b644e5e4d2b643d2a644408983de..afda72fad361fc4311a820f1ab95a50c04352279 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,9 +37,10 @@ pipeline { def smokeURL = env.SMOKE_URL def janusServer = env.JANUS_SERVER def reticulumServer = env.RETICULUM_SERVER + def farsparkServer = env.FARSPARK_SERVER def slackURL = env.SLACK_URL - def habCommand = "sudo /usr/bin/hab-docker-studio -k mozillareality run /bin/bash scripts/hab-build-and-push.sh \\\"${baseAssetsPath}\\\" \\\"${assetBundleServer}\\\" \\\"${janusServer}\\\" \\\"${reticulumServer}\\\" \\\"${targetS3Url}\\\" \\\"${env.BUILD_NUMBER}\\\" \\\"${env.GIT_COMMIT}\\\"" + def habCommand = "sudo /usr/bin/hab-docker-studio -k mozillareality run /bin/bash scripts/hab-build-and-push.sh \\\"${baseAssetsPath}\\\" \\\"${assetBundleServer}\\\" \\\"${janusServer}\\\" \\\"${reticulumServer}\\\" \\\"${farsparkServer}\\\" \\\"${targetS3Url}\\\" \\\"${env.BUILD_NUMBER}\\\" \\\"${env.GIT_COMMIT}\\\"" sh "/usr/bin/script --return -c ${shellString(habCommand)} /dev/null" def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim() @@ -49,7 +50,7 @@ pipeline { "<https://github.com/mozilla/hubs/commit/$gitSha|$gitSha> " + "Hubs: ```${gitSha} ${gitMessage}```\n" + "<${smokeURL}?required_version=${env.BUILD_NUMBER}|Smoke Test> - to push:\n" + - "`/mr hubs deploy ${targetS3Url}`" + "`/mr hubs deploy ${env.BUILD_NUMBER} ${targetS3Url}`" ) def payload = 'payload=' + JsonOutput.toJson([ text : text, diff --git a/PRIVACY.md b/PRIVACY.md index cc2b0c62ddc7d4309ee2e79ddf6b2815d179144b..3fdb4ebb0b6e1ac8128386f3358d1e2151d67e76 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,10 +1,10 @@ -# Privacy Notice for Hubs by Mozilla +# Privacy Notice for Hubs and Spoke -Version 2.0, Effective July 23, 2018 +Version 3.0, October 16, 2018 ## At Mozilla (that’s us), we believe that privacy is fundamental to a healthy internet. -In this Privacy Notice, we explain what data may be accessible to Mozilla or others when you use [Hubs by Mozilla](https://hubs.mozilla.com). We also adhere to the practices outlined in the Mozilla [privacy policy](https://www.mozilla.org/en-US/privacy/) for how we receive, handle, and share information we collect from Hubs. +In this Privacy Notice, we explain what data may be accessible to Mozilla or others when you use [Hubs](https://hubs.mozilla.com) or [Spoke](https://hubs.mozilla.com/spoke). We also adhere to the practices outlined in the Mozilla [privacy policy](https://www.mozilla.org/en-US/privacy/) for how we receive, handle, and share information we collect from Hubs. ## Things you should know: @@ -16,17 +16,26 @@ In this Privacy Notice, we explain what data may be accessible to Mozilla or oth - **Avatar data**: We receive and send to others in the Room the name of your Avatar, its position in the Room, and your interactions with objects in the Room. Mozilla does not record or store this data. You can optionally store information about your Avatar in your browser’s local storage. - **Room data**: Rooms are publicly accessible to anyone with the URL. Mozilla receives data about the virtual objects and Avatars in a Room and shares that data with others in the Room. - **Voice data**: If your microphone is on, Mozilla receives and sends audio to other users in the Room. Mozilla does not record or store the audio. *Be aware that once you agree to let Hubs use your microphone, it will stay on as long as you remain in a Hubs room, unless you turn it off.* -- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops) +- You can learn more by looking at the [code itself](https://github.com/mozilla/hubs) for Hubs. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops) </details> <details open> <summary> - <strong>Mozilla receives data you share to display to the room.</strong> + <strong>Mozilla receives data you create and share with Spoke and Hubs.</strong> </summary> -- **Images and Video**: Mozilla receives video and image file links to process and display them in the Room. Mozilla stores this data as long as you remain in the Room. -- **Scenes**: Mozilla receives 3D Room model links and the name of the Room in order to process and display the Room. Mozilla stores the name and the URL for the link you share so you and others with the link to the Room can use it again. -- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops) +- **Images and Video**: Mozilla receives video and image file links to process and display them in the Hubs Room. Mozilla stores this data as long as you remain in the Room. +- **Scenes You Share**: Mozilla receives 3D Room model links and the name of the Room in order to process and display the Room. Mozilla stores the name and the URL for the link you share so you and others with the link to the Room can use it again. + +<details open> + <summary> + <strong>Mozilla receives data when you create and publish Scenes with Spoke.</strong> + </summary> + +- **Scenes You Create**: When you create a scene with Spoke, Mozilla receives a copy of that scene. Mozilla stores that data in order to be able to process and display the scene through Hubs. +- **Publishing Your Scene**: When you publish a scene to Hubs using Spoke, Mozilla will ask for your email address to send you a link to verify your scene. Mozilla will receive and store your email address to allow you to log in and view your 3D Room models. Mozilla stores a hashed version of email addresses you use to publish a scene, so the stored versions are not available in readable form. +- **Remixing and Promotion**: When you use Spoke to publish a scene to Hubs, you have the option to “Allow Remixing with Creative Commons [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/)†or “allow Mozilla to promote my scene.†If you choose one or both of these options, Mozilla will share your scene publicly and you will have the option of including attribution information, which will also be publicly available. +- You can learn more by looking at the [code itself](https://github.com/mozillareality/spoke) for Spoke. </details> <details open> @@ -37,5 +46,5 @@ In this Privacy Notice, we explain what data may be accessible to Mozilla or oth - **Technical data**: We receive and store data about Room URLs and names; the type of device you use to interact with Hubs, as well as its operating system, language, the name and version of browser; and other data to load and operate the Room. - **Interaction data**: We receive data about your interactions with the Hubs service itself such as the number of Rooms created, the maximum number of users in a particular room at one same time, the start and end time of a user’s interaction with Hubs, the amount of time a user interacts with Hubs through Virtual Reality, the first time in a particular month or day that a user begins to use Hubs. Mozilla uses third party services to store and analyze these operational messages. - **Error Data**: In order to diagnose problems, Hubs sends Mozilla logs of error messages (which include the Room URL, response time for requests, the page you were on when you encountered the error, your operating system, browser information, and may include your IP address). -- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops) +- You can learn more by looking at the [code itself](https://github.com/mozilla/hubs) for Hubs. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops) </details> diff --git a/README.md b/README.md index 503057f930fe9fc7cfe37c01a18990860274f5a7..1182a0ebec1fe7acab1ed8b8b537d0d9d7c7f0b8 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca ## Query Params -- `room` - Id of the room (an integer) that you want to join - `allow_multi` - Allow multiple instances off the app in the same browser session - `enable_screen_sharing` - Enable screen sharing - `accept_screen_shares` - Display screens shared by other users @@ -62,6 +61,7 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca - `disable_telemetry` - If `true` disables Sentry telemetry. - `log_filter` - A `debug` style filter for setting the logging level. - `debug` - If `true` performs verbose logging of Janus and NAF traffic. +- `disableTunnel` - Tunnel vision is on by default. Disable the tunnel vision by this parameter. ## Additional Resources diff --git a/TERMS.md b/TERMS.md index 2e5a317cd498ff2c78cd528aed090617adc95776..930b81ceeac0dec4142c400308bec43ec4810566 100644 --- a/TERMS.md +++ b/TERMS.md @@ -1,22 +1,34 @@ -# Terms of Service for Hubs by Mozilla +# Terms of Service for Hubs and Spoke -Version 2.0, Effective July 23, 2018 +Version 3.0, Effective October 16, 2018 -[Hubs by Mozilla](https://hubs.mozilla.com) is a real-time communications platform for Virtual Reality, Augmented Reality, Desktop, Laptop, Mobile, or however else you browse the internet. These Terms of Service explain your rights and responsibilities when you use Hubs. +[Hubs by Mozilla](https://hubs.mozilla.com) is a real-time communications platform for Virtual Reality, Augmented Reality, Desktop, Laptop, Mobile, or however else you browse the internet. + +Spoke is a tool to arrange 3D models into scenes for use in Hubs. + +These Terms of Service explain your rights and responsibilities when you use Hubs. ### 1. Privacy Policy The Hubs [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) explains what information we collect when you use Hubs by Mozilla and how that information is handled and shared. ### 2. Communications and Content -Hubs allows users to send information (such as audio, video, images, and 3D models) to other users. By using Hubs, you agree to give Mozilla all rights necessary to operate Hubs by Mozilla. This includes, but is not limited to, a license and permission to transmit and display the information you send through Hubs and to gather and share information as described in the [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) for Hubs by Mozilla. -When you submit information to Hubs, you grant us a worldwide, royalty-free, perpetual, irrevocable, non-exclusive, transferable, and sublicensable license to use, copy, modify, adapt, prepare derivative works from, distribute, perform, and display that information, audio, video, images, or 3D models. You also agree that we may remove metadata associated with the information or data you submit, and you irrevocably waive any claims and assertions of moral rights or attribution with respect to the data you submit. +Hubs allows users to send information (such as audio, video, images, 3D models, and scenes) to other users. + +Spoke allows users to arrange 3D Room models into scenes that can appear in Hubs. + +By using Hubs or Spoke, you agree to give Mozilla all rights necessary to operate Hubs and Spoke. This includes, but is not limited to, a license and permission to process, transmit, and display the information you send through Hubs or Spoke. It also includes permission to gather and share information as described in the [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) for Hubs and Spoke. + +When you submit information to Hubs or Spoke, you grant us a worldwide, royalty-free, perpetual, irrevocable, non-exclusive, transferable, and sublicensable license to use, copy, modify, adapt, prepare derivative works from, distribute, perform, and display that information, audio, video, images, or 3D models. You also agree that we may remove metadata associated with the information or data you submit, and you irrevocably waive any claims and assertions of moral rights or attribution with respect to the data you submit. If you allow allow remixing of a scene you create using Spoke, you agree to license your scene under a [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/legalcode) license. + +You also represent and warrant that you have the authority to grant Mozilla all rights and permissions necessary for the operation of Hubs and Spoke. -You also represent and warrant that you have the authority to grant Mozilla all rights and permissions necessary for the operation of Hubs by Mozilla. To learn more about how Hubs operates, you can see the source code [here](https://github.com/mozilla/hubs). +To learn more about how Hubs operates, you can see the source code [here](https://github.com/mozilla/hubs). +To learn more about how Spoke operates, you can see the source code [here](https://github.com/mozillareality/spoke). -Any ideas, suggestions, and feedback about Hubs that you provide to us are entirely voluntary, and you agree that Mozilla may use such ideas, suggestions, and feedback without compensation or obligation to you. +Any ideas, suggestions, and feedback about Hubs or Spoke that you provide to us are entirely voluntary, and you agree that Mozilla may use such ideas, suggestions, and feedback without compensation or obligation to you. -You are solely responsible for the information you send using Hubs and the consequences of sending that information. +You are solely responsible for the information you send, create, or edit using Hubs or Spoke, and the consequences of sending, creating, or editing that information. ### 3. Conditions of Use By using Mozilla Hubs, you agree that your use will comply with Mozilla’s [Conditions of Use](https://www.mozilla.org/en-US/about/legal/acceptable-use/). Mozilla reserves the right to remove any content, suspend any users, and shut down any room it reasonably believes has violated these conditions. @@ -24,21 +36,21 @@ By using Mozilla Hubs, you agree that your use will comply with Mozilla’s [Con Please also be aware of [Mozilla’s Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/), which address participation in Mozilla communities. ### 4. Mozilla's Rights -Mozilla does not grant you any intellectual property rights in Hubs unless these Terms specifically say otherwise. For example, these Terms do not provide the right to use any of Mozilla’s copyrights, trade names, trademarks, service marks, logos, domain names, or other distinctive brand features. +Mozilla does not grant you any intellectual property rights in Hubs or Spoke unless these Terms specifically say otherwise. For example, these Terms do not provide the right to use any of Mozilla’s copyrights, trade names, trademarks, service marks, logos, domain names, or other distinctive brand features. -Mozilla distributes the Hubs software under an open source license. To learn more, you can read the [license itself](https://github.com/mozilla/hubs/blob/master/LICENSE) or read the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). +Mozilla distributes the Hubs and Spoke software under an open source license. To learn more, you can read the [license for Spoke]((https://github.com/mozillareality/spoke/blob/master/LICENSE)), and you can read the [license for Hubs](https://github.com/mozilla/hubs/blob/master/LICENSE) or read the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). ### 5. Services Interruption; Term; Termination -We are continuing to develop Hubs. As a result, we plan to upgrade and change Hubs over time. To do this, we might have to temporarily suspend Hubs and it is not always possible for us to give notice. You will not be entitled to claim expenses or damages for such suspension or limitation of the use of Hubs. +We are continuing to develop Hubs and Spoke. As a result, we plan to upgrade and change them over time. To do this, we might have to temporarily suspend their service and it is not always possible for us to give notice. You will not be entitled to claim expenses or damages for such suspension or limitation of the use of Hubs or Spoke. -These Terms apply to your use of Hubs and will continue to apply until ended by either you or upon notice from Mozilla. You can choose to end them at any time for any reason by discontinuing your use of Hubs. +These Terms apply to your use of Hubs and Spoke and will continue to apply until ended by either you or upon notice from Mozilla. You can choose to end them at any time for any reason by discontinuing your use of Hubs and Spoke. -We may cut off your access to Hubs, either temporarily or permanently at any time for any reason. This includes, but is not limited to, situations where we reasonably believe: (i) you have violated these Terms (ii) you create risk or possible legal exposure for Mozilla; or (iii) providing and operating Hubs is no longer commercially viable. If possible, we will make reasonable efforts to notify you through Hubs. +We may cut off your access to Hubs or Spoke, either temporarily or permanently at any time for any reason. This includes, but is not limited to, situations where we reasonably believe: (i) you have violated these Terms (ii) you create risk or possible legal exposure for Mozilla; or (iii) providing and operating Hubs is no longer commercially viable. If possible, we will make reasonable efforts to notify you through the relevant program, either Hubs or Spoke . -In all such cases, these Terms shall terminate, including, without limitation, your license to use Hubs, except that the sections with the following titles shall continue to apply: Indemnification, Disclaimer; Limitation of Liability and Miscellaneous. +In all such cases, these Terms shall terminate, including, without limitation, your license to use Hubs and Spoke, except that the sections with the following titles shall continue to apply: Indemnification, Disclaimer; Limitation of Liability and Miscellaneous. ### 6. Indemnification -You agree to defend, indemnify and hold harmless Mozilla, and its respective parent and affiliate companies, contractors, contributors, licensors, partners, directors, officers, employees and agents ("Indemnified Parties") from and against any and all third party claims and expenses, including attorneys' fees, arising out of or related to your use of Hubs. This includes, but is not limited to, claims and expenses from any content you transmit using Hubs. +You agree to defend, indemnify and hold harmless Mozilla, and its respective parent and affiliate companies, contractors, contributors, licensors, partners, directors, officers, employees and agents ("Indemnified Parties") from and against any and all third party claims and expenses, including attorneys' fees, arising out of or related to your use of Hubs or Spoke. This includes, but is not limited to, claims and expenses from any content you transmit, edit, or create using Hubs or Spoke. ### 7. Disclaimer; Limitation of Liability THE SERVICES ARE PROVIDED "AS IS" WITH ALL FAULTS. TO THE EXTENT PERMITTED BY LAW, THE INDEMNIFIED PARTIES, HEREBY DISCLAIM ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES THAT THE SERVICES ARE FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, AND NON-INFRINGING. @@ -49,17 +61,17 @@ THIS LIMITATION WILL APPLY NOTWITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE OF A EXCEPT AS REQUIRED BY LAW, THE INDEMNIFIED PARTIES, WILL NOT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES ARISING OUT OF OR IN ANY WAY RELATING TO THESE TERMS OR THE USE OF OR INABILITY TO USE THE SERVICES, INCLUDING WITHOUT LIMITATION DIRECT AND INDIRECT DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, LOST PROFITS, LOSS OF DATA, AND COMPUTER FAILURE OR MALFUNCTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND REGARDLESS OF THE THEORY (CONTRACT, TORT, OR OTHERWISE) UPON WHICH SUCH CLAIM IS BASED. THE COLLECTIVE LIABILITY OF THE INDEMNIFIED PARTIES, UNDER THIS AGREEMENT WILL NOT EXCEED $500 (FIVE HUNDRED DOLLARS). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL, CONSEQUENTIAL, OR SPECIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. ### 8. Modifications to These Terms -Mozilla may update these Terms from time to time. We will post the updated Terms online. If the changes are substantive, we will announce the update through Mozilla's usual channels for such announcements such as blog posts, forums, or in the particular service itself, in this case: Hubs by Mozilla. +Mozilla may update these Terms from time to time. We will post the updated Terms online. If the changes are substantive, we may announce the update through Mozilla's usual channels for such announcements such as blog posts, forums, or in the particular service itself, in this case: Hubs and Spoke. -Your continued use of Hubs after we post the new Terms constitutes your acceptance of the new Terms. To make your review more convenient, we will post an effective date at the top of this page. +Your continued use of Hubs or Spoke after we post the new Terms constitutes your acceptance of the new Terms. To make your review more convenient, we will post an effective date at the top of this page. ### 9. Miscellaneous -These Terms make up the entire agreement between you and Mozilla concerning Hubs. The laws of the state of California, U.S.A (excluding its conflict of law provisions) govern this agreement. +These Terms make up the entire agreement between you and Mozilla concerning Hubs and Spoke. The laws of the state of California, U.S.A (excluding its conflict of law provisions) govern this agreement. If any portion of these Terms is held to be invalid or unenforceable, the remaining portions remain in full force and effect. If there is a conflict or ambiguity between a translated version of these terms and the English language version, the English language version applies. ### 10. Contact Us -For support, to provide feedback, or to report abuse of Hubs or violations of the Conditions of Use, you can email us at [hubs@mozilla.com](mailto:hubs@mozilla.com). +For support, to provide feedback, or to report abuse of Hubs or Spoke or violations of the Conditions of Use, you can email us at [hubs@mozilla.com](mailto:hubs@mozilla.com). To report a claim of copyright or trademark infringement, see [our policy](https://www.mozilla.org/en-US/about/legal/report-infringement/). diff --git a/doc/image_orientations.gif b/doc/image_orientations.gif new file mode 100755 index 0000000000000000000000000000000000000000..89c4c3ea1444de6b32e1cecc2d2825e430c833e8 Binary files /dev/null and b/doc/image_orientations.gif differ diff --git a/package-lock.json b/package-lock.json index f79952ce2dfca0a74db294fc0c18d5ec2d2746a8..35c52ccb91e8c416cf7897d217d553390281fda4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -518,9 +518,7 @@ "requires": { "@tweenjs/tween.js": "^16.8.0", "browserify-css": "^0.8.2", - "debug": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a", "deep-assign": "^2.0.0", - "document-register-element": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90", "envify": "^3.4.1", "load-bmfont": "^1.2.3", "object-assign": "^4.0.1", @@ -534,7 +532,16 @@ "dependencies": { "debug": { "version": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a", - "from": "github:ngokevin/debug#noTimestamp" + "from": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a" + }, + "document-register-element": { + "version": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90", + "from": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90" + }, + "three": { + "version": "0.94.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.94.0.tgz", + "integrity": "sha1-TObbfyv795wtc0RKpuPPwIoy12I=" } } }, @@ -547,6 +554,47 @@ "version": "github:mozillareality/aframe-input-mapping-component#03932457c5318db243e811d2767fe0c5a8c7e9e0", "from": "github:mozillareality/aframe-input-mapping-component#hubs/master" }, + "aframe-inspector": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/aframe-inspector/-/aframe-inspector-0.8.3.tgz", + "integrity": "sha512-zTLfIjuG6CHhFMAQH4UaYeEeqDFLr5ZBL/2JaCvidhi+odMvJJuZqqH/uZk50bsQh2umtb929AimWvmE1edLjQ==", + "requires": { + "classnames": "^2.2.5", + "clipboard": "^1.5.12", + "invariant": "^2.2.2", + "lodash.debounce": "^4.0.6", + "prop-types": "^15.6.0", + "react": "^15.3.0", + "react-dom": "^15.3.0", + "react-file-reader-input": "^1.1.4", + "react-select": "^1.0.0-rc.1" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + }, + "react-dom": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, "aframe-motion-capture-components": { "version": "github:mozillareality/aframe-motion-capture-components#1ca616fa67b627e447b23b35a09da175d8387668", "from": "aframe-motion-capture-components@github:mozillareality/aframe-motion-capture-components#1ca616fa67b627e447b23b35a09da175d8387668" @@ -578,6 +626,11 @@ "version": "github:mozillareality/aframe-teleport-controls#14f296cad85cea6d15ee5ba08b142526ff9573f4", "from": "github:mozillareality/aframe-teleport-controls#hubs/master" }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.5.2", "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz", @@ -742,6 +795,11 @@ "resolved": "https://registry.npmjs.org/array-shuffle/-/array-shuffle-1.0.1.tgz", "integrity": "sha1-fqSIKjVrS8pfVF4LblLq9tlxVXo=" }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=" + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz", @@ -763,6 +821,11 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz", @@ -852,8 +915,7 @@ "async-each": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, "async-foreach": { "version": "0.1.3", @@ -1950,6 +2012,11 @@ "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=", "dev": true }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -1992,12 +2059,22 @@ "resolved": "https://registry.npmjs.org/base62/-/base62-1.2.8.tgz", "integrity": "sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==" }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.0", "resolved": "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha1-yrHmEY8FEJXli1KBrqjBzSK/wOM=", "dev": true }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz", @@ -2014,6 +2091,14 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "3.2.0", "resolved": "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz", @@ -2023,8 +2108,7 @@ "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" }, "binaryextensions": { "version": "2.1.1", @@ -2032,6 +2116,11 @@ "integrity": "sha1-MgmlHKSkrVQaO409am1bg6JIWTU=", "dev": true }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -2057,7 +2146,6 @@ "version": "1.18.2", "resolved": "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz", "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "dev": true, "requires": { "bytes": "3.0.0", "content-type": "~1.0.4", @@ -2074,8 +2162,7 @@ "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" } } }, @@ -2356,8 +2443,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "cacache": { "version": "10.0.4", @@ -2443,6 +2529,11 @@ "callsites": "^0.2.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "0.2.0", "resolved": "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz", @@ -2757,6 +2848,16 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "clipboard": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz", @@ -2972,11 +3073,20 @@ } } }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compressible": { "version": "2.0.14", @@ -3019,6 +3129,38 @@ "typedarray": "^0.0.6" } }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + } + } + }, "connect-history-api-fallback": { "version": "1.5.0", "resolved": "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", @@ -3055,8 +3197,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "convert-source-map": { "version": "1.5.1", @@ -3067,8 +3208,7 @@ "cookie": { "version": "0.3.1", "resolved": "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "cookie-signature": { "version": "1.0.6", @@ -3140,8 +3280,7 @@ "core-js": { "version": "2.5.7", "resolved": "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4=", - "dev": true + "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4=" }, "core-util-is": { "version": "1.0.2", @@ -3218,6 +3357,16 @@ "sha.js": "^2.4.8" } }, + "create-react-class": { + "version": "15.6.3", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", + "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3355,6 +3504,11 @@ "array-find-index": "^1.0.1" } }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz", @@ -3398,7 +3552,6 @@ "version": "2.6.9", "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "dev": true, "requires": { "ms": "2.0.0" } @@ -3616,6 +3769,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -3625,8 +3783,7 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "des.js": { "version": "1.0.0", @@ -3687,6 +3844,11 @@ "defined": "^1.0.0" } }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=" + }, "diff": { "version": "3.5.0", "resolved": "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz", @@ -3748,10 +3910,6 @@ "esutils": "^2.0.2" } }, - "document-register-element": { - "version": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90", - "from": "github:dmarcos/document-register-element#8ccc532b7" - }, "dom-converter": { "version": "0.1.4", "resolved": "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz", @@ -3761,6 +3919,17 @@ "utila": "~0.3" } }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -3890,8 +4059,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { "version": "2.6.1", @@ -3926,6 +4094,11 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "emoji-regex": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", + "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==" + }, "emojis-list": { "version": "2.1.0", "resolved": "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz", @@ -3935,8 +4108,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "encoding": { "version": "0.1.12", @@ -3955,6 +4127,90 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.5.tgz", + "integrity": "sha512-j1DWIcktw4hRwrv6nWx++5nFH2X64x16MAG2P0Lmi5Dvdfi3I+Jhc7JKJIdAmDJa+5aZ/imHV7dWRPy2Cqjh3A==", + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "~1.1.5" + }, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "requires": { + "mime-types": "~2.1.11", + "negotiator": "0.6.1" + } + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + } + } + }, + "engine.io-client": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.5.tgz", + "integrity": "sha512-AYTgHyeVUPitsseqjoedjhYJapNVoSPShbZ+tEUX9/73jgZ/Z3sUlJf9oYgdEBBdVhupUpUqSxH0kBCXlQnmZg==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~1.1.5", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -3966,6 +4222,11 @@ "tapable": "^1.0.0" } }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + }, "entities": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz", @@ -4083,8 +4344,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -4298,8 +4558,7 @@ "eventemitter3": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", - "dev": true + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" }, "events": { "version": "1.1.1", @@ -4375,6 +4634,50 @@ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", "dev": true }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "requires": { + "array-slice": "^0.2.3", + "array-unique": "^0.2.1", + "braces": "^0.1.2" + }, + "dependencies": { + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "requires": { + "expand-range": "^0.1.0" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "requires": { + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=" + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=" + } + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -4497,8 +4800,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", - "dev": true + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, "extend-shallow": { "version": "2.0.1", @@ -4957,7 +5259,6 @@ "version": "1.5.7", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz", "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==", - "dev": true, "requires": { "debug": "^3.1.0" }, @@ -4966,7 +5267,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -5074,14 +5374,12 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, "optional": true, "requires": { "nan": "^2.9.2", @@ -5091,24 +5389,20 @@ "abbrev": { "version": "1.1.1", "bundled": true, - "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "dev": true + "bundled": true }, "aproba": { "version": "1.2.0", "bundled": true, - "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.4", "bundled": true, - "dev": true, "optional": true, "requires": { "delegates": "^1.0.0", @@ -5117,13 +5411,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "dev": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5132,34 +5424,28 @@ "chownr": { "version": "1.0.1", "bundled": true, - "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "dev": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "dev": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "dev": true + "bundled": true }, "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true, "optional": true }, "debug": { "version": "2.6.9", "bundled": true, - "dev": true, "optional": true, "requires": { "ms": "2.0.0" @@ -5168,25 +5454,21 @@ "deep-extend": { "version": "0.5.1", "bundled": true, - "dev": true, "optional": true }, "delegates": { "version": "1.0.0", "bundled": true, - "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", "bundled": true, - "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", "bundled": true, - "dev": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -5195,13 +5477,11 @@ "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true, "optional": true }, "gauge": { "version": "2.7.4", "bundled": true, - "dev": true, "optional": true, "requires": { "aproba": "^1.0.3", @@ -5217,7 +5497,6 @@ "glob": { "version": "7.1.2", "bundled": true, - "dev": true, "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -5231,13 +5510,11 @@ "has-unicode": { "version": "2.0.1", "bundled": true, - "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.21", "bundled": true, - "dev": true, "optional": true, "requires": { "safer-buffer": "^2.1.0" @@ -5246,7 +5523,6 @@ "ignore-walk": { "version": "3.0.1", "bundled": true, - "dev": true, "optional": true, "requires": { "minimatch": "^3.0.4" @@ -5255,7 +5531,6 @@ "inflight": { "version": "1.0.6", "bundled": true, - "dev": true, "optional": true, "requires": { "once": "^1.3.0", @@ -5264,19 +5539,16 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "dev": true + "bundled": true }, "ini": { "version": "1.3.5", "bundled": true, - "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5284,26 +5556,22 @@ "isarray": { "version": "1.0.0", "bundled": true, - "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", "bundled": true, - "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "dev": true + "bundled": true }, "minipass": { "version": "2.2.4", "bundled": true, - "dev": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5312,7 +5580,6 @@ "minizlib": { "version": "1.1.0", "bundled": true, - "dev": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -5321,7 +5588,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "dev": true, "requires": { "minimist": "0.0.8" } @@ -5329,13 +5595,11 @@ "ms": { "version": "2.0.0", "bundled": true, - "dev": true, "optional": true }, "needle": { "version": "2.2.0", "bundled": true, - "dev": true, "optional": true, "requires": { "debug": "^2.1.2", @@ -5346,7 +5610,6 @@ "node-pre-gyp": { "version": "0.10.0", "bundled": true, - "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -5364,7 +5627,6 @@ "nopt": { "version": "4.0.1", "bundled": true, - "dev": true, "optional": true, "requires": { "abbrev": "1", @@ -5374,13 +5636,11 @@ "npm-bundled": { "version": "1.0.3", "bundled": true, - "dev": true, "optional": true }, "npm-packlist": { "version": "1.1.10", "bundled": true, - "dev": true, "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -5390,7 +5650,6 @@ "npmlog": { "version": "4.1.2", "bundled": true, - "dev": true, "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -5401,19 +5660,16 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "dev": true + "bundled": true }, "object-assign": { "version": "4.1.1", "bundled": true, - "dev": true, "optional": true }, "once": { "version": "1.4.0", "bundled": true, - "dev": true, "requires": { "wrappy": "1" } @@ -5421,19 +5677,16 @@ "os-homedir": { "version": "1.0.2", "bundled": true, - "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", "bundled": true, - "dev": true, "optional": true }, "osenv": { "version": "0.1.5", "bundled": true, - "dev": true, "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -5443,19 +5696,16 @@ "path-is-absolute": { "version": "1.0.1", "bundled": true, - "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", "bundled": true, - "dev": true, "optional": true }, "rc": { "version": "1.2.7", "bundled": true, - "dev": true, "optional": true, "requires": { "deep-extend": "^0.5.1", @@ -5467,7 +5717,6 @@ "minimist": { "version": "1.2.0", "bundled": true, - "dev": true, "optional": true } } @@ -5475,7 +5724,6 @@ "readable-stream": { "version": "2.3.6", "bundled": true, - "dev": true, "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -5490,7 +5738,6 @@ "rimraf": { "version": "2.6.2", "bundled": true, - "dev": true, "optional": true, "requires": { "glob": "^7.0.5" @@ -5498,43 +5745,36 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, - "dev": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", "bundled": true, - "dev": true, "optional": true }, "sax": { "version": "1.2.4", "bundled": true, - "dev": true, "optional": true }, "semver": { "version": "5.5.0", "bundled": true, - "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", "bundled": true, - "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", "bundled": true, - "dev": true, "optional": true }, "string-width": { "version": "1.0.2", "bundled": true, - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5544,7 +5784,6 @@ "string_decoder": { "version": "1.1.1", "bundled": true, - "dev": true, "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -5553,7 +5792,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5561,13 +5799,11 @@ "strip-json-comments": { "version": "2.0.1", "bundled": true, - "dev": true, "optional": true }, "tar": { "version": "4.4.1", "bundled": true, - "dev": true, "optional": true, "requires": { "chownr": "^1.0.1", @@ -5582,13 +5818,11 @@ "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true, "optional": true }, "wide-align": { "version": "1.1.2", "bundled": true, - "dev": true, "optional": true, "requires": { "string-width": "^1.0.2" @@ -5596,13 +5830,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "dev": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "dev": true + "bundled": true } } }, @@ -5792,7 +6024,6 @@ "version": "7.1.2", "resolved": "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5975,6 +6206,14 @@ } } }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "requires": { + "delegate": "^3.1.2" + } + }, "got": { "version": "8.3.2", "resolved": "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz", @@ -6080,12 +6319,25 @@ "ansi-regex": "^2.0.0" } }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "requires": { + "isarray": "0.0.1" + } + }, "has-color": { "version": "0.1.7", "resolved": "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz", "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", "dev": true }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz", @@ -6392,7 +6644,6 @@ "version": "1.6.3", "resolved": "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -6410,7 +6661,6 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, "requires": { "eventemitter3": "^3.0.0", "follow-redirects": "^1.0.0", @@ -6560,8 +6810,7 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, "inflight": { "version": "1.0.6", @@ -6790,7 +7039,6 @@ "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz", "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, "requires": { "binary-extensions": "^1.0.0" } @@ -7114,8 +7362,7 @@ "isbinaryfile": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz", - "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", - "dev": true + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=" }, "isexe": { "version": "2.0.0", @@ -7379,8 +7626,7 @@ "json3": { "version": "3.3.2", "resolved": "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=" }, "json5": { "version": "0.5.1", @@ -7488,20 +7734,164 @@ } } }, - "keyv": { - "version": "3.0.0", - "resolved": "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha1-RJI7o55osSp87H32wyaMAx8u83M=", - "dev": true, + "karma": { + "version": "0.13.22", + "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", + "integrity": "sha1-B3ULG9Bj1+fnuRvNLmNU2PKqh0Q=", "requires": { - "json-buffer": "3.0.0" - } - }, - "killable": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz", - "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", - "dev": true + "batch": "^0.5.3", + "bluebird": "^2.9.27", + "body-parser": "^1.12.4", + "chokidar": "^1.4.1", + "colors": "^1.1.0", + "connect": "^3.3.5", + "core-js": "^2.1.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "expand-braces": "^0.1.1", + "glob": "^7.0.0", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^3.8.0", + "log4js": "^0.6.31", + "mime": "^1.3.4", + "minimatch": "^3.0.0", + "optimist": "^0.6.1", + "rimraf": "^2.3.3", + "socket.io": "^1.4.5", + "source-map": "^0.5.3", + "useragent": "^2.1.6" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "batch": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", + "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=" + }, + "bluebird": { + "version": "2.11.0", + "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + } + } + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha1-RJI7o55osSp87H32wyaMAx8u83M=", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "killable": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz", + "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", + "dev": true }, "kind-of": { "version": "3.2.2", @@ -7554,6 +7944,14 @@ "immediate": "~3.0.5" } }, + "linkify-it": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", + "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", + "requires": { + "uc.micro": "^1.0.1" + } + }, "listr": { "version": "0.14.1", "resolved": "https://registry.yarnpkg.com/listr/-/listr-0.14.1.tgz", @@ -7768,6 +8166,11 @@ } } }, + "load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ=" + }, "loader-runner": { "version": "2.3.0", "resolved": "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz", @@ -7830,8 +8233,17 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, "lodash.mergewith": { "version": "4.6.1", @@ -7872,6 +8284,33 @@ } } }, + "log4js": { + "version": "0.6.38", + "resolved": "http://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "requires": { + "readable-stream": "~1.0.2", + "semver": "~4.3.3" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" + } + } + }, "loglevel": { "version": "1.6.1", "resolved": "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz", @@ -7931,7 +8370,6 @@ "version": "4.1.3", "resolved": "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz", "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=", - "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -7940,8 +8378,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" } } }, @@ -8045,8 +8482,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "mem": { "version": "1.1.0", @@ -8249,14 +8685,12 @@ "mime-db": { "version": "1.35.0", "resolved": "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=", - "dev": true + "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=" }, "mime-types": { "version": "2.1.19", "resolved": "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz", "integrity": "sha1-ceRkU3p++BwV8tudl+kT/A/2BvA=", - "dev": true, "requires": { "mime-db": "~1.35.0" } @@ -8451,9 +8885,9 @@ "dev": true }, "naf-janus-adapter": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/naf-janus-adapter/-/naf-janus-adapter-0.11.0.tgz", - "integrity": "sha512-jLwcs4TRj7Dur0bF9RjP9UyjIkdKpwNZ3q/zCXT+ytrHGRXWt4rX+9acdpg2QVEmPMumN+rr9NUPliNWpRxD3w==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/naf-janus-adapter/-/naf-janus-adapter-0.13.2.tgz", + "integrity": "sha512-mr7xrHTlz9V2ZCIrh9HjN5qL6Yqoebgw5roG4jEUJHGV3pSQY7mtOd6DEqpDFcuGRr2RE0mTpe/L4UdZhQmImw==", "requires": { "debug": "^3.1.0", "minijanus": "0.6.2", @@ -8461,20 +8895,24 @@ }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, "nan": { "version": "2.10.0", "resolved": "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz", - "integrity": "sha1-ltDNYQ69WNS03pzAxoKM2pnHVI8=", - "dev": true + "integrity": "sha1-ltDNYQ69WNS03pzAxoKM2pnHVI8=" }, "nanomatch": { "version": "1.2.13", @@ -8541,8 +8979,7 @@ "negotiator": { "version": "0.6.1", "resolved": "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "neo-async": { "version": "2.5.1", @@ -8551,7 +8988,7 @@ "dev": true }, "networked-aframe": { - "version": "github:mozillareality/networked-aframe#b0ece8ba80479fa6912969fa03bc4cf3f30c4026", + "version": "github:mozillareality/networked-aframe#1dd7e0aa62bd119c214fec7e9137d4447f40cba0", "from": "github:mozillareality/networked-aframe#master", "requires": { "buffered-interpolation": "^0.2.4", @@ -8909,6 +9346,11 @@ "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz", @@ -8983,7 +9425,6 @@ "version": "2.3.0", "resolved": "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, "requires": { "ee-first": "1.1.1" } @@ -9017,6 +9458,27 @@ "is-wsl": "^1.1.0" } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz", @@ -9031,6 +9493,11 @@ "wordwrap": "~1.0.0" } }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" + }, "ora": { "version": "0.2.3", "resolved": "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz", @@ -9104,8 +9571,7 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", @@ -9300,17 +9766,40 @@ "resolved": "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parserlib": { "version": "0.2.5", "resolved": "https://registry.yarnpkg.com/parserlib/-/parserlib-0.2.5.tgz", "integrity": "sha1-hZB92GBaoGq7PdKV1QuyuPpN0Rc=", "dev": true }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, "pascalcase": { "version": "0.1.1", @@ -9397,9 +9886,8 @@ "dev": true }, "phoenix": { - "version": "1.3.3", - "resolved": "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.3.tgz", - "integrity": "sha1-tw86tLkR+v0jZggrP4R9XOs0CTs=" + "version": "github:gfodor/phoenix-js#9e6f6acdca5c9642429564fa28b657a2057deedd", + "from": "github:gfodor/phoenix-js#master" }, "pify": { "version": "3.0.0", @@ -9816,8 +10304,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", - "dev": true + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "progress": { "version": "2.0.0", @@ -9872,8 +10359,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "public-encrypt": { "version": "4.0.2", @@ -9923,8 +10409,7 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" }, "quad-indices": { "version": "2.0.1", @@ -10026,7 +10511,6 @@ "version": "2.3.2", "resolved": "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz", "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "dev": true, "requires": { "bytes": "3.0.0", "http-errors": "1.6.2", @@ -10037,14 +10521,12 @@ "depd": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", - "dev": true + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" }, "http-errors": { "version": "1.6.2", "resolved": "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz", "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "dev": true, "requires": { "depd": "1.1.1", "inherits": "2.0.3", @@ -10055,14 +10537,12 @@ "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, "setprototypeof": { "version": "1.0.3", "resolved": "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" } } }, @@ -10088,6 +10568,34 @@ "prop-types": "^15.6.0" } }, + "react-emoji-render": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-0.4.6.tgz", + "integrity": "sha512-ARB8E4j/dndQxC7Bn4b+Oymt7pqhh9GjP87NYcxC8KONejysnXD5O9KpnJeW/U3Ke3+XsWrWAr9K5riVA6emfg==", + "requires": { + "classnames": "^2.2.5", + "emoji-regex": "^6.4.1", + "lodash.flatten": "^4.4.0", + "prop-types": "^15.5.8", + "string-replace-to-array": "^1.0.1" + } + }, + "react-file-reader-input": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-1.1.4.tgz", + "integrity": "sha1-1rD1V3k6Oz7iBuqTAoPaNCM3y6g=", + "requires": { + "karma": "^0.13.22" + } + }, + "react-input-autosize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", + "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-intl": { "version": "2.4.0", "resolved": "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz", @@ -10099,6 +10607,36 @@ "invariant": "^2.1.1" } }, + "react-linkify": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-0.2.2.tgz", + "integrity": "sha512-0S8cvUNtEgfJpIGDPKklyrnrTffJ63WuJAc4KaYLBihl5TjgH5cHUmYD+AXLpsV+CVmfoo/56SUNfrZcY4zYMQ==", + "requires": { + "linkify-it": "^2.0.3", + "prop-types": "^15.5.8", + "tlds": "^1.57.0" + } + }, + "react-select": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", + "integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==", + "requires": { + "classnames": "^2.2.4", + "prop-types": "^15.5.8", + "react-input-autosize": "^2.1.2" + } + }, + "react-youtube": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.8.0.tgz", + "integrity": "sha512-kQFR0XTpgGRtzJ54HKDaCtAGr34mgB/BVFeCAL0WUjpIKZBcWtFrKJhYkoKfvWK7aTzJuQ57xojTjG7V6JzILA==", + "requires": { + "fast-deep-equal": "^2.0.1", + "prop-types": "^15.5.3", + "youtube-player": "^5.5.1" + } + }, "read-chunk": { "version": "2.1.0", "resolved": "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz", @@ -10165,7 +10703,6 @@ "version": "2.3.6", "resolved": "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10179,14 +10716,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -10197,7 +10732,6 @@ "version": "2.1.0", "resolved": "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz", "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "minimatch": "^3.0.2", @@ -10557,8 +11091,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.8.1", @@ -10637,7 +11170,6 @@ "version": "2.6.2", "resolved": "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", - "dev": true, "requires": { "glob": "^7.0.5" } @@ -10690,8 +11222,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", - "dev": true + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" }, "safe-regex": { "version": "1.1.0", @@ -10868,6 +11399,11 @@ "resolved": "https://registry.yarnpkg.com/sdp/-/sdp-2.7.4.tgz", "integrity": "sha1-ysdrDi8W9VJD0lvAQy9ru1SIv8E=" }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz", @@ -10960,8 +11496,7 @@ "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" }, "set-value": { "version": "2.0.0", @@ -10983,8 +11518,7 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "sha.js": { "version": "2.4.11", @@ -11068,6 +11602,11 @@ "simple-concat": "^1.0.0" } }, + "sister": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.1.tgz", + "integrity": "sha512-aG41gNRHRRxPq52MpX4vtm9tapnr6ENmHUx8LMAJWCOplEMwXzh/dp5WIo52Wl8Zlc/VUyHLJ2snX0ck+Nma9g==" + }, "slash": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz", @@ -11136,6 +11675,128 @@ "kind-of": "^3.2.0" } }, + "socket.io": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.4.tgz", + "integrity": "sha1-L37O3DORvy1cc+KR/iM+bjTU3QA=", + "requires": { + "debug": "2.3.3", + "engine.io": "~1.8.4", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.4", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=" + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + } + } + }, + "socket.io-client": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.4.tgz", + "integrity": "sha1-7J+CA1btme9tNX8HVtZIcXvdQoE=", + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "~1.8.4", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=" + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -11367,8 +12028,7 @@ "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, "stdout-stream": { "version": "1.4.1", @@ -11423,6 +12083,16 @@ "resolved": "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-replace-to-array": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz", + "integrity": "sha1-yT66mZpe4k1zGuu69auja18Y978=", + "requires": { + "invariant": "^2.2.1", + "lodash.flatten": "^4.2.0", + "lodash.isstring": "^4.0.1" + } + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz", @@ -11925,8 +12595,8 @@ } }, "super-hands": { - "version": "github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc", - "from": "super-hands@github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc" + "version": "github:mozillareality/aframe-super-hands-component#68d022fd24c6c986ec3af09a3e88b75323e9803f", + "from": "github:mozillareality/aframe-super-hands-component#feature/drawing" }, "supports-color": { "version": "5.4.0", @@ -12037,9 +12707,8 @@ } }, "three-pathfinding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/three-pathfinding/-/three-pathfinding-0.7.0.tgz", - "integrity": "sha512-UwWvzgio1UFe81n5jKHNzB4B+AG3wfZ54OKp7bTb1MHuC3cy6RTtr0dbbiPQQoqxzr+DRArR2DUwQSEknw5+nw==" + "version": "github:mozillareality/three-pathfinding#a52f437eaaf1b608c5f7fed046846bdbd79c75e7", + "from": "github:mozillareality/three-pathfinding#hubs/master" }, "three-to-cannon": { "version": "1.3.0", @@ -12081,15 +12750,29 @@ "setimmediate": "^1.0.4" } }, + "tiny-emitter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", + "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" + }, + "tlds": { + "version": "1.203.1", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.203.1.tgz", + "integrity": "sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz", "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -12263,7 +12946,6 @@ "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "dev": true, "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.18" @@ -12279,6 +12961,11 @@ "resolved": "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz", "integrity": "sha1-p7/ZL1bt+xFwg7aeMdKqiILUse0=" }, + "uc.micro": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", + "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" + }, "uglify-es": { "version": "3.3.9", "resolved": "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz", @@ -12345,6 +13032,11 @@ } } }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + }, "underscore": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", @@ -12484,8 +13176,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", @@ -12645,6 +13336,15 @@ "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", "dev": true }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "requires": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, "util": { "version": "0.10.4", "resolved": "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz", @@ -12678,8 +13378,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.3.2", @@ -12796,6 +13495,11 @@ "indexof": "0.0.1" } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz", @@ -13288,6 +13992,20 @@ "slide": "^1.1.5" } }, + "ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "requires": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=" + }, "x-is-string": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", @@ -13344,6 +14062,11 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", @@ -13473,6 +14196,11 @@ } } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yeoman-environment": { "version": "2.3.1", "resolved": "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.3.1.tgz", @@ -13638,6 +14366,16 @@ "dev": true } } + }, + "youtube-player": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.1.tgz", + "integrity": "sha512-1d62W9She0B1uKNyY6K7jtWFbOW3dYsm6hyKzrh11BLOuYFzkt8K6AcQ3QdPF3aU47dzhZ82clzOJVVWus4HTw==", + "requires": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } } } } diff --git a/package.json b/package.json index ef9d2cf054627149ee29373884c1b6ed1c45c37f..bee3e1b3d3c7b14a9d5f939f369ade4f0b49c437 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "prettier": "prettier --write '*.js' 'src/**/*.js'", "lint:js": "eslint '*.js' 'scripts/**/*.js' 'src/**/*.js'", "lint:html": "htmlhint 'src/**/*.html'", - "lint": "npm run lint:js && npm run lint:html" + "lint": "npm run lint:js && npm run lint:html", + "test": "npm run lint && npm run build" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.2", @@ -28,6 +29,7 @@ "aframe": "github:mozillareality/aframe#bugfix/oculus-go-controller-reconnect-pre-e0c8ff7", "aframe-billboard-component": "^1.0.0", "aframe-input-mapping-component": "github:mozillareality/aframe-input-mapping-component#hubs/master", + "aframe-inspector": "^0.8.3", "aframe-motion-capture-components": "github:mozillareality/aframe-motion-capture-components#1ca616fa67b627e447b23b35a09da175d8387668", "aframe-physics-extras": "github:mozillareality/aframe-physics-extras#bugfix/physics-collider-world", "aframe-physics-system": "github:mozillareality/aframe-physics-system#ecc5c9c533d6d9c71f8d6453ab961ed074d44b1c", @@ -43,18 +45,21 @@ "jsonschema": "^1.2.2", "jszip": "^3.1.5", "moving-average": "^1.0.0", - "naf-janus-adapter": "^0.11.0", + "naf-janus-adapter": "^0.13.2", "networked-aframe": "github:mozillareality/networked-aframe#master", "nipplejs": "github:mozillareality/nipplejs#mr-social-client/master", - "phoenix": "^1.3.0", + "phoenix": "github:gfodor/phoenix-js#master", "raven-js": "^3.20.1", "react": "^16.1.1", "react-dom": "^16.1.1", + "react-emoji-render": "^0.4.6", "react-intl": "^2.4.0", + "react-youtube": "^7.8.0", + "react-linkify": "^0.2.2", "screenfull": "^3.3.2", - "super-hands": "github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc", + "super-hands": "github:mozillareality/aframe-super-hands-component#feature/drawing", "three": "github:mozillareality/three.js#8b1886c384371c3e6305b757d1db7577c5201a9b", - "three-pathfinding": "^0.7.0", + "three-pathfinding": "github:mozillareality/three-pathfinding#hubs/master", "three-to-cannon": "1.3.0", "uuid": "^3.2.1", "webrtc-adapter": "^6.0.2" diff --git a/scripts/bot/package-lock.json b/scripts/bot/package-lock.json index fc2a26a35abb1f21b7e1d9801159a6eb8f8c69f3..6b4096f478cdf40a22477eb20dda838344af0ecf 100644 --- a/scripts/bot/package-lock.json +++ b/scripts/bot/package-lock.json @@ -4,106 +4,108 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "requires": { - "fd-slicer": "1.0.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha1-mDi1wzkrliutAx5qTF4QJKvsRc4=", + "dev": true, "requires": { - "minimist": "0.0.8" + "es6-promisify": "^5.0.0" } }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=" + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=", + "dev": true }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha1-h/yqOimDWOCt5uRCz86EB0DRrQQ=", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "dev": true, "requires": { - "wrappy": "1.0.2" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + "docopt": { + "version": "0.6.2", + "resolved": "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz", + "integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=", + "dev": true }, "es6-promise": { "version": "4.2.4", "resolved": "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha1-3EIhwrFlGHYL2MOaUtjzVvwA7Sk=" + "integrity": "sha1-3EIhwrFlGHYL2MOaUtjzVvwA7Sk=", + "dev": true }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } }, "extract-zip": { "version": "1.6.7", "resolved": "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz", "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, "requires": { "concat-stream": "1.6.2", "debug": "2.6.9", @@ -111,229 +113,274 @@ "yauzl": "2.4.1" } }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, - "proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=" + "glob": { + "version": "7.1.2", + "resolved": "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, "https-proxy-agent": { "version": "2.2.1", "resolved": "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=", + "dev": true, "requires": { - "agent-base": "4.2.0", - "debug": "3.1.0" + "agent-base": "^4.1.0", + "debug": "^3.1.0" }, "dependencies": { "debug": { "version": "3.1.0", "resolved": "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz", "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, "requires": { "ms": "2.0.0" } } } }, - "puppeteer": { - "version": "1.3.0", - "resolved": "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.3.0.tgz", - "integrity": "sha1-9XHF8nFTyhZKgYjmMozi5JRoePM=", - "requires": { - "debug": "2.6.9", - "extract-zip": "1.6.7", - "https-proxy-agent": "2.2.1", - "mime": "1.6.0", - "progress": "2.0.0", - "proxy-from-env": "1.0.0", - "rimraf": "2.6.2", - "ws": "3.3.3" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "puppeteer": { + "version": "1.3.0", + "resolved": "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.3.0.tgz", + "integrity": "sha1-9XHF8nFTyhZKgYjmMozi5JRoePM=", + "dev": true, + "requires": { + "debug": "^2.6.8", + "extract-zip": "^1.6.5", + "https-proxy-agent": "^2.1.0", + "mime": "^1.3.4", + "progress": "^2.0.0", + "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.1", + "ws": "^3.0.0" } }, "query-string": { "version": "5.1.1", "resolved": "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz", "integrity": "sha1-p4wBK3HBfgXy4/ojGd0zBoLvs8s=", + "dev": true, "requires": { - "decode-uri-component": "0.2.0", - "object-assign": "4.1.1", - "strict-uri-encode": "1.1.0" + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha1-mDi1wzkrliutAx5qTF4QJKvsRc4=", - "requires": { - "es6-promisify": "5.0.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, "requires": { - "glob": "7.1.2" + "glob": "^7.0.5" } }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha1-h/yqOimDWOCt5uRCz86EB0DRrQQ=" - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "2.0.0" - } + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", + "dev": true }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "4.2.4" - } + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, "requires": { - "safe-buffer": "5.1.2" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "safe-buffer": "~5.1.0" } }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz", - "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", - "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.2", - "ultron": "1.1.1" - } + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", + "dev": true }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=" + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "ws": { + "version": "3.3.3", + "resolved": "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "dev": true, "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz", - "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=" - }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, "requires": { - "pend": "1.2.0" + "fd-slicer": "~1.0.1" } - }, - "docopt": { - "version": "0.6.2", - "resolved": "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz", - "integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=" } } -} \ No newline at end of file +} diff --git a/scripts/bot/run-bot.js b/scripts/bot/run-bot.js index fbcef11f4121d5cf16f4babadedc58e118075d2d..6992f6e435bee774da40c0dedd2f4e677d476d25 100755 --- a/scripts/bot/run-bot.js +++ b/scripts/bot/run-bot.js @@ -21,17 +21,13 @@ function log(...objs) { console.log.call(null, [new Date().toISOString()].concat(objs).join(" ")); } -function error(...objs) { - console.error.call(null, [new Date().toISOString()].concat(objs).join(" ")); -} - (async () => { const browser = await puppeteer.launch({ ignoreHTTPSErrors: true }); const page = await browser.newPage(); await page.setBypassCSP(true); page.on("console", msg => log("PAGE: ", msg.text())); - page.on("error", err => error("ERROR: ", err.toString().split("\n")[0])); - page.on("pageerror", err => error("PAGE ERROR: ", err.toString().split("\n")[0])); + page.on("error", err => log("ERROR: ", err.toString().split("\n")[0])); + page.on("pageerror", err => log("PAGE ERROR: ", err.toString().split("\n")[0])); const baseUrl = options["--url"] || `https://${options["--host"]}/hub.html`; @@ -41,7 +37,7 @@ function error(...objs) { }; const roomOption = options["--room"]; if (roomOption) { - params.room = roomOption; + params.hub_id = roomOption; } const url = `${baseUrl}?${querystring.stringify(params)}`; @@ -80,7 +76,30 @@ function error(...objs) { setTimeout(loadFiles, backoff); } }; + await loadFiles(); + + // Do a periodic sanity check of the state of the bots. + setInterval(async function() { + let avatarCounts; + try { + avatarCounts = await page.evaluate(() => ({ + connectionCount: Object.keys(NAF.connection.adapter.occupants).length, + avatarCount: document.querySelectorAll("[networked-avatar]").length - 1 + })); + log(JSON.stringify(avatarCounts)); + } catch (e) { + // Ignore errors. This usually happens when the page is shutting down. + } + // Check for more than two connections to allow for a margin where we have a connection but the a-frame + // entity has not initialized yet. + if (avatarCounts && avatarCounts.connectionCount > 2 && avatarCounts.avatarCount === 0) { + // It seems the bots have dog-piled on to a restarting server, so we're going to shut things down and + // let the hubs-ops bash script restart us. + log("Detected avatar dog-pile. Restarting."); + process.exit(1); + } + }, 60 * 1000); } catch (e) { log("Navigation error", e.message); setTimeout(navigate, 1000); diff --git a/scripts/hab-build-and-push.sh b/scripts/hab-build-and-push.sh index 48693732a8902bc487f72eb87e8ff27a02fa9af5..f9ffb81ad1ef8e780bdc4efd8499eee9c0eab4c4 100755 --- a/scripts/hab-build-and-push.sh +++ b/scripts/hab-build-and-push.sh @@ -4,9 +4,10 @@ export BASE_ASSETS_PATH=$1 export ASSET_BUNDLE_SERVER=$2 export JANUS_SERVER=$3 export RETICULUM_SERVER=$4 -export TARGET_S3_URL=$5 -export BUILD_NUMBER=$6 -export GIT_COMMIT=$7 +export FARSPARK_SERVER=$5 +export TARGET_S3_URL=$6 +export BUILD_NUMBER=$7 +export GIT_COMMIT=$8 export BUILD_VERSION="${BUILD_NUMBER} (${GIT_COMMIT})" # To build + push to S3 run: @@ -21,12 +22,16 @@ pushd "$DIR/.." rm /usr/bin/env ln -s "$(hab pkg path core/coreutils)/bin/env" /usr/bin/env -hab pkg install -b core/coreutils core/bash core/node core/git core/aws-cli +hab pkg install -b core/coreutils core/bash core/node core/git core/aws-cli core/python2 npm ci --verbose --no-progress +npm rebuild node-sass # HACK sometimes node-sass build fails +npm rebuild node-sass # HACK sometimes node-sass build fails +npm rebuild node-sass # HACK sometimes node-sass build fails npm run build mkdir dist/pages mv dist/*.html dist/pages aws s3 sync --acl public-read --cache-control "max-age=31556926" dist/assets "$TARGET_S3_URL/assets" aws s3 sync --acl public-read --cache-control "no-cache" --delete dist/pages "$TARGET_S3_URL/pages/latest" +aws s3 sync --acl public-read --cache-control "no-cache" --delete dist/pages "$TARGET_S3_URL/pages/releases/${BUILD_NUMBER}" diff --git a/src/assets/camera_tool.glb b/src/assets/camera_tool.glb new file mode 100644 index 0000000000000000000000000000000000000000..e94ba67f563a22d2d0a63f38376af4ad2d287913 Binary files /dev/null and b/src/assets/camera_tool.glb differ diff --git a/src/assets/grid.png b/src/assets/grid.png index d653b44c3e2bf1c1d3943677376a3ba9924d073e..d6e26c7bced17cbb86b36c5c4476fdb9f2ce7110 100644 Binary files a/src/assets/grid.png and b/src/assets/grid.png differ diff --git a/src/assets/hud/bubble_off-hover.png b/src/assets/hud/bubble_off-hover.png index f9359727df9966493016ee57191cff80ffddcfd2..3b52264590794507e135ddf346b0bd8a9249c99f 100644 Binary files a/src/assets/hud/bubble_off-hover.png and b/src/assets/hud/bubble_off-hover.png differ diff --git a/src/assets/hud/bubble_off.png b/src/assets/hud/bubble_off.png index ff8bac978dcb3493444a6176a91ecf4f1d459756..1904a680336978acfe7706c7d9f0e1385239af21 100644 Binary files a/src/assets/hud/bubble_off.png and b/src/assets/hud/bubble_off.png differ diff --git a/src/assets/hud/bubble_on-hover.png b/src/assets/hud/bubble_on-hover.png index ee8e5c64d8c7471a1428548b40e9f1078dc541fe..e07bed80922252a1f0cde8b02495f5035e48e596 100644 Binary files a/src/assets/hud/bubble_on-hover.png and b/src/assets/hud/bubble_on-hover.png differ diff --git a/src/assets/hud/bubble_on.png b/src/assets/hud/bubble_on.png index 90dc62d45bbeb96a3977fe95321613a518984b7f..c91f0def3f67ad75f51afc86e57bbeb59c05e95d 100644 Binary files a/src/assets/hud/bubble_on.png and b/src/assets/hud/bubble_on.png differ diff --git a/src/assets/hud/create_object-hover.png b/src/assets/hud/create_object-hover.png index 3936231f84148e546ce366987e47f4667012f765..b8cf7c7040c6422a028ccb82bce4d310a3b6ac57 100644 Binary files a/src/assets/hud/create_object-hover.png and b/src/assets/hud/create_object-hover.png differ diff --git a/src/assets/hud/create_object.png b/src/assets/hud/create_object.png index af63116b547ba073bb97d95ff2e24e3cec5dd0ae..6f17d2cbf1219032e93cc28b8efa3ba38d5e47c3 100644 Binary files a/src/assets/hud/create_object.png and b/src/assets/hud/create_object.png differ diff --git a/src/assets/hud/freeze_off-hover.png b/src/assets/hud/freeze_off-hover.png index 2917c8fcb41e521496b9e06b6a5dea74a5ad272a..e1f363e5e21ccd958c05125a2c403c1319c440de 100644 Binary files a/src/assets/hud/freeze_off-hover.png and b/src/assets/hud/freeze_off-hover.png differ diff --git a/src/assets/hud/freeze_off.png b/src/assets/hud/freeze_off.png index 0b789a8ebded359fa38aac7648f0ea7fd4d9917b..f31a943be8fda599842e61c9494ee4efa36cd457 100644 Binary files a/src/assets/hud/freeze_off.png and b/src/assets/hud/freeze_off.png differ diff --git a/src/assets/hud/freeze_on-hover.png b/src/assets/hud/freeze_on-hover.png index 784be98321121a6599d85357344690f39dbd2553..d1dcf3ada87839abab28a40453eadf10b445d33b 100644 Binary files a/src/assets/hud/freeze_on-hover.png and b/src/assets/hud/freeze_on-hover.png differ diff --git a/src/assets/hud/freeze_on.png b/src/assets/hud/freeze_on.png index f9d10766b5101fcd8da0e5340812a89d0baa9515..7e92538c0e0084e5537c7d2fbf72538218b993d0 100644 Binary files a/src/assets/hud/freeze_on.png and b/src/assets/hud/freeze_on.png differ diff --git a/src/assets/hud/mute_off-hover.png b/src/assets/hud/mute_off-hover.png index ba4261d0ed95a3e6feae8a28092767b1914af9f9..832f8896167131981a0e99be39e81b2ffd85b90a 100644 Binary files a/src/assets/hud/mute_off-hover.png and b/src/assets/hud/mute_off-hover.png differ diff --git a/src/assets/hud/mute_off.png b/src/assets/hud/mute_off.png index 38f1bc95d10f52dfe6300a1d144a470987dda741..07f188d5a21c153fe9267e2c68f65a6222dbf15e 100644 Binary files a/src/assets/hud/mute_off.png and b/src/assets/hud/mute_off.png differ diff --git a/src/assets/hud/mute_on-hover.png b/src/assets/hud/mute_on-hover.png index 6efb40f54a0ec16ce58df6eb0ef70090a557d316..a959722b25152a076d0d5b7fa6593d2fc50205b2 100644 Binary files a/src/assets/hud/mute_on-hover.png and b/src/assets/hud/mute_on-hover.png differ diff --git a/src/assets/hud/mute_on.png b/src/assets/hud/mute_on.png index 0c1ec01983c6ac97261d52ebb470c31d1c0ca0c7..4b6a07a378250c63749830a6dc680a4b6131371e 100644 Binary files a/src/assets/hud/mute_on.png and b/src/assets/hud/mute_on.png differ diff --git a/src/assets/hud/spawn_camera-hover.png b/src/assets/hud/spawn_camera-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..f3ac1877a49e60780f8b0c28aa3ce22799600154 Binary files /dev/null and b/src/assets/hud/spawn_camera-hover.png differ diff --git a/src/assets/hud/spawn_camera.png b/src/assets/hud/spawn_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..fd176add98ef6d8d5b4afea6f20b167581212b0a Binary files /dev/null and b/src/assets/hud/spawn_camera.png differ diff --git a/src/assets/hud/spawn_pen-hover.png b/src/assets/hud/spawn_pen-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..d0dc40dcd5b6a849c03256fdde5adb7abe3656b8 Binary files /dev/null and b/src/assets/hud/spawn_pen-hover.png differ diff --git a/src/assets/hud/spawn_pen.png b/src/assets/hud/spawn_pen.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5c65fa26308abf396728e3a14e45d997463d68 Binary files /dev/null and b/src/assets/hud/spawn_pen.png differ diff --git a/src/assets/hud/spawn_photo-hover.png b/src/assets/hud/spawn_photo-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..9588f2fd8256da42e505b687887459ef62e7fac8 Binary files /dev/null and b/src/assets/hud/spawn_photo-hover.png differ diff --git a/src/assets/hud/spawn_photo.png b/src/assets/hud/spawn_photo.png new file mode 100644 index 0000000000000000000000000000000000000000..8e93e8e739fa86310eacebb515a43f3d011317b0 Binary files /dev/null and b/src/assets/hud/spawn_photo.png differ diff --git a/src/assets/hud/tooltip.9.png b/src/assets/hud/tooltip.9.png index 8f01466f1551e8cc8bb046e4e434c0b99b721a9b..6b67f18c8cdefcff34b445c0461d38f9149dd84d 100644 Binary files a/src/assets/hud/tooltip.9.png and b/src/assets/hud/tooltip.9.png differ diff --git a/src/assets/hud/watch.glb b/src/assets/hud/watch.glb deleted file mode 100644 index 306fcfd7414d24584ecbf59aa3942318b2f33211..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/watch.glb and /dev/null differ diff --git a/src/assets/images/account.svg b/src/assets/images/account.svg index a39a815fd97c9157c2de36ea7fd7fc98888cb58f..f3edafbfaceda74d3ec551e0a146a63843d8177e 100755 --- a/src/assets/images/account.svg +++ b/src/assets/images/account.svg @@ -4,7 +4,7 @@ <g id="Canvas" transform="translate(-42 105)"> <g id="account"> <g id="Vector"> -<use xlink:href="#path0_fill" transform="translate(42 -105)" fill="#FFFFFF"/> +<use xlink:href="#path0_fill" transform="translate(42 -105)" fill="#FF3464"/> </g> </g> </g> diff --git a/src/assets/images/daydream_entry.svg b/src/assets/images/daydream_entry.svg index 1b61b29ffe41f915c27ab8e6d42fed323ce96530..01ec49e30166f83ab63df19e9aa43896bba1ac7c 100755 --- a/src/assets/images/daydream_entry.svg +++ b/src/assets/images/daydream_entry.svg @@ -1,39 +1,38 @@ -<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M88.5 45C88.5 69.0244 69.0244 88.5 45 88.5C20.9756 88.5 1.5 69.0244 1.5 45C1.5 20.9756 20.9756 1.5 45 1.5C69.0244 1.5 88.5 20.9756 88.5 45Z" fill="#2F80ED" stroke="white" stroke-width="3"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M24.0352 16.2517L24.0362 16.2536L29.7039 12.0451L24.0352 16.2517ZM33.2408 9.42045L33.3011 9.37578C32.06 1.60484 24.3347 -1.25267 19.4698 0.495649C16.7573 1.47041 14.5183 4.04674 14.868 7.7953C15.6821 8.10795 16.5948 8.56465 17.536 9.16258C16.8441 8.73008 16.1155 8.34044 15.352 7.99818C15.3281 7.98729 15.3024 7.97584 15.2753 7.96409L15.2316 7.94517L15.1892 7.92717C15.0808 7.88136 14.9644 7.83424 14.8737 7.79756L16.0569 19.2312L16.0547 19.2311L14.8688 7.80232C7.55981 4.99833 1.0708 10.1695 0.143126 15.283C-0.374147 18.134 0.798705 21.4164 4.19989 22.986C4.29376 22.9102 4.39099 22.8341 4.49145 22.7579C4.39184 22.8342 4.284 22.92 4.20044 22.9868L14.6263 27.6703L4.20117 22.9903C-1.8667 27.9573 -0.632447 36.2375 3.30481 39.6027C5.49817 41.4773 8.87268 41.9886 11.9222 39.8159C11.9226 39.8182 11.9229 39.8205 11.9233 39.8228C13.1672 47.5898 20.8544 50.7252 25.7183 48.9773C28.4307 48.0025 30.636 45.3177 30.2863 41.5692L30.2842 41.5684L29.1584 30.1621C29.9004 30.1975 30.6449 30.1873 31.3873 30.131C30.6648 30.1875 29.9228 30.2008 29.1659 30.1629L30.2872 41.5648C37.5962 44.3688 44.1041 39.1928 45.0319 34.0793C45.5486 31.2308 44.3566 28.0236 40.9621 26.4526L40.9681 26.4478L40.9768 26.4518C47.0447 21.4848 45.8285 13.2045 41.8912 9.8394C39.6967 7.9637 36.3506 7.2043 33.2998 9.38105L33.2996 9.37947L33.2408 9.42045ZM11.8336 39.0404C11.7841 38.4644 11.762 37.8917 11.7664 37.3237C11.7582 37.9294 11.7823 38.5047 11.8336 39.0404ZM13.5902 29.5318C13.8549 28.9848 14.1464 28.451 14.4634 27.9321C14.1392 28.4578 13.8491 28.9924 13.5902 29.5318ZM22.9946 35.9856C22.3098 35.117 21.691 34.1657 21.1791 33.1383L21.1803 33.1375C21.6982 34.1405 22.3046 35.0942 22.9946 35.9856ZM29.8655 41.3963L29.8961 41.4097L29.9335 41.426C29.4347 41.2141 28.9065 40.9503 28.3627 40.6368C28.8473 40.9107 29.3484 41.1643 29.8655 41.3963ZM40.5527 26.7652C39.4503 27.5638 38.2872 28.2194 37.0853 28.7352C38.4524 28.1575 39.6294 27.4643 40.5527 26.7652ZM33.2643 14.5439L33.2563 14.5952L33.2476 14.6505C33.3387 14.0502 33.4002 13.4419 33.4306 12.8275C33.4058 13.3863 33.3538 13.9605 33.2643 14.5439ZM33.327 9.67726C33.3475 9.88878 33.3664 10.1061 33.383 10.3291C33.3741 10.2272 33.3643 10.1252 33.3537 10.0231C33.3511 9.99919 33.3485 9.97062 33.3459 9.93886L33.3433 9.90513L33.3386 9.8408L33.3318 9.74559L33.327 9.67726ZM23.8401 15.8842C23.3004 14.8945 22.6726 13.955 21.9626 13.08C22.6559 13.925 23.2943 14.8602 23.8401 15.8842ZM15.604 19.2154C14.4546 19.1897 13.3005 19.2735 12.1589 19.469C13.2774 19.2655 14.4367 19.1677 15.604 19.2154ZM5.24286 22.2272C5.18664 22.2647 5.13061 22.3025 5.07471 22.3407C5.03424 22.3684 4.99383 22.3963 4.95349 22.4243C4.88074 22.4749 4.80829 22.5261 4.73602 22.578C4.69299 22.6088 4.65002 22.6399 4.60718 22.6712L4.60419 22.6734C4.80511 22.5245 5.01837 22.3754 5.24286 22.2272Z" transform="translate(21.9045 19.8255)" fill="url(#paint0_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M16.0295 12.229L14.8425 0.790123C7.53357 -2.01386 1.04458 3.15736 0.116862 8.2708C-0.400378 11.1218 0.772477 14.4042 4.17365 15.9738C6.59782 14.0154 11.2827 11.8803 16.0295 12.229Z" transform="translate(21.9307 26.8173)" fill="url(#paint1_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M16.1176 12.1553L14.9278 0.782117C7.58484 -2.00893 1.04493 3.18264 0.112904 8.27249C-0.406741 11.1103 0.84878 14.2947 4.26577 15.857C6.697 13.8998 11.129 11.9068 16.1176 12.1553Z" transform="matrix(0.497686 -0.867357 0.864683 0.502317 18 55.3502)" fill="url(#paint2_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M16.0926 12.1431L14.9659 0.790397C7.62292 -2.00065 1.04732 3.10738 0.115298 8.19723C-0.404347 11.035 0.809439 14.2841 4.22643 15.8464C6.63823 13.9 11.2588 11.8587 16.0926 12.1431Z" transform="matrix(-0.497686 -0.867357 0.864683 -0.502317 40.5922 73)" fill="url(#paint3_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M15.9815 12.1915L14.8602 0.789647C7.5512 -2.01434 1.04328 3.16169 0.115562 8.27513C-0.401678 11.1261 0.792924 14.3363 4.1941 15.9059C6.54445 13.9641 10.9322 11.9386 15.9815 12.1915Z" transform="translate(67.0519 62.1595) rotate(180)" fill="url(#paint4_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M16.0794 12.148L14.9513 0.783652C7.60839 -2.0074 1.05938 3.16856 0.127349 8.25841C-0.392295 11.0962 0.632515 14.3774 4.0495 15.9397C5.24452 15.0732 6.74795 14.0894 8.52884 13.3998C10.7325 12.5465 13.2716 12.0251 16.0794 12.148Z" transform="matrix(-0.497686 0.867357 -0.864683 -0.502317 71 33.6823)" fill="url(#paint5_radial)"/> -<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M16.0401 12.2374L14.7485 0.773668C7.40559 -2.01738 1.05413 3.26113 0.122106 8.35098C-0.397539 11.1888 0.705495 14.4131 4.12248 15.9754C6.50059 14.048 11.0115 11.9513 16.0401 12.2374Z" transform="matrix(0.497686 0.867357 -0.864683 0.502317 48.5344 16)" fill="url(#paint6_radial)"/> +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M36.9396 28.0771L36.9407 28.0791L42.6084 23.8706L36.9396 28.0771ZM46.1453 21.2459L46.2056 21.2012C44.9645 13.4303 37.2392 10.5728 32.3743 12.3211C29.6618 13.2959 27.4228 15.8722 27.7725 19.6208C28.5866 19.9334 29.4993 20.3901 30.4405 20.988C29.7485 20.5556 29.02 20.1659 28.2565 19.8237C28.2326 19.8128 28.2068 19.8013 28.1798 19.7896L28.136 19.7706L28.0937 19.7526C27.9853 19.7068 27.8689 19.6597 27.7781 19.623L28.9614 31.0567L28.9592 31.0566L27.7733 19.6278C20.4643 16.8238 13.9753 21.995 13.0476 27.1085C12.5303 29.9594 13.7032 33.2419 17.1044 34.8115C17.1982 34.7357 17.2955 34.6596 17.3959 34.5834C17.2963 34.6596 17.1885 34.7455 17.1049 34.8122L27.5308 39.4958L17.1057 34.8158C11.0378 39.7828 12.272 48.063 16.2093 51.4281C18.4026 53.3028 21.7772 53.8141 24.8267 51.6413C24.8271 51.6437 24.8274 51.646 24.8278 51.6483C26.0717 59.4153 33.7589 62.5507 38.6227 60.8028C41.3351 59.828 43.5405 57.1432 43.1908 53.3947L43.1887 53.3938L42.0629 41.9876C42.8049 42.023 43.5494 42.0128 44.2918 41.9565C43.5693 42.013 42.8273 42.0263 42.0704 41.9884L43.1917 53.3903C50.5007 56.1943 57.0086 51.0182 57.9363 45.9048C58.4531 43.0563 57.261 39.8491 53.8666 38.2781L53.8726 38.2733L53.8813 38.2772C59.9492 33.3103 58.733 25.03 54.7957 21.6649C52.6011 19.7892 49.2551 19.0298 46.2043 21.2065L46.204 21.2049L46.1453 21.2459ZM24.7381 50.8659C24.6885 50.2898 24.6664 49.7171 24.6708 49.1492C24.6627 49.7549 24.6868 50.3301 24.7381 50.8659ZM26.4947 41.3573C26.7594 40.8103 27.0509 40.2765 27.3679 39.7576C27.0437 40.2833 26.7535 40.8179 26.4947 41.3573ZM35.899 47.811C35.2143 46.9425 34.5955 45.9912 34.0836 44.9638L34.0848 44.9629C34.6027 45.966 35.2091 46.9197 35.899 47.811ZM42.77 53.2217L42.8006 53.2352L42.838 53.2515C42.3392 53.0395 41.811 52.7758 41.2672 52.4623C41.7518 52.7362 42.2529 52.9898 42.77 53.2217ZM53.4572 38.5906C52.3548 39.3893 51.1917 40.0449 49.9897 40.5607C51.3569 39.983 52.5339 39.2898 53.4572 38.5906ZM46.1688 26.3694L46.1608 26.4207L46.1521 26.476C46.2432 25.8756 46.3047 25.2674 46.3351 24.6529C46.3103 25.2118 46.2582 25.786 46.1688 26.3694ZM46.2315 21.5027C46.252 21.7142 46.2709 21.9316 46.2875 22.1546C46.2786 22.0527 46.2688 21.9506 46.2582 21.8485C46.2556 21.8247 46.253 21.7961 46.2504 21.7643L46.2478 21.7306L46.243 21.6663L46.2363 21.5711L46.2315 21.5027ZM36.7446 27.7097C36.2049 26.7199 35.5771 25.7805 34.8671 24.9054C35.5604 25.7504 36.1988 26.6857 36.7446 27.7097ZM28.5085 31.0408C27.3591 31.0152 26.205 31.0989 25.0634 31.2945C26.1819 31.0909 27.3412 30.9931 28.5085 31.0408ZM18.1473 34.0527C18.0911 34.0902 18.0351 34.128 17.9792 34.1662C17.9387 34.1939 17.8983 34.2218 17.858 34.2498C17.7852 34.3004 17.7128 34.3516 17.6405 34.4034C17.5975 34.4343 17.5545 34.4654 17.5117 34.4967L17.5087 34.4989C17.7096 34.35 17.9229 34.2008 18.1473 34.0527Z" fill="url(#paint0_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M28.9603 31.0463L27.7733 19.6074C20.4643 16.8034 13.9753 21.9747 13.0476 27.0881C12.5303 29.939 13.7032 33.2215 17.1044 34.7911C19.5285 32.8327 24.2134 30.6976 28.9603 31.0463Z" fill="url(#paint1_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M27.532 39.4763L17.1056 34.7954C11.0378 39.7623 12.272 48.0426 16.2093 51.4077C18.4045 53.2839 21.7828 53.7945 24.8343 51.6155C24.3519 48.5236 24.8343 43.6784 27.532 39.4763Z" fill="url(#paint2_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M34.0831 44.9423L24.8273 51.6222C26.0684 59.3932 33.7578 62.5307 38.6228 60.7824C41.3352 59.8076 43.5405 57.1228 43.1909 53.3743C40.3075 52.2601 36.2428 49.2778 34.0831 44.9423Z" fill="url(#paint3_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M42.0704 41.968L43.1917 53.3699C50.5007 56.1739 57.0086 50.9978 57.9363 45.8844C58.4536 43.0334 57.259 39.8232 53.8578 38.2536C51.5074 40.1955 47.1197 42.221 42.0704 41.968Z" fill="url(#paint4_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M43.4933 33.5267L53.8813 38.2568C59.9492 33.2899 58.733 25.0096 54.7957 21.6445C52.6005 19.7683 49.2533 19.0089 46.2018 21.1879C46.3564 22.6597 46.4588 24.4579 46.1687 26.349C45.8099 28.6889 44.997 31.1531 43.4933 33.5267Z" fill="url(#paint5_radial)"/> +<path opacity="0.65" fill-rule="evenodd" clip-rule="evenodd" d="M36.9358 28.0596L46.2056 21.1809C44.9645 13.4099 37.2392 10.5524 32.3742 12.3008C29.6618 13.2755 27.4228 15.8519 27.7725 19.6004C30.6226 20.6949 34.6806 23.5543 36.9358 28.0596Z" fill="url(#paint6_radial)"/> <defs> -<radialGradient id="paint0_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.4524 -69.2011) scale(94.3261 103.221) rotate(50.7566)"> -<stop stop-color="#808DFF"/> -<stop offset="0.412807" stop-color="#EDEDED"/> -<stop offset="1" stop-color="#6575FF"/> +<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.6669 15.2447) rotate(53.2605) scale(49.8783 48.801)"> +<stop stop-color="#F5F5F5"/> +<stop offset="0.412807" stop-color="white"/> +<stop offset="1" stop-color="#DBDBDB"/> </radialGradient> -<radialGradient id="paint1_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.78495 30.4596) scale(21.7507 21.6751) rotate(-102.974)"> +<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(19.3019 36.2829) rotate(-103.017) scale(10.8394 10.8734)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> -<radialGradient id="paint2_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.79475 30.2369) scale(21.8702 21.5166) rotate(-102.974)"> +<radialGradient id="paint2_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.1801 50.5029) rotate(-163.042) scale(10.7545 10.9388)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> -<radialGradient id="paint3_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.79197 30.2166) scale(21.8362 21.5022) rotate(-102.974)"> +<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.3906 50.7488) rotate(136.694) scale(10.7723 10.8964)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> -<radialGradient id="paint4_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.7796 30.3301) scale(21.6855 21.5829) rotate(-102.974)"> +<radialGradient id="paint4_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(51.6998 36.7682) rotate(76.9667) scale(10.7941 10.8401)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> -<radialGradient id="paint5_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.7905 30.3946) scale(21.8183 21.6288) rotate(-102.974)"> +<radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.7493 22.4711) rotate(17.0544) scale(10.8065 10.917)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> -<radialGradient id="paint6_radial" cx="0.5" cy="0.5" r="0.5" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1.78613 30.4626) scale(21.765 21.6772) rotate(-102.974)"> +<radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.6037 22.3039) rotate(-43.1622) scale(10.8536 10.8674)"> <stop stop-color="white"/> <stop offset="1" stop-color="white" stop-opacity="0.01"/> </radialGradient> diff --git a/src/assets/images/default_thumbnail.png b/src/assets/images/default_thumbnail.png index 55ae93b221662e08cec3877c5cb5544997f7a8a7..d90b1d8a12dbb00030165d458f36ddfb64239ea6 100644 Binary files a/src/assets/images/default_thumbnail.png and b/src/assets/images/default_thumbnail.png differ diff --git a/src/assets/images/desktop_screen_entry.svg b/src/assets/images/desktop_screen_entry.svg index 8e25150d633190c696e3aa4e984227c4d9d3c0e1..2010d8b2f0d7e5ecd69229e08f1102cbd0f31523 100755 --- a/src/assets/images/desktop_screen_entry.svg +++ b/src/assets/images/desktop_screen_entry.svg @@ -1,5 +1,4 @@ -<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M88.5 45C88.5 69.0244 69.0244 88.5 45 88.5C20.9756 88.5 1.5 69.0244 1.5 45C1.5 20.9756 20.9756 1.5 45 1.5C69.0244 1.5 88.5 20.9756 88.5 45Z" fill="#2F80ED" stroke="white" stroke-width="3"/> -<rect width="20" height="3" transform="translate(35 62)" fill="white"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3 0C1.34314 0 0 1.34314 0 3V31C0 32.6569 1.34314 34 3 34H38C39.6569 34 41 32.6569 41 31V3C41 1.34314 39.6569 0 38 0H3ZM36 6H5V28H36V6Z" transform="translate(24 28)" fill="white"/> +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="25" y="52" width="20" height="3" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 18C15.3431 18 14 19.3431 14 21V49C14 50.6569 15.3431 52 17 52H52C53.6569 52 55 50.6569 55 49V21C55 19.3431 53.6569 18 52 18H17ZM50 24H19V46H50V24Z" fill="white"/> </svg> diff --git a/src/assets/images/device_entry.svg b/src/assets/images/device_entry.svg index b0512c0a3e35d0cfce07d07525d17289dea4bf44..3be49f7306ea8946ff28f91c6aa0a2ceb8c77221 100755 --- a/src/assets/images/device_entry.svg +++ b/src/assets/images/device_entry.svg @@ -1,9 +1,9 @@ -<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M88.5 45C88.5 69.0244 69.0244 88.5 45 88.5C20.9756 88.5 1.5 69.0244 1.5 45C1.5 20.9756 20.9756 1.5 45 1.5C69.0244 1.5 88.5 20.9756 88.5 45Z" fill="#2F80ED" stroke="white" stroke-width="3"/> -<path d="M6.43085 10.78C4.63879 11.0707 0.989932 11.8268 0 12.3286C0.407286 0.547339 6.88406 -1.40044 10.4308 0.779999C6.20865 1.90137 6.32903 8.23614 6.43085 10.78Z" transform="translate(63.9308 26.22) scale(-1 1)" fill="#C4C4C4"/> -<path d="M6.43085 10.78C4.63879 11.0707 0.989932 11.8268 0 12.3286C0.407286 0.547339 6.88406 -1.40044 10.4308 0.779999C6.20865 1.90137 6.32903 8.23614 6.43085 10.78Z" transform="translate(63.9308 26.22) scale(-1 1)" fill="#EFEFEF"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5605 5.80482C25.9101 5.97699 27.2066 5.21939 27.7191 3.95909C28.2562 2.6386 28.7836 1.47353 29.2965 0.449768C30.5023 0.486954 31.5953 0.526215 32.5396 0.576248C37.6468 0.846878 41.0005 2.3575 43.082 4.39674C41.652 5.02917 39.9324 5.87749 38.2806 6.7238C36.5808 7.59465 34.9323 8.47398 33.7101 9.1348C33.0986 9.46544 32.5928 9.74187 32.2397 9.93584C32.063 10.0328 31.9245 10.1093 31.8299 10.1615L31.6837 10.2425C31.1482 10.5401 30.8475 11.1333 30.924 11.741L30.9331 20.2067C30.9927 20.6802 31.274 21.097 31.6908 21.3294C32.1127 21.5701 32.6088 21.5076 33.0902 21.3629L33.2202 21.3031C33.3336 21.2509 33.4994 21.1743 33.71 21.0762C33.9433 20.9677 34.2318 20.8327 34.5654 20.6754C34.8342 20.5487 35.1323 20.4074 35.4543 20.2536L35.6971 20.1377C37.2061 19.4175 39.9301 18.1175 41.7683 17.163C42.9724 16.5377 44.1934 15.8805 45.311 15.2375C44.9258 16.2405 44.4028 17.0819 43.8096 17.6619L26.5396 27.0762C25.1221 26.0333 25.607 25.4727 26.4716 24.473C28.0096 22.6948 30.7492 19.5272 26.1215 9.78375C20.5023 5.35844 7.8877 5.27588 2.50793 5.24068C0.768799 5.22929 -0.214233 5.22285 0.0396118 5.07625C1.75507 4.08553 12.9818 0.917801 16.6978 0C16.611 0.1875 16.5275 0.367569 16.4472 0.539215C16.4064 0.626404 16.3665 0.711426 16.3274 0.794144C15.9169 1.6626 15.947 2.67513 16.4086 3.51761C16.8701 4.36008 17.7072 4.93059 18.66 5.05214L24.5605 5.80482Z" transform="translate(22.9604 35.9238)" fill="white" stroke="white"/> -<path d="M22.1005 13L16.2 13.7527C8.38013 -5.47592 0.509107 1.80677 0 14.3013C0.0509108 6.10544 2.69312 -0.0498744 10.3976 0.000304595C16.5612 0.0404478 19.9736 8.50061 22.1005 13Z" transform="translate(64.1005 25) scale(-1 1)" fill="white"/> -<path d="M3.07742 20.6423C2.4711 19.613 2.00635 17.7557 2.00006 14.4631C2.43733 8.15539 3.498 6.26762 5.0883 5.3118C6.01698 4.75364 7.34242 4.37637 9.41959 4.05502C10.6769 3.86051 12.0676 3.70364 13.6888 3.52076C14.7747 3.39827 15.9639 3.26412 17.2858 3.09918C18.6038 2.93472 19.7807 2.77388 20.8505 2.62767C22.3081 2.42847 23.567 2.25642 24.7135 2.13937C26.7031 1.93625 27.9151 1.95812 28.7146 2.18707C29.3326 2.36405 29.7427 2.66797 30.1138 3.44848C30.5489 4.36362 30.8831 5.86199 31.0919 8.33907C31.0808 14.4601 29.8232 16.4976 28.3144 17.5182C27.4432 18.1075 26.2773 18.5099 24.6232 18.8794C23.8055 19.062 22.9194 19.2267 21.9273 19.4105L21.8974 19.4161C20.9396 19.5935 19.8961 19.7869 18.7925 20.0223C16.8122 20.3393 14.9038 20.7345 13.1884 21.0899C12.3664 21.2601 11.5886 21.4212 10.8685 21.5601C8.517 22.0137 6.76926 22.233 5.45062 22.0716C4.27945 21.9284 3.58934 21.5113 3.07742 20.6423Z" transform="translate(53.092 39.8761) scale(-1 1)" stroke="white" stroke-width="4"/> -<path d="M0 10.3292L0.685179 4.47255C4.92876 3.04264 9.69318 1.20359 12.5676 0.221048C14.5183 -0.445764 15.9365 0.435362 16.0866 2.33438C16.1919 3.66552 15.1923 4.93186 13.973 5.41957C12.7537 5.90728 0 10.3292 0 10.3292Z" transform="translate(55.4919 43.7408) rotate(-6.67284)" fill="white"/> +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M48.5 26C50.2921 26.2907 53.9409 27.0468 54.9308 27.5486C54.5236 15.7673 48.0468 13.8196 44.5 16C48.7222 17.1214 48.6018 23.4561 48.5 26Z" fill="#C4C4C4"/> +<path d="M48.5 26C50.2921 26.2907 53.9409 27.0468 54.9308 27.5486C54.5236 15.7673 48.0468 13.8196 44.5 16C48.7222 17.1214 48.6018 23.4561 48.5 26Z" fill="#EFEFEF"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M38.5209 30.7286C39.8705 30.9007 41.167 30.1431 41.6795 28.8828C42.2166 27.5623 42.744 26.3973 43.2569 25.3735C44.4626 25.4107 45.5557 25.45 46.5 25.5C51.6072 25.7706 54.9609 27.2812 57.0424 29.3205C55.6124 29.9529 53.8928 30.8012 52.241 31.6476C50.5412 32.5184 48.8927 33.3977 47.6705 34.0585C47.059 34.3892 46.5532 34.6656 46.2001 34.8596C46.0234 34.9566 45.8849 35.033 45.7903 35.0853L45.644 35.1663C45.1086 35.4638 44.8079 36.057 44.8844 36.6648L44.8935 45.1305C44.9531 45.6039 45.2344 46.0208 45.6512 46.2532C46.0731 46.4938 46.5692 46.4314 47.0506 46.2866L47.1806 46.2269C47.294 46.1746 47.4598 46.098 47.6704 46C47.9037 45.8914 48.1922 45.7565 48.5258 45.5992C48.7946 45.4724 49.0927 45.3311 49.4147 45.1773L49.6575 45.0614C51.1664 44.3413 53.8905 43.0412 55.7287 42.0867C56.9328 41.4615 58.1537 40.8042 59.2714 40.1612C58.8862 41.1642 58.3632 42.0057 57.77 42.5856L40.5 52C39.0825 50.957 39.5674 50.3965 40.432 49.3968C41.97 47.6185 44.7096 44.4509 40.0818 34.7075C34.4626 30.2822 21.8481 30.1996 16.4683 30.1644C14.7292 30.153 13.7462 30.1466 14 30C15.7155 29.0093 26.9421 25.8416 30.6582 24.9238C30.5714 25.1113 30.4879 25.2913 30.4076 25.463C30.3668 25.5502 30.3269 25.6352 30.2878 25.7179C29.8773 26.5863 29.9074 27.5989 30.369 28.4414C30.8304 29.2838 31.6675 29.8543 32.6204 29.9759L38.5209 30.7286Z" fill="white"/> +<path d="M41.6795 28.8828L41.2163 28.6945L41.2163 28.6945L41.6795 28.8828ZM38.5209 30.7286L38.5842 30.2326L38.5842 30.2326L38.5209 30.7286ZM43.2569 25.3735L43.2723 24.8738L42.953 24.8639L42.8099 25.1496L43.2569 25.3735ZM46.5 25.5L46.5265 25.0007L46.5265 25.0007L46.5 25.5ZM57.0424 29.3205L57.2447 29.7778L57.9192 29.4795L57.3923 28.9633L57.0424 29.3205ZM52.241 31.6476L52.469 32.0925L52.469 32.0925L52.241 31.6476ZM47.6705 34.0585L47.4327 33.6187L47.4327 33.6187L47.6705 34.0585ZM46.2001 34.8596L46.4407 35.2979L46.4408 35.2978L46.2001 34.8596ZM45.7903 35.0853L45.5485 34.6477L45.548 34.6479L45.7903 35.0853ZM45.644 35.1663L45.4018 34.7289L45.4012 34.7292L45.644 35.1663ZM44.8844 36.6648L45.3844 36.6643L45.3844 36.6332L45.3805 36.6023L44.8844 36.6648ZM44.8935 45.1305L44.3935 45.131L44.3935 45.1621L44.3974 45.193L44.8935 45.1305ZM45.6512 46.2532L45.8989 45.8188L45.8947 45.8165L45.6512 46.2532ZM47.0506 46.2866L47.1945 46.7655L47.2278 46.7555L47.2594 46.7409L47.0506 46.2866ZM47.1806 46.2269L47.3894 46.6812L47.3898 46.681L47.1806 46.2269ZM47.6704 46L47.8814 46.4533L47.8814 46.4533L47.6704 46ZM48.5258 45.5992L48.7391 46.0514L48.7391 46.0514L48.5258 45.5992ZM49.4147 45.1773L49.1994 44.7261L49.1993 44.7261L49.4147 45.1773ZM49.6575 45.0614L49.4422 44.6102L49.4422 44.6102L49.6575 45.0614ZM55.7287 42.0867L55.4983 41.643L55.4983 41.643L55.7287 42.0867ZM59.2714 40.1612L59.7381 40.3405L60.2432 39.0252L59.022 39.7278L59.2714 40.1612ZM57.77 42.5856L58.0093 43.0246L58.0701 42.9915L58.1196 42.9431L57.77 42.5856ZM40.5 52L40.2037 52.4027L40.46 52.5913L40.7393 52.439L40.5 52ZM40.432 49.3968L40.8102 49.7238V49.7238L40.432 49.3968ZM40.0818 34.7075L40.5335 34.493L40.4832 34.3872L40.3912 34.3147L40.0818 34.7075ZM16.4683 30.1644L16.4651 30.6644H16.4651L16.4683 30.1644ZM14 30L13.7499 29.567L13.7499 29.567L14 30ZM30.6582 24.9238L31.1119 25.1338L31.5495 24.1886L30.5383 24.4383L30.6582 24.9238ZM30.4076 25.463L30.8605 25.6748L30.8605 25.6748L30.4076 25.463ZM30.2878 25.7179L29.8358 25.5041L29.8357 25.5042L30.2878 25.7179ZM30.369 28.4414L30.8075 28.2011L30.8075 28.2011L30.369 28.4414ZM32.6204 29.9759L32.6837 29.4799L32.6204 29.9759ZM41.2163 28.6945C40.7892 29.7447 39.7088 30.3761 38.5842 30.2326L38.4577 31.2246C40.0321 31.4254 41.5447 30.5416 42.1427 29.0712L41.2163 28.6945ZM42.8099 25.1496C42.2905 26.1863 41.7577 27.3633 41.2163 28.6945L42.1427 29.0712C42.6754 27.7614 43.1975 26.6083 43.7039 25.5975L42.8099 25.1496ZM46.5265 25.0007C45.5764 24.9504 44.4789 24.911 43.2723 24.8738L43.2415 25.8733C44.4464 25.9104 45.5349 25.9496 46.4735 25.9993L46.5265 25.0007ZM57.3923 28.9633C55.2011 26.8166 51.7191 25.2759 46.5265 25.0007L46.4735 25.9993C51.4953 26.2654 54.7206 27.7459 56.6925 29.6777L57.3923 28.9633ZM52.469 32.0925C54.1179 31.2477 55.8276 30.4045 57.2447 29.7778L56.8402 28.8632C55.3972 29.5014 53.6677 30.3548 52.013 31.2026L52.469 32.0925ZM47.9083 34.4984C49.1286 33.8386 50.7736 32.9611 52.469 32.0925L52.013 31.2026C50.3088 32.0757 48.6568 32.9569 47.4327 33.6187L47.9083 34.4984ZM46.4408 35.2978C46.7931 35.1043 47.2979 34.8284 47.9083 34.4984L47.4327 33.6187C46.8201 33.95 46.3134 34.2269 45.9594 34.4213L46.4408 35.2978ZM46.0321 35.5229C46.1264 35.4708 46.2645 35.3946 46.4407 35.2979L45.9594 34.4213C45.7823 34.5186 45.6434 34.5952 45.5485 34.6477L46.0321 35.5229ZM45.8863 35.6037L46.0325 35.5227L45.548 34.6479L45.4018 34.7289L45.8863 35.6037ZM45.3805 36.6023C45.3295 36.1972 45.5299 35.8017 45.8869 35.6033L45.4012 34.7292C44.6872 35.1259 44.2863 35.9169 44.3883 36.7273L45.3805 36.6023ZM45.3935 45.1299L45.3844 36.6643L44.3844 36.6653L44.3935 45.131L45.3935 45.1299ZM45.8947 45.8165C45.6169 45.6615 45.4293 45.3836 45.3896 45.068L44.3974 45.193C44.4769 45.8242 44.852 46.38 45.4077 46.6898L45.8947 45.8165ZM46.9067 45.8078C46.454 45.9439 46.1332 45.9525 45.8989 45.8188L45.4034 46.6875C46.0129 47.0351 46.6843 46.9188 47.1945 46.7655L46.9067 45.8078ZM46.9718 45.7726L46.8418 45.8323L47.2594 46.7409L47.3894 46.6812L46.9718 45.7726ZM47.4594 45.5467C47.2495 45.6444 47.0843 45.7207 46.9714 45.7728L47.3898 46.681C47.5037 46.6285 47.6701 46.5516 47.8814 46.4533L47.4594 45.5467ZM48.3125 45.1469C47.9798 45.3039 47.692 45.4384 47.4594 45.5467L47.8814 46.4533C48.1153 46.3444 48.4046 46.2091 48.7391 46.0514L48.3125 45.1469ZM49.1993 44.7261C48.878 44.8795 48.5806 45.0205 48.3125 45.1469L48.7391 46.0514C49.0085 45.9243 49.3073 45.7827 49.6302 45.6285L49.1993 44.7261ZM49.4422 44.6102L49.1994 44.7261L49.6301 45.6286L49.8729 45.5127L49.4422 44.6102ZM55.4983 41.643C53.6691 42.5928 50.9535 43.8889 49.4422 44.6102L49.8729 45.5127C51.3793 44.7937 54.1119 43.4897 55.9591 42.5305L55.4983 41.643ZM59.022 39.7278C57.9126 40.3661 56.6984 41.0198 55.4983 41.643L55.9591 42.5305C57.1672 41.9031 58.3949 41.2423 59.5207 40.5946L59.022 39.7278ZM58.1196 42.9431C58.7765 42.3009 59.3341 41.3925 59.7381 40.3405L58.8046 39.982C58.4382 40.9359 57.95 41.7104 57.4205 42.2281L58.1196 42.9431ZM40.7393 52.439L58.0093 43.0246L57.5307 42.1466L40.2607 51.561L40.7393 52.439ZM40.0538 49.0697C39.843 49.3134 39.6308 49.5579 39.4674 49.7941C39.3039 50.0304 39.1557 50.3043 39.1145 50.6168C39.0235 51.3074 39.4737 51.8656 40.2037 52.4027L40.7963 51.5973C40.1089 51.0914 40.0927 50.8479 40.1059 50.7475C40.1169 50.6645 40.1634 50.5457 40.2897 50.3631C40.4161 50.1804 40.5887 49.9799 40.8102 49.7238L40.0538 49.0697ZM39.6302 34.922C41.9222 39.7476 42.3544 42.8735 42.0735 44.9728C41.7952 47.0523 40.8106 48.1947 40.0538 49.0697L40.8102 49.7238C41.5914 48.8206 42.7455 47.4901 43.0647 45.1054C43.3811 42.7404 42.8693 39.4108 40.5335 34.493L39.6302 34.922ZM16.4651 30.6644C19.1564 30.682 23.627 30.7117 28.122 31.2857C32.6411 31.8627 37.0783 32.9786 39.7725 35.1003L40.3912 34.3147C37.4662 32.0111 32.7865 30.8731 28.2486 30.2937C23.6865 29.7112 19.1601 29.682 16.4716 29.6644L16.4651 30.6644ZM13.7499 29.567C13.7145 29.5875 13.6378 29.6347 13.5725 29.7184C13.4953 29.8174 13.4056 30.0157 13.5009 30.2425C13.5736 30.4154 13.7129 30.4913 13.7561 30.5134C13.8154 30.5439 13.8724 30.5607 13.9101 30.5703C14.05 30.6061 14.2529 30.622 14.4621 30.6323C14.907 30.6543 15.6013 30.6588 16.4651 30.6644L16.4716 29.6644C15.5962 29.6587 14.9294 29.6542 14.5115 29.6335C14.4083 29.6284 14.3255 29.6226 14.2617 29.616C14.2299 29.6128 14.2051 29.6096 14.1863 29.6067C14.177 29.6053 14.17 29.604 14.1649 29.603C14.1598 29.602 14.1576 29.6015 14.158 29.6015C14.1581 29.6016 14.1627 29.6028 14.1707 29.6055C14.1781 29.6081 14.1933 29.6138 14.2129 29.6239C14.2319 29.6336 14.356 29.6962 14.4228 29.8549C14.5121 30.0675 14.4256 30.2506 14.3611 30.3334C14.3084 30.4009 14.2538 30.4308 14.2501 30.433L13.7499 29.567ZM30.5383 24.4383C28.6705 24.8997 24.9271 25.9233 21.4371 26.955C19.6917 27.4711 18.0053 27.9904 16.6459 28.4436C15.3085 28.8894 14.2317 29.2888 13.7499 29.567L14.2501 30.433C14.626 30.2159 15.5959 29.8477 16.9621 29.3923C18.3062 28.9442 19.9804 28.4285 21.7206 27.914C25.2019 26.8848 28.9298 25.8657 30.7781 25.4092L30.5383 24.4383ZM30.8605 25.6748C30.9412 25.5022 31.0251 25.3215 31.1119 25.1338L30.2045 24.7137C30.1178 24.901 30.0345 25.0804 29.9547 25.2512L30.8605 25.6748ZM30.7398 25.9317C30.7793 25.8481 30.8195 25.7624 30.8605 25.6748L29.9547 25.2512C29.9141 25.3379 29.8745 25.4223 29.8358 25.5041L30.7398 25.9317ZM30.8075 28.2011C30.4228 27.4991 30.3977 26.6553 30.7398 25.9316L29.8357 25.5042C29.3568 26.5174 29.392 27.6987 29.9305 28.6816L30.8075 28.2011ZM32.6837 29.4799C31.8896 29.3786 31.192 28.9032 30.8075 28.2011L29.9304 28.6816C30.4689 29.6645 31.4455 30.3301 32.5572 30.4719L32.6837 29.4799ZM38.5842 30.2326L32.6837 29.4799L32.5572 30.4719L38.4577 31.2246L38.5842 30.2326Z" fill="white"/> +<path d="M33 27L38.9005 27.7527C46.7204 8.52408 54.5914 15.8068 55.1005 28.3013C55.0496 20.1054 52.4074 13.9501 44.7029 14.0003C38.5393 14.0404 35.1269 22.5006 33 27Z" fill="white"/> +<path d="M41.0146 49.5184C41.6209 48.4891 42.0856 46.6318 42.0919 43.3392C41.6547 37.0315 40.594 35.1437 39.0037 34.1879C38.075 33.6298 36.7496 33.2525 34.6724 32.9311C33.4151 32.7366 32.0244 32.5798 30.4032 32.3969C29.3173 32.2744 28.1281 32.1402 26.8062 31.9753C25.4882 31.8108 24.3113 31.65 23.2415 31.5038C21.7839 31.3046 20.525 31.1325 19.3785 31.0155C17.3889 30.8124 16.1769 30.8342 15.3774 31.0632C14.7593 31.2402 14.3493 31.5441 13.9782 32.3246C13.5431 33.2397 13.2089 34.7381 13.0001 37.2152C13.0112 43.3363 14.2688 45.3737 15.7776 46.3943C16.6488 46.9836 17.8147 47.386 19.4688 47.7555C20.2865 47.9381 21.1726 48.1028 22.1646 48.2866L22.1946 48.2922C23.1523 48.4696 24.1959 48.663 25.2995 48.8984C27.2798 49.2154 29.1882 49.6107 30.9036 49.966C31.7256 50.1363 32.5034 50.2974 33.2235 50.4363C35.575 50.8898 37.3227 51.1091 38.6414 50.9478C39.8125 50.8045 40.5026 50.3874 41.0146 49.5184Z" stroke="white" stroke-width="4"/> +<path d="M47.6921 43L47.6921 37.1034C51.7408 35.1901 56.2593 32.8099 59 31.5C60.86 30.611 62.371 31.3214 62.7408 33.1901C63 34.5 62.1543 35.8739 61 36.5C59.8457 37.1261 47.6921 43 47.6921 43Z" fill="white"/> </svg> diff --git a/src/assets/images/dropdown_arrow.png b/src/assets/images/dropdown_arrow.png old mode 100755 new mode 100644 index caa42c1ffed82796540acdc192201cf20e822e0b..e1d9ca7519e1e4b25c49e5679c79f02f9f142486 Binary files a/src/assets/images/dropdown_arrow.png and b/src/assets/images/dropdown_arrow.png differ diff --git a/src/assets/images/dropdown_arrow@2x.png b/src/assets/images/dropdown_arrow@2x.png old mode 100755 new mode 100644 index d4e74eb212652021837a17d860578c6f7114dcd5..4bff3fa637c792deb4101bc6f5e18657b2798cd2 Binary files a/src/assets/images/dropdown_arrow@2x.png and b/src/assets/images/dropdown_arrow@2x.png differ diff --git a/src/assets/images/generic_vr_entry.svg b/src/assets/images/generic_vr_entry.svg index a47a2e8fd8aec7ececd8742b6a061991c9f8fcc4..3f9f733d866b403ff347a1fcecbfb071c734a0cb 100755 --- a/src/assets/images/generic_vr_entry.svg +++ b/src/assets/images/generic_vr_entry.svg @@ -1,4 +1,3 @@ -<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M88.5 45C88.5 69.0244 69.0244 88.5 45 88.5C20.9756 88.5 1.5 69.0244 1.5 45C1.5 20.9756 20.9756 1.5 45 1.5C69.0244 1.5 88.5 20.9756 88.5 45Z" fill="#2F80ED" stroke="white" stroke-width="3"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M42.1704 0H2.71227C1.24057 0 0 1.22604 0 2.73805V25.2622C0 26.7742 1.24057 28 2.77108 28H13.5496C14.7053 28 15.6958 27.3015 16.112 26.3081L19.2462 18.8289C19.7747 17.5677 21.0324 16.6805 22.5 16.6805C23.9676 16.6805 25.2253 17.5677 25.7538 18.8289L28.888 26.3081C29.3042 27.3015 30.2947 28 31.3916 28H42.1704C43.7594 28 45 26.7742 45 25.2622V2.73805C45 1.22604 43.7594 0 42.1704 0ZM12.294 18.9274C9.53693 18.9274 7.30457 16.7226 7.30457 14C7.30457 11.2774 9.53693 9.07287 12.294 9.07287C15.0507 9.07287 17.2785 11.2774 17.2785 14C17.2785 16.7226 15.0459 18.9274 12.294 18.9274ZM32.7086 18.9251C29.9531 18.9251 27.7215 16.7214 27.7215 14C27.7215 11.2789 29.9531 9.07514 32.7086 9.07514C35.4641 9.07514 37.6957 11.2789 37.6957 14C37.6957 16.7214 35.4641 18.9251 32.7086 18.9251Z" transform="translate(22 32)" fill="white"/> +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M56.1704 22H16.7123C15.2406 22 14 23.226 14 24.7381V47.2622C14 48.7742 15.2406 50 16.7711 50H27.5496C28.7053 50 29.6958 49.3015 30.112 48.3081L33.2462 40.8289C33.7747 39.5677 35.0324 38.6805 36.5 38.6805C37.9676 38.6805 39.2253 39.5677 39.7538 40.8289L42.888 48.3081C43.3042 49.3015 44.2947 50 45.3916 50H56.1704C57.7594 50 59 48.7742 59 47.2622V24.7381C59 23.226 57.7594 22 56.1704 22ZM26.294 40.9274C23.5369 40.9274 21.3046 38.7226 21.3046 36C21.3046 33.2774 23.5369 31.0729 26.294 31.0729C29.0507 31.0729 31.2785 33.2774 31.2785 36C31.2785 38.7226 29.0459 40.9274 26.294 40.9274ZM46.7086 40.9251C43.9531 40.9251 41.7215 38.7214 41.7215 36C41.7215 33.2789 43.9531 31.0751 46.7086 31.0751C49.4641 31.0751 51.6957 33.2789 51.6957 36C51.6957 38.7214 49.4641 40.9251 46.7086 40.9251Z" fill="white"/> </svg> diff --git a/src/assets/images/giphy_logo.png b/src/assets/images/giphy_logo.png index f979e8bed55a6eda4f7dba98eeb4ce6297f4f837..8c38308381469eca8b28b6237629ae5fc84d8b6c 100644 Binary files a/src/assets/images/giphy_logo.png and b/src/assets/images/giphy_logo.png differ diff --git a/src/assets/images/help-hud.png b/src/assets/images/help-hud.png index 43956c0d39fe0f08cf600e8cc56fd23c2ff85b1a..aa9a7b2895801e1eb4c73636d37128f6ab76fd1c 100644 Binary files a/src/assets/images/help-hud.png and b/src/assets/images/help-hud.png differ diff --git a/src/assets/images/help-hud@2x.png b/src/assets/images/help-hud@2x.png index 512ad316d0d2a8d8e3ac19350f81c96da4c50dfb..91827176b4da245f0bca5236fa919cb4fa45d34d 100644 Binary files a/src/assets/images/help-hud@2x.png and b/src/assets/images/help-hud@2x.png differ diff --git a/src/assets/images/hub-preview-light-no-shadow.png b/src/assets/images/hub-preview-light-no-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..33b5a62b9cace3f96de2fcd0f9ffa798505ded58 Binary files /dev/null and b/src/assets/images/hub-preview-light-no-shadow.png differ diff --git a/src/assets/images/hub-preview-white.png b/src/assets/images/hub-preview-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ac46fff59b711770956d0dcfa5af5e485b17f3 Binary files /dev/null and b/src/assets/images/hub-preview-white.png differ diff --git a/src/assets/images/hub-preview.png b/src/assets/images/hub-preview.png old mode 100755 new mode 100644 index 5a976607e2539031d67dc17e727ecff02740c3ad..b074eb052a8ff4febf10ff7aaaec641c746ee485 Binary files a/src/assets/images/hub-preview.png and b/src/assets/images/hub-preview.png differ diff --git a/src/assets/images/level_background.png b/src/assets/images/level_background.png old mode 100755 new mode 100644 index 5e0a40e2cfd8499413dc5bcdeef03e8742eceecc..00287346ddb5e69f4ebe97312d63c202b70c9b6e Binary files a/src/assets/images/level_background.png and b/src/assets/images/level_background.png differ diff --git a/src/assets/images/level_background@2x.png b/src/assets/images/level_background@2x.png old mode 100755 new mode 100644 index 3ecfb6efa54aa24de6de19ba825841182ca18e18..9fe4532dfde1a1ac3c5d5746c1b0ee9acb395ebe Binary files a/src/assets/images/level_background@2x.png and b/src/assets/images/level_background@2x.png differ diff --git a/src/assets/images/level_fill.png b/src/assets/images/level_fill.png old mode 100755 new mode 100644 index 49a4f8a75064870db870bf994e8b25671205bfb0..df489e09b60cf901e741e6ca9d32c8264bcd6596 Binary files a/src/assets/images/level_fill.png and b/src/assets/images/level_fill.png differ diff --git a/src/assets/images/level_fill@2x.png b/src/assets/images/level_fill@2x.png old mode 100755 new mode 100644 index 28f313bc9d541fc92fd65c03945b35c1affdf9cb..f73602ed3a405cf74cd9f97d009d6227c5760dd1 Binary files a/src/assets/images/level_fill@2x.png and b/src/assets/images/level_fill@2x.png differ diff --git a/src/assets/images/link_dialog_header.svg b/src/assets/images/link_dialog_header.svg new file mode 100755 index 0000000000000000000000000000000000000000..773243545d048ed0a1920d66d085d3df1e8f4bca --- /dev/null +++ b/src/assets/images/link_dialog_header.svg @@ -0,0 +1,3 @@ +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M56.1704 22H16.7123C15.2406 22 14 23.226 14 24.7381V47.2622C14 48.7742 15.2406 50 16.7711 50H27.5496C28.7053 50 29.6958 49.3015 30.112 48.3081L33.2462 40.8289C33.7747 39.5677 35.0324 38.6805 36.5 38.6805C37.9676 38.6805 39.2253 39.5677 39.7538 40.8289L42.888 48.3081C43.3042 49.3015 44.2947 50 45.3916 50H56.1704C57.7594 50 59 48.7742 59 47.2622V24.7381C59 23.226 57.7594 22 56.1704 22ZM26.294 40.9274C23.5369 40.9274 21.3046 38.7226 21.3046 36C21.3046 33.2774 23.5369 31.0729 26.294 31.0729C29.0507 31.0729 31.2785 33.2774 31.2785 36C31.2785 38.7226 29.0459 40.9274 26.294 40.9274ZM46.7086 40.9251C43.9531 40.9251 41.7215 38.7214 41.7215 36C41.7215 33.2789 43.9531 31.0751 46.7086 31.0751C49.4641 31.0751 51.6957 33.2789 51.6957 36C51.6957 38.7214 49.4641 40.9251 46.7086 40.9251Z" fill="black"/> +</svg> diff --git a/src/assets/images/mic_denied.png b/src/assets/images/mic_denied.png index b6697e643bcdd328611f96c768c664a746c871c7..6f57351189a1d0638cc4d841ef59c96ec4599701 100644 Binary files a/src/assets/images/mic_denied.png and b/src/assets/images/mic_denied.png differ diff --git a/src/assets/images/mic_denied@2x.png b/src/assets/images/mic_denied@2x.png index 601c8684f3afdbdf30ef7932c89081caeebf0692..83470ea0ad4dde86fe8d132ae1e4e868fb35be2a 100644 Binary files a/src/assets/images/mic_denied@2x.png and b/src/assets/images/mic_denied@2x.png differ diff --git a/src/assets/images/mic_granted.png b/src/assets/images/mic_granted.png index 8b63010075d391879a5cffeccc273f9afb0d38d5..47b9b41175864c197ecae4a6d505ae254bb13bf7 100644 Binary files a/src/assets/images/mic_granted.png and b/src/assets/images/mic_granted.png differ diff --git a/src/assets/images/mic_granted@2x.png b/src/assets/images/mic_granted@2x.png index 734ad47e1c7185357ce11719901849055f213c21..7fa7b625a0df68c60c9dd675f13bbd93ec139ac3 100644 Binary files a/src/assets/images/mic_granted@2x.png and b/src/assets/images/mic_granted@2x.png differ diff --git a/src/assets/images/mic_level.png b/src/assets/images/mic_level.png old mode 100755 new mode 100644 index e4c1367ddf78efd48173a3d0a64c4c48c953a871..b686c35f4ee44ea3848855430a04660a38cf39d7 Binary files a/src/assets/images/mic_level.png and b/src/assets/images/mic_level.png differ diff --git a/src/assets/images/mic_level@2x.png b/src/assets/images/mic_level@2x.png old mode 100755 new mode 100644 index 621f944ed0b07b1a625a2627f5646406fcefbd98..7f67862e6ecc46bf7f9235446c7756e4eb131bc2 Binary files a/src/assets/images/mic_level@2x.png and b/src/assets/images/mic_level@2x.png differ diff --git a/src/assets/images/mic_small.png b/src/assets/images/mic_small.png old mode 100755 new mode 100644 index cc41c2bb0d36ace15751bffae7d342369da2b0b3..79fe87c3ded7a9a7a6b39a7bf7c380baad6eca77 Binary files a/src/assets/images/mic_small.png and b/src/assets/images/mic_small.png differ diff --git a/src/assets/images/mic_small@2x.png b/src/assets/images/mic_small@2x.png index cc41c2bb0d36ace15751bffae7d342369da2b0b3..119e441d67cbf1cfd05973942bb1b6e651ae33ec 100644 Binary files a/src/assets/images/mic_small@2x.png and b/src/assets/images/mic_small@2x.png differ diff --git a/src/assets/images/mobile_screen_entry.svg b/src/assets/images/mobile_screen_entry.svg index 5c5b77cda1c2796d18090b29052e3981631131e6..2e602ba7431697de855c50ea844b8159c06b2f18 100755 --- a/src/assets/images/mobile_screen_entry.svg +++ b/src/assets/images/mobile_screen_entry.svg @@ -1,8 +1,6 @@ -<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M88.5 45C88.5 69.0244 69.0244 88.5 45 88.5C20.9756 88.5 1.5 69.0244 1.5 45C1.5 20.9756 20.9756 1.5 45 1.5C69.0244 1.5 88.5 20.9756 88.5 45Z" fill="#2F80ED" stroke="white" stroke-width="3"/> -<mask id="path-2-inside-1" fill="white"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5 0C2.23859 0 0 2.23859 0 5V33C0 35.7614 2.23859 38 5 38H20C22.7614 38 25 35.7614 25 33V5C25 2.23859 22.7614 0 20 0H5ZM22 4H3V29H22V4Z"/> +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-inside-1" fill="white"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M28 17C25.2386 17 23 19.2386 23 22V50C23 52.7614 25.2386 55 28 55H43C45.7614 55 48 52.7614 48 50V22C48 19.2386 45.7614 17 43 17H28ZM45 21H26V46H45V21Z"/> </mask> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5 0C2.23859 0 0 2.23859 0 5V33C0 35.7614 2.23859 38 5 38H20C22.7614 38 25 35.7614 25 33V5C25 2.23859 22.7614 0 20 0H5ZM22 4H3V29H22V4Z" transform="translate(32 26)" fill="#2F80ED"/> -<path d="M3 4V1H0V4H3ZM22 4H25V1H22V4ZM3 29H0V32H3V29ZM22 29V32H25V29H22ZM3 5C3 3.89544 3.89544 3 5 3V-3C0.581732 -3 -3 0.581732 -3 5H3ZM3 33V5H-3V33H3ZM5 35C3.89544 35 3 34.1046 3 33H-3C-3 37.4183 0.581732 41 5 41V35ZM20 35H5V41H20V35ZM22 33C22 34.1046 21.1046 35 20 35V41C24.4183 41 28 37.4183 28 33H22ZM22 5V33H28V5H22ZM20 3C21.1046 3 22 3.89544 22 5H28C28 0.581732 24.4183 -3 20 -3V3ZM5 3H20V-3H5V3ZM3 7H22V1H3V7ZM6 29V4H0V29H6ZM22 26H3V32H22V26ZM19 4V29H25V4H19Z" transform="translate(32 26)" fill="white" mask="url(#path-2-inside-1)"/> +<path d="M26 21V18H23V21H26ZM45 21H48V18H45V21ZM26 46H23V49H26V46ZM45 46V49H48V46H45ZM26 22C26 20.8954 26.8954 20 28 20V14C23.5817 14 20 17.5817 20 22H26ZM26 50V22H20V50H26ZM28 52C26.8954 52 26 51.1046 26 50H20C20 54.4183 23.5817 58 28 58V52ZM43 52H28V58H43V52ZM45 50C45 51.1046 44.1046 52 43 52V58C47.4183 58 51 54.4183 51 50H45ZM45 22V50H51V22H45ZM43 20C44.1046 20 45 20.8954 45 22H51C51 17.5817 47.4183 14 43 14V20ZM28 20H43V14H28V20ZM26 24H45V18H26V24ZM29 46V21H23V46H29ZM45 43H26V49H45V43ZM42 21V46H48V21H42Z" fill="white" mask="url(#path-1-inside-1)"/> </svg> diff --git a/src/assets/images/moz-logo-black.png b/src/assets/images/moz-logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..549cf9898b752c7c201e91b718e2a83cc128d242 Binary files /dev/null and b/src/assets/images/moz-logo-black.png differ diff --git a/src/assets/images/presence_desktop.png b/src/assets/images/presence_desktop.png new file mode 100755 index 0000000000000000000000000000000000000000..4dbaafa1733fb55971581d9c2fd368f4ca9e0971 Binary files /dev/null and b/src/assets/images/presence_desktop.png differ diff --git a/src/assets/images/presence_phone.png b/src/assets/images/presence_phone.png new file mode 100755 index 0000000000000000000000000000000000000000..4b18d742ad8c9ddbb71fe7e1b9d897e48c73d5bb Binary files /dev/null and b/src/assets/images/presence_phone.png differ diff --git a/src/assets/images/presence_vr.png b/src/assets/images/presence_vr.png new file mode 100755 index 0000000000000000000000000000000000000000..fde03d7020a2252a3722c76ee1ebe21dc6f488ef Binary files /dev/null and b/src/assets/images/presence_vr.png differ diff --git a/src/assets/images/speaker_level.png b/src/assets/images/speaker_level.png old mode 100755 new mode 100644 index f0557615258997bb7c54e7a6028e052c9c8a33f4..3dc0b53a51b05ec55ba7b6d053c4efce49a11d0e Binary files a/src/assets/images/speaker_level.png and b/src/assets/images/speaker_level.png differ diff --git a/src/assets/images/speaker_level@2x.png b/src/assets/images/speaker_level@2x.png old mode 100755 new mode 100644 index 3d60f4b8d287742ad3076ae7e63f988cca029f89..82a627701c10f55275cd5dc161bba589e05007ee Binary files a/src/assets/images/speaker_level@2x.png and b/src/assets/images/speaker_level@2x.png differ diff --git a/src/assets/images/spoke_logo.png b/src/assets/images/spoke_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..f331e87ddbc8e429207dcd2879c2670419041cac Binary files /dev/null and b/src/assets/images/spoke_logo.png differ diff --git a/src/assets/images/spoke_logo_black.png b/src/assets/images/spoke_logo_black.png new file mode 100755 index 0000000000000000000000000000000000000000..10abf285713f8a6860a37c20008ab54d5348047c Binary files /dev/null and b/src/assets/images/spoke_logo_black.png differ diff --git a/src/assets/images/twitter.svg b/src/assets/images/twitter.svg new file mode 100755 index 0000000000000000000000000000000000000000..4f16790673574fb41a23496659eb9659c910f7ba --- /dev/null +++ b/src/assets/images/twitter.svg @@ -0,0 +1,3 @@ +<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M153.6 301.6C247.9 301.6 299.5 223.4 299.5 155.7C299.5 153.5 299.5 151.3 299.4 149.1C309.4 141.9 318.1 132.8 325 122.5C315.8 126.6 305.9 129.3 295.5 130.6C306.1 124.3 314.2 114.2 318.1 102.2C308.2 108.1 297.2 112.3 285.5 114.6C276.1 104.6 262.8 98.4 248.1 98.4C219.8 98.4 196.8 121.4 196.8 149.7C196.8 153.7 197.3 157.6 198.1 161.4C155.5 159.3 117.7 138.8 92.4 107.8C88 115.4 85.5 124.2 85.5 133.6C85.5 151.4 94.6 167.1 108.3 176.3C99.9 176 92 173.7 85.1 169.9C85.1 170.1 85.1 170.3 85.1 170.6C85.1 195.4 102.8 216.2 126.2 220.9C121.9 222.1 117.4 222.7 112.7 222.7C109.4 222.7 106.2 222.4 103.1 221.8C109.6 242.2 128.6 257 151 257.4C133.4 271.2 111.3 279.4 87.3 279.4C83.2 279.4 79.1 279.2 75.1 278.7C97.7 293.1 124.7 301.6 153.6 301.6Z" fill="white"/> +</svg> diff --git a/src/assets/images/warning_icon.png b/src/assets/images/warning_icon.png index d0394f96acda411c490f5ae224df90c6c2fdd802..fe4b88a85813cbce74f84ba3b69042726f981f63 100644 Binary files a/src/assets/images/warning_icon.png and b/src/assets/images/warning_icon.png differ diff --git a/src/assets/images/warning_icon@2x.png b/src/assets/images/warning_icon@2x.png index 14c27504afa3e5aaa2e76f25f023162a19d2c7dd..2ab7a21899c831b11809c9b6aa02b2517b721d0a 100644 Binary files a/src/assets/images/warning_icon@2x.png and b/src/assets/images/warning_icon@2x.png differ diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss index edc8b391990a9c81dd67ca2579ffc2991a169ac3..197c036f569d78ac8b81e8271eb3aeadb1a477ff 100644 --- a/src/assets/stylesheets/2d-hud.scss +++ b/src/assets/stylesheets/2d-hud.scss @@ -5,25 +5,34 @@ display: flex; justify-content: center; align-items: center; - height: 80px; width: 100%; - user-select: none; &:local(.top) { top: 10px; + height: 80px; } - &:local(.bottom) { - bottom: 20px; + &:local(.column) { + flex-direction: column; + bottom: 0; + z-index: 1; } } +:local(.bottom) { + margin-bottom: 20px; +} + +:local(.hide) { + display: none; +} + :local(.panel) { display: flex; justify-content: space-around; align-items: center; padding: 5px; - background-color: rgba(#4F4F4F, 0.45); + background-color: $hud-panel-background; } :local(.panel.left) { @@ -42,6 +51,14 @@ margin-left: -40px; } +:local(.panel.up) { + border-top-right-radius: 30px; + border-top-left-radius: 30px; + padding-top: 5px; + padding-bottom: 45px; + margin-bottom: -40px; +} + :local(.iconButton) { width: 40px; height: 40px; @@ -90,6 +107,20 @@ background-image: url(../hud/bubble_on-hover.png); } +:local(.iconButton.spawn_pen) { + background-image: url(../hud/spawn_pen.png); +} +:local(.iconButton.spawn_pen:hover) { + background-image: url(../hud/spawn_pen-hover.png); +} + +:local(.iconButton.spawn_camera) { + background-image: url(../hud/spawn_camera.png); +} +:local(.iconButton.spawn_camera:hover) { + background-image: url(../hud/spawn_camera-hover.png); +} + :local(.iconButton.freeze) { background-image: url(../hud/freeze_off.png); } @@ -109,3 +140,7 @@ :local(.iconButton.create-object:hover) { background-image: url(../hud/create_object-hover.png); } + +:local(.iconButton.mobile-media-picker) { + background-image: url(../hud/spawn_photo.png); +} diff --git a/src/assets/stylesheets/audio.scss b/src/assets/stylesheets/audio.scss index c12b548a2ce6f89e379b6bd3d8301bbbc3f47422..7b784427543ab8e9baab0179930aef73474890f1 100644 --- a/src/assets/stylesheets/audio.scss +++ b/src/assets/stylesheets/audio.scss @@ -5,6 +5,7 @@ flex-direction: column; height: 100%; width: 100%; + margin: 24px; &__enter-button-container { flex: 1; @@ -14,6 +15,7 @@ } &__enter-button { @extend %bottom-action-button; + @extend %wide-button; } &__title { @@ -41,14 +43,16 @@ @extend %glass-text; appearance: none; - background-color: rgba(64, 64, 64, 0.2); + background-color: white; -moz-appearance: none; -webkit-appearance: none; + border: 1px solid #e2e2e2; padding: 6px; + font-weight: bold; + font-size: 0.9em; padding-left: 15px; padding-right: 30px; color: white; - font-size: 1.1em; width: 90%; height: 50px; } @@ -110,6 +114,7 @@ justify-content: flex-start; align-items: center; overflow-y: auto; + margin: 24px; &__grant-container { flex: 1; @@ -153,6 +158,7 @@ &__next { @extend %bottom-button; + @extend %wide-button; flex: 1 1; } diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss index 6428a7a68429149616380ac400c1dc48aa5913a2..7257167b83a75fdb4981e821f56fb108b097101c 100644 --- a/src/assets/stylesheets/avatar-selector.scss +++ b/src/assets/stylesheets/avatar-selector.scss @@ -6,6 +6,7 @@ canvas { border-radius: 8px; + background-color: #aaa; } .avatar-selector { diff --git a/src/assets/stylesheets/create-object-dialog.scss b/src/assets/stylesheets/create-object-dialog.scss index 9500fa41e328409b46915719dfa3f3cb5f4de6b0..be18f09ec6532f66347e3302bff2e2aded285a8b 100644 --- a/src/assets/stylesheets/create-object-dialog.scss +++ b/src/assets/stylesheets/create-object-dialog.scss @@ -9,6 +9,7 @@ } :local(.action-button) { + @extend %unselectable; @extend %bottom-action-button; margin-left: 6px; margin-right: 6px; @@ -20,48 +21,54 @@ } :local(.buttons) { + @extend %unselectable; display: flex; flex-direction: row; align-items: center; } :local(.small-button) { + @extend %unselectable; margin-left: 0.25em; font-size: 2em; align-self: center; } :local(.cancel-icon) { - color: white; + color: $darkest-grey; + cursor: pointer; + &:hover { color: #FF3D7F } } :local(.upload-icon) { - color: white; + color: $action-color; + cursor: pointer; + &:hover { - color: #2F80ED; + color: $action-color-light; } } :local(.input-border) { display: flex; - border: 0.25em solid white; - border-radius: 1em; + @extend %rounded-border; + @extend %default-font; margin: 1em; padding: 0.5em 0.75em; width: 100%; box-sizing: border-box; - @extend %default-font; } :local(.left-side-of-input) { + @extend %default-font; flex-grow: 1; border: none; white-space: nowrap; background: transparent; - color: white; + color: black; font-size: 1.2em; align-self: center; overflow: hidden; diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index 5dc16221c4584917a1ae14997f608bb048073bab..f0424741082257a992e0adfd24de80703d1899c7 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -3,33 +3,62 @@ :local(.entry-dialog) { display: flex; flex-direction: column; - height: 100%; align-items: center; + height: 100%; + position: relative; + + :local(.collapse) { + @extend %fa-icon-button; + color: black; + position: absolute; + top: 0px; + right: 12px; + width: 32px; + height: 32px; + } + + :local(.expand) { + @extend %fa-icon-button; + color: white; + position: absolute; + top: -64px; + right: 12px; + width: 38px; + height: 38px; + padding-left: 1px; + padding-bottom: 1px; + background-color: $action-color; + border-radius: 19px; + } } :local(.entry-button) { + @extend %action-button; + width: 272px; display: flex; - margin: 20px 0; - margin-bottom: 0; + text-align: left; + flex-direction: row; cursor: pointer; - background: none; color: white; - border: none; align-items: center; @extend %default-font; + border: 0; + border-radius: 12px; + margin: 6px; + padding: 12px 18px; + height: auto; :local(.icon) { - @extend %glass-icon; - flex: 1 1 90px; - min-width: 90px; - min-height: 90px; + height: 40px; + width: 40px; } :local(.label) { @extend %glass-text; + color: white; flex: 10 1 auto; margin-left: 20px; - font-size: 1.5em; + font-size: 1.0em; display: flex; flex-direction: column; justify-content: center; @@ -40,7 +69,8 @@ } :local(.subtitle) { - font-size: 0.7em; + font-size: 0.8em; + font-weight: normal; color: $light-text; } } @@ -50,31 +80,75 @@ display: flex; flex-direction: column; flex: 1; + text-align: center; + margin: 24px; + min-height: 150px; + height: 100%; + width: 100%; + + :local(.title) { + @extend %top-title; + @extend %glass-text; + margin-bottom: 12px; + margin-right: 8px; + margin-left: 8px; + } + + :local(.name) { + @extend %top-title; + @extend %glass-text; + margin-bottom: 4px; + margin-right: 8px; + margin-left: 8px; + } + + :local(.lobby) { + margin-bottom: 24px; + margin-right: 8px; + margin-left: 8px; + font-size: 0.9em; + } + + :local(.center) { + @extend %glass-text; + flex: 10; + width: 100%; + } + + :local(.profile-name) { + margin-top: 4px; + margin-bottom: 32px; + @extend %default-font; + font-size: 1.1em; + color: $action-color; + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + } + + :local(.profile-icon) { + cursor: pointer; + width: 16px; + height: 16px; + margin-right: 12px; + } :local(.button-container) { - margin: auto; - margin-top: -72px; text-align: center; - flex: 10; display: flex; flex-direction: column; - min-height: max-content; - justify-content: center; + height: 100%; + justify-content: flex-end; + align-items: center; - :local(.invite-button) { + :local(.action-button) { @extend %action-button; - align-self: center; - width: 75%; - margin-top: 28px; } - :local(.presence-info) { - margin: 18px; - font-size: 1.3em; - - :local(.people) { - font-weight: bold; - } + :local(.wide-button) { + @extend %wide-button; } } @@ -108,6 +182,6 @@ text-align: center; margin-top: 10px; cursor: pointer; - color: $grey-text; + color: $dark-grey-text; } } diff --git a/src/assets/stylesheets/hub-create.scss b/src/assets/stylesheets/hub-create.scss index ebead263f24b472f915effd1887f6b4db8460ed8..bc9145df6e1246e5a34e60f29efc7fffe7297414 100644 --- a/src/assets/stylesheets/hub-create.scss +++ b/src/assets/stylesheets/hub-create.scss @@ -1,5 +1,20 @@ @import 'shared'; +:local(.placeholder) { + width: 690px; + height: 460px; + + @media (max-width: 768px) , (max-height: 715px) { + width: 525px; + height: 350px; + } + + @media (max-width: 520px) { + width: 90%; + height: 300px; + } +} + :local(.create-panel) { display: flex; flex-direction: column; @@ -50,13 +65,11 @@ font-size: 1.5em; width: 100%; text-align: center; - margin-top: 275px; } @media (max-width: 520px) { padding-left: 15px; font-size: 1.2em; - margin-top: 165px; } } @@ -81,7 +94,6 @@ width: 700px; height: 100%; box-sizing: border-box; - border: 3px solid white; border-radius: 14px; overflow: hidden; pointer-events: none; @@ -101,6 +113,7 @@ height: 100%; object-fit: cover; position: absolute; + filter: contrast(0.9) brightness(1.1); } :local(.labels) { @@ -109,8 +122,7 @@ height: 100%; top: 0; left: 0; - background: rgb(2,0,36); - background: linear-gradient(0deg, rgba(2,0,36,0.324) 0%, rgba(1,0,11,0.1189076314119398) 60%, rgba(0,0,0,0.3242297602634804) 100%); + background: linear-gradient(0deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.0) 50%, rgba(0,0,0,0.4) 100%); :local(.custom-button) { @extend %default-font; @@ -200,30 +212,6 @@ } } } - - :local(.link-code) { - position: absolute; - - bottom: -36px; - - @media (max-height: 715px) { - bottom: -28px; - } - - width: 100%; - text-shadow: 0px 0px 4px rgba(0, 0, 0, 1.0); - text-align: center; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - :local(.link) { - color: white; - font-size: 1.2em; - text-decoration-color: $light-grey; - } - } } } diff --git a/src/assets/stylesheets/hub.scss b/src/assets/stylesheets/hub.scss index c7ea6d87c861f86ecda0c747e239467b2be32537..8ce382b26542abc5df4cc138c0846035c700f81a 100644 --- a/src/assets/stylesheets/hub.scss +++ b/src/assets/stylesheets/hub.scss @@ -7,16 +7,20 @@ @import 'entry'; @import 'audio'; @import 'info-dialog'; +@import 'shared'; .a-enter-vr, .a-orientation-modal { display: none; } +.grab-cursor { + cursor: grab; +} + .no-cursor { cursor: none; } - .webxr-realities, .webxr-sessions { - user-select: none; + @extend %unselectable } diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index ed742c0a9b1f707031b5cdb417ca727dcd4a3b5a..66595775efe1e43c7a835987940ff288eee63ef1 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -1,6 +1,7 @@ @import 'shared'; @import 'hub-create'; @import 'info-dialog'; +@import 'loader'; $header-section-width: 350px; @@ -11,8 +12,8 @@ $header-section-width: 350px; body { margin: 0; padding: 0; - background-color: black; - color: white; + background-color: white; + color: black; } .home-root { @@ -47,20 +48,22 @@ body { position: fixed; top: 0; left: 0; - opacity: 0.5; + opacity: 0.3; min-width: 100%; - filter: saturate(1.5); min-height: 100%; z-index: 1; } :local(.header-content) { padding: 0.5em 1.75em 0.5em 1.75em; - background-color: $darkest-transparent; + background-color: white; min-height: 65px; height: 65px; display: flex; - border-bottom: 1px solid $darkest-grey; + + @media (max-width: 768px), (max-height: 715px) { + display: none; + } :local(.title-and-nav) { display: flex; @@ -97,19 +100,17 @@ body { align-items: center; a { - margin: 0px 16px 0px 0px; - color: white; + margin: 0px 32px 0px 0px; + color: black; font-weight: bold; font-size: 1.4em; text-decoration: none; } - } - } - :local(.ident) { - text-align: right; - flex: 1 1 $header-section-width; - width: $header-section-width; + a:first-child { + margin-left: 16px; + } + } } } @@ -118,75 +119,52 @@ body { min-height: 740px; display: flex; flex-direction: column; + justify-content: center; position: relative; @media (max-width: 768px) { - padding: 1em 1.5em 1em 1.5em; - justify-content: space-around; + padding: 0px; min-height: 490px; } + @media (max-height: 715px) { + justify-content: flex-start; + } + :local(.attribution) { position: absolute; right: 12px; - bottom: 12px; - color: $grey-text; - text-shadow: 0px 0px 2px rgba(32, 32, 32, 1.0); - opacity: 0.5; + top: 12px; + color: white; + opacity: 0.8; a { - color: $grey-text; + font-weight: bold; + color: white; + } + + @media (max-width: 768px) { + display: none; } } :local(.container) { - padding-top: 2vw; - padding-left: 2.1em; - padding-right: 2.1em; - flex: 2; - text-shadow: 0px 0px 2px rgba(32, 32, 32, 1.0); + display: flex; + flex-direction: column; + align-items: center; - @media (max-height: 720px) { - padding-bottom: 0px; - flex: 1; + :local(.logo) { + width: 350px; } + padding: 2em; + :local(.title) { - font-size: 4vw; + text-align: center; + font-size: 1.5em; font-weight: bold; - @media (max-width: 768px) , (max-height: 715px) { - font-size: 1.9em; - } - - @media (max-width: 768px) { - text-align: center; - } - - @media (min-width: 1824px) { - font-size: 4.5em; - } - } - - :local(.subtitle) { - font-size: 2.5vw; - font-weight: lighter; - color: $light-text; - - @media (max-width: 768px) , (max-height: 715px) { - font-size: 1.1em; - margin-top: 0.2em; - } - - @media (max-width: 768px) { - text-align: center; - } - - @media (min-width: 1824px) { - font-size: 2.8em; - } - - @media (max-height: 720px) { + @media (max-height: 715px) { display: none; } } @@ -195,46 +173,77 @@ body { :local(.create) { padding: 2.1em; padding-bottom: 3.5vw; - flex: 4; position: relative; @media (max-width: 768px) { padding: 0.5em; } } + + :local(.join-button) { + display: flex; + justify-content: center; + + a { + margin-top: 8px; + @extend %action-button; + } + } } :local(.footer-content) { - padding: 1em 2.25em 1em 2.25em; - background-color: $darkest-transparent; + padding: 1em 2.25em; + margin: 24px; + + @media (max-width: 768px) { + padding: 1em; + margin: 0; + } + min-height: 80px; display: flex; - border-top: 2px solid #242424; align-items: center; - justify-content: center; + justify-content: flex-end; :local(.links) { text-align: center; - color: $dark-grey; + color: black; display: flex; flex-direction: column; + :local(.moz-logo) { + width: 172px; + height: 49px; + margin-left: 18px; + + @media (max-width: 768px) { + width: 113px; + height: 32px; + margin: 0; + } + } + :local(.top) { display: flex; justify-content: space-between; + align-items: flex-end; } :local(.link) { - color: $grey-text; - margin-left: 8px; - margin-right: 8px; + color: $dark-grey; + font-weight: bold; + margin: 0 18px; + + @media (max-width: 768px) { + display: none; + } } :local(.bottom) { margin-top: 8px; a { - color: $grey-text; + color: black; } } } diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index b922d80c28464ce0cf59b2a7ff4bb8992552b4b2..d724730d4c3d24fd66072d618c2739b298c0048e 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -4,8 +4,8 @@ top: 0; left: 0; position: fixed; - color: white; - z-index: 2; + color: black; + z-index: 10; } .dialog { @@ -14,7 +14,7 @@ grid-template-rows: 1fr 20px minmax(200px, max-content) 20px 1fr; width: 100%; height: 100%; - background-color: rgba(0,0,0,.2); + background-color: rgba(0,0,0,.85); &__box { grid-column: 3; @@ -24,13 +24,13 @@ &__contents { padding: 30px; - background-color: rgba(0,0,0,0.9); box-shadow: 0px 0px 30px 1px #202020; - border-radius: 8px; + border-radius: 12px; display: flex; flex-direction: column; text-align: center; position: relative; + background-color: white; &__title { @extend %top-title; @@ -38,15 +38,16 @@ } &__body { - font-size: 1.1em; - color: $grey-text; + font-size: 0.9em; + font-weight: bold; + color: black; display: flex; flex-direction: column; height: 100%; overflow-y: auto; margin: 10px 0; - a { color: white } + a { color: black } } &__links { @@ -55,17 +56,18 @@ margin: 16px 0; a { margin: 0 12px; - color: $light-text; + color: black; } } &__close { position: absolute; - left: 12px; + right: 12px; top: 6px; - color: white; - font-size: 1.4em; + color: black; + font-size: 1.8em; + font-weight: bold; background: none; cursor: pointer; @@ -79,7 +81,7 @@ margin-top: 0; } -.invite-form, .add-media-form, .custom-scene-form { +.invite-form, .invite-team-form, .add-media-form, .custom-scene-form { display: flex; flex-direction: column; align-items: center; @@ -100,7 +102,7 @@ &__link_field { @extend %rounded-border; @extend %default-font; - color: $light-text; + color: black; font-size: 1.2em; background-color: transparent; line-height: 2.0em; @@ -122,6 +124,12 @@ } } +.invite-team-form { + &__action-button { + width: 350px; + } +} + .mailing-list-form { display: flex; height: 100%; @@ -137,10 +145,10 @@ &__email_field { @extend %rounded-border; @extend %default-font; - color: $light-text; + color: black; font-size: 1.2em; background-color: transparent; - line-height: 2.0em; + line-height: 2.5em; padding-left: 1.25em; padding-right: 1.25em; margin: 0.5em 0; diff --git a/src/assets/stylesheets/invite-dialog.scss b/src/assets/stylesheets/invite-dialog.scss new file mode 100644 index 0000000000000000000000000000000000000000..f28210e2492ad663f31aa1f08874c3f89c5ac755 --- /dev/null +++ b/src/assets/stylesheets/invite-dialog.scss @@ -0,0 +1,126 @@ +@import 'shared.scss'; + +:local(.dialog) { + background-color: $action-color-transparent; + border-radius: 12px; + box-shadow: 0px 5px 30px 1px #333; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 10px; + padding: 14px; + text-align: center; + position: relative; + font-size: 1.0em; + color: white; + z-index: 3; + + a { + color: white; + text-decoration: underline; + font-weight: bold; + } + + :local(.link-button) { + @extend %action-button; + @extend %action-button-selected; + min-width: auto; + margin-top: 4px; + flex: 1; + + @media (max-height: 370px) { + display: none; + } + } +} + +:local(.close) { + position: absolute; + width: 30px; + height: 30px; + right: 12px; + font-size: 2.0em; + top: 0px; + cursor: pointer; +} + +:local(.attach-point) { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid $action-color; + position: absolute; + top: -5px; +} + +:local(.code) { + color: black; + font-weight: bold; + text-decoration: none; + font-size: 2.0em; + display: flex; + margin: 12px; +} + +:local(.keep-open) { + font-size: 0.8em; +} + +:local(.domain) { + input { + @extend %default-font; + font-weight: bold; + text-decoration: none; + color: black; + text-align: center; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + margin: 12px; + font-size: 1.8em; + padding: 14px; + display: block; + width: 295px; + } +} + + +:local(.digit) { + padding: 0 8px; + margin: 2px; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + width: 32px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; +} + +:local(.digit_2) { + margin-right: 8px; +} + +:local(.digit_3) { + margin-left: 8px; +} + +:local(.code-loading-panel) { + background: none; +} + +:local(.hub-link-link) { + font-size: 1.2em; +} + +:local(.buttons) { + display: flex; + justify-content: space-between; + width: 100%; + + button, a { + margin: 0 12px; + } +} diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss index aafcac281e33a362fc69871f9474fff32ddd73b7..d137a31fb2fffea16af362eb4a6adcb999081d4e 100644 --- a/src/assets/stylesheets/link-dialog.scss +++ b/src/assets/stylesheets/link-dialog.scss @@ -1,19 +1,80 @@ +@import 'shared.scss'; + +:local(.dialog) { + position: absolute; + color: white; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 10; + pointer-events: auto; +} + +:local(.header) { + font-size: 2em; + font-weight: bold; + margin-bottom: 12px; + text-align: center; + + @media(max-height: 420px) { + display: none; + } +} + +:local(.contents) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + :local(.domain) , :local(.code) { color: white; - font-family: monospace; font-weight: bold; text-decoration: none; } :local(.domain) { + @extend %default-font; font-size: 3em; + font-weight: bold; + text-decoration: none; + color: black; + text-align: center; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + margin: 12px; padding: 14px; display: block; + width: 295px; } :local(.code) { - font-size: 4.0em; - padding: 8px; + color: black; + font-weight: bold; + text-decoration: none; + font-size: 2.5em; + display: flex; + margin: 12px; +} + +:local(.imageHeader) { + width: 100px; + height: 100px; + background-color: white; + border-radius: 50px; + fill: black; + margin-bottom: 24px; + padding-right: 4px; + + @media(max-height: 420px) { + display: none; + } } :local(.keep-open) { @@ -22,8 +83,34 @@ :local(.digit) { padding: 0 8px; + margin: 2px; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + width: 32px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; } :local(.code-loading-panel) { background: none; } + +:local(.close) { + position: absolute; + width: 30px; + height: 30px; + right: 12px; + font-size: 3.0em; + top: 0px; + cursor: pointer; +} + +:local(.close-button) { + @extend %action-button; + background-color: $darker-grey; + margin-top: 24px; +} + diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss index b94325c5039062d7848a4d254df9d27b88a7ceb4..ee36c30f0086562c06388b9e13bb2f8e4dbf9699 100644 --- a/src/assets/stylesheets/link.scss +++ b/src/assets/stylesheets/link.scss @@ -8,12 +8,23 @@ body { margin: 0; padding: 0; - background-color: black; - color: white; + background-color: white; + color: black; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + overflow-y: hidden; +} + +button { + outline: none; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + -webkit-tap-highlight-color: transparent; } a { - color: white; + color: black; } .link-root { @@ -27,6 +38,29 @@ a { position: absolute; } +:local(.logo) { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; + justify-content: center; + margin-top: 24px; + + img { + width: 247px; + height: 57px; + } + + @media(max-height: 600px) { + display: none; + } + + @media(max-width: 690px) { + display: none; + } +} + :local(.link) { display: flex; align-items: center; @@ -38,9 +72,9 @@ a { display: flex; flex-direction: row; align-items: center; - justify-content: center; - color: $grey-text; - font-size: 1.4em; + color: black; + font-weight: bold; + font-size: 1.2em; @media (max-width: 690px) { flex-direction: column; @@ -74,6 +108,7 @@ a { :local(.entry-footer-image) { width: 200px; margin: 12px; + margin-top: 24px; } @@ -87,7 +122,8 @@ a { } :local(.header) { - margin: 16px; + text-align: center; + white-space: pre-wrap; } :local(.footer) { @@ -109,36 +145,44 @@ a { grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr 1fr; text-align: center; + background-color: #f7f7f7; + border-radius: 24px; + padding: 12px; } :local(.keypad-button) { @extend %big-icon-button; - font-size: 1.8em; - font-family: sans-serif; - border: 4px $light-grey solid; + @extend %default-font; + font-weight: bold; + color: $action-color; + font-size: 1.4em; border-radius: 128px; + box-shadow: 0px 2px 12px #ccc; min-width: 80px; min-height: 80px; cursor: pointer; line-height: 68px; margin: 8px; + background: white; } :local(.keypad-button):active { - background-color: $darker-grey; -} - -:local(.keypad-zero-button) { - grid-column: 2; + background-color: $light-grey; } :local(.keypad-button):disabled { color: $light-grey; - border: 6px $dark-grey solid; } -:local(.keypad-backspace) , :local(.keypad-backspace):disabled , :local(.keypad-backspace):active { +:local(.keypad-backspace) , :local(.keypad-backspace):disabled , :local(.keypad-backspace):active, :local(.keypad-toggle-mode), :local(.keypad-toggle-mode):disabled, :local(.keypad-toggle-mode):active { border: none; + background: transparent; + box-shadow: none; + color: black; +} + +:local(.keypad-backspace) { + grid-column: 3; } :local(.keypad-backspace):active { @@ -146,37 +190,66 @@ a { color: $light-grey; } -:local(.entered-digits) { - font-face: monospace; +:local(.keypad-toggle-mode) { + font-size: 1.0em; +} + +:local(.entered) { + @extend %default-font; height: 100px; width: 300px; text-align: center; - font-size: 3.0em; - color: white; + font-size: 2.0em; + color: black; display: flex; justify-content: center; } -:local(.digit) { +:local(.char) { margin: 8px; } -:local(.digit-input) { +:local(.char-input) { + @extend %default-font; + font-weight: bold; outline-style: none; appearance:textfield; -moz-appearance:textfield; -webkit-appearance:textfield; background: transparent; - color: white; margin: 0; - font-size: 64pt; + font-size: 52pt; border: 0; width: 295px; letter-spacing: 0.08em; text-align: center; } -:local(.digit-input::placeholder) { +:local(.char-input::placeholder) { letter-spacing: 0; } +:local(.headset-icon) { + width: 64px; + height: 64px; + background-color: #333; + border-radius: 32px; + margin-bottom: 8px; + padding-right: 2px; + cursor: pointer; +} + +:local(.link-headset-footer-link) { + display: flex; + align-items: center; + + img { + width: 32px; + height: 32px; + border-radius: 32px; + margin-bottom: 8px; + margin-right: 16px; + padding-right: 2px; + cursor: pointer; + } +} diff --git a/src/assets/stylesheets/loader.scss b/src/assets/stylesheets/loader.scss index 74ccb011d068d9013c78f61817d4c4d1f24053ab..ff7e4098dca76956fe35013765d158930c3b6c5f 100644 --- a/src/assets/stylesheets/loader.scss +++ b/src/assets/stylesheets/loader.scss @@ -11,7 +11,7 @@ position: absolute; top: 0; left: 0; - background-color: black; + background-color: #d7e5ec; width: 100%; height: 100%; display: flex; @@ -21,8 +21,8 @@ } .loading-panel__logo { - width: 165px; - height: 33px; + width: 247px; + height: 57px; margin: 20px 0; } diff --git a/src/assets/stylesheets/presence-list.scss b/src/assets/stylesheets/presence-list.scss new file mode 100644 index 0000000000000000000000000000000000000000..3e7ab0517586fff3db4a83c19f6202b5de1d247d --- /dev/null +++ b/src/assets/stylesheets/presence-list.scss @@ -0,0 +1,82 @@ +@import 'shared.scss'; + +:local(.attach-point) { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid $white-transparent; + position: absolute; + top: -5px; + left: 44px; + + @media(max-width: 768px), (max-height: 420px) { + left: 34px; + } +} + +:local(.presence-list) { + position: absolute; + top: 72px; + left: 16px; + bottom: 0; + z-index: 5; +} + +:local(.contents) { + background-color: white; + border-radius: 12px; + padding: 12px 18px; + min-width: 308px; + max-height: 75%; + overflow-y: auto; + pointer-events: auto; +} + +:local(.rows) { + display: flex; + flex-direction: column; + align-items: center; +} + +:local(.row) { + width: 100%; + display: flex; + flex-direction: row; + font-weight: bold; + justify-content: space-between; + align-items: center; + margin: 6px 0; +} + +:local(.device) { + width: 32px; + height: 32px; + position: relative; + margin: 0px 12px 0px 0px; + + img { + position: absolute; + left: 2px; + width: 32px; + height: 32px; + } +} + +:local(.display-name) { + flex: 10; + white-space: nowrap; + margin-right: 24px; + max-width: 45vw; + overflow: hidden; +} + +:local(.self-display-name) { + text-decoration: underline; +} + +:local(.presence) { + flex: 1; + white-space: nowrap; + text-align: right; +} diff --git a/src/assets/stylesheets/presence-log.scss b/src/assets/stylesheets/presence-log.scss new file mode 100644 index 0000000000000000000000000000000000000000..2738cb3583eee8fe193d652232af609773a983a5 --- /dev/null +++ b/src/assets/stylesheets/presence-log.scss @@ -0,0 +1,73 @@ +@import 'shared.scss'; + +:local(.presence-log) { + align-self: flex-start; + flex: 10; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + margin-bottom: 8px; + margin-top: 90px; + overflow: hidden; + width: 100%; + + :local(.presence-log-entry) { + @extend %default-font; + pointer-events: auto; + + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; + -ms-user-select: text; + + background-color: $white-transparent; + margin: 8px 64px 8px 16px; + font-size: 0.8em; + padding: 8px 16px; + border-radius: 16px; + + a { + color: $action-color; + } + + @media (max-width: 1000px) { + max-width: 75%; + } + } + + :local(.expired) { + visibility: hidden; + opacity: 0; + transform: translateY(-8px); + transition: visibility 0s 0.5s, opacity 0.5s linear, transform 0.5s; + } + +} + +:local(.presence-log-in-room) { + max-height: 200px; + + @media(min-height: 800px) and (min-width: 600px) { + max-height: 400px; + } + + position: absolute; + bottom: 165px; + + :local(.presence-log-entry) { + background-color: $hud-panel-background; + color: $light-text; + + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + } +} + +:local(.emoji) { + // Undo annoying CSS in emoji plugin + margin: auto !important; + vertical-align: 0em !important; +} diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index e0434e022f35280e56848c6b12e609928567237a..4b01331d346967f8c3c5ca538f7fe4ab332ae205 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -1,4 +1,6 @@ -.profile-entry { +@import 'shared'; + +:local(.profile-entry) { position: absolute; top: 0; left: 0; @@ -8,20 +10,34 @@ align-items: center; display: flex; pointer-events: auto; + z-index: 10; + background-color: rgba(255, 255, 255, 0.95); + + :local(.logo) { + width: 150px; + position: absolute; + right: 32px; + bottom: 32px; + display: none; + + @media (min-width: 500px) { + display: block; + } + } - &__avatar-selector-container { + :local(.avatar-selector-container) { flex: 1; position: relative; - margin-bottom: 0.5em; width: 95%; text-align: center; + margin: 16px; .loading-panel { background: transparent; } } - &__avatar-selector { + :local(.avatar-selector) { border: none; width: 95%; height: 100%; @@ -29,18 +45,16 @@ position: relative; } - &__form { + :local(.form) { height: 100%; } - &__box { - border-radius: 18px; - box-shadow: 0px 0px 30px 1px #202020; + :local(.box) { display: flex; flex-direction: column; justify-content: space-between; align-items: center; - padding: 15px; + margin-top: 32px; flex: 1 1 100%; width: 60vw; min-width: 300px; @@ -49,7 +63,7 @@ max-height: 1000px; height: 90%; - &__links { + :local(.links) { display: flex; justify-content: center; width: 100%; @@ -57,102 +71,43 @@ a { margin: 0px 12px; - color: $light-text; + color: $dark-grey-text; } } - &--darkened { + :local(.darkened) { background-color: $darkest-transparent; } } - &__subtitle { + :local(.title) { + @extend %top-title; width: 100%; + color: black; text-align: center; - font-size: 1.2em; - color: $grey-text; } - &__display-name-label { + :local(.display-name) { font-size: 1.2em; margin-right: 0.5em; } - &__form-field-text { - @extend %rounded-border; + :local(.form-field-text) { + @extend %form-field-on-white; @extend %default-font; - color: $light-text; - font-size: 1.2em; - background-color: transparent; + font-size: 1.1em; + color: black; + outline: none; line-height: 2.0em; padding-left: 1.25em; padding-right: 1.25em; margin: 0.5em 0; } - &__form-submit { + :local(.form-submit) { @extend %bottom-action-button; margin: 0; min-height: max-content; + margin-top: 16px; } } - -.profile-info-header { - display: flex; - width: 100%; - justify-content: space-between; - min-height: 80px; - - &__menu-buttons { - display: flex; - margin: 0 0 0 20px; - - &__menu-button { - @extend %fa-icon-button; - margin-left: 16px; - - &__icon { - @extend %glass-icon; - @extend %fa-icon-big; - - background: transparent; - color: white; - } - } - } - - &__icon { - cursor: pointer; - width: 20px; - height: 20px; - margin: 15px; - } - - &__profile_display_name { - margin: 0 30px 0 0; - - @extend %glass-text; - - img { - @extend %glass-icon; - } - - margin-left: 16px; - cursor: pointer; - font-size: 1.2em; - line-height: 50px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: flex; - align-items: center; - } - - &__app_name { - font-size: 1.8em; - padding-right: 18px; - line-height: 50px; - white-space: nowrap; - } -} - diff --git a/src/assets/stylesheets/scene-ui.scss b/src/assets/stylesheets/scene-ui.scss new file mode 100644 index 0000000000000000000000000000000000000000..bd3abf0a6d82cf898081da35c23add7ce692b024 --- /dev/null +++ b/src/assets/stylesheets/scene-ui.scss @@ -0,0 +1,149 @@ +@import 'shared'; + +:local(.ui) { + @extend %default-font; + + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + pointer-events: none; + color: white; +} + +:local(.whiteOverlay) { + background-color: rgba(255, 255, 255, 0.2); + top: 0; + left: 0; + width: 100%; + height: 100%; + position: absolute; +} + +:local(.grid) { + display: grid; + grid-template-columns: 1fr 20px minmax(200px, 400px) 20px 1fr; + grid-template-rows: 1fr 3fr 1fr; + width: 100%; + height: 100%; +} + +:local(.mainPanel) { + grid-column: 3; + grid-row: 2; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + pointer-events: auto; + + button { + @extend %action-button; + border: 0; + } +} + +:local(.logoTagline) { + @extend %background-agnostic; + font-weight: bold; + text-align: center; + font-size: 1.2em; + margin-bottom: 32px; +} + +:local(.tweetButton) { + @extend %action-button; + margin-top: 12px; + background-color: #1b95e0; + align-self: center; + padding-right: 32px; + display: flex; + flex-direction: row; + + img { + width: 42px; + height: 42px; + margin-right: 6px; + } +} + +:local(.logo) { + width: 100%; + display: block; + + img { + width: 100%; + } +} + +:local(.info) { + position: absolute; + color: black; + text-shadow: 0px 0px 10px #888; + bottom: 12px; + left: 12px; + display: flex; + flex-direction: column; +} + +:local(.name) { + font-weight: bold; + font-size: 1.6em; +} + +:local(.attribution) { + font-size: 1.0em; + white-space: wrap; +} + +:local(.screenshot) { + position: absolute; + width: 115%; + height: 115%; + top: -40px; + left: -40px; + + img { + width: 100%; + height: 100%; + } + + background-color: black; + filter: blur(10px); +} + +:local(.screenshotHidden) { + visibility: hidden; + opacity: 0; + transition: visibility 0s 0.5s, opacity 0.5s linear; +} + +:local(.spoke) { + @media(max-width: 768px) { + display: none; + } + + pointer-events: auto; + position: absolute; + right: 12px; + bottom: 8px; + display: flex; + flex-direction: column; + text-align: left; + font-size: 0.8em; + font-weight: bold; + + :local(.madeWith) { + color: black; + text-shadow: 0px 0px 4px #333; + margin-left: 4px; + position: absolute; + top: 0px; + left: 0; + } + + img { + width: 200px; + } +} diff --git a/src/assets/stylesheets/scene.scss b/src/assets/stylesheets/scene.scss new file mode 100644 index 0000000000000000000000000000000000000000..44e6591aacfa994bb79a4a0354b129ae171c4741 --- /dev/null +++ b/src/assets/stylesheets/scene.scss @@ -0,0 +1,2 @@ +@import 'shared'; +@import 'loader'; diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index 1e52392c3cd57c4704d7822c3f31e8adf79c086c..79a12fc6febb433dfa5453fa2729454971cbb690 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -1,22 +1,34 @@ $light-transparent: rgba(0, 0, 0, 0.2); +$white-transparent: rgba(255, 255, 255, 0.90); $dark-transparent: rgba(0, 0, 0, 0.4); $darker-transparent: rgba(0, 0, 0, 0.6); $darkest-transparent: rgba(0, 0, 0, 0.9); $grey-text: rgba(192, 192, 192, 1.0); +$dark-grey-text: rgba(64, 64, 64, 1.0); $light-text: rgba(240, 240, 240, 1.0); $light-grey: lightgrey; $dark-grey: rgba(128, 128, 128, 1.0); $darker-grey: rgba(64, 64, 64, 1.0); $darkest-grey: rgba(32, 32, 32, 1.0); +$action-color: #FF3464; +$action-color-light: #FF74A4; +$action-color-transparent: rgba(255, 52, 100, 0.9); +$hud-panel-background: rgba(79, 79, 79, 0.45); + +%unselectable { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} %default-font { - font-family: 'Zilla Slab', sans-serif; + font-family: 'Open Sans', sans-serif; } %glass-text { - font-family: 'Zilla Slab', sans-serif; - text-shadow: 0px 0px 4px rgba(0, 0, 0, 1.0); - color: white; + font-family: 'Open Sans', sans-serif; + color: black; } %glass-icon { @@ -25,21 +37,38 @@ $darkest-grey: rgba(32, 32, 32, 1.0); } %rounded-border { - border: 3px solid white; + border: 1px solid #e2e2e2; box-sizing: border-box; - border-radius: 14px; + border-radius: 10px; +} + +%form-field-on-white { + border: 1px solid #e2e2e2; + background-color: white; + border-radius: 10px; } %big-action-button { @extend %default-font; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; outline-style: none; - font-size: 2em; + font-size: 18px; font-weight: bold; cursor: pointer; - border: 3px solid white; - border-radius: 26px; - padding: 12px 64px 12px 64px; - background: #2F80ED; + border-radius: 32px; + border: 0; + width: 350px; + padding: 18px; + text-decoration: none; + + @media (max-width: 768px) { + width: auto; + padding: 18px 48px; + } + + background: #FF3464; color: white; display: flex; align-items: center; @@ -55,33 +84,50 @@ $darkest-grey: rgba(32, 32, 32, 1.0); margin: 16px 0; } +%wide-button { + width: 350px; + + @media (max-width: 768px) { + width: 327px; + } +} + %action-button { @extend %default-font; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + text-decoration: none; outline-style: none; font-weight: bold; cursor: pointer; - border: 3px solid white; - border-radius: 26px; - height: 64px; - padding: 12px; - background: #2F80ED; - font-size: 1.3em; + border: 0; + border-radius: 28px; + padding: 0px 18px; + + background: #FF3464; + font-size: 1em; color: white; display: flex; align-items: center; flex-direction: column; justify-content: center; min-width: 150px; + height: 48px; +} + +%action-button-selected { + background: white; + color: $action-color; } %bottom-action-button { @extend %bottom-button; - background: #2F80ED; - font-size: 1.3em; + background: #FF3464; } %top-title { - font-size: 1.3em; + font-size: 1.5em; font-weight: bold; } @@ -107,18 +153,23 @@ $darkest-grey: rgba(32, 32, 32, 1.0); -webkit-appearance: none; width: 2em; height: 2em; - border: 3px solid white; + border: 1px solid #e2e2e2; border-radius: 9px; vertical-align: sub; margin: 0 0.6em } %checkbox-checked { - border: 9px double white; - outline: 9px solid white; + border: 9px double #aaa; + outline: 9px solid #aaa; outline-offset: -18px; } +%background-agnostic { + color: black; + text-shadow: 0px 0px 10px #888; +} + %fa-icon-button { @extend %default-font; margin: 16px 0; @@ -126,7 +177,7 @@ $darkest-grey: rgba(32, 32, 32, 1.0); display: block; background: none; border: none; - color: white; + color: black; cursor: pointer; font-size: 0.8em; outline-style: none; @@ -155,7 +206,7 @@ $darkest-grey: rgba(32, 32, 32, 1.0); vertical-align: sub; line-height: 42px; svg { - margin-left: 0px; + margin-right: 2px; } } diff --git a/src/assets/stylesheets/spoke.scss b/src/assets/stylesheets/spoke.scss new file mode 100644 index 0000000000000000000000000000000000000000..68f850879027e250f7bcce75a16a88692cf4a6e3 --- /dev/null +++ b/src/assets/stylesheets/spoke.scss @@ -0,0 +1,238 @@ +@import 'shared'; +@import 'loader'; + +$spoke-action-color: #2F80ED; +$breakpoint: 1280px; +$mobile-breakpoint-width: 450px; + +body { + @extend %default-font; + background: black; + color: white; + margin: 0; + overflow: hidden; +} + +:local(.bg) { + background: radial-gradient(#222C41 0%, black 100%); + top: 0; + left: 0; + position: absolute; + width: 100%; + height: 100%; + z-index: -2; +} + +:local(.ui) { + display: flex; + position: relative; + flex-direction: column; +} + +:local(.content) { + display: flex; + flex-direction: column; + height: calc(100vh - 72px); + align-items: center; + justify-content: center; +} + +:local(.header) { + font-size: 1.1em; + font-weight: bold; + color: white; +} + +:local(.header-links) { + display: flex; + margin: 24px 12px; + + @media(max-width: $mobile-breakpoint-width) { + justify-content: space-between; + } + + a { + color: white; + text-decoration: none; + margin: 0px 18px; + } +} + +:local(.hero-pane) { + display: flex; + height: 100%; + position: relative; + justify-content: center; + align-items: center; + + @media(max-width: $breakpoint) { + flex-direction: column; + } +} + +:local(.spoke-logo) { + position: relative; + + img { + width: 400px; + + @media(max-width: $mobile-breakpoint-width) { + width: 320px; + } + } +} + +:local(.primary-tagline) { + position: absolute; + right: 6px; + bottom: -8px; + font-weight: bold; + font-size: 1.7em; +} + +:local(.secondary-tagline) { + font-weight: lighter; + font-size: 1.5em; + text-align: center; + margin-top: 48px; + margin-left: 12px; + margin-right: 12px; + + @media(max-width: $mobile-breakpoint-width) { + font-size: 1.3em; + } + + a { + color: white; + } +} + +:local(.hero-message) { + width: 600px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + margin: 0 36px; + + @media(max-width: $breakpoint) { + text-shadow: 0px 0px 4px rgba(0, 0, 0, 1.0); + order: 2; + width: 100%; + margin: 32px 0; + height: auto; + } +} + +:local(.hero-video) { + display: flex; + flex-direction: column; + justify-content: center; + position: relative; + margin-left: 32px; + flex: 1; + z-index: -1; + border-radius: 8px; + + @media(max-width: $breakpoint) { + display: none; + } +} + +:local(.preview-video) { + width: 90%; + border-radius: 12px 0 0 12px; + z-index: -1; +} + +:local(.download-button) { + @extend %action-button; + @extend %wide-button; + height: 64px; + border-radius: 32px; + + background-color: $spoke-action-color; + margin-top: 64px; + display: flex; + flex-direction: column; + + :local(.version) { + font-size: 0.8em; + font-weight: lighter; + } +} + +:local(.play-button), :local(.close-video) { + @extend %action-button; + background-color: $darker-grey; + margin: auto; + margin-top: 64px; + padding: 0px 82px; +} + +:local(.close-video) { + margin-top: 12px; +} + +:local(.browse-versions) { + color: $grey-text; + margin-top: 12px; + display: block; + text-align: center; + width: 100%; +} + +:local(.thank-you) { + text-align: center; + + a { + color: $grey-text; + } +} + +:local(.player-overlay) { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; +} + +:local(.player-content) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +:local(.player-video) { + width: 960px; + height: 540px; + + @media (max-width: 980px), (max-height: 760px) { + width: 640px; + height: 360px; + } + + @media (max-width: 650px), (max-height: 580px) { + width: 480px; + height: 270px; + } + + @media (max-width: 490px), (max-height: 480px) { + width: 320px; + height: 180px; + } +} + +:local(.attribution) { + position: absolute; + bottom: -40px; + right: 84px; + color: $darker-grey; +} diff --git a/src/assets/stylesheets/ui-root.scss b/src/assets/stylesheets/ui-root.scss index 2c3a7f4633d2a415faefeebe65baee20032b9886..11de7f546b713ca297e99e92e090bb355e55e334 100644 --- a/src/assets/stylesheets/ui-root.scss +++ b/src/assets/stylesheets/ui-root.scss @@ -1,6 +1,6 @@ @import 'shared'; -#ui-root .ui { +:local(.ui) { @extend %default-font; width: 100%; @@ -9,22 +9,21 @@ left: 0; position: absolute; pointer-events: none; - color: white; - &__help-icon { + :local(.help-icon) { @extend %fa-icon-button; pointer-events: auto; position: absolute; top: 0px; - left: 14px; + right: 14px; - &__icon { - background: rgba(33, 33, 33, 0.5); + i { + background: white; border-radius: 36px; display: inline-block; font-size: 22px; vertical-align: sub; - line-height: 38px; + line-height: 34px; border: 0; width: 36px; height: 36px; @@ -32,84 +31,239 @@ } } -.blurred { - filter: blur(5px) saturate(1.1) brightness(1.1); -} - -.ui-dialog { - display: grid; - grid-template-columns: 1fr 20px minmax(200px, 400px) 20px 1fr; - grid-template-rows: 1fr 20px minmax(200px, 600px) 20px 1fr; - width: 100%; +:local(.ui-dialog) { + position: absolute; + top: 0; + left: 0; height: 100%; - user-select: none; - - &--darkened { - background-color: $dark-transparent; - } -} - -.ui-dialog-box { - grid-column: 3; - grid-row: 3; - position: relative; + width: 100%; + @extend %unselectable; + flex-direction: column; + display: flex; + justify-content: flex-end; + align-items: center; } -.ui-dialog-box-contents { - background-color: $light-transparent; - border-radius: 18px; +:local(.ui-dialog-box-contents) { + background-color: $white-transparent; + border-radius: 18px 18px 0 0; width: 100%; - height: 100%; + max-width: 600px; + z-index: 2; + position: relative; - &--backgrounded { + :local(.backgrounded) { filter: blur(1px); opacity: 0.7; pointer-events: none; } } -.ui-interactive { +:local(.ui-interactive) { pointer-events: auto; + @extend %unselectable; } -:local(.nag-button) { +:local(.invite-container) { + @extend %unselectable; position: absolute; - top: 110px; + top: 0; left: 0; + margin-top: 16px; width: 100%; display: flex; + flex-direction: column; align-items: center; justify-content: center; + pointer-events: auto; + + button { + @extend %action-button; + pointer-events: auto; + } + + @media (max-height: 320px) { + display: none; + } + + :local(.hide-small-screens) { + display: none; + + @media (min-height: 737px) { + display: flex; + } + } + + :local(.invite-mini-button) { + display: none; + background-color: $hud-panel-background; + height: 28px; + font-size: 0.8em; + + @media (max-height: 736px) and (min-height: 325px) { + display: flex; + } + } +} + +:local(.invite-container-inverted) { + button { + @extend %action-button-selected; + } +} + +:local(.invite-container-below-hud) { + margin-top: 100px; +} + +:local(.nag-corner-button) { + position: absolute; + bottom: 42px; + width: 100%; + display: flex; + align-items: center; + justify-content: flex-end; height: 80px; pointer-events: none; - + button { - @extend %big-action-button; + @extend %default-font; + font-size: 1.3em; + font-weight: bold; + cursor: pointer; + min-width: 150px; + margin-right: 12px; + margin-left: 12px; + white-space: nowrap; + background: none; + border: none; pointer-events: auto; + padding: 16px 28px; + height: 58px; + color: $light-text; + text-shadow: 0px 0px 4px rgba(0, 0, 0, 1.0); + text-decoration: underline; + } + + @media (max-width: 740px) { + display: none; } } :local(.presence-info) { + @extend %unselectable; text-align: right; position: absolute; top: 0; - right: 16px; + left: 16px; margin: 16px 0; display: flex; align-items: center; justify-content: flex-end; font-size: 1.3em; - text-shadow: 0px 0px 4px rgba(0, 0, 0, 1.0); - -webkit-filter: drop-shadow( 0px 0px 3px #606060 ); - filter: drop-shadow( 0px 0px 3px #606060 ); + background-color: white; + border-radius: 24px; + font-weight: bold; + padding: 8px 18px; + pointer-events: auto; + cursor: pointer; @media (min-width: 769px) and (min-height: 421px) { flex: 1; } @media (max-width: 768px) , (max-height: 420px) { - margin: 16px 8px; + margin: 16px 0; + margin-right: 0; + padding: 2px 8px; } :local(.occupant-count) { margin: 0 12px; } } + +:local(.presence-info-selected) { + color: $action-color; +} + +:local(.message-entry) { + position: relative; + margin: 8px 24px 24px 24px; + height: 48px; + display: flex; + justify-content: center; + align-items: center; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 16px; +} + +:local(.message-entry-input) { + @extend %default-font; + pointer-events: auto; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + outline-style: none; + background-color: transparent; + color: black; + padding: 8px 1.25em; + line-height: 2em; + font-size: 1.1em; + width: 100%; + border: 0px; + height: 32px; + margin-right: 100px; +} + +:local(.message-entry-input)::placeholder{ + color: $dark-grey; + font-weight: 300; + font-style: italic; +} + +:local(.message-entry-submit) { + @extend %action-button; + position: absolute; + right: 12px; + height: 32px; + min-width: 80px; +} + +:local(.message-entry-in-room) { + @media(max-width: 900px) { + display:none; + } + + position: absolute; + left: 16px; + bottom: 20px; + width: 33%; + height: 48px; + display: flex; + justify-content: center; + align-items: center; + background-color: $darker-grey; + border-radius: 16px; + pointer-events: auto; + opacity: 0.3; + transition: opacity 0.25s linear; + + :local(.message-entry-input-in-room) { + color: white; + padding: 8px 1.25em; + } + + :local(.message-entry-submit-in-room) { + border: 0; + visibility: hidden; + } +} + +:local(.message-entry-in-room):hover { + opacity: 1.0; + transition: opacity 0.25s linear; + + :local(.message-entry-submit-in-room) { + visibility: visible; + } +} diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 857c1840a2d5de3ac814f3f19835af1bb0aed8c3..47fef3295a0b67e0216c506bb504b8e96c9bf2d0 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -1,44 +1,53 @@ { "en": { + "auth.verifying": "Verifying...", + "auth.verified-title": "E-Mail Verified!", + "auth.verified": "Your email has been verified!", + "auth.spoke-verified": "You email has been verified! {br} You can now close this browser tab and return to Spoke.", + "entry.enter-room-title": "Lobby", + "entry.enter-room": "Enter Room", "entry.screen-prefix": "Enter on ", "entry.desktop-screen": "Screen", "entry.mobile-screen": "Phone", "entry.mobile-safari": "Safari", - "entry.generic-prefix": "Enter with ", - "entry.generic-medium": "PC VR", + "entry.generic-prefix": " ", + "entry.generic-medium": "Connected Headset", "entry.generic-subtitle-desktop": "Oculus or SteamVR", "entry.gearvr-prefix": "Enter on ", "entry.gearvr-medium": "Gear VR", - "entry.device-prefix-desktop": "Use a ", - "entry.device-prefix-mobile": "Use a ", + "entry.choose-device": "Choose Device", + "entry.device-prefix-desktop": " ", + "entry.device-prefix-mobile": " ", "entry.device-medium": "Mobile Headset", - "entry.device-subtitle-desktop": "Standalone or Phone Clip-in", - "entry.device-subtitle-mobile": "Standalone or Phone Clip-in", + "entry.device-subtitle-desktop": "Standalone or Mobile VR", + "entry.device-subtitle-mobile": "Standalone or Mobile VR", "entry.device-subtitle-vr": "Phone or PC", "entry.cardboard": "Enter on Google Cardboard", "entry.daydream-prefix": "Enter on ", "entry.daydream-medium": "Daydream", "entry.daydream-via-chrome": "Using Google Chrome", "entry.invite-others": "invite others", - "entry.invite-others-nag": "invite others to join", + "entry.invite-others-nag": "invite", + "entry.invite-team-nag": "Invite a hubs team member", "entry.enable-screen-sharing": "Share my desktop", "entry.return-to-vr": "Enter in VR", - "profile.save": "save", + "entry.lobby": "Lobby", + "profile.save": "Accept", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", - "profile.header": "Your display name:", + "profile.header": "Name & Avatar", "profile.avatar-selector.loading": "Loading Avatars...", "profile.terms_of_use": "Terms of Use", "profile.privacy_notice": "Privacy Notice", - "audio.title": "Test your audio", + "audio.title": "Audio Setup", "audio.subtitle-desktop": "Confirm HMD speaker output", "audio.subtitle-mobile": "Earphones are recommended", - "audio.enter-now": "enter now", + "audio.enter-now": "Enter Now", "audio.hmd-mic-warning": "Your HMD mic is not chosen", "audio.grant-title": "Grant mic permissions", "audio.grant-subtitle": "Mic access needed to be heard by others", "audio.granted-title": "Mic permissions granted", "audio.granted-subtitle": "You can still mute yourself in-game", - "audio.granted-next": "next", + "audio.granted-next": "Next", "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.", "exit.subtitle.closed": "This room is no longer available.", "exit.subtitle.full": "This room is full, please try again later.", @@ -48,31 +57,57 @@ "autoexit.title_units": " seconds", "autoexit.subtitle": "You have started another session.", "autoexit.cancel": "CANCEL", + "presence.entered_room": "entered the room.", + "presence.join_lobby": "joined the lobby.", + "presence.leave": "left.", + "presence.name_change": "is now known as", + "presence.in_lobby": "Lobby", + "presence.in_room": "In Room", "home.room_create_options": "options", - "home.room_create_button": "create a room", + "home.room_create_button": "Create Room", "home.create_name.validation_warning": "Invalid name, limited to 4 to 64 characters and limited symbols.", "home.join_us": "Join the Conversation", + "home.join_room": "Join Room", "home.report_issue": "Report Issues", - "home.source_link": "source", - "home.about_link": "about", + "home.source_link": "Source", + "home.spoke_link": "Spoke", + "home.about_link": "About", + "home.community_link": "Community", "home.privacy_notice": "Privacy Notice", "home.terms_of_use": "Terms of Use", "home.get_updates": "Get Updates", - "home.hero_title": "A new way to get together online.", - "home.hero_subtitle": "Laugh, play, get stuff done, or just hang out.", + "home.hero_title": "A new way to get together", "home.made_with_love": "made with 🦆 by ", "home.environment_author_by": " by ", "home.dialog.close": "CLOSE", - "home.have_entry_code": "have a link code?", "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in", "mailing_list.privacy_link": "this Privacy Notice", + "link.link_page_header_entry": "Enter your code:", + "link.link_page_header_headset": "Enter headset link code:", + "link.linking_a_headset": "Linking a Headset?", + "link.try_again": "We couldn't find that code.\nPlease try again.", + "help.report_issue": "Report an Issue", + "scene.logo_tagline": "A new way to get together", + "scene.create_button": "Create a room with this scene", + "scene.tweet_button": "Share on Twitter", "link.in_your_browser": "In your headset's browser, go to:", - "link.enter_code": "Then, enter this link code:", - "link.do_not_close": "Keep this dialog open to use this code.", - "link.link_page_header": "Enter your code:", - "link.dont_have_a_code": "Don't have a code?", - "link.create_a_room": "Create a Room", - "link.try_again": "We couldn't find that code. Please try again.", - "help.report_issue": "Report an Issue" + "link.enter_code": "Then, enter this one-time link code:", + "link.do_not_close": "Keep this open to use this code.", + "link.connect_headset": "Link VR Headset", + "link.cancel": "cancel", + "invite.enter_via": "Enter via ", + "invite.tweet": "tweet", + "invite.and_enter_code": " with code:", + "invite.or_visit": "or share permalink", + "spoke.primary_tagline": "make your space", + "spoke.secondary_tagline": "Create 3D social scenes for ", + "spoke.thank_you": "Thank you for downloading Spoke!", + "spoke.download_win": "Download for Windows", + "spoke.download_macos": "Download for Mac", + "spoke.download_linux": "Download for Linux", + "spoke.download_unsupported": "View Releases", + "spoke.browse_all_versions": "Browse All Versions", + "spoke.close": "Close", + "spoke.play_button": "Learn Spoke in 5 Minutes" } } diff --git a/src/assets/video/spoke.mp4 b/src/assets/video/spoke.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..00ecc3251d7c6cc73560e38676d71792889652c2 Binary files /dev/null and b/src/assets/video/spoke.mp4 differ diff --git a/src/assets/video/spoke.webm b/src/assets/video/spoke.webm new file mode 100644 index 0000000000000000000000000000000000000000..dad54ce9aed95f9e579816b0e7292e96263534ae Binary files /dev/null and b/src/assets/video/spoke.webm differ diff --git a/src/components/animation-mixer.js b/src/components/animation-mixer.js index 480a4de234751aa4dd2088d9f497f043f3c46b85..5adca61251db0e8e2a0c7619ef419a9cfc77513b 100644 --- a/src/components/animation-mixer.js +++ b/src/components/animation-mixer.js @@ -3,38 +3,15 @@ * @component animation-mixer */ AFRAME.registerComponent("animation-mixer", { - init() { - this.mixer = null; - - const object3DMap = this.el.object3DMap; - const rootObject3D = object3DMap.mesh || object3DMap.scene; - - if (rootObject3D) { - this.setAnimationMixer(rootObject3D); - } else { - this.onModelLoaded = this.onModelLoaded.bind(this); - this.el.addEventListener("model-loaded", this.onModelLoaded); - } - }, - - onModelLoaded(event) { - const sceneObject3D = event.detail.model; - this.setAnimationMixer(sceneObject3D); - - this.el.removeEventListener(this.onModelLoaded); - }, - - setAnimationMixer(rootObject3D) { - this.mixer = new THREE.AnimationMixer(rootObject3D); + initMixer(animations) { + this.mixer = new THREE.AnimationMixer(this.el.object3D); + this.el.object3D.animations = animations; + this.animations = animations; }, tick: function(t, dt) { if (this.mixer) { this.mixer.update(dt / 1000); } - }, - - destroy() { - this.el.removeEventListener(this.onModelLoaded); } }); diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js index 71798ba4e05f81398f73942176fa1a94671bfef6..26726d30f57fb970e54c39df01198e7178ffe584 100644 --- a/src/components/audio-feedback.js +++ b/src/components/audio-feedback.js @@ -46,7 +46,7 @@ AFRAME.registerComponent("networked-audio-analyser", { AFRAME.registerComponent("scale-audio-feedback", { schema: { minScale: { default: 1 }, - maxScale: { default: 2 } + maxScale: { default: 1.5 } }, tick() { diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js new file mode 100644 index 0000000000000000000000000000000000000000..cc6bbefb233cecc6d28980fd2d1630c5d1730952 --- /dev/null +++ b/src/components/camera-tool.js @@ -0,0 +1,146 @@ +import { addMedia } from "../utils/media-utils"; +import { ObjectTypes } from "../object-types"; + +import cameraModelSrc from "../assets/camera_tool.glb"; + +const cameraModelPromise = new Promise(resolve => new THREE.GLTFLoader().load(cameraModelSrc, resolve)); + +const snapCanvas = document.createElement("canvas"); +async function pixelsToPNG(pixels, width, height) { + snapCanvas.width = width; + snapCanvas.height = height; + const context = snapCanvas.getContext("2d"); + + const imageData = context.createImageData(width, height); + imageData.data.set(pixels); + const bitmap = await createImageBitmap(imageData); + context.scale(1, -1); + context.drawImage(bitmap, 0, -height); + const blob = await new Promise(resolve => snapCanvas.toBlob(resolve)); + return new File([blob], "snap.png", { type: "image/png" }); +} + +AFRAME.registerComponent("camera-tool", { + schema: { + previewFPS: { default: 6 }, + imageWidth: { default: 1024 }, + imageHeight: { default: 1024 / (16 / 9) } + }, + + init() { + this.stateAdded = this.stateAdded.bind(this); + + this.lastUpdate = performance.now(); + + this.renderTarget = new THREE.WebGLRenderTarget(this.data.imageWidth, this.data.imageHeight, { + format: THREE.RGBAFormat, + minFilter: THREE.LinearFilter, + magFilter: THREE.NearestFilter, + encoding: THREE.sRGBEncoding, + depth: false, + stencil: false + }); + + this.camera = new THREE.PerspectiveCamera(50, this.renderTarget.width / this.renderTarget.height, 0.1, 30000); + this.camera.rotation.set(0, Math.PI, 0); + this.el.setObject3D("camera", this.camera); + + const material = new THREE.MeshBasicMaterial({ + map: this.renderTarget.texture + }); + + // Bit of a hack here to only update the renderTarget when the screens are in view and at a reduced FPS + material.map.isVideoTexture = true; + material.map.update = () => { + if (performance.now() - this.lastUpdate >= 1000 / this.data.previewFPS) { + this.updateRenderTargetNextTick = true; + } + }; + + cameraModelPromise.then(model => { + const mesh = model.scene.clone(); + mesh.scale.set(2, 2, 2); + this.el.setObject3D("mesh", mesh); + + const width = 0.28; + const geometry = new THREE.PlaneGeometry(width, width / this.camera.aspect); + + const screen = new THREE.Mesh(geometry, material); + screen.rotation.set(0, Math.PI, 0); + screen.position.set(0, 0, -0.042); + this.el.setObject3D("screen", screen); + + const selfieScreen = new THREE.Mesh(geometry, material); + selfieScreen.position.set(0, 0.4, 0); + selfieScreen.scale.set(-2, 2, 2); + this.el.setObject3D("selfieScreen", selfieScreen); + + this.updateRenderTargetNextTick = true; + }); + }, + + play() { + this.el.addEventListener("stateadded", this.stateAdded); + }, + + pause() { + this.el.removeEventListener("stateadded", this.stateAdded); + }, + + stateAdded(evt) { + if (evt.detail === "activated") { + this.takeSnapshotNextTick = true; + } + }, + + tock: (function() { + const tempScale = new THREE.Vector3(); + return function tock() { + const sceneEl = this.el.sceneEl; + const renderer = this.renderer || sceneEl.renderer; + const now = performance.now(); + + if (!this.playerHead) { + const headEl = document.getElementById("player-head"); + this.playerHead = headEl && headEl.object3D; + } + + if (this.takeSnapshotNextTick || this.updateRenderTargetNextTick) { + if (this.playerHead) { + tempScale.copy(this.playerHead.scale); + this.playerHead.scale.set(1, 1, 1); + } + const tmpVRFlag = renderer.vr.enabled; + const tmpOnAfterRender = sceneEl.object3D.onAfterRender; + delete sceneEl.object3D.onAfterRender; + renderer.vr.enabled = false; + renderer.render(sceneEl.object3D, this.camera, this.renderTarget, true); + renderer.vr.enabled = tmpVRFlag; + sceneEl.object3D.onAfterRender = tmpOnAfterRender; + if (this.playerHead) { + this.playerHead.scale.copy(tempScale); + } + this.lastUpdate = now; + this.updateRenderTargetNextTick = false; + } + + if (this.takeSnapshotNextTick) { + const width = this.renderTarget.width; + const height = this.renderTarget.height; + if (!this.snapPixels) { + this.snapPixels = new Uint8Array(width * height * 4); + } + renderer.readRenderTargetPixels(this.renderTarget, 0, 0, width, height, this.snapPixels); + pixelsToPNG(this.snapPixels, width, height).then(file => { + const { entity, orientation } = addMedia(file, "#interactable-media", undefined, true); + orientation.then(() => { + entity.object3D.position.copy(this.el.object3D.position).add(new THREE.Vector3(0, -0.5, 0)); + entity.object3D.rotation.copy(this.el.object3D.rotation); + sceneEl.emit("object_spawned", { objectType: ObjectTypes.CAMERA }); + }); + }); + this.takeSnapshotNextTick = false; + } + }; + })() +}); diff --git a/src/components/character-controller.js b/src/components/character-controller.js index 6edfd332a2fdc707d3d5a484b468218ff3b55000..b0dd84323e3d6eb7027df2d938fbb66a37705789 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -1,6 +1,7 @@ const CLAMP_VELOCITY = 0.01; const MAX_DELTA = 0.2; const EPS = 10e-6; +const MAX_WARNINGS = 10; /** * Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly. @@ -25,6 +26,8 @@ AFRAME.registerComponent("character-controller", { this.accelerationInput = new THREE.Vector3(0, 0, 0); this.pendingSnapRotationMatrix = new THREE.Matrix4(); this.angularVelocity = 0; // Scalar value because we only allow rotation around Y + this._withinWarningLimit = true; + this._warningCount = 0; this.setAccelerationInput = this.setAccelerationInput.bind(this); this.snapRotateLeft = this.snapRotateLeft.bind(this); this.snapRotateRight = this.snapRotateRight.bind(this); @@ -158,26 +161,41 @@ AFRAME.registerComponent("character-controller", { }; })(), + _warnWithWarningLimit: function(msg) { + if (!this._withinWarningLimit) return; + this._warningCount++; + if (this._warningCount > MAX_WARNINGS) { + this._withinWarningLimit = false; + msg = "Warning count exceeded. Will not log further warnings"; + } + console.warn("character-controller", msg); + }, + + _setNavNode: function(pos) { + if (this.navNode !== null) return; + const { pathfinder } = this.el.sceneEl.systems.nav; + this.navNode = + pathfinder.getClosestNode(pos, this.navZone, this.navGroup, true) || + pathfinder.getClosestNode(pos, this.navZone, this.navGroup); + }, + setPositionOnNavMesh: function(start, end, object3D) { - const pathfinder = this.el.sceneEl.systems.nav.pathfinder; - const zone = this.navZone; - if (zone in pathfinder.zones) { - if (this.navGroup == null) { - this.navGroup = pathfinder.getGroup(zone, end); - } - this.navNode = this.navNode || pathfinder.getClosestNode(end, zone, this.navGroup, true); - this.navNode = pathfinder.clampStep(start, end, this.navNode, zone, this.navGroup, object3D.position); + const { pathfinder } = this.el.sceneEl.systems.nav; + if (!(this.navZone in pathfinder.zones)) return; + if (this.navGroup === null) { + this.navGroup = pathfinder.getGroup(this.navZone, end, true, true); } + this._setNavNode(end); + this.navNode = pathfinder.clampStep(start, end, this.navNode, this.navZone, this.navGroup, object3D.position); }, resetPositionOnNavMesh: function(position, navPosition, object3D) { - const pathfinder = this.el.sceneEl.systems.nav.pathfinder; - const zone = this.navZone; - if (zone in pathfinder.zones) { - this.navGroup = pathfinder.getGroup(zone, position); - this.navNode = pathfinder.getClosestNode(navPosition, zone, this.navGroup, true) || this.navNode; - this.navNode = pathfinder.clampStep(position, position, this.navNode, zone, this.navGroup, object3D.position); - } + const { pathfinder } = this.el.sceneEl.systems.nav; + if (!(this.navZone in pathfinder.zones)) return; + this.navGroup = pathfinder.getGroup(this.navZone, navPosition, true, true); + this.navNode = null; + this._setNavNode(navPosition); + pathfinder.clampStep(position, navPosition, this.navNode, this.navZone, this.navGroup, object3D.position); }, updateVelocity: function(dt) { diff --git a/src/components/css-class.js b/src/components/css-class.js index 77882e9bce9e2e00c6d02f02677b9388b8110d14..4d4da5fd332a2a06574a43346b5c801b74067138 100644 --- a/src/components/css-class.js +++ b/src/components/css-class.js @@ -6,6 +6,7 @@ AFRAME.registerComponent("css-class", { schema: { type: "string" }, + multiple: true, init() { this.el.classList.add(this.data); }, diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index c801f76188260c695d7121ef242a00e1d5db4ea9..421b084c309710e9442dd3f44019fdcb0cad94d5 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -3,39 +3,113 @@ const TARGET_TYPE_INTERACTABLE = 2; const TARGET_TYPE_UI = 4; const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI; +/** + * Manages targeting and physical cursor location. Has the following responsibilities: + * + * - Tracking which entities in the scene can be targeted by the cursor (`objects`). + * - Performing a raycast per-frame or on-demand to identify which entity is being currently targeted. + * - Updating the visual presentation and position of the `cursor` entity and `line` component per frame. + * - Sending an event when an entity is targeted or un-targeted. + */ AFRAME.registerComponent("cursor-controller", { - dependencies: ["raycaster", "line"], + dependencies: ["line"], schema: { cursor: { type: "selector" }, camera: { type: "selector" }, - maxDistance: { default: 3 }, - minDistance: { default: 0 }, + far: { default: 3 }, + near: { default: 0 }, cursorColorHovered: { default: "#2F80ED" }, cursorColorUnhovered: { default: "#FFFFFF" }, rayObject: { type: "selector" }, - useMousePos: { default: true }, - drawLine: { default: false } + drawLine: { default: false }, + objects: { default: "" } }, init: function() { this.enabled = true; - this.inVR = false; - this.isMobile = AFRAME.utils.device.isMobile(); this.currentTargetType = TARGET_TYPE_NONE; - this.currentDistance = this.data.maxDistance; + this.currentDistance = this.data.far; this.currentDistanceMod = 0; this.mousePos = new THREE.Vector2(); this.wasCursorHovered = false; - this.origin = new THREE.Vector3(); - this.direction = new THREE.Vector3(); - this.raycasterAttr = this.el.getAttribute("raycaster"); - this.controllerQuaternion = new THREE.Quaternion(); this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); this._handleCursorLoaded = this._handleCursorLoaded.bind(this); this.data.cursor.addEventListener("loaded", this._handleCursorLoaded); + + // raycaster state + this.targets = []; + this.intersection = null; + this.raycaster = new THREE.Raycaster(); + this.setDirty = this.setDirty.bind(this); + this.dirty = true; + }, + + update: function() { + this.raycaster.far = this.data.far; + this.raycaster.near = this.data.near; + this.setDirty(); + }, + + play: function() { + this.observer = new MutationObserver(this.setDirty); + this.observer.observe(this.el.sceneEl, { childList: true, attributes: true, subtree: true }); + this.el.sceneEl.addEventListener("object3dset", this.setDirty); + this.el.sceneEl.addEventListener("object3dremove", this.setDirty); + }, + + pause: function() { + this.observer.disconnect(); + this.el.sceneEl.removeEventListener("object3dset", this.setDirty); + this.el.sceneEl.removeEventListener("object3dremove", this.setDirty); + }, + + setDirty: function() { + this.dirty = true; }, + populateEntities: function(selector, target) { + target.length = 0; + const els = this.data.objects ? this.el.sceneEl.querySelectorAll(this.data.objects) : this.el.sceneEl.children; + for (let i = 0; i < els.length; i++) { + if (els[i].object3D) { + target.push(els[i].object3D); + } + } + }, + + emitIntersectionEvents: function(prevIntersection, currIntersection) { + // if we are now intersecting something, and previously we were intersecting nothing or something else + if (currIntersection && (!prevIntersection || currIntersection.object.el !== prevIntersection.object.el)) { + this.data.cursor.emit("raycaster-intersection", { el: currIntersection.object.el }); + } + // if we were intersecting something, but now we are intersecting nothing or something else + if (prevIntersection && (!currIntersection || currIntersection.object.el !== prevIntersection.object.el)) { + this.data.cursor.emit("raycaster-intersection-cleared", { el: prevIntersection.object.el }); + } + }, + + performRaycast: (function() { + const rayObjectRotation = new THREE.Quaternion(); + const rawIntersections = []; + return function performRaycast(targets) { + if (this.data.rayObject) { + const rayObject = this.data.rayObject.object3D; + rayObject.updateMatrixWorld(); + rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld); + this.raycaster.ray.origin.setFromMatrixPosition(rayObject.matrixWorld); + this.raycaster.ray.direction.set(0, 0, -1).applyQuaternion(rayObjectRotation); + } else { + this.raycaster.setFromCamera(this.mousePos, this.data.camera.components.camera.camera); // camera + } + const prevIntersection = this.intersection; + rawIntersections.length = 0; + this.raycaster.intersectObjects(targets, true, rawIntersections); + this.intersection = rawIntersections.find(x => x.object.el); + this.emitIntersectionEvents(prevIntersection, this.intersection); + }; + })(), + enable: function() { this.enabled = true; }, @@ -45,14 +119,7 @@ AFRAME.registerComponent("cursor-controller", { this.setCursorVisibility(false); }, - updateRay: function() { - this.raycasterAttr.origin = this.origin; - this.raycasterAttr.direction = this.direction; - this.el.setAttribute("raycaster", this.raycasterAttr, true); - }, - tick: (() => { - const rayObjectRotation = new THREE.Quaternion(); const cameraPos = new THREE.Vector3(); return function() { @@ -60,27 +127,20 @@ AFRAME.registerComponent("cursor-controller", { return; } - if (this.data.useMousePos) { - this.setRaycasterWithMousePos(); - } else { - const rayObject = this.data.rayObject.object3D; - rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld); - this.direction - .set(0, 0, -1) - .applyQuaternion(rayObjectRotation) - .normalize(); - this.origin.setFromMatrixPosition(rayObject.matrixWorld); - this.updateRay(); + if (this.dirty) { + this.populateEntities(this.data.objects, this.targets); + this.dirty = false; } - const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); - if (isGrabbing) { + this.performRaycast(this.targets); + + if (this.isInteracting()) { const distance = Math.min( - this.data.maxDistance, - Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod) + this.data.far, + Math.max(this.data.near, this.currentDistance - this.currentDistanceMod) ); - this.direction.multiplyScalar(distance); - this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + this.data.cursor.object3D.position.copy(this.raycaster.ray.origin); + this.data.cursor.object3D.position.addScaledVector(this.raycaster.ray.direction, distance); } else { this.currentDistanceMod = 0; this.updateDistanceAndTargetType(); @@ -96,7 +156,10 @@ AFRAME.registerComponent("cursor-controller", { } if (this.data.drawLine) { - this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); + this.el.setAttribute("line", { + start: this.raycaster.ray.origin.clone(), + end: this.data.cursor.object3D.position.clone() + }); } // The cursor will always be oriented towards the player about its Y axis, so objects held by the cursor will rotate towards the player. @@ -106,26 +169,15 @@ AFRAME.registerComponent("cursor-controller", { }; })(), - setRaycasterWithMousePos: function() { - const camera = this.data.camera.components.camera.camera; - const raycaster = this.el.components.raycaster.raycaster; - raycaster.setFromCamera(this.mousePos, camera); - this.origin.copy(raycaster.ray.origin); - this.direction.copy(raycaster.ray.direction); - this.updateRay(); - }, - updateDistanceAndTargetType: function() { - let intersection = null; - const intersections = this.el.components.raycaster.intersections; - if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - intersection = intersections[0]; + const intersection = this.intersection; + if (intersection && intersection.distance <= this.data.far) { this.data.cursor.object3D.position.copy(intersection.point); - this.currentDistance = intersections[0].distance; + this.currentDistance = intersection.distance; } else { - this.currentDistance = this.data.maxDistance; - this.direction.multiplyScalar(this.currentDistance); - this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + this.currentDistance = this.data.far; + this.data.cursor.object3D.position.copy(this.raycaster.ray.origin); + this.data.cursor.object3D.position.addScaledVector(this.raycaster.ray.direction, this.currentDistance); } if (!intersection) { @@ -147,12 +199,15 @@ AFRAME.registerComponent("cursor-controller", { }, forceCursorUpdate: function() { - this.setRaycasterWithMousePos(); - this.el.components.raycaster.checkIntersections(); + this.performRaycast(this.targets); this.updateDistanceAndTargetType(); this.data.cursor.components["static-body"].syncToPhysics(); }, + isInteracting: function() { + return this.data.cursor.components["super-hands"].state.has("grab-start"); + }, + startInteraction: function() { if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) { this.data.cursor.emit("cursor-grab", {}); @@ -161,22 +216,24 @@ AFRAME.registerComponent("cursor-controller", { return false; }, - moveCursor: function(x, y) { - this.mousePos.set(x, y); - }, - endInteraction: function() { this.data.cursor.emit("cursor-release", {}); }, + moveCursor: function(x, y) { + this.mousePos.set(x, y); + }, + changeDistanceMod: function(delta) { - const { minDistance, maxDistance } = this.data; + const { near, far } = this.data; const targetDistanceMod = this.currentDistanceMod + delta; const moddedDistance = this.currentDistance - targetDistanceMod; - if (moddedDistance > maxDistance || moddedDistance < minDistance) { - return; + if (moddedDistance > far || moddedDistance < near) { + return false; } + this.currentDistanceMod = targetDistanceMod; + return true; }, _handleCursorLoaded: function() { @@ -185,6 +242,8 @@ AFRAME.registerComponent("cursor-controller", { }, remove: function() { + this.emitIntersectionEvents(this.intersection, null); + this.intersection = null; this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); } }); diff --git a/src/components/gltf-bundle.js b/src/components/gltf-bundle.js index 72163ec542922ea21c5c7612b54b193f5c256a97..7ad7840822faac29b7e70f148fe2ff12ae3d6819 100644 --- a/src/components/gltf-bundle.js +++ b/src/components/gltf-bundle.js @@ -28,7 +28,7 @@ AFRAME.registerComponent("gltf-bundle", { const src = new URL(asset.src, this.baseURL).href; const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src, inflate: true }); + gltfEl.setAttribute("gltf-model-plus", { src, useCache: false, inflate: true }); loaded.push(new Promise(resolve => gltfEl.addEventListener("model-loaded", resolve))); this.el.appendChild(gltfEl); } diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index d3c8b56c0e9cf632b50b64a8539d9f4f1d180500..b4d25bb3989aea9619a3ebc2d4f6b3bc8e5de9cf 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -1,9 +1,10 @@ +import nextTick from "../utils/next-tick"; import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js"; import MobileStandardMaterial from "../materials/MobileStandardMaterial"; import cubeMapPosX from "../assets/images/cubemap/posx.jpg"; import cubeMapNegX from "../assets/images/cubemap/negx.jpg"; import cubeMapPosY from "../assets/images/cubemap/posy.jpg"; -import cubeMapNegY from "../assets/images/cubemap/negx.jpg"; +import cubeMapNegY from "../assets/images/cubemap/negy.jpg"; import cubeMapPosZ from "../assets/images/cubemap/posz.jpg"; import cubeMapNegZ from "../assets/images/cubemap/negz.jpg"; @@ -32,57 +33,54 @@ AFRAME.GLTFModelPlus = { } }; -// From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87 -// Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573 -function cloneGltf(gltf) { - const skinnedMeshes = {}; - gltf.scene.traverse(node => { - if (!node.name) { - node.name = node.uuid; - } - if (node.isSkinnedMesh) { - skinnedMeshes[node.name] = node; - } - }); +function parallelTraverse(a, b, callback) { + callback(a, b); - const clone = { - animations: gltf.animations, - scene: gltf.scene.clone(true) - }; + for (let i = 0; i < a.children.length; i++) { + parallelTraverse(a.children[i], b.children[i], callback); + } +} - const cloneBones = {}; - const cloneSkinnedMeshes = {}; +// Modified version of Don McCurdy's AnimationUtils.clone +// https://github.com/mrdoob/three.js/pull/14494 +function cloneSkinnedMesh(source) { + const cloneLookup = new Map(); - clone.scene.traverse(node => { - if (node.isBone) { - cloneBones[node.name] = node; - } + const clone = source.clone(); - if (node.isSkinnedMesh) { - cloneSkinnedMeshes[node.name] = node; - } + parallelTraverse(source, clone, function(sourceNode, clonedNode) { + cloneLookup.set(sourceNode, clonedNode); }); - for (const name in skinnedMeshes) { - const skinnedMesh = skinnedMeshes[name]; - const skeleton = skinnedMesh.skeleton; - const cloneSkinnedMesh = cloneSkinnedMeshes[name]; + source.traverse(function(sourceMesh) { + if (!sourceMesh.isSkinnedMesh) return; - const orderedCloneBones = []; + const sourceBones = sourceMesh.skeleton.bones; + const clonedMesh = cloneLookup.get(sourceMesh); - for (let i = 0; i < skeleton.bones.length; ++i) { - const cloneBone = cloneBones[skeleton.bones[i].name]; - orderedCloneBones.push(cloneBone); - } + clonedMesh.skeleton = sourceMesh.skeleton.clone(); - cloneSkinnedMesh.bind(new THREE.Skeleton(orderedCloneBones, skeleton.boneInverses), cloneSkinnedMesh.matrixWorld); + clonedMesh.skeleton.bones = sourceBones.map(function(sourceBone) { + if (!cloneLookup.has(sourceBone)) { + throw new Error("Required bones are not descendants of the given object."); + } - // cloneSkinnedMesh.material = skinnedMesh.material.clone(); - } + return cloneLookup.get(sourceBone); + }); + + clonedMesh.bind(clonedMesh.skeleton, sourceMesh.bindMatrix); + }); return clone; } +function cloneGltf(gltf) { + return { + animations: gltf.animations, + scene: cloneSkinnedMesh(gltf.scene) + }; +} + /// Walks the tree of three.js objects starting at the given node, using the GLTF data /// and template data to construct A-Frame entities and components when necessary. /// (It's unnecessary to construct entities for subtrees that have no component data @@ -100,7 +98,15 @@ const inflateEntities = function(node, templates, isRoot) { } } - const nodeHasBehavior = node.userData.components || node.name in templates; + const hubsComponents = node.userData.gltfExtensions && node.userData.gltfExtensions.HUBS_components; + + // We can remove support for legacy components when our environment, avatar and interactable models are + // updated to match Spoke output. + const legacyComponents = node.userData.components; + + const entityComponents = hubsComponents || legacyComponents; + + const nodeHasBehavior = !!entityComponents || node.name in templates; if (!nodeHasBehavior && !childEntities.length && !isRoot) { return null; // we don't need an entity for this node } @@ -138,7 +144,7 @@ const inflateEntities = function(node, templates, isRoot) { node.matrix.identity(); el.setObject3D(node.type.toLowerCase(), node); - if (node.userData.components && "nav-mesh" in node.userData.components) { + if (entityComponents && "nav-mesh" in entityComponents) { el.setObject3D("mesh", node); } @@ -155,7 +161,6 @@ const inflateEntities = function(node, templates, isRoot) { node.parent.animations = node.animations; } - const entityComponents = node.userData.components; if (entityComponents) { for (const prop in entityComponents) { if (entityComponents.hasOwnProperty(prop) && AFRAME.GLTFModelPlus.components.hasOwnProperty(prop)) { @@ -184,12 +189,6 @@ function attachTemplate(root, name, templateRoot) { } } -function nextTick() { - return new Promise(resolve => { - setTimeout(resolve, 0); - }); -} - function getFilesFromSketchfabZip(src) { return new Promise((resolve, reject) => { const worker = new SketchfabZipWorker(); @@ -284,6 +283,7 @@ AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, contentType: { type: "string" }, + useCache: { default: true }, inflate: { default: false } }, @@ -305,6 +305,18 @@ AFRAME.registerComponent("gltf-model-plus", { }); }, + async loadModel(src, contentType, technique, useCache) { + if (useCache) { + if (!GLTFCache[src]) { + GLTFCache[src] = await loadGLTF(src, contentType, technique); + } + + return cloneGltf(GLTFCache[src]); + } else { + return await loadGLTF(src, contentType, technique); + } + }, + async applySrc(src, contentType) { try { // If the src attribute is a selector, get the url from the asset item. @@ -324,11 +336,7 @@ AFRAME.registerComponent("gltf-model-plus", { return; } - if (!GLTFCache[src]) { - GLTFCache[src] = loadGLTF(src, contentType, this.preferredTechnique); - } - - const model = cloneGltf(await GLTFCache[src]); + const gltf = await this.loadModel(src, contentType, this.preferredTechnique, this.data.useCache); // If we started loading something else already // TODO: there should be a way to cancel loading instead @@ -337,8 +345,13 @@ AFRAME.registerComponent("gltf-model-plus", { // If we had inflated something already before, clean that up this.removeInflatedEl(); - this.model = model.scene || model.scenes[0]; - this.model.animations = model.animations; + this.model = gltf.scene || gltf.scenes[0]; + this.model.animations = gltf.animations; + + if (gltf.animations.length > 0) { + this.el.setAttribute("animation-mixer", {}); + this.el.components["animation-mixer"].initMixer(gltf.animations); + } let object3DToSet = this.model; if (this.data.inflate && (this.inflatedEl = inflateEntities(this.model, this.templates, true))) { @@ -352,7 +365,9 @@ AFRAME.registerComponent("gltf-model-plus", { attachTemplate(this.el, name, this.templates[name]); } } + this.el.setObject3D("mesh", object3DToSet); + this.el.emit("model-loaded", { format: "gltf", model: this.model }); } catch (e) { delete GLTFCache[src]; diff --git a/src/components/grabbable-toggle.js b/src/components/grabbable-toggle.js new file mode 100644 index 0000000000000000000000000000000000000000..5a11d5698d5ba53cbafeab63286bf142b1699bfb --- /dev/null +++ b/src/components/grabbable-toggle.js @@ -0,0 +1,185 @@ +/* global AFRAME, THREE */ +const inherit = AFRAME.utils.extendDeep; +const physicsCore = require("super-hands/reaction_components/prototypes/physics-grab-proto.js"); +const buttonsCore = require("super-hands/reaction_components/prototypes/buttons-proto.js"); +// new object with all core modules +const base = inherit({}, physicsCore, buttonsCore); +AFRAME.registerComponent( + "grabbable-toggle", + inherit(base, { + schema: { + maxGrabbers: { type: "int", default: NaN }, + invert: { default: false }, + suppressY: { default: false }, + primaryReleaseEvents: { default: ["primary_hand_release"] }, + secondaryReleaseEvents: { default: ["secondary_hand_release"] } + }, + init: function() { + this.GRABBED_STATE = "grabbed"; + this.GRAB_EVENT = "grab-start"; + this.UNGRAB_EVENT = "grab-end"; + this.grabbed = false; + this.grabbers = []; + this.constraints = new Map(); + this.deltaPositionIsValid = false; + this.grabDistance = undefined; + this.grabDirection = { x: 0, y: 0, z: -1 }; + this.grabOffset = { x: 0, y: 0, z: 0 }; + // persistent object speeds up repeat setAttribute calls + this.destPosition = { x: 0, y: 0, z: 0 }; + this.deltaPosition = new THREE.Vector3(); + this.targetPosition = new THREE.Vector3(); + this.physicsInit(); + + this.el.addEventListener(this.GRAB_EVENT, e => this.start(e)); + this.el.addEventListener(this.UNGRAB_EVENT, e => this.end(e)); + this.el.addEventListener("mouseout", e => this.lostGrabber(e)); + + this.toggle = false; + this.lastGrabber = null; + }, + update: function() { + this.physicsUpdate(); + this.xFactor = this.data.invert ? -1 : 1; + this.zFactor = this.data.invert ? -1 : 1; + this.yFactor = (this.data.invert ? -1 : 1) * !this.data.suppressY; + }, + tick: (function() { + const q = new THREE.Quaternion(); + const v = new THREE.Vector3(); + + return function() { + let entityPosition; + if (this.grabber) { + // reflect on z-axis to point in same direction as the laser + this.targetPosition.copy(this.grabDirection); + this.targetPosition + .applyQuaternion(this.grabber.object3D.getWorldQuaternion(q)) + .setLength(this.grabDistance) + .add(this.grabber.object3D.getWorldPosition(v)) + .add(this.grabOffset); + if (this.deltaPositionIsValid) { + // relative position changes work better with nested entities + this.deltaPosition.sub(this.targetPosition); + entityPosition = this.el.getAttribute("position"); + this.destPosition.x = entityPosition.x - this.deltaPosition.x * this.xFactor; + this.destPosition.y = entityPosition.y - this.deltaPosition.y * this.yFactor; + this.destPosition.z = entityPosition.z - this.deltaPosition.z * this.zFactor; + this.el.setAttribute("position", this.destPosition); + } else { + this.deltaPositionIsValid = true; + } + this.deltaPosition.copy(this.targetPosition); + } + }; + })(), + remove: function() { + this.el.removeEventListener(this.GRAB_EVENT, this.start); + this.el.removeEventListener(this.UNGRAB_EVENT, this.end); + this.physicsRemove(); + }, + start: function(evt) { + if (evt.defaultPrevented || !this.startButtonOk(evt)) { + return; + } + // room for more grabbers? + let grabAvailable = !Number.isFinite(this.data.maxGrabbers) || this.grabbers.length < this.data.maxGrabbers; + if (Number.isFinite(this.data.maxGrabbers) && !grabAvailable && this.grabbed) { + this.grabbers[0].components["super-hands"].onGrabEndButton(); + grabAvailable = true; + } + if (this.grabbers.indexOf(evt.detail.hand) === -1 && grabAvailable) { + if (!evt.detail.hand.object3D) { + console.warn("grabbable entities must have an object3D"); + return; + } + this.grabbers.push(evt.detail.hand); + // initiate physics if available, otherwise manual + if (!this.physicsStart(evt) && !this.grabber) { + this.grabber = evt.detail.hand; + this.resetGrabber(); + } + // notify super-hands that the gesture was accepted + if (evt.preventDefault) { + evt.preventDefault(); + } + this.grabbed = true; + this.el.addState(this.GRABBED_STATE); + } + }, + end: function(evt) { + const handIndex = this.grabbers.indexOf(evt.detail.hand); + if (evt.defaultPrevented || !this.endButtonOk(evt)) { + return; + } + + const type = evt.detail && evt.detail.buttonEvent ? evt.detail.buttonEvent.type : null; + + if (this.toggle && this.lastGrabber !== this.grabbers[0]) { + this.toggle = false; + this.lastGrabber = null; + } + + if (handIndex !== -1) { + this.grabber = this.grabbers[0]; + } + + if ((this.isPrimaryRelease(type) && !this.toggle) || this.isSecondaryRelease(type)) { + this.toggle = true; + this.lastGrabber = this.grabbers[0]; + return; + } else if (this.toggle && this.isPrimaryRelease(type)) { + this.toggle = false; + this.lastGrabber = null; + } + + if (handIndex !== -1) { + this.grabbers.splice(handIndex, 1); + this.grabber = this.grabbers[0]; + } + + this.physicsEnd(evt); + if (!this.resetGrabber()) { + this.grabbed = false; + this.el.removeState(this.GRABBED_STATE); + } + if (evt.preventDefault) { + evt.preventDefault(); + } + }, + resetGrabber: (() => { + const objPos = new THREE.Vector3(); + const grabPos = new THREE.Vector3(); + return function() { + if (!this.grabber) { + return false; + } + const raycaster = this.grabber.getAttribute("raycaster"); + this.deltaPositionIsValid = false; + this.grabDistance = this.el.object3D + .getWorldPosition(objPos) + .distanceTo(this.grabber.object3D.getWorldPosition(grabPos)); + if (raycaster) { + this.grabDirection = raycaster.direction; + this.grabOffset = raycaster.origin; + } + return true; + }; + })(), + lostGrabber: function(evt) { + const i = this.grabbers.indexOf(evt.relatedTarget); + // if a queued, non-physics grabber leaves the collision zone, forget it + if (i !== -1 && evt.relatedTarget !== this.grabber && !this.physicsIsConstrained(evt.relatedTarget)) { + this.grabbers.splice(i, 1); + } + }, + + isPrimaryRelease(type) { + return this.data.primaryReleaseEvents.indexOf(type) !== -1; + }, + + isSecondaryRelease(type) { + return this.data.secondaryReleaseEvents.indexOf(type) !== -1; + } + }) +); diff --git a/src/components/hand-poses.js b/src/components/hand-poses.js index 98c1282cb714ae3b5a2d915ff5a5e237b127ca87..e45f083d48dca6b315cd626b2187e3c91e4e3016 100644 --- a/src/components/hand-poses.js +++ b/src/components/hand-poses.js @@ -1,3 +1,5 @@ +import { findAncestorWithComponent } from "../utils/scene-graph"; + const POSES = { open: "allOpen", thumbDown: "thumbDown", @@ -22,12 +24,10 @@ AFRAME.registerComponent("hand-pose", { init() { this.pose = 0; this.animatePose = this.animatePose.bind(this); - this.mixer = this.el.components["animation-mixer"]; - const object3DMap = this.mixer.el.object3DMap; - const rootObj = object3DMap.mesh || object3DMap.scene; - this.clipActionObject = rootObj.parent; + const mixerEl = findAncestorWithComponent(this.el, "animation-mixer"); + this.mixer = mixerEl.components["animation-mixer"].mixer; const suffix = this.id == "left" ? "_L" : "_R"; - this.from = this.to = this.mixer.mixer.clipAction(POSES.open + suffix, this.clipActionObject); + this.from = this.to = this.mixer.clipAction(POSES.open + suffix); this.from.play(); const getNetworkedAvatar = el => { @@ -59,15 +59,15 @@ AFRAME.registerComponent("hand-pose", { const duration = 0.065; const suffix = this.id == "left" ? "_L" : "_R"; - this.from = this.mixer.mixer.clipAction(prev + suffix, this.clipActionObject); - this.to = this.mixer.mixer.clipAction(curr + suffix, this.clipActionObject); + this.from = this.mixer.clipAction(prev + suffix); + this.to = this.mixer.clipAction(curr + suffix); this.from.fadeOut(duration); this.to.fadeIn(duration); this.to.play(); this.from.play(); - this.mixer.mixer.update(0.001); + this.mixer.update(0.001); } }); diff --git a/src/components/heightfield.js b/src/components/heightfield.js new file mode 100644 index 0000000000000000000000000000000000000000..57853a5ccb54ac0060212264e82e21b62a1ffbfe --- /dev/null +++ b/src/components/heightfield.js @@ -0,0 +1,27 @@ +/* global CANNON */ +AFRAME.registerComponent("heightfield", { + init() { + this.el.addEventListener("componentinitialized", e => { + if (e.detail.name === "static-body") { + this.generateAndAddHeightfield(this.el.components["static-body"]); + } + }); + this.el.setAttribute("static-body", { shape: "none", mass: 0 }); + }, + generateAndAddHeightfield(body) { + const { offset, distance, data } = this.data; + + const orientation = new CANNON.Quaternion(); + orientation.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); + + const rotation = new CANNON.Quaternion(); + rotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2); + rotation.mult(orientation, orientation); + + const cannonOffset = new CANNON.Vec3(offset.x, offset.y, offset.z); + + const shape = new CANNON.Heightfield(data, { elementSize: distance }); + + body.addShape(shape, cannonOffset, orientation); + } +}); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index a4fe8c9655ecffe3161a86b09fa3b6f6cdbdb19b..aa0d8f83f1bf2e57a6a884b16c154b41d22a0bb2 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -11,23 +11,25 @@ AFRAME.registerComponent("in-world-hud", { init() { this.mic = this.el.querySelector(".mic"); this.freeze = this.el.querySelector(".freeze"); - this.bubble = this.el.querySelector(".bubble"); + this.pen = this.el.querySelector(".pen"); + this.cameraBtn = this.el.querySelector(".cameraBtn"); this.background = this.el.querySelector(".bg"); const renderOrder = window.APP.RENDER_ORDER; this.mic.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.freeze.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; - this.bubble.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; + this.pen.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; + this.cameraBtn.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND; this.updateButtonStates = () => { this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted")); this.freeze.setAttribute("icon-button", "active", this.el.sceneEl.is("frozen")); - this.bubble.setAttribute("icon-button", "active", this.el.sceneEl.is("spacebubble")); + this.pen.setAttribute("icon-button", "active", this.el.sceneEl.is("pen")); }; this.updateButtonStates(); this.onStateChange = evt => { - if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "spacebubble")) return; + if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen")) return; this.updateButtonStates(); }; @@ -39,8 +41,12 @@ AFRAME.registerComponent("in-world-hud", { this.el.emit("action_freeze"); }; - this.onBubbleClick = () => { - this.el.emit("action_space_bubble"); + this.onPenClick = () => { + this.el.emit("spawn_pen"); + }; + + this.onCameraClick = () => { + this.el.emit("action_spawn_camera"); }; }, @@ -50,7 +56,8 @@ AFRAME.registerComponent("in-world-hud", { this.mic.addEventListener("click", this.onMicClick); this.freeze.addEventListener("click", this.onFreezeClick); - this.bubble.addEventListener("click", this.onBubbleClick); + this.pen.addEventListener("mousedown", this.onPenClick); + this.cameraBtn.addEventListener("click", this.onCameraClick); }, pause() { @@ -59,6 +66,7 @@ AFRAME.registerComponent("in-world-hud", { this.mic.removeEventListener("click", this.onMicClick); this.freeze.removeEventListener("click", this.onFreezeClick); - this.bubble.removeEventListener("click", this.onBubbleClick); + this.pen.removeEventListener("mousedown", this.onPenClick); + this.cameraBtn.removeEventListener("click", this.onCameraClick); } }); diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js index df36b38d8c4bb81a2f8fd98824cbd3ecf4ef7f15..356213fbf319e5037bcb21166aa11e794ccc5f94 100644 --- a/src/components/input-configurator.js +++ b/src/components/input-configurator.js @@ -21,7 +21,6 @@ AFRAME.registerComponent("input-configurator", { this.isMobile = AFRAME.utils.device.isMobile(); this.eventHandlers = []; this.controllerQueue = []; - this.hasPointingDevice = false; this.cursor = this.data.cursorController.components["cursor-controller"]; this.gazeTeleporter = this.data.gazeTeleporter.components["teleport-controls"]; this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; @@ -89,12 +88,10 @@ AFRAME.registerComponent("input-configurator", { this.actionEventHandler = new ActionEventHandler(this.el.sceneEl, this.cursor); this.eventHandlers.push(this.actionEventHandler); - this.cursor.el.setAttribute("cursor-controller", "useMousePos", !this.inVR); - if (this.inVR) { this.cameraController.pause(); this.cursorRequiresManagement = true; - this.cursor.el.setAttribute("cursor-controller", "minDistance", 0); + this.cursor.el.setAttribute("cursor-controller", "near", 0); if (this.isMobile) { this.eventHandlers.push(new GearVRMouseEventsHandler(this.cursor, this.gazeTeleporter)); } else { @@ -108,7 +105,7 @@ AFRAME.registerComponent("input-configurator", { this.addLookOnMobile(); } else { this.eventHandlers.push(new MouseEventsHandler(this.cursor, this.cameraController)); - this.cursor.el.setAttribute("cursor-controller", "minDistance", 0.3); + this.cursor.el.setAttribute("cursor-controller", "near", 0.3); } } }, @@ -145,25 +142,30 @@ AFRAME.registerComponent("input-configurator", { }, updateController: function() { - this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR; - this.cursor.el.setAttribute("cursor-controller", "drawLine", this.hasPointingDevice); - this.cursor.setCursorVisibility(true); + const controllerData = this.controllerQueue.length ? this.controllerQueue[0] : null; - if (this.hasPointingDevice) { - const controllerData = this.controllerQueue[0]; - const hand = controllerData.handedness; + if (controllerData) { this.controller = controllerData.controller; - this.cursor.el.setAttribute("cursor-controller", { - rayObject: hand === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject - }); + this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); } else { this.controller = null; - this.cursor.el.setAttribute("cursor-controller", { rayObject: this.data.gazeCursorRayObject }); + this.actionEventHandler.setHandThatAlsoDrivesCursor(null); } - if (this.actionEventHandler) { - this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); + let rayObject; + let drawLine; + if (controllerData && this.inVR) { + rayObject = + controllerData.handedness === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject; + drawLine = true; + } else if (this.inVR) { + rayObject = this.data.gazeCursorRayObject; + drawLine = false; + } else { + rayObject = null; + drawLine = false; } + this.cursor.el.setAttribute("cursor-controller", { rayObject, drawLine }); } }); diff --git a/src/components/loop-animation.js b/src/components/loop-animation.js index 76d3a0ec2135d174abf6c014363d3ea8561bd97e..8cd158427ca688a17a501f02fff3f7fffac675f7 100644 --- a/src/components/loop-animation.js +++ b/src/components/loop-animation.js @@ -1,62 +1,61 @@ +import { findAncestorWithComponent } from "../utils/scene-graph"; + /** * Loops the given clip using this entity's animation mixer * @component loop-animation */ AFRAME.registerComponent("loop-animation", { - dependencies: ["animation-mixer"], schema: { - clip: { type: "string", required: true } + clip: { type: "string" } }, + init() { - const object3DMap = this.el.object3DMap; - this.model = object3DMap.mesh || object3DMap.scene; + this.mixerEl = findAncestorWithComponent(this.el, "animation-mixer"); - if (this.model) { - this.mixer = this.el.components["animation-mixer"].mixer; - } else { - this.onModelLoaded = this.onModelLoaded.bind(this); - this.el.addEventListener("model-loaded", this.onModelLoaded); + if (!this.mixerEl) { + console.warn("loop-animation component could not find an animation-mixer in its ancestors."); + return; } - }, - - onModelLoaded(event) { - const animationMixerComponent = this.el.components["animation-mixer"]; - this.model = event.detail.model; - this.mixer = animationMixerComponent.mixer; - - this.updateClipState(true); - this.el.removeEventListener(this.onModelLoaded); + this.updateClip(); }, update(oldData) { - if (oldData.clip !== this.data.clip && this.model) { - this.updateClipState(true); + if (oldData.clip !== this.data.clip && this.mixerEl) { + this.updateClip(); } }, - updateClipState(play) { - const model = this.model; + updateClip() { + const { mixer, animations } = this.mixerEl.components["animation-mixer"]; const clipName = this.data.clip; - for (const clip of this.model.animations) { - if (clip.name === clipName) { - const action = this.mixer.clipAction(clip, model.parent); + if (animations.length === 0) { + return; + } + + let clip; - if (play) { - action.enabled = true; - action.setLoop(THREE.LoopRepeat, Infinity).play(); - } else { - action.stop(); - } + if (!clipName) { + clip = animations[0]; + } else { + clip = animations.find(({ name }) => name === clipName); + } - break; - } + if (!clip) { + return; } + + const action = mixer.clipAction(clip, this.el.object3D); + action.enabled = true; + action.setLoop(THREE.LoopRepeat, Infinity).play(); + this.currentAction = action; }, destroy() { - this.updateClipState(false); - this.el.removeEventListener(this.onModelLoaded); + if (this.currentAction) { + this.currentAction.enabled = false; + this.currentAction.stop(); + } } }); diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 802d582c5a1b1dfac3961420dcf8bfde6d0619b6..5dcd47beef202851c03b2cd28d8089875873914f 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -1,5 +1,6 @@ import { getBox, getScaleCoefficient } from "../utils/auto-box-collider"; -import { resolveMedia, fetchMaxContentIndex } from "../utils/media-utils"; +import { guessContentType, proxiedUrlFor, resolveUrl } from "../utils/media-utils"; +import { addAnimationComponents } from "../utils/animation"; import "three/examples/js/loaders/GLTFLoader"; import loadingObjectSrc from "../assets/LoadingObject_Atom.glb"; @@ -9,17 +10,28 @@ gltfLoader.load(loadingObjectSrc, gltf => { loadingObject = gltf; }); +const fetchContentType = url => { + return fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type")); +}; + +const fetchMaxContentIndex = url => { + return fetch(url).then(r => parseInt(r.headers.get("x-max-content-index"))); +}; + AFRAME.registerComponent("media-loader", { schema: { src: { type: "string" }, - index: { type: "number" }, - resize: { default: false } + resize: { default: false }, + resolve: { default: false }, + contentType: { default: null } }, init() { this.onError = this.onError.bind(this); this.showLoader = this.showLoader.bind(this); this.clearLoadingTimeout = this.clearLoadingTimeout.bind(this); + this.shapeAdded = false; + this.hasBakedShapes = false; }, setShapeAndScale(resize) { @@ -27,9 +39,10 @@ AFRAME.registerComponent("media-loader", { const box = getBox(this.el, mesh); const scaleCoefficient = resize ? getScaleCoefficient(0.5, box) : 1; this.el.object3DMap.mesh.scale.multiplyScalar(scaleCoefficient); - if (this.el.body && this.el.body.shapes.length > 1) { + if (this.el.body && this.shapeAdded && this.el.body.shapes.length > 1) { this.el.removeAttribute("shape"); - } else { + this.shapeAdded = false; + } else if (!this.hasBakedShapes) { const center = new THREE.Vector3(); const { min, max } = box; const halfExtents = { @@ -43,6 +56,7 @@ AFRAME.registerComponent("media-loader", { shape: "box", halfExtents: halfExtents }); + this.shapeAdded = true; } }, @@ -72,6 +86,7 @@ AFRAME.registerComponent("media-loader", { this.loadingClip.play(); } this.el.setObject3D("mesh", mesh); + this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > 0); this.setShapeAndScale(true); delete this.showLoaderTimeout; }, @@ -87,7 +102,7 @@ AFRAME.registerComponent("media-loader", { async update(oldData) { try { - const { src, index } = this.data; + const { src } = this.data; if (src !== oldData.src && !this.showLoaderTimeout) { this.showLoaderTimeout = setTimeout(this.showLoader, 100); @@ -95,49 +110,54 @@ AFRAME.registerComponent("media-loader", { if (!src) return; - const { raw, origin, images, contentType } = await resolveMedia(src, false, index); + let canonicalUrl = src; + let accessibleUrl = src; + let contentType = this.data.contentType; + + if (this.data.resolve) { + const result = await resolveUrl(src); + canonicalUrl = result.origin; + contentType = (result.meta && result.meta.expected_content_type) || contentType; + } + + // todo: we don't need to proxy for many things if the canonical URL has permissive CORS headers + accessibleUrl = proxiedUrlFor(canonicalUrl); + + // if the component creator didn't know the content type, we didn't get it from reticulum, and + // we don't think we can infer it from the extension, we need to make a HEAD request to find it out + contentType = contentType || guessContentType(canonicalUrl) || (await fetchContentType(accessibleUrl)); // We don't want to emit media_resolved for index updates. if (src !== oldData.src) { - this.el.emit("media_resolved", { src, raw, origin, contentType }); + this.el.emit("media_resolved", { src, raw: accessibleUrl, contentType }); } - const isPDF = contentType.startsWith("application/pdf"); if (contentType.startsWith("video/") || contentType.startsWith("audio/")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-image"); this.el.addEventListener("video-loaded", this.clearLoadingTimeout, { once: true }); - this.el.setAttribute("media-video", { src: raw }); + this.el.setAttribute("media-video", { src: accessibleUrl }); this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] }); - } else if (contentType.startsWith("image/") || isPDF) { + } else if (contentType.startsWith("image/")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); - this.el.addEventListener( - "image-loaded", - async () => { - this.clearLoadingTimeout(); - if (isPDF) { - const maxIndex = await fetchMaxContentIndex(src, images.png); - this.el.setAttribute("media-pager", { index, maxIndex }); - } - }, - { once: true } - ); - const imageSrc = isPDF ? images.png : raw; - const imageContentType = isPDF ? "image/png" : contentType; - - if (!isPDF) { - this.el.removeAttribute("media-pager"); - } - - this.el.setAttribute("media-image", { src: imageSrc, contentType: imageContentType }); + this.el.addEventListener("image-loaded", this.clearLoadingTimeout, { once: true }); + this.el.removeAttribute("media-pager"); + this.el.setAttribute("media-image", { src: accessibleUrl, contentType }); + this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] }); + } else if (contentType.startsWith("application/pdf")) { + this.el.removeAttribute("gltf-model-plus"); + this.el.removeAttribute("media-video"); + // two small differences: + // 1. we pass the canonical URL to the pager so it can easily make subresource URLs + // 2. we don't remove the media-image component -- media-pager uses that internally + this.el.setAttribute("media-pager", { src: canonicalUrl }); + this.el.addEventListener("preview-loaded", this.clearLoadingTimeout, { once: true }); this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] }); } else if ( contentType.includes("application/octet-stream") || contentType.includes("x-zip-compressed") || - contentType.startsWith("model/gltf") || - src.endsWith(".gltf") || - src.endsWith(".glb") + contentType.startsWith("model/gltf") ) { this.el.removeAttribute("media-image"); this.el.removeAttribute("media-video"); @@ -146,13 +166,15 @@ AFRAME.registerComponent("media-loader", { "model-loaded", () => { this.clearLoadingTimeout(); + this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > (this.shapeAdded ? 1 : 0)); this.setShapeAndScale(this.data.resize); + addAnimationComponents(this.el); }, { once: true } ); this.el.addEventListener("model-error", this.onError, { once: true }); this.el.setAttribute("gltf-model-plus", { - src: raw, + src: accessibleUrl, contentType: contentType, inflate: true }); @@ -168,48 +190,63 @@ AFRAME.registerComponent("media-loader", { AFRAME.registerComponent("media-pager", { schema: { - index: { type: "string" }, - maxIndex: { type: "string" } + src: { type: "string" }, + index: { default: 0 } }, init() { + this.toolbar = null; this.onNext = this.onNext.bind(this); this.onPrev = this.onPrev.bind(this); - - const template = document.getElementById("paging-toolbar"); - this.el.appendChild(document.importNode(template.content, true)); - this.toolbar = this.el.querySelector(".paging-toolbar"); - // we have to wait a tick for the attach callbacks to get fired for the elements in a template - setTimeout(() => { - this.nextButton = this.el.querySelector(".next-button [text-button]"); - this.prevButton = this.el.querySelector(".prev-button [text-button]"); - this.pageLabel = this.el.querySelector(".page-label"); - - this.nextButton.addEventListener("click", this.onNext); - this.prevButton.addEventListener("click", this.onPrev); - - this.update(); - }, 0); + this.el.addEventListener("image-loaded", async e => { + // unfortunately, since we loaded the page image in an img tag inside media-image, we have to make a second + // request for the same page to read out the max-content-index header + this.maxIndex = await fetchMaxContentIndex(e.detail.src); + // if this is the first image we ever loaded, set up the UI + if (this.toolbar == null) { + const template = document.getElementById("paging-toolbar"); + this.el.appendChild(document.importNode(template.content, true)); + this.toolbar = this.el.querySelector(".paging-toolbar"); + // we have to wait a tick for the attach callbacks to get fired for the elements in a template + setTimeout(() => { + this.nextButton = this.el.querySelector(".next-button [text-button]"); + this.prevButton = this.el.querySelector(".prev-button [text-button]"); + this.pageLabel = this.el.querySelector(".page-label"); + + this.nextButton.addEventListener("click", this.onNext); + this.prevButton.addEventListener("click", this.onPrev); + + this.update(); + this.el.emit("preview-loaded"); + }, 0); + } else { + this.update(); + } + }); }, update() { - if (!this.pageLabel) return; - this.pageLabel.setAttribute("text", "value", `${this.data.index + 1}/${this.data.maxIndex + 1}`); - this.repositionToolbar(); + if (!this.data.src) return; + const pageSrc = proxiedUrlFor(this.data.src, this.data.index); + this.el.setAttribute("media-image", { src: pageSrc, contentType: "image/png" }); + if (this.pageLabel) { + this.pageLabel.setAttribute("text", "value", `${this.data.index + 1}/${this.maxIndex + 1}`); + this.repositionToolbar(); + } }, remove() { - this.nextButton.removeEventListener("click", this.onNext); - this.prevButton.removeEventListener("click", this.onPrev); - this.el.removeChild(this.toolbar); + if (this.toolbar) { + this.el.removeChild(this.toolbar); + } }, onNext() { - this.el.setAttribute("media-loader", "index", Math.min(this.data.index + 1, this.data.maxIndex)); + this.el.setAttribute("media-pager", "index", Math.min(this.data.index + 1, this.maxIndex)); }, onPrev() { - this.el.setAttribute("media-loader", "index", Math.max(this.data.index - 1, 0)); + this.el.setAttribute("media-pager", "index", Math.max(this.data.index - 1, 0)); }, repositionToolbar() { diff --git a/src/components/media-views.js b/src/components/media-views.js index 0b12d5130e8c86ffbd18cfdda3f3c1256c8608d5..37afb0d8c7bbc8f536c12fbc7bed5a4268716932 100644 --- a/src/components/media-views.js +++ b/src/components/media-views.js @@ -420,6 +420,6 @@ AFRAME.registerComponent("media-image", { fitToTexture(this.el, texture); - this.el.emit("image-loaded"); + this.el.emit("image-loaded", { src: this.data.src }); } }); diff --git a/src/components/offset-relative-to.js b/src/components/offset-relative-to.js index e00acd4e57a1842e5b8116c5448a3c2088443eab..877cfcf8a118a13eb94200925d1ac53053d9faa1 100644 --- a/src/components/offset-relative-to.js +++ b/src/components/offset-relative-to.js @@ -13,6 +13,9 @@ AFRAME.registerComponent("offset-relative-to", { on: { type: "string" }, + orientation: { + default: 1 // see doc/image_orientations.gif + }, selfDestruct: { default: false } @@ -27,6 +30,9 @@ AFRAME.registerComponent("offset-relative-to", { }, updateOffset: (function() { + const y = new THREE.Vector3(0, 1, 0); + const z = new THREE.Vector3(0, 0, -1); + const QUARTER_CIRCLE = Math.PI / 2; const offsetVector = new THREE.Vector3(); return function() { const obj = this.el.object3D; @@ -40,6 +46,38 @@ AFRAME.registerComponent("offset-relative-to", { this.el.body && this.el.body.position.copy(obj.position); target.getWorldQuaternion(obj.quaternion); this.el.body && this.el.body.quaternion.copy(obj.quaternion); + + // See doc/image_orientations.gif + switch (this.data.orientation) { + case 8: + obj.rotateOnAxis(z, 3 * QUARTER_CIRCLE); + break; + case 7: + obj.rotateOnAxis(z, 3 * QUARTER_CIRCLE); + obj.rotateOnAxis(y, 2 * QUARTER_CIRCLE); + break; + case 6: + obj.rotateOnAxis(z, QUARTER_CIRCLE); + break; + case 5: + obj.rotateOnAxis(z, QUARTER_CIRCLE); + obj.rotateOnAxis(y, 2 * QUARTER_CIRCLE); + break; + case 4: + obj.rotateOnAxis(z, 2 * QUARTER_CIRCLE); + obj.rotateOnAxis(y, 2 * QUARTER_CIRCLE); + break; + case 3: + obj.rotateOnAxis(z, 2 * QUARTER_CIRCLE); + break; + case 2: + obj.rotateOnAxis(y, 2 * QUARTER_CIRCLE); + break; + case 1: + default: + break; + } + if (this.data.selfDestruct) { if (this.data.on) { this.el.sceneEl.removeEventListener(this.data.on, this.updateOffset); diff --git a/src/components/pitch-yaw-rotator.js b/src/components/pitch-yaw-rotator.js index 7af5799e72077977f81abd5ddff3a4686c46fe1f..bb7eb4c56d2ec9a21fb58d72deda5be1402c35a3 100644 --- a/src/components/pitch-yaw-rotator.js +++ b/src/components/pitch-yaw-rotator.js @@ -1,4 +1,6 @@ const degToRad = THREE.Math.degToRad; +const radToDeg = THREE.Math.radToDeg; + AFRAME.registerComponent("pitch-yaw-rotator", { schema: { minPitch: { default: -50 }, @@ -13,10 +15,16 @@ AFRAME.registerComponent("pitch-yaw-rotator", { look(deltaPitch, deltaYaw) { const { minPitch, maxPitch } = this.data; this.pitch += deltaPitch; - this.pitch = Math.max(minPitch, Math.min(maxPitch, this.pitch)); + this.pitch = THREE.Math.clamp(this.pitch, minPitch, maxPitch); this.yaw += deltaYaw; }, + set(pitch, yaw) { + const { minPitch, maxPitch } = this.data; + this.pitch = THREE.Math.clamp(radToDeg(pitch), minPitch, maxPitch); + this.yaw = radToDeg(yaw); + }, + tick() { this.el.object3D.rotation.set(degToRad(this.pitch), degToRad(this.yaw), 0); this.el.object3D.rotation.order = "YXZ"; diff --git a/src/components/scene-components.js b/src/components/scene-components.js new file mode 100644 index 0000000000000000000000000000000000000000..f7e562787e60612b1c5b1a57147416746898f1a2 --- /dev/null +++ b/src/components/scene-components.js @@ -0,0 +1,23 @@ +import "./ambient-light"; +import "./animation-mixer"; +import "./audio-feedback"; +import "./css-class"; +import "./directional-light"; +import "./duck"; +import "./gltf-model-plus"; +import "./heightfield"; +import "./hemisphere-light"; +import "./hide-when-quality"; +import "./layers"; +import "./loop-animation"; +import "./media-loader"; +import "./point-light"; +import "./quack"; +import "./scene-shadow"; +import "./scene-preview-camera"; +import "./skybox"; +import "./spawn-controller"; +import "./spot-light"; +import "./sticky-object"; +import "./super-spawner"; +import "./water"; diff --git a/src/components/scene-preview-camera.js b/src/components/scene-preview-camera.js new file mode 100644 index 0000000000000000000000000000000000000000..2576d487b7932670bf5c9349649f392feda27372 --- /dev/null +++ b/src/components/scene-preview-camera.js @@ -0,0 +1,65 @@ +/** + * Nicely pans the camera for previewing a scene. There's some weirdness with this right now + * since it ends up panning in a direction dependent upon the start camera orientation, + * but it's good enough for now. + */ +function lerp(start, end, t) { + return (1 - t) * start + t * end; +} + +AFRAME.registerComponent("scene-preview-camera", { + schema: { + duration: { default: 90, type: "number" }, + positionOnly: { default: false, type: "boolean" } + }, + + init: function() { + this.startPoint = this.el.object3D.position.clone(); + this.startRotation = new THREE.Quaternion(); + this.startRotation.setFromEuler(this.el.object3D.rotation); + + this.targetPoint = new THREE.Vector3(1, 0.5, -0.5); + this.targetPoint.applyMatrix4(this.el.object3D.matrix); + this.targetPoint.add(new THREE.Vector3(0, 0, -2)); + + const targetRotDelta = new THREE.Euler(-0.15, 0.0, 0.15); + this.targetRotation = new THREE.Quaternion(); + this.targetRotation.setFromEuler(targetRotDelta); + this.targetRotation.premultiply(this.startRotation); + + this.startTime = new Date().getTime(); + this.backwards = false; + this.ranOnePass = false; + }, + + tick: function() { + let t = (new Date().getTime() - this.startTime) / (1000.0 * this.data.duration); + t = Math.min(1.0, Math.max(0.0, t)); + + if (!this.ranOnePass) { + t = t * (2 - t); + } else { + t = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + } + + const from = this.backwards ? this.targetPoint : this.startPoint; + const to = this.backwards ? this.startPoint : this.targetPoint; + const fromRot = this.backwards ? this.targetRotation : this.startRotation; + const toRot = this.backwards ? this.startRotation : this.targetRotation; + const newRot = new THREE.Quaternion(); + + THREE.Quaternion.slerp(fromRot, toRot, newRot, t); + + this.el.object3D.position.set(lerp(from.x, to.x, t), lerp(from.y, to.y, t), lerp(from.z, to.z, t)); + + if (!this.data.positionOnly) { + this.el.object3D.rotation.setFromQuaternion(newRot); + } + + if (t >= 0.9999) { + this.ranOnePass = true; + this.backwards = !this.backwards; + this.startTime = new Date().getTime(); + } + } +}); diff --git a/src/components/skybox.js b/src/components/skybox.js index e5f26b12fe93d006fa15920378223dc1d566243b..04508ac2ac00dd78433ac2965c50a59c9c360551 100644 --- a/src/components/skybox.js +++ b/src/components/skybox.js @@ -271,6 +271,12 @@ AFRAME.registerComponent("skybox", { const z = distance * Math.sin(phi) * Math.cos(theta); uniforms.sunPosition.value.set(x, y, z).normalize(); + + // HACK Remove this if condition and always set the scale based on distance when the existing environments + // have their sky scales set to 1. + if (this.el.object3D.scale.x === 1) { + this.sky.scale.set(distance, distance, distance); + } } }, diff --git a/src/components/spot-light.js b/src/components/spot-light.js index bcfd65dae2f03679c019235cd1338986bf5dd2ed..3d97f3c674dbcd14bde2b34b618c70e6be21073e 100644 --- a/src/components/spot-light.js +++ b/src/components/spot-light.js @@ -11,6 +11,7 @@ AFRAME.registerComponent("spot-light", { init() { const el = this.el; this.light = new THREE.SpotLight(); + this.light.position.set(0, 0, 0); this.light.target.position.set(0, 0, 1); this.light.add(this.light.target); this.light.decay = 2; diff --git a/src/components/stats-plus.css b/src/components/stats-plus.css index f3850c47b80cbc9f456c30f9ef1a1e7b17fb500f..f710791c800d43edb4f1ddfcf3962828ac017bb8 100644 --- a/src/components/stats-plus.css +++ b/src/components/stats-plus.css @@ -14,11 +14,14 @@ font-family: monospace; cursor: pointer; position: absolute; - top: 40px; - right: 16px; + top: 50px; + right: 6px; padding: 8px 12px; color: #aaa; font-size: 10px; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; user-select: none; } @@ -26,5 +29,8 @@ right: 10px; left: auto; top: 10px; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; user-select: none; } diff --git a/src/components/sticky-object.js b/src/components/sticky-object.js index d84c82313100622864b9457161b71b842edf6309..61d04814a95b2e30cc79743a039dec1c0a6d5235 100644 --- a/src/components/sticky-object.js +++ b/src/components/sticky-object.js @@ -47,17 +47,21 @@ AFRAME.registerComponent("sticky-object", { }, _onRelease() { + // Happens if the object is still being held by another hand + if (this.el.is("grabbed")) return; + if ( - !this.el.is("grabbed") && this.data.autoLockOnRelease && this.el.body.velocity.lengthSquared() < this.data.autoLockSpeedLimit * this.data.autoLockSpeedLimit ) { this.setLocked(true); } + this.el.body.collisionResponse = true; }, _onGrab() { this.setLocked(false); + this.el.body.collisionResponse = false; }, remove() { diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 844dc1df1706f2a51f55afd531e41898724fcb77..d2e04e23911cb499c1ce2c9eb7a1a62ab1ae2e53 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -6,13 +6,18 @@ AFRAME.registerComponent("super-networked-interactable", { schema: { hapticsMassVelocityFactor: { default: 0.1 }, - counter: { type: "selector" } + counter: { type: "selector" }, + scrollScaleDelta: { default: 0.1 }, + minScale: { default: 0.1 }, + maxScale: { default: 100 } }, init: function() { this.system = this.el.sceneEl.systems.physics; this.counter = this.data.counter.components["networked-counter"]; this.hand = null; + this.currentScale = new THREE.Vector3(); + this.currentScale.copy(this.el.getAttribute("scale")); NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.networkedEl = networkedEl; @@ -23,31 +28,29 @@ AFRAME.registerComponent("super-networked-interactable", { } }); + this._stateAdded = this._stateAdded.bind(this); this._onGrabStart = this._onGrabStart.bind(this); + this._onGrabEnd = this._onGrabEnd.bind(this); this._onOwnershipLost = this._onOwnershipLost.bind(this); this.el.addEventListener("grab-start", this._onGrabStart); + this.el.addEventListener("grab-end", this._onGrabEnd); this.el.addEventListener("ownership-lost", this._onOwnershipLost); + this.el.addEventListener("stateadded", this._stateAdded); this.system.addComponent(this); }, remove: function() { this.counter.deregister(this.el); this.el.removeEventListener("grab-start", this._onGrabStart); + this.el.removeEventListener("grab-end", this._onGrabEnd); this.el.removeEventListener("ownership-lost", this._onOwnershipLost); + this.el.removeEventListener("stateadded", this._stateAdded); this.system.removeComponent(this); }, - afterStep: function() { - if (this.el.is("grabbed") && this.hand && this.hand.components.hasOwnProperty("haptic-feedback")) { - const hapticFeedback = this.hand.components["haptic-feedback"]; - let velocity = this.el.body.velocity.lengthSquared() * this.el.body.mass * this.data.hapticsMassVelocityFactor; - velocity = Math.min(1, velocity); - hapticFeedback.pulse(velocity); - } - }, - _onGrabStart: function(e) { this.hand = e.detail.hand; + this.hand.emit("haptic_pulse", { intensity: "high" }); if (this.networkedEl && !NAF.utils.isMine(this.networkedEl)) { if (NAF.utils.takeOwnership(this.networkedEl)) { this.el.setAttribute("body", { type: "dynamic" }); @@ -57,6 +60,11 @@ AFRAME.registerComponent("super-networked-interactable", { this.hand = null; } } + this.currentScale.copy(this.el.getAttribute("scale")); + }, + + _onGrabEnd: function(e) { + if (e.detail.hand) e.detail.hand.emit("haptic_pulse", { intensity: "high" }); }, _onOwnershipLost: function() { @@ -64,5 +72,26 @@ AFRAME.registerComponent("super-networked-interactable", { this.el.emit("grab-end", { hand: this.hand }); this.hand = null; this.counter.deregister(this.el); + }, + + _changeScale: function(delta) { + if (this.el.is("grabbed") && this.el.components.hasOwnProperty("stretchable")) { + this.currentScale.addScalar(delta).clampScalar(this.data.minScale, this.data.maxScale); + this.el.setAttribute("scale", this.currentScale); + this.el.components["stretchable"].stretchBody(this.el, this.currentScale); + } + }, + + _stateAdded(evt) { + switch (evt.detail) { + case "scaleUp": + this._changeScale(-this.data.scrollScaleDelta); + break; + case "scaleDown": + this._changeScale(this.data.scrollScaleDelta); + break; + default: + break; + } } }); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index f760a29e25cc1fb680b55a0b504a42202948f0da..77e18f67a737f5bf5fdfddf30b41e624d65ef1a4 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -4,7 +4,7 @@ import { ObjectContentOrigins } from "../object-types"; let nextGrabId = 0; /** - * Spawns networked objects when grabbed. + * Spawns networked objects when grabbed or when a specified event is fired. * @namespace network * @component super-spawner */ @@ -13,7 +13,17 @@ AFRAME.registerComponent("super-spawner", { /** * Source of the media asset the spawner will spawn when grabbed. This can be a gltf, video, or image, or a url that the reticiulm media API can resolve to a gltf, video, or image. */ - src: { default: "https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf" }, + src: { default: "" }, + + /** + * Whether to use the Reticulum media resolution API to interpret the src URL (e.g. find a video URL for Youtube videos.) + */ + resolve: { default: false }, + + /** + * The template to use for this object + */ + template: { default: "" }, /** * Spawn the object at a custom position, rather than at the center of the spanwer. @@ -27,11 +37,17 @@ AFRAME.registerComponent("super-spawner", { useCustomSpawnRotation: { default: false }, spawnRotation: { type: "vec4" }, + /** + * Spawn the object with a custom scale, rather than copying that of the spawner. + */ + useCustomSpawnScale: { default: false }, + spawnScale: { type: "vec3" }, + /** * The events to emit for programmatically grabbing and releasing objects */ - grabEvents: { default: ["cursor-grab", "hand_grab"] }, - releaseEvents: { default: ["cursor-release", "hand_release"] }, + grabEvents: { default: ["cursor-grab", "primary_hand_grab"] }, + releaseEvents: { default: ["cursor-release", "primary_hand_release"] }, /** * The spawner will become invisible and ungrabbable for this ammount of time after being grabbed. This can prevent rapidly spawning objects. @@ -41,7 +57,22 @@ AFRAME.registerComponent("super-spawner", { /** * Center the spawned object on the hand that grabbed it after it finishes loading. By default the object will be grabbed relative to where the spawner was grabbed */ - centerSpawnedObject: { default: false } + centerSpawnedObject: { default: false }, + + /** + * Optional event to listen for to spawn an object on the preferred superHand + */ + spawnEvent: { type: "string" }, + + /** + * The superHand to use if an object is spawned via spawnEvent + */ + superHand: { type: "selector" }, + + /** + * The cursor superHand to use if an object is spawned via spawnEvent + */ + cursorSuperHand: { type: "selector" } }, init() { @@ -49,16 +80,26 @@ AFRAME.registerComponent("super-spawner", { this.cooldownTimeout = null; this.onGrabStart = this.onGrabStart.bind(this); this.onGrabEnd = this.onGrabEnd.bind(this); + + this.onSpawnEvent = this.onSpawnEvent.bind(this); + + this.sceneEl = document.querySelector("a-scene"); }, play() { this.el.addEventListener("grab-start", this.onGrabStart); this.el.addEventListener("grab-end", this.onGrabEnd); + if (this.data.spawnEvent) { + this.sceneEl.addEventListener(this.data.spawnEvent, this.onSpawnEvent); + } }, pause() { this.el.removeEventListener("grab-start", this.onGrabStart); this.el.removeEventListener("grab-end", this.onGrabEnd); + if (this.data.spawnEvent) { + this.sceneEl.removeEventListener(this.data.spawnEvent, this.onSpawnEvent); + } if (this.cooldownTimeout) { clearTimeout(this.cooldownTimeout); @@ -72,6 +113,36 @@ AFRAME.registerComponent("super-spawner", { this.heldEntities.clear(); }, + async onSpawnEvent() { + const controllerCount = this.el.sceneEl.components["input-configurator"].controllerQueue.length; + const using6DOF = controllerCount > 1 && this.el.sceneEl.is("vr-mode"); + const hand = using6DOF ? this.data.superHand : this.data.cursorSuperHand; + + if (this.cooldownTimeout || !hand) { + return; + } + + const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER, this.data.resolve).entity; + + hand.object3D.getWorldPosition(entity.object3D.position); + hand.object3D.getWorldQuaternion(entity.object3D.quaternion); + if (this.data.useCustomSpawnScale) { + entity.object3D.scale.copy(this.data.spawnScale); + } + + this.activateCooldown(); + + await waitForEvent("body-loaded", entity); + + hand.object3D.getWorldPosition(entity.object3D.position); + + if (!using6DOF) { + for (let i = 0; i < this.data.grabEvents.length; i++) { + hand.emit(this.data.grabEvents[i], { targetEntity: entity }); + } + } + }, + async onGrabStart(e) { if (this.cooldownTimeout) { return; @@ -84,13 +155,15 @@ AFRAME.registerComponent("super-spawner", { const thisGrabId = nextGrabId++; this.heldEntities.set(hand, thisGrabId); - const entity = addMedia(this.data.src, ObjectContentOrigins.SPAWNER); + const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER, this.data.resolve).entity; + entity.object3D.position.copy( this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.object3D.position ); entity.object3D.rotation.copy( this.data.useCustomSpawnRotation ? this.data.spawnRotation : this.el.object3D.rotation ); + entity.object3D.scale.copy(this.data.useCustomSpawnScale ? this.data.spawnScale : this.el.object3D.scale); this.activateCooldown(); diff --git a/src/components/tools/drawing-manager.js b/src/components/tools/drawing-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..08d1ef5e0e8b1b0cfacee02905f1ad7ba6502ca7 --- /dev/null +++ b/src/components/tools/drawing-manager.js @@ -0,0 +1,51 @@ +/** + * Drawing Manager + * Manages what networked-drawings are available to pen components + * @namespace drawing + * @component drawing-manager + */ +AFRAME.registerComponent("drawing-manager", { + init() { + this._onComponentInitialized = this._onComponentInitialized.bind(this); + + this.drawingToPen = new Map(); + }, + + remove() { + if (this.drawingEl) { + this.drawingEl.removeEventListener("componentinitialized", this._onComponentInitialized); + } + }, + + _onComponentInitialized(e) { + if (e.detail.name == "networked-drawing") { + this.drawing = this.drawingEl.components["networked-drawing"]; + } + }, + + createDrawing() { + if (!this.drawingEl) { + this.drawingEl = document.createElement("a-entity"); + this.drawingEl.setAttribute("networked", "template: #interactable-drawing"); + this.el.sceneEl.appendChild(this.drawingEl); + + this.drawingEl.addEventListener("componentinitialized", this._onComponentInitialized); + } + }, + + getDrawing(pen) { + //TODO: future handling of multiple drawings + if (this.drawing && (!this.drawingToPen.has(this.drawing) || this.drawingToPen.get(this.drawing) === pen)) { + this.drawingToPen.set(this.drawing, pen); + return this.drawing; + } + + return null; + }, + + returnDrawing(pen) { + if (this.drawingToPen.has(this.drawing) && this.drawingToPen.get(this.drawing) === pen) { + this.drawingToPen.delete(this.drawing); + } + } +}); diff --git a/src/components/tools/networked-drawing.js b/src/components/tools/networked-drawing.js new file mode 100644 index 0000000000000000000000000000000000000000..518de9cd82489111ce6585861df99cfd26776015 --- /dev/null +++ b/src/components/tools/networked-drawing.js @@ -0,0 +1,540 @@ +/* global THREE */ +/** + * Networked Drawing + * Creates procedurally generated 'lines' (or tubes) that are networked. + * @namespace drawing + * @component networked-drawing + */ + +import SharedBufferGeometryManager from "../../utils/sharedbuffergeometrymanager"; + +const MSG_CONFIRM_CONNECT = 0; +const MSG_BUFFER_DATA = 1; +const MSG_BUFFER_DATA_FULL = 2; + +function copyData(fromArray, toArray, fromIndex, toIndex) { + let i = fromIndex - 1; + let j = -1; + while (i + 1 <= toIndex) { + toArray[++j] = fromArray[++i]; + } +} + +AFRAME.registerComponent("networked-drawing", { + schema: { + segments: { default: 8 }, //the number of "sides" the procedural tube should have + defaultRadius: { default: 0.01 }, //the radius of the procedural tube + maxDrawTimeout: { default: 600000 }, //the maximum time a drawn line will live + maxLines: { default: 50 }, //how many lines can persist before lines older than minDrawTime are removed + maxPointsPerLine: { default: 250 } //the max number of points a single line can have + }, + + init() { + this._receiveData = this._receiveData.bind(this); + + this.networkBuffer = []; + + this.sendNetworkBufferQueue = []; + + this.drawStarted = false; + this.lineStarted = false; + this.remoteLineStarted = false; + + this.receivedBufferParts = 0; + this.bufferIndex = 0; + this.connectedToOwner = false; + this.networkBufferInitialized = false; + + const options = { + vertexColors: THREE.VertexColors + }; + + this.color = new THREE.Color(); + this.radius = this.data.defaultRadius; + this.segments = this.data.segments; + + const material = new THREE.MeshStandardMaterial(options); + this.sharedBufferGeometryManager = new SharedBufferGeometryManager(); + // NOTE: 20 is approximate for how many floats per point are added. + // maxLines + 1 because a line can be currently drawing while at maxLines. + // Multiply by 1/3 (0.333) because 3 floats per vertex (x, y, z). + const maxBufferSize = Math.round(this.data.maxPointsPerLine * 20 * (this.data.maxLines + 1) * 0.333); + this.sharedBufferGeometryManager.addSharedBuffer(0, material, THREE.TriangleStripDrawMode, maxBufferSize); + + this.lastPoint = new THREE.Vector3(); + + this.lastSegments = []; + this.currentSegments = []; + for (let x = 0; x < this.segments; x++) { + this.lastSegments[x] = { + position: new THREE.Vector3(), + normal: new THREE.Vector3() + }; + this.currentSegments[x] = { + position: new THREE.Vector3(), + normal: new THREE.Vector3() + }; + } + + this.sharedBuffer = this.sharedBufferGeometryManager.getSharedBuffer(0); + this.drawing = this.sharedBuffer.drawing; + const sceneEl = document.querySelector("a-scene"); + this.scene = sceneEl.object3D; + this.scene.add(this.drawing); + + this.prevIdx = Object.assign({}, this.sharedBuffer.idx); + this.idx = Object.assign({}, this.sharedBuffer.idx); + this.vertexCount = 0; //number of vertices added for current line (used for line deletion). + this.networkBufferCount = 0; //number of items added to networkBuffer for current line (used for line deletion). + this.currentPointCount = 0; //number of points added for current line (used for maxPointsPerLine). + this.networkBufferHistory = []; //tracks vertexCount and networkBufferCount so that lines can be deleted. + + NAF.connection.onConnect(() => { + NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.networkedEl = networkedEl; + this.networkId = NAF.utils.getNetworkId(this.networkedEl); + this.drawingId = "drawing-" + this.networkId; + NAF.connection.subscribeToDataChannel(this.drawingId, this._receiveData); + }); + }); + }, + + remove() { + NAF.connection.unsubscribeToDataChannel(this.drawingId, this._receiveData); + + this.scene.remove(this.drawing); + }, + + tick() { + const connected = NAF.connection.isConnected() && this.networkedEl; + const isMine = connected && NAF.utils.isMine(this.networkedEl); + + if (!this.connectedToOwner && connected) { + const owner = NAF.utils.getNetworkOwner(this.networkedEl); + if (!isMine && NAF.connection.hasActiveDataChannel(owner)) { + NAF.connection.sendDataGuaranteed(owner, this.drawingId, { + type: MSG_CONFIRM_CONNECT, + clientId: NAF.clientId + }); + this.connectedToOwner = true; + } + } + + if (this.networkBuffer.length > 0 && connected) { + if (!isMine) { + this._drawFromNetwork(); + } else if (this.bufferIndex < this.networkBuffer.length) { + this._broadcastDrawing(); + } + } + + //TODO: handle possibility that a clientId gets stuck in sendNetworkBufferQueue + //if that client disconnects before this executes and an activeDataChannel is opened. + if (isMine && this.sendNetworkBufferQueue.length > 0) { + const connected = []; + for (let i = 0; i < this.sendNetworkBufferQueue.length; i++) { + if (NAF.connection.hasActiveDataChannel(this.sendNetworkBufferQueue[i])) { + connected.push(this.sendNetworkBufferQueue[i]); + } + } + for (let j = 0; j < connected.length; j++) { + const pos = this.sendNetworkBufferQueue.indexOf(connected[j]); + this._sendNetworkBuffer(connected[j]); + this.sendNetworkBufferQueue.splice(pos, 1); + } + } + + this._deleteLines(); + }, + + _broadcastDrawing: (() => { + const copyArray = []; + return function() { + copyArray.length = 0; + copyData(this.networkBuffer, copyArray, this.bufferIndex, this.networkBuffer.length - 1); + this.bufferIndex = this.networkBuffer.length; + NAF.connection.broadcastDataGuaranteed(this.drawingId, { type: MSG_BUFFER_DATA, buffer: copyArray }); + }; + })(), + + _drawFromNetwork: (() => { + const position = new THREE.Vector3(); + const direction = new THREE.Vector3(); + const normal = new THREE.Vector3(); + return function() { + const head = this.networkBuffer[0]; + let didWork = false; + while (head != null && this.networkBuffer.length >= 10) { + position.set(this.networkBuffer[0], this.networkBuffer[1], this.networkBuffer[2]); + direction.set(this.networkBuffer[3], this.networkBuffer[4], this.networkBuffer[5]); + this.radius = Math.round(direction.length() * 1000) / 1000; //radius is encoded as length of direction vector + direction.normalize(); + normal.set(this.networkBuffer[6], this.networkBuffer[7], this.networkBuffer[8]); + this.color.setHex(Math.round(normal.length()) - 1); //color is encoded as length of normal vector + normal.normalize(); + + this.networkBuffer.splice(0, 9); + + if (!this.remoteLineStarted) { + this.startDraw(position, direction, normal); + this.remoteLineStarted = true; + } + + if (this.networkBuffer[0] === null) { + this._endDraw(position, direction, normal); + this.remoteLineStarted = false; + this.networkBuffer.shift(); + } else { + this._draw(position, direction, normal); + didWork = true; + } + } + if (didWork) this._updateBuffer(); + }; + })(), + + _deleteLines() { + const length = this.networkBufferHistory.length; + if (length > 0) { + const now = Date.now(); + const time = this.networkBufferHistory[0].time; + if (length > this.data.maxLines || time + this.data.maxDrawTimeout <= now) { + const datum = this.networkBufferHistory[0]; + if (length > 1) { + datum.idxLength += 2 - (this.segments % 2); + this.networkBufferHistory[1].idxLength -= 2 - (this.segments % 2); + } + this.idx.position = datum.idxLength; + this.idx.uv = datum.idxLength; + this.idx.normal = datum.idxLength; + this.idx.color = datum.idxLength; + this.sharedBuffer.remove(this.prevIdx, this.idx); + this.networkBufferHistory.shift(); + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { + this.networkBuffer.splice(0, datum.networkBufferCount); + this.bufferIndex -= datum.networkBufferCount; + } + } + } + }, + + _sendNetworkBuffer: (() => { + const copyArray = []; + //This number needs to be approx. < ~6000 based on napkin math + //see: https://github.com/webrtc/adapter/blob/682e0f2439e139da6c0c406370eae820637b8sc1a/src/js/common_shim.js#L157 + const chunkAmount = 3000; + return function(clientId) { + if (NAF.utils.isMine(this.networkedEl)) { + if (this.networkBuffer.length <= chunkAmount) { + NAF.connection.sendDataGuaranteed(clientId, this.drawingId, { + type: MSG_BUFFER_DATA_FULL, + parts: 1, + buffer: this.networkBuffer + }); + } else { + let start = 0; + let end = 0; + while (end < this.networkBuffer.length) { + end = Math.min(end + chunkAmount, this.networkBuffer.length); + copyArray.length = 0; + copyData(this.networkBuffer, copyArray, start, end - 1); + start = end; + NAF.connection.sendDataGuaranteed(clientId, this.drawingId, { + type: MSG_BUFFER_DATA_FULL, + parts: Math.ceil(this.networkBuffer.length / chunkAmount), + buffer: copyArray + }); + } + } + } + }; + })(), + + _receiveData(_, dataType, data) { + switch (data.type) { + case MSG_CONFIRM_CONNECT: + this.sendNetworkBufferQueue.push(data.clientId); + break; + case MSG_BUFFER_DATA: + if (this.networkBufferInitialized) { + this.networkBuffer.push.apply(this.networkBuffer, data.buffer); + } + break; + case MSG_BUFFER_DATA_FULL: + this.networkBuffer.push.apply(this.networkBuffer, data.buffer); + if (++this.receivedBufferParts >= data.parts) { + this.networkBufferInitialized = true; + } + break; + } + }, + + getLastPoint() { + return this.lastPoint; + }, + + startDraw(position, direction, normal, color, radius) { + if (!NAF.connection.isConnected()) { + return; + } + + this.drawStarted = true; + + if (color) { + this.color.set(color); + } + if (radius) this.radius = radius; + + this.lastPoint.copy(position); + this._addToNetworkBuffer(position, direction, normal); + }, + + draw(position, direction, normal, color, radius) { + if (!NAF.connection.isConnected() || !this.drawStarted) { + return; + } + + if (color && color != "#" + this.color.getHexString().toUpperCase()) { + this.color.set(color); + } + if (radius) this.radius = radius; + + this._addToNetworkBuffer(position, direction, normal); + this._draw(position, direction, normal); + + this._updateBuffer(); + }, + + _draw: (() => { + const capNormal = new THREE.Vector3(); + return function(position, direction, normal, radiusMultiplier = 1.0) { + if (!this.lineStarted) { + this._generateSegments(this.lastSegments, position, direction, normal, this.radius * radiusMultiplier); + + if (this.networkBufferHistory.length === 0) { + //start with CW faceculling order + this._addDegenerateTriangle(); + } else { + //only do the following if the sharedBuffer is not empty + this._restartPrimitive(); + this._addDegenerateTriangle(); + if (this.segments % 2 === 0) { + //flip faceculling order if even numbered segments + this._addDegenerateTriangle(); + } + } + + //get normal for tip of cap + capNormal.copy(direction).negate(); + //get normals for rim of cap + for (let i = 0; i < this.segments; i++) { + this.lastSegments[i].normal.add(capNormal).multiplyScalar(0.5); + } + + this._drawCap(this.lastPoint, this.lastSegments, capNormal); + if (this.segments % 2 !== 0) { + //flip faceculling order if odd numbered segments + this._addDegenerateTriangle(); + } + + this.lineStarted = true; + } else { + this._generateSegments(this.currentSegments, position, direction, normal, this.radius * radiusMultiplier); + this._drawCylinder(); + + if (this.currentPointCount > this.data.maxPointsPerLine) { + this._drawEndCap(position, direction); + this._endLine(); + } + } + this.lastPoint.copy(position); + }; + })(), + + endDraw(position, direction, normal) { + this._endDraw(position, direction, normal); + this._updateBuffer(); + }, + + _endDraw(position, direction, normal) { + if (!this.lineStarted && this.drawStarted) { + this._drawPoint(position); + } else if (this.lineStarted && this.drawStarted) { + this._addToNetworkBuffer(position, direction, normal); + this._draw(position, direction, normal); + this._drawEndCap(position, direction); + } + this._endLine(); + }, + + _drawEndCap: (() => { + const projectedDirection = new THREE.Vector3(); + const projectedPoint = new THREE.Vector3(); + return function(position, direction) { + if (this.lineStarted && this.drawStarted) { + projectedDirection.copy(direction).multiplyScalar(this.radius); + projectedPoint.copy(position).add(projectedDirection); + this._addDegenerateTriangle(); //flip faceculling order before drawing end-cap + this._drawCap(projectedPoint, this.lastSegments, direction); + } + }; + })(), + + _endLine() { + if (!this.drawStarted) return; + + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) this._pushToNetworkBuffer(null); + + const datum = { + networkBufferCount: this.networkBufferCount, + idxLength: this.vertexCount - 1, + time: Date.now() + }; + this.networkBufferHistory.push(datum); + this.vertexCount = 0; + this.networkBufferCount = 0; + this.currentPointCount = 0; + this.lineStarted = false; + this.drawStarted = false; + }, + + _addToNetworkBuffer: (() => { + const copyDirection = new THREE.Vector3(); + const copyNormal = new THREE.Vector3(); + return function(position, direction, normal) { + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { + ++this.currentPointCount; + this._pushToNetworkBuffer(position.x); + this._pushToNetworkBuffer(position.y); + this._pushToNetworkBuffer(position.z); + copyDirection.copy(direction); + copyDirection.setLength(this.radius); //encode radius as length of direction vector + this._pushToNetworkBuffer(copyDirection.x); + this._pushToNetworkBuffer(copyDirection.y); + this._pushToNetworkBuffer(copyDirection.z); + copyNormal.copy(normal); + copyNormal.setLength(this.color.getHex() + 1); //encode color as length, add one in case color is black + this._pushToNetworkBuffer(copyNormal.x); + this._pushToNetworkBuffer(copyNormal.y); + this._pushToNetworkBuffer(copyNormal.z); + } + }; + })(), + + _pushToNetworkBuffer(val) { + ++this.networkBufferCount; + this.networkBuffer.push(val); + }, + + //draw a cylinder from last to current segments + _drawCylinder() { + //average the normals with the normals from the lastSegment + //not a perfect normal calculation, but works well enough + for (let i = 0; i < this.segments; i++) { + this.currentSegments[i].normal.add(this.lastSegments[i].normal).multiplyScalar(0.5); + } + + for (let i = 0; i != this.segments + 1; i++) { + this._addVertex(this.lastSegments[i % this.segments]); + this._addVertex(this.currentSegments[i % this.segments]); + } + + for (let i = 0; i < this.segments; i++) { + this.lastSegments[i].position.copy(this.currentSegments[i].position); + this.lastSegments[i].normal.copy(this.currentSegments[i].normal); + } + }, + + //draw a standalone point in space + _drawPoint: (() => { + const up = new THREE.Vector3(0, 1, 0); + const down = new THREE.Vector3(0, -1, 0); + const left = new THREE.Vector3(1, 0, 0); + const projectedDirection = new THREE.Vector3(); + const projectedPoint = new THREE.Vector3(); + return function(position) { + projectedDirection.copy(up).multiplyScalar(this.radius * 0.75); + projectedPoint.copy(position).add(projectedDirection); + this.lastPoint.copy(projectedPoint); + + projectedDirection.copy(up).multiplyScalar(this.radius * 0.5); + projectedPoint.copy(position).add(projectedDirection); + this._draw(projectedPoint, down, left, 0.75); + + this._draw(position, down, left); + + projectedDirection.copy(down).multiplyScalar(this.radius * 0.5); + projectedPoint.copy(position).add(projectedDirection); + this._draw(projectedPoint, down, left, 0.75); + + projectedDirection.copy(down).multiplyScalar(this.radius * 0.75); + projectedPoint.copy(position).add(projectedDirection); + + this._addDegenerateTriangle(); //discarded + this._drawCap(projectedPoint, this.lastSegments, down); + }; + })(), + + //draw a cap to start/end a line + _drawCap(point, segments, normal) { + let segmentIndex = 0; + for (let i = 0; i < this.segments * 2 - (this.segments % 2); i++) { + if ((i - 2) % 4 === 0) { + this._addVertex({ position: point, normal: normal }); + } else { + this._addVertex(segments[segmentIndex % this.segments]); + if ((i + 1) % 5 !== 0) { + ++segmentIndex; + } + } + } + }, + + _restartPrimitive() { + this.sharedBuffer.restartPrimitive(); + ++this.vertexCount; + }, + + _updateBuffer() { + this.sharedBuffer.update(); + }, + + _addVertex(segment) { + const point = segment.position; + const normal = segment.normal; + this.sharedBuffer.addVertex(point.x, point.y, point.z); + this.sharedBuffer.addColor(this.color.r, this.color.g, this.color.b); + + if (normal) { + this.sharedBuffer.addNormal(normal.x, normal.y, normal.z); + } else { + ++this.sharedBuffer.idx.normal; + } + + ++this.sharedBuffer.idx.uv; + ++this.vertexCount; + }, + + _addDegenerateTriangle() { + this._addVertex(this.lastSegments[0]); + }, + + //calculate the segments for a given point + _generateSegments(segmentsList, point, forward, up, radius) { + const angleIncrement = (Math.PI * 2) / this.segments; + for (let i = 0; i < this.segments; i++) { + const segment = segmentsList[i].position; + this._rotatePointAroundAxis(segment, point, forward, up, angleIncrement * i, radius); + segmentsList[i].normal.subVectors(segment, point).normalize(); + } + }, + + _rotatePointAroundAxis: (() => { + const calculatedDirection = new THREE.Vector3(); + return function(out, point, axis, up, angle, radius) { + calculatedDirection.copy(up); + calculatedDirection.applyAxisAngle(axis, angle); + out.copy(point).add(calculatedDirection.normalize().multiplyScalar(radius)); + }; + })() +}); diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js new file mode 100644 index 0000000000000000000000000000000000000000..855c94e0a1d11f5905dd20f55acdda27ebad227d --- /dev/null +++ b/src/components/tools/pen.js @@ -0,0 +1,188 @@ +/** + * Pen tool + * A tool that allows drawing on networked-drawing components. + * @namespace drawing + * @component pen + */ + +function almostEquals(epsilon, u, v) { + return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon; +} + +AFRAME.registerComponent("pen", { + schema: { + drawFrequency: { default: 5 }, //frequency of polling for drawing points + minDistanceBetweenPoints: { default: 0.01 }, //minimum distance to register new drawing point + camera: { type: "selector" }, + drawingManager: { type: "string" }, + color: { type: "color", default: "#FF0033" }, + availableColors: { + default: [ + "#FF0033", + "#FFFF00", + "#0099FF", + "#00FF33", + "#9900FF", + "#FF6600", + "#8D5524", + "#C68642", + "#E0AC69", + "#F1C27D", + "#FFDBAC", + "#FFFFFF", + "#222222", + "#111111", + "#000000" + ] + }, + radius: { default: 0.01 }, //drawing geometry radius + minRadius: { default: 0.005 }, + maxRadius: { default: 0.2 } + }, + + init() { + this._stateAdded = this._stateAdded.bind(this); + this._stateRemoved = this._stateRemoved.bind(this); + + this.timeSinceLastDraw = 0; + + this.lastPosition = new THREE.Vector3(); + this.lastPosition.copy(this.el.object3D.position); + + this.direction = new THREE.Vector3(1, 0, 0); + + this.currentDrawing = null; + + this.normal = new THREE.Vector3(); + + this.worldPosition = new THREE.Vector3(); + + this.colorIndex = 0; + + this.grabbed = false; + }, + + play() { + this.drawingManager = document.querySelector(this.data.drawingManager).components["drawing-manager"]; + this.drawingManager.createDrawing(); + + this.el.parentNode.addEventListener("stateadded", this._stateAdded); + this.el.parentNode.addEventListener("stateremoved", this._stateRemoved); + }, + + pause() { + this.el.parentNode.removeEventListener("stateadded", this._stateAdded); + this.el.parentNode.removeEventListener("stateremoved", this._stateRemoved); + }, + + update(prevData) { + if (prevData.color != this.data.color) { + this.el.setAttribute("color", this.data.color); + } + if (prevData.radius != this.data.radius) { + this.el.setAttribute("radius", this.data.radius); + } + }, + + tick(t, dt) { + this.el.object3D.getWorldPosition(this.worldPosition); + + if (!almostEquals(0.005, this.worldPosition, this.lastPosition)) { + this.direction.subVectors(this.worldPosition, this.lastPosition).normalize(); + this.lastPosition.copy(this.worldPosition); + } + + if (this.currentDrawing) { + const time = this.timeSinceLastDraw + dt; + if ( + time >= this.data.drawFrequency && + this.currentDrawing.getLastPoint().distanceTo(this.worldPosition) >= this.data.minDistanceBetweenPoints + ) { + this._getNormal(this.normal, this.worldPosition, this.direction); + this.currentDrawing.draw(this.worldPosition, this.direction, this.normal, this.data.color, this.data.radius); + } + + this.timeSinceLastDraw = time % this.data.drawFrequency; + } + }, + + //helper function to get normal of direction of drawing cross direction to camera + _getNormal: (() => { + const directionToCamera = new THREE.Vector3(); + return function(normal, position, direction) { + directionToCamera.subVectors(position, this.data.camera.object3D.position).normalize(); + normal.crossVectors(direction, directionToCamera); + }; + })(), + + _startDraw() { + this.currentDrawing = this.drawingManager.getDrawing(this); + if (this.currentDrawing) { + this.el.object3D.getWorldPosition(this.worldPosition); + this._getNormal(this.normal, this.worldPosition, this.direction); + + this.currentDrawing.startDraw(this.worldPosition, this.direction, this.normal, this.data.color, this.data.radius); + } + }, + + _endDraw() { + if (this.currentDrawing) { + this.timeSinceLastDraw = 0; + this.el.object3D.getWorldPosition(this.worldPosition); + this._getNormal(this.normal, this.worldPosition, this.direction); + this.currentDrawing.endDraw(this.worldPosition, this.direction, this.normal); + this.drawingManager.returnDrawing(this); + this.currentDrawing = null; + } + }, + + _changeColor(mod) { + this.colorIndex = (this.colorIndex + mod + this.data.availableColors.length) % this.data.availableColors.length; + this.data.color = this.data.availableColors[this.colorIndex]; + this.el.setAttribute("color", this.data.color); + }, + + _changeRadius(mod) { + this.data.radius = Math.max(this.data.minRadius, Math.min(this.data.radius + mod, this.data.maxRadius)); + this.el.setAttribute("radius", this.data.radius); + }, + + _stateAdded(evt) { + switch (evt.detail) { + case "activated": + this._startDraw(); + break; + case "colorNext": + this._changeColor(1); + break; + case "colorPrev": + this._changeColor(-1); + break; + case "radiusUp": + this._changeRadius(this.data.minRadius); + break; + case "radiusDown": + this._changeRadius(-this.data.minRadius); + break; + case "grabbed": + this.grabbed = true; + break; + default: + break; + } + }, + + _stateRemoved(evt) { + switch (evt.detail) { + case "activated": + this._endDraw(); + break; + case "grabbed": + this.grabbed = false; + this._endDraw(); + break; + default: + break; + } + } +}); diff --git a/src/components/virtual-gamepad-controls.css b/src/components/virtual-gamepad-controls.css index 4270c36ad7be261997f7ab77218e9ea817269620..77c77e1adf03d22f108c2dbdd8f0f5bb1657385e 100644 --- a/src/components/virtual-gamepad-controls.css +++ b/src/components/virtual-gamepad-controls.css @@ -6,20 +6,20 @@ :local(.touchZone.left) { left: 0; - right: 50%; + right: 55%; } :local(.touchZone.right) { - left: 50%; + left: 55%; right: 0; } :local(.mockJoystickContainer) { position: absolute; - height: 20vh; + height: 0; left: 0; right: 0; - bottom: 0; + bottom: 10vh; display: flex; align-items: center; justify-content: space-around; diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index cb016f74a6289ed238792d19803ca32bbe80cf95..73c7d628ee70e5b1182f6a035a0aeef701488221 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -1,9 +1,13 @@ import "./components/gltf-model-plus"; +AFRAME.GLTFModelPlus.registerComponent("duck", "duck"); AFRAME.GLTFModelPlus.registerComponent("quack", "quack"); AFRAME.GLTFModelPlus.registerComponent("sound", "sound"); AFRAME.GLTFModelPlus.registerComponent("collision-filter", "collision-filter"); AFRAME.GLTFModelPlus.registerComponent("css-class", "css-class"); +AFRAME.GLTFModelPlus.registerComponent("interactable", "css-class", (el, componentName) => { + el.setAttribute(componentName, "interactable"); +}); AFRAME.GLTFModelPlus.registerComponent("scene-shadow", "scene-shadow"); AFRAME.GLTFModelPlus.registerComponent("super-spawner", "super-spawner"); AFRAME.GLTFModelPlus.registerComponent("gltf-model-plus", "gltf-model-plus"); @@ -24,6 +28,7 @@ AFRAME.GLTFModelPlus.registerComponent("scale-audio-feedback", "scale-audio-feed AFRAME.GLTFModelPlus.registerComponent("animation-mixer", "animation-mixer"); AFRAME.GLTFModelPlus.registerComponent("loop-animation", "loop-animation"); AFRAME.GLTFModelPlus.registerComponent("shape", "shape"); +AFRAME.GLTFModelPlus.registerComponent("heightfield", "heightfield"); AFRAME.GLTFModelPlus.registerComponent( "box-collider", "shape", @@ -43,7 +48,13 @@ AFRAME.GLTFModelPlus.registerComponent( }; })() ); -AFRAME.GLTFModelPlus.registerComponent("visible", "visible"); +AFRAME.GLTFModelPlus.registerComponent("visible", "visible", (el, componentName, componentData) => { + if (typeof componentData === "object") { + el.setAttribute(componentName, componentData.visible); + } else { + el.setAttribute(componentName, componentData); + } +}); AFRAME.GLTFModelPlus.registerComponent("spawn-point", "spawn-point"); AFRAME.GLTFModelPlus.registerComponent("hoverable", "hoverable"); AFRAME.GLTFModelPlus.registerComponent("sticky-zone", "sticky-zone"); @@ -57,4 +68,7 @@ AFRAME.GLTFModelPlus.registerComponent("nav-mesh", "nav-mesh", (el, _componentNa nav.loadMesh(node, zone); } }); + // There isn't actually an a-frame nav-mesh component, but we want to tag this el as a nav-mesh since + // nav-mesh-helper will query for it later. + el.setAttribute("nav-mesh"); }); diff --git a/src/hub.html b/src/hub.html index 9b9161d2a27f6505f9d105b0bd3a1d9dde1831a5..4884abca46af25a088c333af4fab7ad673731ec9 100644 --- a/src/hub.html +++ b/src/hub.html @@ -2,14 +2,14 @@ <html> <head> - <!-- DO NOT REMOVE/EDIT THIS COMMENT - HUB_META_TAGS --> + <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link rel="shortcut icon" type="image/png" href="/favicon.ico"> <title>Get together | Hubs by Mozilla</title> - <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700" rel="stylesheet"> </head> <body> @@ -21,6 +21,7 @@ </audio> <a-scene + class="grab-cursor" renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;" gamma-factor networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" @@ -30,6 +31,7 @@ personal-space-bubble="debug: false;" vr-mode-ui="enabled: false" pinch-to-move + stats-plus="false" input-configurator=" gazeCursorRayObject: #player-camera; cursorController: #cursor-controller; @@ -56,6 +58,10 @@ <img id="freeze-off-hover" crossorigin="anonymous" src="./assets/hud/freeze_off-hover.png"> <img id="freeze-on" crossorigin="anonymous" src="./assets/hud/freeze_on.png"> <img id="freeze-on-hover" crossorigin="anonymous" src="./assets/hud/freeze_on-hover.png"> + <img id="spawn-pen" crossorigin="anonymous" src="./assets/hud/spawn_pen.png"> + <img id="spawn-pen-hover" crossorigin="anonymous" src="./assets/hud/spawn_pen-hover.png"> + <img id="spawn-camera" crossorigin="anonymous" src="./assets/hud/spawn_camera.png"> + <img id="spawn-camera-hover" crossorigin="anonymous" src="./assets/hud/spawn_camera-hover.png"> <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item> <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item> @@ -68,9 +74,6 @@ <a-asset-item id="botrobert" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotRobert_Avatar-e9554880f3.gltf"></a-asset-item> <a-asset-item id="botwoody" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotWoody_Avatar-0140485a23.gltf"></a-asset-item> - <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> - <a-asset-item id="interactable-duck" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf"></a-asset-item> - <a-asset-item id="quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="specialquack" src="./assets/sfx/specialquack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> @@ -91,7 +94,7 @@ <a-entity class="model" gltf-model-plus="inflate: true"> <template data-name="RootScene"> - <a-entity ik-controller hand-pose__left hand-pose__right animation-mixer space-invader-mesh="meshName: Bot_Skinned"></a-entity> + <a-entity ik-controller hand-pose__left hand-pose__right space-invader-mesh="meshName: Bot_Skinned"></a-entity> </template> <template data-name="Neck"> @@ -156,15 +159,78 @@ position-at-box-shape-border="target:.delete-button" destroy-at-extreme-distances rotation + activatable__increase-scale="buttonStartEvents: scroll_right; buttonEndEvents: horizontal_scroll_release; activatedState: scaleUp;" + activatable__decrease-scale="buttonStartEvents: scroll_left; buttonEndEvents: horizontal_scroll_release; activatedState: scaleDown;" > <!-- HACK: rotation component above is required for its side effect of setting YXZ order --> <a-entity class="delete-button" visible-while-frozen> <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> - <a-entity text=" value:Delete; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + <a-entity text=" value:Remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + </a-entity> + </a-entity> + </template> + + <template id="pen-interactable"> + <a-entity + class="interactable toggle" + super-networked-interactable="counter: #pen-counter;" + body="type: dynamic; shape: none; mass: 1;" + grabbable-toggle="maxGrabbers: 1;" + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + hoverable + activatable__draw-hand="buttonStartEvents: secondary_hand_grab; buttonEndEvents: secondary_hand_release;" + activatable__draw-cursor="buttonStartEvents: secondary-cursor-grab; buttonEndEvents: secondary-cursor-release;" + activatable__color-next="buttonStartEvents: next_color, scroll_right; buttonEndEvents: thumb_up, secondary_hand_release, horizontal_scroll_release; activatedState: colorNext;" + activatable__color-prev="buttonStartEvents: previous_color, scroll_left; buttonEndEvents: thumb_up, secondary_hand_release, horizontal_scroll_release; activatedState: colorPrev;" + activatable__increase-radius="buttonStartEvents: increase_radius, scroll_up; buttonEndEvents: thumb_up, secondary_hand_release, vertical_scroll_release; activatedState: radiusUp;" + activatable__decrease-radius="buttonStartEvents: decrease_radius, scroll_down; buttonEndEvents: thumb_up, secondary_hand_release, vertical_scroll_release; activatedState: radiusDown;" + scale="0.5 0.5 0.5" + > + <a-sphere + id="pen" + scale="1.5, 1.5, 1.5" + position="0 -0.18 0" + radius="0.02" + color="#FF0033" + pen="camera: #player-camera; drawingManager: #drawing-manager" + segments-width="16" + segments-height="12" + ></a-sphere> + <a-entity class="delete-button" visible-while-frozen> + <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> + <a-entity text=" value:Remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> </a-entity> </a-entity> </template> + <template id="interactable-camera"> + <a-entity + class="interactable toggle" + grabbable-toggle="maxGrabbers: 1;" + hoverable + activatable__snap-hand="buttonStartEvents: secondary_hand_grab; buttonEndEvents: secondary_hand_release;" + activatable__snap-cursor="buttonStartEvents: secondary-cursor-grab; buttonEndEvents: secondary-cursor-release;" + camera-tool + body="type: dynamic; shape: none; mass: 1;" + shape="shape: box; halfExtents: 0.22 0.145 0.1; offset: 0 0.02 0" + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + super-networked-interactable="counter: #camera-counter;" + position-at-box-shape-border="target:.delete-button" + rotation + > + <a-entity class="delete-button" visible-while-frozen> + <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> + <a-entity text=" value:Remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + </a-entity> + </a-entity> + </template> + + <template id="interactable-drawing"> + <a-entity + networked-drawing + ></a-entity> + </template> + <template id="paging-toolbar"> <a-entity class="paging-toolbar" visible-to-owner> <a-entity class="prev-button" position="-0.3 0 0"> @@ -199,11 +265,18 @@ <a-mixin id="controller-super-hands" super-hands=" - colliderEvent: collisions; colliderEventProperty: els; - colliderEndEvent: collisions; colliderEndEventProperty: clearedEls; - grabStartButtons: hand_grab; grabEndButtons: hand_release; - stretchStartButtons: hand_grab; stretchEndButtons: hand_release; - dragDropStartButtons: hand_grab; dragDropEndButtons: hand_release;" + colliderEvent: collisions; + colliderEventProperty: els; + colliderEndEvent: collisions; + colliderEndEventProperty: clearedEls; + grabStartButtons: primary_hand_grab, secondary_hand_grab; + grabEndButtons: primary_hand_release, secondary_hand_release; + stretchStartButtons: primary_hand_grab, secondary_hand_grab; + stretchEndButtons: primary_hand_release, secondary_hand_release; + dragDropStartButtons: hand_grab, secondary_hand_grab; + dragDropEndButtons: hand_release, secondary_hand_release; + activateStartButtons: secondary_hand_grab, next_color, previous_color, increase_radius, decrease_radius, scroll_up, scroll_down, scroll_left, scroll_right; + activateEndButtons: secondary_hand_release, vertical_scroll_release, horizontal_scroll_release, thumb_up;" collision-filter="collisionForces: false" physics-collider ></a-mixin> @@ -212,12 +285,18 @@ <!-- Interactables --> <a-entity id="media-counter" networked-counter="max: 10;"></a-entity> + <a-entity id="pen-counter" networked-counter="max: 10;"></a-entity> + + <a-entity id="camera-counter" networked-counter="max: 1;"></a-entity> + + <a-entity id="drawing-manager" drawing-manager></a-entity> + <a-entity id="cursor-controller" cursor-controller=" cursor: #cursor; - camera: #player-camera; " - raycaster="objects: .collidable, .interactable, .ui; far: 3;" + camera: #player-camera; + objects: .collidable, .interactable, .ui;" line="visible: false; color: white; opacity: 0.2;" ></a-entity> @@ -225,29 +304,31 @@ id="cursor" material="depthTest: false; opacity:0.9;" radius="0.02" + segments-width="9" + segments-height="9" static-body="shape: sphere;" collision-filter="collisionForces: false" super-hands=" - colliderEvent: raycaster-intersection; colliderEventProperty: els; - colliderEndEvent: raycaster-intersection-cleared; colliderEndEventProperty: clearedEls; - grabStartButtons: cursor-grab; grabEndButtons: cursor-release; - stretchStartButtons: cursor-grab; stretchEndButtons: cursor-release; - dragDropStartButtons: cursor-grab; dragDropEndButtons: cursor-release;" - segments-height="9" - segments-width="9" - event-repeater="events: raycaster-intersection, raycaster-intersection-cleared; eventSource: #cursor-controller" + colliderEvent: raycaster-intersection; + colliderEndEvent: raycaster-intersection-cleared; + grabStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + grabEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + stretchStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + stretchEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + dragDropStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + dragDropEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + activateStartButtons: secondary-cursor-grab, secondary_hand_grab, next_color, previous_color, increase_radius, decrease_radius, scroll_up, scroll_down, scroll_left, scroll_right; + activateEndButtons: secondary-cursor-release, secondary_hand_release, vertical_scroll_release, horizontal_scroll_release, thumb_up;" ></a-sphere> <!-- Player Rig --> <a-entity id="player-rig" - networked="template: #remote-avatar-template; attachTemplateToLocal: false;" - spawn-controller="loadedEvent: bundleloaded; target: #environment-root" + spawn-controller="loadedEvent: entered; target: #player-rig" wasd-to-analog2d character-controller="pivot: #player-camera" ik-root player-info - networked-avatar cardboard-controls > <a-entity @@ -257,10 +338,13 @@ vr-mode-toggle-playing__hud-controller > <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> - <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> + <a-rounded height="0.08" width="0.5" color="#000000" position="-0.20 0.125 0" radius="0.040" opacity="0.35" class="hud bg"></a-rounded> + <a-entity id="hud-hub-entry-link" text=" value:; width:1.1; align:center;" position="0.05 0.165 0"></a-entity> + <a-rounded height="0.13" width="0.59" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="ui hud mic" material="alphaTest:0.1;"></a-image> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pause; activeTooltipText: Resume; image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.005" class="ui hud freeze"></a-image> - <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Enable Bubble; activeTooltipText: Disable Bubble; image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud bubble" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pen; activeTooltipText: Pen; image: #spawn-pen; hoverImage: #spawn-pen-hover; activeImage: #spawn-pen; activeHoverImage: #spawn-pen-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud pen" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Camera; activeTooltipText: Camera; image: #spawn-camera; hoverImage: #spawn-camera-hover; activeImage: #spawn-camera; activeHoverImage: #spawn-camera-hover" scale="0.1 0.1 0.1" position="0.28 0 0.001" class="ui hud cameraBtn" material="alphaTest:0.1;"></a-image> <a-rounded visible="false" id="hud-tooltip" height="0.08" width="0.3" color="#000000" position="-0.15 -0.2 0" rotation="-20 0 0" radius="0.025" opacity="0.35" class="hud bg"> <a-entity text="value: Mute Mic; align:center;" position="0.15 0.04 0.001" ></a-entity> </a-rounded> @@ -271,8 +355,7 @@ id="player-camera" class="camera" camera - position="0 1.6 0" - personal-space-bubble="radius: 0.4" + personal-space-bubble="radius: 0.4;" pitch-yaw-rotator > <a-entity @@ -329,6 +412,7 @@ missOpacity: 0.1; curveShootingSpeed: 12;" haptic-feedback + event-repeater="events: haptic_pulse; eventSource: #cursor" body="type: static; shape: none;" mixin="controller-super-hands" controls-shape-offset @@ -340,7 +424,6 @@ <template data-name="RootScene"> <a-entity ik-controller - animation-mixer hand-pose__left hand-pose__right hand-pose-controller__left="networkedAvatar:#player-rig;eventSrc:#player-left-controller" @@ -355,7 +438,7 @@ </template> <template data-name="Head"> - <a-entity visible="false" bone-visibility></a-entity> + <a-entity id="player-head" visible="false" bone-visibility></a-entity> </template> <template data-name="LeftHand"> @@ -374,6 +457,17 @@ id="environment-root" nav-mesh-helper static-body="shape: none;" + > + <a-entity id="environment-scene"/> + </a-entity> + + <a-entity + super-spawner=" + template: #pen-interactable; + src: https://asset-bundles-prod.reticulum.io/interactables/DrawingPen/DrawingPen-34fb4aee27.gltf; + spawnEvent: spawn_pen; + superHand: #player-right-controller; + cursorSuperHand: #cursor;" ></a-entity> </a-scene> diff --git a/src/hub.js b/src/hub.js index 8db01eaddef8c92fa5dd4a8da040fa2473615b56..dc9ab1bdc525faf7a56e4619da415017da5a4b38 100644 --- a/src/hub.js +++ b/src/hub.js @@ -18,6 +18,10 @@ import "webrtc-adapter"; import "aframe-slice9-component"; import "aframe-motion-capture-components"; import "./utils/audio-context-fix"; +import { getReticulumFetchUrl } from "./utils/phoenix-utils"; + +import nextTick from "./utils/next-tick"; +import { addAnimationComponents } from "./utils/animation"; import trackpad_dpad4 from "./behaviours/trackpad-dpad4"; import trackpad_scrolling from "./behaviours/trackpad-scrolling"; @@ -25,13 +29,13 @@ import joystick_dpad4 from "./behaviours/joystick-dpad4"; import msft_mr_axis_with_deadzone from "./behaviours/msft-mr-axis-with-deadzone"; import { PressedMove } from "./activators/pressedmove"; import { ReverseY } from "./activators/reversey"; -import { ObjectContentOrigins } from "./object-types"; +import { Presence } from "phoenix"; import "./activators/shortpress"; +import "./components/scene-components"; import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future import "./components/mute-mic"; -import "./components/audio-feedback"; import "./components/bone-mute-state-indicator"; import "./components/bone-visibility"; import "./components/in-world-hud"; @@ -42,17 +46,9 @@ import "./components/character-controller"; import "./components/haptic-feedback"; import "./components/networked-video-player"; import "./components/offset-relative-to"; -import "./components/water"; -import "./components/skybox"; -import "./components/layers"; -import "./components/spawn-controller"; -import "./components/hide-when-quality"; import "./components/player-info"; import "./components/debug"; -import "./components/animation-mixer"; -import "./components/loop-animation"; import "./components/hand-poses"; -import "./components/gltf-model-plus"; import "./components/gltf-bundle"; import "./components/hud-controller"; import "./components/freeze-controller"; @@ -62,27 +58,19 @@ import "./components/block-button"; import "./components/visible-while-frozen"; import "./components/stats-plus"; import "./components/networked-avatar"; -import "./components/css-class"; -import "./components/scene-shadow"; import "./components/avatar-replay"; import "./components/media-views"; import "./components/pinch-to-move"; import "./components/look-on-mobile"; import "./components/pitch-yaw-rotator"; import "./components/input-configurator"; -import "./components/sticky-object"; import "./components/auto-scale-cannon-physics-body"; import "./components/position-at-box-shape-border"; import "./components/remove-networked-object-button"; import "./components/destroy-at-extreme-distances"; -import "./components/media-loader"; import "./components/gamma-factor"; -import "./components/ambient-light"; -import "./components/directional-light"; -import "./components/hemisphere-light"; -import "./components/point-light"; -import "./components/spot-light"; import "./components/visible-to-owner"; +import "./components/camera-tool"; import ReactDOM from "react-dom"; import React from "react"; @@ -91,7 +79,8 @@ import HubChannel from "./utils/hub-channel"; import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; -import { addMedia, resolveMedia } from "./utils/media-utils"; +import { proxiedUrlFor } from "./utils/media-utils"; +import SceneEntryManager from "./scene-entry-manager"; import "./systems/nav"; import "./systems/personal-space-bubble"; @@ -99,7 +88,6 @@ import "./systems/app-mode"; import "./systems/exit-on-blur"; import "./gltf-component-mappings"; -import { DEFAULT_ENVIRONMENT_URL } from "./assets/environments/environments"; import { App } from "./App"; @@ -121,24 +109,28 @@ import "aframe-physics-extras"; import "super-hands"; import "./components/super-networked-interactable"; import "./components/networked-counter"; -import "./components/super-spawner"; import "./components/event-repeater"; import "./components/controls-shape-offset"; -import "./components/duck"; -import "./components/quack"; +import "./components/grabbable-toggle"; import "./components/cardboard-controls"; import "./components/cursor-controller"; import "./components/nav-mesh-helper"; +import "./systems/tunnel-effect"; + +import "./components/tools/pen"; +import "./components/tools/networked-drawing"; +import "./components/tools/drawing-manager"; import registerNetworkSchemas from "./network-schemas"; -import { inGameActions, config as inputConfig } from "./input-mappings"; +import { config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; -import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js"; +import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; + import qsTruthy from "./utils/qs_truthy"; const isBotMode = qsTruthy("bot"); @@ -165,11 +157,51 @@ concurrentLoadDetector.start(); store.init(); -function mountUI(scene, props = {}) { +function getPlatformUnsupportedReason() { + if (typeof RTCDataChannelEvent === "undefined") return "no_data_channels"; + return null; +} + +function pollForSupportAvailability(callback) { + const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability"); + let isSupportAvailable = null; + + const updateIfChanged = () => + fetch(availabilityUrl).then(({ ok }) => { + if (isSupportAvailable === ok) return; + isSupportAvailable = ok; + callback(isSupportAvailable); + }); + + updateIfChanged(); + setInterval(updateIfChanged, 30000); +} + +function setupLobbyCamera() { + const camera = document.querySelector("#player-camera"); + const previewCamera = document.querySelector("#environment-scene").object3D.getObjectByName("scene-preview-camera"); + + if (previewCamera) { + camera.object3D.position.copy(previewCamera.position); + camera.object3D.rotation.copy(previewCamera.rotation); + camera.object3D.rotation.reorder("YXZ"); + camera.object3D.updateMatrix(); + } else { + const cameraPos = camera.object3D.position; + camera.object3D.position.set(cameraPos.x, 2.5, cameraPos.z); + } + + camera.setAttribute("scene-preview-camera", "positionOnly: true; duration: 60"); + camera.components["pitch-yaw-rotator"].set(camera.object3D.rotation.x, camera.object3D.rotation.y); +} + +let uiProps = {}; + +function mountUI(props = {}) { + const scene = document.querySelector("a-scene"); const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.get("vr_entry_type"); const enableScreenSharing = qsTruthy("enable_screen_sharing"); - const showProfileEntry = !store.state.activity.hasChangedName; ReactDOM.render( <UIRoot @@ -181,7 +213,6 @@ function mountUI(scene, props = {}) { forcedVREntryType, enableScreenSharing, store, - showProfileEntry, ...props }} />, @@ -189,374 +220,276 @@ function mountUI(scene, props = {}) { ); } -const onReady = async () => { - const scene = document.querySelector("a-scene"); - const hubChannel = new HubChannel(store); - const linkChannel = new LinkChannel(store); +function remountUI(props) { + uiProps = { ...uiProps, ...props }; + mountUI(uiProps); +} - document.querySelector("canvas").classList.add("blurred"); - window.APP.scene = scene; +async function handleHubChannelJoined(entryManager, hubChannel, data) { + const scene = document.querySelector("a-scene"); - registerNetworkSchemas(); + if (NAF.connection.isConnected()) { + // Send complete sync on phoenix re-join. + NAF.connection.entities.completeSync(null, true); + return; + } - let uiProps = { linkChannel }; + const hub = data.hubs[0]; + const defaultSpaceTopic = hub.topics[0]; + const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb"); + const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle"); + const sceneUrl = (glbAsset || bundleAsset).src; + const hasExtension = /\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl); + + console.log(`Scene URL: ${sceneUrl}`); + const environmentScene = document.querySelector("#environment-scene"); + + if (glbAsset || hasExtension) { + const gltfEl = document.createElement("a-entity"); + gltfEl.setAttribute("gltf-model-plus", { src: proxiedUrlFor(sceneUrl), useCache: false, inflate: true }); + gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded")); + environmentScene.appendChild(gltfEl); + } else { + // TODO kill bundles + environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`); + } - mountUI(scene); + remountUI({ + hubId: hub.hub_id, + hubName: hub.name, + hubEntryCode: hub.entry_code, + onSendMessage: hubChannel.sendMessage + }); - const remountUI = props => { - uiProps = { ...uiProps, ...props }; - mountUI(scene, uiProps); - }; + document + .querySelector("#hud-hub-entry-link") + .setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" }); - const applyProfileFromStore = playerRig => { - const displayName = store.state.profile.displayName; - playerRig.setAttribute("player-info", { - displayName, - avatarSrc: "#" + (store.state.profile.avatarId || "botdefault") - }); - const hudController = playerRig.querySelector("[hud-controller]"); - hudController.setAttribute("hud-controller", { showTip: !store.state.activity.hasFoundFreeze }); - document.querySelector("a-scene").emit("username-changed", { username: displayName }); - }; + scene.setAttribute("networked-scene", { + room: hub.hub_id, + serverURL: process.env.JANUS_SERVER, + debug: !!isDebug + }); - const exitScene = () => { - if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { - NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop()); - } - if (hubChannel) { - hubChannel.disconnect(); - } - const scene = document.querySelector("a-scene"); - if (scene) { - if (scene.renderer) { - scene.renderer.setAnimationLoop(null); // Stop animation loop, TODO A-Frame should do this - } - document.body.removeChild(scene); - } - }; + while (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) await nextTick(); - const enterScene = async (mediaStream, enterInVR, hubId) => { - const scene = document.querySelector("a-scene"); - if (!isBotMode) { - scene.classList.add("no-cursor"); - } - const playerRig = document.querySelector("#player-rig"); - document.querySelector("canvas").classList.remove("blurred"); - scene.render(); + scene.components["networked-scene"] + .connect() + .then(() => { + NAF.connection.adapter.reliableTransport = (clientId, dataType, data) => { + const payload = { dataType, data }; - if (enterInVR) { - scene.enterVR(); - } + if (clientId) { + payload.clientId = clientId; + } - AFRAME.registerInputActions(inGameActions, "default"); + hubChannel.channel.push("naf", payload); + }; + }) + .catch(connectError => { + // hacky until we get return codes + const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i); + console.error(connectError); + remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" }); + entryManager.exitScene(); - scene.setAttribute("networked-scene", { - room: hubId, - serverURL: process.env.JANUS_SERVER + return; }); +} - if (isDebug) { - scene.setAttribute("networked-scene", { debug: true }); - } +async function runBotMode(scene, entryManager) { + const noop = () => {}; + scene.renderer = { setAnimationLoop: noop, render: noop }; - scene.setAttribute("stats-plus", false); + while (!NAF.connection.isConnected()) await nextTick(); + entryManager.enterSceneWhenLoaded(new MediaStream(), false); +} - if (isMobile || qsTruthy("mobile")) { - playerRig.setAttribute("virtual-gamepad-controls", {}); - } +document.addEventListener("DOMContentLoaded", async () => { + const scene = document.querySelector("a-scene"); + const hubChannel = new HubChannel(store); + const entryManager = new SceneEntryManager(hubChannel); + entryManager.init(); - const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); - applyProfileOnPlayerRig(); - store.addEventListener("statechanged", applyProfileOnPlayerRig); + const linkChannel = new LinkChannel(store); - const avatarScale = parseInt(qs.get("avatar_scale"), 10); + window.APP.scene = scene; - if (avatarScale) { - playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); - } + registerNetworkSchemas(); + remountUI({ hubChannel, linkChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); - const videoTracks = mediaStream ? mediaStream.getVideoTracks() : []; - let sharingScreen = videoTracks.length > 0; + pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable })); - const screenEntityId = `${NAF.clientId}-screen`; - let screenEntity = document.getElementById(screenEntityId); + const platformUnsupportedReason = getPlatformUnsupportedReason(); - scene.addEventListener("action_share_screen", () => { - sharingScreen = !sharingScreen; - if (sharingScreen) { - for (const track of videoTracks) { - mediaStream.addTrack(track); - } - } else { - for (const track of mediaStream.getVideoTracks()) { - mediaStream.removeTrack(track); - } - } - NAF.connection.adapter.setLocalMediaStream(mediaStream); - screenEntity.setAttribute("visible", sharingScreen); - }); + if (platformUnsupportedReason) { + remountUI({ platformUnsupportedReason }); + entryManager.exitScene(); + return; + } - document.body.addEventListener("blocked", ev => { - NAF.connection.entities.removeEntitiesOfClient(ev.detail.clientId); - }); + if (qs.get("required_version") && process.env.BUILD_VERSION) { + const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)" - document.body.addEventListener("unblocked", ev => { - NAF.connection.entities.completeSync(ev.detail.clientId); - }); + if (qs.get("required_version") !== buildNumber) { + remountUI({ roomUnavailableReason: "version_mismatch" }); + setTimeout(() => document.location.reload(), 5000); + entryManager.exitScene(); + return; + } + } - const offset = { x: 0, y: 0, z: -1.5 }; - const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { - const entity = addMedia(src, contentOrigin, true); + const availableVREntryTypes = await getAvailableVREntryTypes(); - entity.setAttribute("offset-relative-to", { - target: "#player-camera", - offset - }); - }; + if (availableVREntryTypes.isInHMD) { + remountUI({ availableVREntryTypes, forcedVREntryType: "vr" }); + } else { + remountUI({ availableVREntryTypes }); + } - scene.addEventListener("add_media", e => { - const contentOrigin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL; + const environmentScene = document.querySelector("#environment-scene"); - spawnMediaInfrontOfPlayer(e.detail, contentOrigin); - }); + environmentScene.addEventListener("bundleloaded", () => { + remountUI({ environmentSceneLoaded: true }); - scene.addEventListener("object_spawned", e => { - if (hubChannel) { - hubChannel.sendObjectSpawnedEvent(e.detail.objectType); - } - }); + for (const modelEl of environmentScene.children) { + addAnimationComponents(modelEl); + } - document.addEventListener("paste", e => { - if (e.target.nodeName === "INPUT") return; + setupLobbyCamera(); - const url = e.clipboardData.getData("text"); - const files = e.clipboardData.files && e.clipboardData.files; - if (url) { - spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); - } else { - for (const file of files) { - spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.CLIPBOARD); - } - } - }); + // Replace renderer with a noop renderer to reduce bot resource usage. + if (isBotMode) { + runBotMode(scene, entryManager); + } + }); - document.addEventListener("dragover", e => { - e.preventDefault(); - }); + // Connect to reticulum over phoenix channels to get hub info. + const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0]; + console.log(`Hub ID: ${hubId}`); - document.addEventListener("drop", e => { - e.preventDefault(); - const url = e.dataTransfer.getData("url"); - const files = e.dataTransfer.files; - if (url) { - spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); - } else { - for (const file of files) { - spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.FILE); - } - } - }); + const socket = connectToReticulum(isDebug); + remountUI({ sessionId: socket.params().session_id }); - if (!qsTruthy("offline")) { - document.body.addEventListener("connected", () => { - if (!isBotMode) { - hubChannel.sendEntryEvent().then(() => { - store.update({ activity: { lastEnteredAt: new Date().toISOString() } }); - }); - } - remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 }); - }); + // Hub local channel + const context = { + mobile: isMobile, + hmd: availableVREntryTypes.isInHMD + }; - document.body.addEventListener("clientConnected", () => { - remountUI({ - occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 - }); - }); + const joinPayload = { profile: store.state.profile, context }; + const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload); - document.body.addEventListener("clientDisconnected", () => { - remountUI({ - occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1 - }); - }); + hubPhxChannel + .join() + .receive("ok", async data => { + hubChannel.setPhoenixChannel(hubPhxChannel); + await handleHubChannelJoined(entryManager, hubChannel, data); + }) + .receive("error", res => { + if (res.reason === "closed") { + entryManager.exitScene(); + remountUI({ roomUnavailableReason: "closed" }); + } - scene.components["networked-scene"].connect().catch(connectError => { - // hacky until we get return codes - const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i); - console.error(connectError); - remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" }); - exitScene(); + console.error(res); + }); - return; - }); + const hubPhxPresence = new Presence(hubPhxChannel); + const presenceLogEntries = []; - if (isDebug) { - NAF.connection.adapter.session.options.verbose = true; - } + const addToPresenceLog = entry => { + entry.key = Date.now().toString(); - if (isBotMode) { - playerRig.setAttribute("avatar-replay", { - camera: "#player-camera", - leftController: "#player-left-controller", - rightController: "#player-right-controller" - }); + presenceLogEntries.push(entry); + remountUI({ presenceLogEntries }); - const audioEl = document.createElement("audio"); - const audioInput = document.querySelector("#bot-audio-input"); - audioInput.onchange = () => { - audioEl.loop = true; - audioEl.muted = true; - audioEl.crossorigin = "anonymous"; - audioEl.src = URL.createObjectURL(audioInput.files[0]); - document.body.appendChild(audioEl); - }; - const dataInput = document.querySelector("#bot-data-input"); - dataInput.onchange = () => { - const url = URL.createObjectURL(dataInput.files[0]); - playerRig.setAttribute("avatar-replay", { recordingUrl: url }); - }; - await new Promise(resolve => audioEl.addEventListener("canplay", resolve)); - mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]); - audioEl.play(); - } + // Fade out and then remove + setTimeout(() => { + entry.expired = true; + remountUI({ presenceLogEntries }); - if (mediaStream) { - NAF.connection.adapter.setLocalMediaStream(mediaStream); - - if (screenEntity) { - screenEntity.setAttribute("visible", sharingScreen); - } else if (sharingScreen) { - const sceneEl = document.querySelector("a-scene"); - screenEntity = document.createElement("a-entity"); - screenEntity.id = screenEntityId; - screenEntity.setAttribute("offset-relative-to", { - target: "#player-camera", - offset: "0 0 -2", - on: "action_share_screen" - }); - screenEntity.setAttribute("networked", { template: "#video-template" }); - sceneEl.appendChild(screenEntity); - } - } - } + setTimeout(() => { + presenceLogEntries.splice(presenceLogEntries.indexOf(entry), 1); + remountUI({ presenceLogEntries }); + }, 5000); + }, entryManager.hasEntered() ? 10000 : 30000); // Fade out things faster once entered. }; - const getPlatformUnsupportedReason = () => { - if (typeof RTCDataChannelEvent === "undefined") { - return "no_data_channels"; - } - - return null; - }; + let isInitialSync = true; - remountUI({ enterScene, exitScene }); + hubPhxPresence.onSync(() => { + remountUI({ presences: hubPhxPresence.state }); - const platformUnsupportedReason = getPlatformUnsupportedReason(); + if (!isInitialSync) return; + // Wire up join/leave event handlers after initial sync. + isInitialSync = false; - if (platformUnsupportedReason) { - remountUI({ platformUnsupportedReason: platformUnsupportedReason }); - exitScene(); - return; - } + hubPhxPresence.onJoin((sessionId, current, info) => { + const meta = info.metas[info.metas.length - 1]; - if (qs.get("required_version") && process.env.BUILD_VERSION) { - const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)" - if (qs.get("required_version") !== buildNumber) { - remountUI({ roomUnavailableReason: "version_mismatch" }); - setTimeout(() => document.location.reload(), 5000); - exitScene(); - return; - } - } + if (current) { + // Change to existing presence + const isSelf = sessionId === socket.params().session_id; + const currentMeta = current.metas[0]; - getAvailableVREntryTypes().then(availableVREntryTypes => { - if (availableVREntryTypes.isInHMD) { - remountUI({ availableVREntryTypes, forcedVREntryType: "vr" }); - } else if (availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) { - remountUI({ availableVREntryTypes, forcedVREntryType: "gearvr" }); - } else { - remountUI({ availableVREntryTypes }); - } - }); + if (!isSelf && currentMeta.presence !== meta.presence && meta.profile.displayName) { + addToPresenceLog({ + type: "entered", + presence: meta.presence, + name: meta.profile.displayName + }); + } - const environmentRoot = document.querySelector("#environment-root"); - - const initialEnvironmentEl = document.createElement("a-entity"); - initialEnvironmentEl.addEventListener("bundleloaded", () => { - remountUI({ initialEnvironmentLoaded: true }); - // We never want to stop the render loop when were running in "bot" mode. - if (!isBotMode) { - // Stop rendering while the UI is up. We restart the render loop in enterScene. - // Wait a tick plus some margin so that the environments actually render. - setTimeout(() => scene.renderer.setAnimationLoop(null), 100); - } else { - const noop = () => {}; - // Replace renderer with a noop renderer to reduce bot resource usage. - scene.renderer = { setAnimationLoop: noop, render: noop }; - } - }); - environmentRoot.appendChild(initialEnvironmentEl); - - const setRoom = (hubId, hubName) => { - if (!isBotMode) { - remountUI({ hubId, hubName }); - } else { - const enterSceneImmediately = () => enterScene(new MediaStream(), false, hubId); - if (scene.hasLoaded) { - enterSceneImmediately(); + if (currentMeta.profile && meta.profile && currentMeta.profile.displayName !== meta.profile.displayName) { + addToPresenceLog({ + type: "display_name_changed", + oldName: currentMeta.profile.displayName, + newName: meta.profile.displayName + }); + } } else { - scene.addEventListener("loaded", enterSceneImmediately); + // New presence + const meta = info.metas[0]; + + if (meta.presence && meta.profile.displayName) { + addToPresenceLog({ + type: "join", + presence: meta.presence, + name: meta.profile.displayName + }); + } } - } - }; - - if (qs.has("room")) { - // If ?room is set, this is `yarn start`, so just use a default environment and query string room. - setRoom(qs.get("room") || "default"); - initialEnvironmentEl.setAttribute("gltf-bundle", { - src: DEFAULT_ENVIRONMENT_URL }); - return; - } - // Connect to reticulum over phoenix channels to get hub info. - const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0]; - console.log(`Hub ID: ${hubId}`); + hubPhxPresence.onLeave((sessionId, current, info) => { + if (current && current.metas.length > 0) return; - const socket = connectToReticulum(); - const channel = socket.channel(`hub:${hubId}`, {}); + const meta = info.metas[0]; - channel - .join() - .receive("ok", async data => { - const hub = data.hubs[0]; - const defaultSpaceTopic = hub.topics[0]; - const sceneUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; - console.log(`Scene URL: ${sceneUrl}`); - - if (/\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl)) { - const resolved = await resolveMedia(sceneUrl, false, 0); - const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, inflate: true }); - gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded")); - initialEnvironmentEl.appendChild(gltfEl); - } else { - // TODO remove, and remove bundleloaded event - initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${sceneUrl}`); + if (meta.profile.displayName) { + addToPresenceLog({ + type: "leave", + name: meta.profile.displayName + }); } + }); + }); - setRoom(hub.hub_id, hub.name); - hubChannel.setPhoenixChannel(channel); - }) - .receive("error", res => { - if (res.reason === "closed") { - exitScene(); - remountUI({ roomUnavailableReason: "closed" }); - } + hubPhxChannel.on("naf", data => { + if (!NAF.connection.adapter) return; + NAF.connection.adapter.onData(data); + }); - console.error(res); - }); + hubPhxChannel.on("message", data => { + const userInfo = hubPhxPresence.state[data.session_id]; + if (!userInfo) return; - linkChannel.setSocket(socket); -}; + addToPresenceLog({ type: "message", name: userInfo.metas[0].profile.displayName, body: data.body }); + }); -document.addEventListener("DOMContentLoaded", onReady); + // Reticulum global channel + const retPhxChannel = socket.channel(`ret`, { hub_id: hubId }); + retPhxChannel.join().receive("error", res => console.error(res)); + linkChannel.setSocket(socket); +}); diff --git a/src/index.html b/src/index.html index f7807252dc6d6756e8d95102347eeeb84e450bce..29c074aab067736ba69f5f80191c9d78643ebd25 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="shortcut icon" type="image/png" href="/favicon.ico"/> <title>Get together | Hubs by Mozilla</title> - <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700" rel="stylesheet"> </head> <body> diff --git a/src/index.js b/src/index.js index 7fddba12131a6d46fea444a42b2001acfe7afeb7..5c9f008ccd2e110ba4753133005ff42c4e8884c7 100644 --- a/src/index.js +++ b/src/index.js @@ -3,17 +3,23 @@ import React from "react"; import ReactDOM from "react-dom"; import registerTelemetry from "./telemetry"; import HomeRoot from "./react-components/home-root"; -import InfoDialog from "./react-components/info-dialog.js"; const qs = new URLSearchParams(location.search); registerTelemetry(); -ReactDOM.render( +const { pathname } = document.location; +const sceneId = qs.get("scene_id") || (pathname.startsWith("/scenes/") && pathname.substring(1).split("/")[1]); + +const root = ( <HomeRoot initialEnvironment={qs.get("initial_environment")} - dialogType={ - qs.has("list_signup") ? InfoDialog.dialogTypes.updates : qs.has("report") ? InfoDialog.dialogTypes.report : null - } - />, - document.getElementById("home-root") + sceneId={sceneId || ""} + authVerify={qs.has("auth_topic")} + authTopic={qs.get("auth_topic")} + authToken={qs.get("auth_token")} + authOrigin={qs.get("auth_origin")} + listSignup={qs.has("list_signup")} + report={qs.has("report")} + /> ); +ReactDOM.render(root, document.getElementById("home-root")); diff --git a/src/input-mappings.js b/src/input-mappings.js index ccf44110bc74e38ab80de143c11f2dcad50d2a5c..6b1b4e955b95acfd13c469804c2f1478fd1435a1 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -54,41 +54,37 @@ const config = { trackpad_dpad4_pressed_center_down: { right: "action_primary_down" }, trackpad_dpad4_pressed_north_down: { right: "action_primary_down" }, trackpad_dpad4_pressed_south_down: { right: "action_primary_down" }, - trackpadup: { right: "action_primary_up" }, + trackpadup: { left: "action_primary_up", right: "action_primary_up" }, menudown: "thumb_down", menuup: "thumb_up", - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], trackpadtouchstart: "thumb_down", trackpadtouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - scroll: { right: "move_duck" } + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + scroll: { left: "scroll_move", right: "scroll_move" } }, "oculus-touch-controls": { - joystick_dpad4_west: { - right: "snap_rotate_left" - }, - joystick_dpad4_east: { - right: "snap_rotate_right" - }, - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], - abuttontouchstart: "thumb_down", + joystick_dpad4_west: { right: "snap_rotate_left" }, + joystick_dpad4_east: { right: "snap_rotate_right" }, + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], + abuttontouchstart: ["thumb_down", "increase_radius"], abuttontouchend: "thumb_up", - bbuttontouchstart: "thumb_down", + bbuttontouchstart: ["thumb_down", "decrease_radius"], bbuttontouchend: "thumb_up", - xbuttontouchstart: "thumb_down", + xbuttontouchstart: ["thumb_down", "increase_radius"], xbuttontouchend: "thumb_up", - ybuttontouchstart: "thumb_down", + ybuttontouchstart: ["thumb_down", "decrease_radius"], ybuttontouchend: "thumb_up", - surfacetouchstart: "thumb_down", + surfacetouchstart: ["thumb_down", "next_color"], surfacetouchend: "thumb_up", thumbsticktouchstart: "thumb_down", thumbsticktouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - "axismove.reverseY": { left: "move", right: "move_duck" }, + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + "axismove.reverseY": { left: "move", right: "scroll_move" }, abuttondown: "action_primary_down", abuttonup: "action_primary_up" }, @@ -103,16 +99,16 @@ const config = { joystick_dpad4_pressed_west_down: { right: "snap_rotate_left" }, joystick_dpad4_pressed_east_down: { right: "snap_rotate_right" }, trackpaddown: { right: "action_primary_down" }, - trackpadup: { right: "action_primary_up" }, + trackpadup: { left: "action_primary_up", right: "action_primary_up" }, menudown: "thumb_down", menuup: "thumb_up", - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], trackpadtouchstart: "thumb_down", trackpadtouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - axisMoveWithDeadzone: { left: "move", right: "move_duck" } + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + axisMoveWithDeadzone: { left: "move", right: "scroll_move" } }, "daydream-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -121,7 +117,7 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - axisMoveWithDeadzone: "move_duck" + axisMoveWithDeadzone: "scroll_move" }, "gearvr-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -130,9 +126,9 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - triggerdown: ["action_primary_down"], - triggerup: ["action_primary_up"], - scroll: "move_duck" + triggerdown: ["action_secondary_down"], + triggerup: ["action_secondary_up"], + scroll: "scroll_move" }, "oculus-go-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -141,9 +137,9 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - triggerdown: ["action_primary_down"], - triggerup: ["action_primary_up"], - scroll: "move_duck" + triggerdown: ["action_secondary_down"], + triggerup: ["action_secondary_up"], + scroll: "scroll_move" }, keyboard: { m_press: "action_mute", @@ -170,44 +166,6 @@ const config = { arrowright_down: "d_down", arrowright_up: "d_up" } - }, - hud: { - "vive-controls": { - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"] - }, - "oculus-touch-controls": { - abuttondown: "action_ui_select_down", - abuttonup: "action_ui_select_up", - gripdown: "middle_ring_pinky_down", - gripup: "middle_ring_pinky_up", - abuttontouchstart: "thumb_down", - abuttontouchend: "thumb_up", - bbuttontouchstart: "thumb_down", - bbuttontouchend: "thumb_up", - xbuttontouchstart: "thumb_down", - xbuttontouchend: "thumb_up", - ybuttontouchstart: "thumb_down", - ybuttontouchend: "thumb_up", - surfacetouchstart: "thumb_down", - surfacetouchend: "thumb_up", - thumbsticktouchstart: "thumb_down", - thumbsticktouchend: "thumb_up", - triggertouchstart: "index_down", - triggertouchend: "index_up" - }, - "daydream-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - }, - "gearvr-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - }, - "oculus-go-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - } } } }; diff --git a/src/link.html b/src/link.html index 954061491faa3b257adedf87df06a26cfc58f79b..f2d46396072f3e580a5543261b534d3b382aa48b 100644 --- a/src/link.html +++ b/src/link.html @@ -6,7 +6,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="shortcut icon" type="image/png" href="/favicon.ico"/> <title>Enter Code | Hubs by Mozilla</title> - <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> </head> diff --git a/src/link.js b/src/link.js index 401fe54d9b8b9bd91df1c2140257710502a23add..7f803b75a780d22bd85e51f6902806efebea1b5e 100644 --- a/src/link.js +++ b/src/link.js @@ -6,6 +6,7 @@ import LinkRoot from "./react-components/link-root"; import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import Store from "./storage/store"; +import { detectInHMD } from "./utils/vr-caps-detect.js"; registerTelemetry(); @@ -17,4 +18,7 @@ const linkChannel = new LinkChannel(store); linkChannel.setSocket(socket); -ReactDOM.render(<LinkRoot store={store} linkChannel={linkChannel} />, document.getElementById("link-root")); +ReactDOM.render( + <LinkRoot store={store} linkChannel={linkChannel} showHeadsetLinkOption={detectInHMD()} />, + document.getElementById("link-root") +); diff --git a/src/network-schemas.js b/src/network-schemas.js index 4c8ac07e05625edffe916a549d96ea0a95bc297f..97f55c606d9b668c260f4ba82a061325e1711181 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -1,15 +1,19 @@ function registerNetworkSchemas() { const vectorRequiresUpdate = epsilon => { - let prev = null; - return curr => { - if (prev === null) { - prev = new THREE.Vector3(curr.x, curr.y, curr.z); - return true; - } else if (!NAF.utils.almostEqualVec3(prev, curr, epsilon)) { - prev.copy(curr); - return true; - } - return false; + return () => { + let prev = null; + + return curr => { + if (prev === null) { + prev = new THREE.Vector3(curr.x, curr.y, curr.z); + return true; + } else if (!NAF.utils.almostEqualVec3(prev, curr, epsilon)) { + prev.copy(curr); + return true; + } + + return false; + }; }; }; @@ -72,10 +76,12 @@ function registerNetworkSchemas() { template: "#video-template", components: [ { - component: "position" + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { - component: "rotation" + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, "visible" ] @@ -101,6 +107,57 @@ function registerNetworkSchemas() { { component: "media-video", property: "videoPaused" + }, + { + component: "media-pager", + property: "index" + } + ] + }); + + NAF.schemas.add({ + template: "#interactable-drawing", + components: [ + { + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) + }, + { + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) + }, + "scale", + "networked-drawing" + ] + }); + + NAF.schemas.add({ + template: "#interactable-camera", + components: ["position", "rotation"] + }); + + NAF.schemas.add({ + template: "#pen-interactable", + components: [ + { + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) + }, + { + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) + }, + "scale", + "media-loader", + { + selector: "#pen", + component: "pen", + property: "radius" + }, + { + selector: "#pen", + component: "pen", + property: "color" } ] }); diff --git a/src/object-types.js b/src/object-types.js index d8f959c2531a59fe713c85b2141a013eed785be5..8c9ecc581b7485f4ad24187986dfd885a5c5f8a3 100644 --- a/src/object-types.js +++ b/src/object-types.js @@ -12,7 +12,7 @@ export const ObjectContentOrigins = { // Enumeration of spawnable object types, used for telemetry, which encapsulates // both the origin of the content for the object and also the type of content // contained in the object. -const ObjectTypes = { +export const ObjectTypes = { URL_IMAGE: 0, URL_VIDEO: 1, URL_MODEL: 2, @@ -38,7 +38,7 @@ const ObjectTypes = { SPAWNER_PDF: 27, SPAWNER_AUDIO: 28, //SPAWNER_TEXT: 29, - //DRAWING: 30, + CAMERA: 30, UNKNOWN: 31 }; diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 15168e999bd4576f80b0f4242ed60675f4557699..74577ff1ec0291d8bd14665fe0a7054c8a0f70e3 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -3,10 +3,11 @@ import PropTypes from "prop-types"; import cx from "classnames"; import styles from "../assets/stylesheets/2d-hud.scss"; +import uiStyles from "../assets/stylesheets/ui-root.scss"; -const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onToggleSpaceBubble }) => ( - <div className={cx(styles.container, styles.top)}> - <div className={cx("ui-interactive", styles.panel, styles.left)}> +const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen, onSpawnCamera }) => ( + <div className={cx(styles.container, styles.top, styles.unselectable)}> + <div className={cx(uiStyles.uiInteractive, styles.panel, styles.left)}> <div className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} title={muted ? "Unmute Mic" : "Mute Mic"} @@ -14,16 +15,15 @@ const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onTo /> </div> <div - className={cx("ui-interactive", styles.iconButton, styles.large, styles.freeze, { [styles.active]: frozen })} + className={cx(uiStyles.uiInteractive, styles.iconButton, styles.large, styles.freeze, { + [styles.active]: frozen + })} title={frozen ? "Resume" : "Pause"} onClick={onToggleFreeze} /> - <div className={cx("ui-interactive", styles.panel, styles.right)}> - <div - className={cx(styles.iconButton, styles.bubble, { [styles.active]: spacebubble })} - title={spacebubble ? "Disable Bubble" : "Enable Bubble"} - onClick={onToggleSpaceBubble} - /> + <div className={cx(uiStyles.uiInteractive, styles.panel, styles.right)}> + <div className={cx(styles.iconButton, styles.spawn_pen)} title={"Drawing Pen"} onClick={onSpawnPen} /> + <div className={cx(styles.iconButton, styles.spawn_camera)} title={"Camera"} onClick={onSpawnCamera} /> </div> </div> ); @@ -31,24 +31,49 @@ const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onTo TopHUD.propTypes = { muted: PropTypes.bool, frozen: PropTypes.bool, - spacebubble: PropTypes.bool, onToggleMute: PropTypes.func, onToggleFreeze: PropTypes.func, - onToggleSpaceBubble: PropTypes.func + onSpawnPen: PropTypes.func, + onSpawnCamera: PropTypes.func }; -const BottomHUD = ({ onCreateObject }) => ( - <div className={cx(styles.container, styles.bottom)}> - <div - className={cx("ui-interactive", styles.iconButton, styles.large, styles.createObject)} - title={"Create Object"} - onClick={onCreateObject} - /> +const BottomHUD = ({ onCreateObject, showPhotoPicker, onMediaPicked }) => ( + <div className={cx(styles.container, styles.column, styles.bottom, styles.unselectable)}> + {showPhotoPicker ? ( + <div className={cx(uiStyles.uiInteractive, styles.panel, styles.up)}> + <input + id="media-picker-input" + className={cx(styles.hide)} + type="file" + accept="image/*" + multiple + onChange={e => { + for (const file of e.target.files) { + onMediaPicked(file); + } + }} + /> + <label htmlFor="media-picker-input"> + <div className={cx(styles.iconButton, styles.mobileMediaPicker)} title={"Pick Media"} /> + </label> + </div> + ) : ( + <div /> + )} + <div> + <div + className={cx(uiStyles.uiInteractive, styles.iconButton, styles.large, styles.createObject)} + title={"Create Object"} + onClick={onCreateObject} + /> + </div> </div> ); BottomHUD.propTypes = { - onCreateObject: PropTypes.func + onCreateObject: PropTypes.func, + showPhotoPicker: PropTypes.bool, + onMediaPicked: PropTypes.func }; export default { TopHUD, BottomHUD }; diff --git a/src/react-components/auth-dialog.js b/src/react-components/auth-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..5c7796863c6521b97edbc0d42571d8bea4b10911 --- /dev/null +++ b/src/react-components/auth-dialog.js @@ -0,0 +1,32 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { injectIntl, FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; + +class AuthDialog extends Component { + static propTypes = { + intl: PropTypes.object, + verifying: PropTypes.bool, + authOrigin: PropTypes.string + }; + + render() { + const { authOrigin, verifying } = this.props; + const { formatMessage } = this.props.intl; + const title = verifying ? formatMessage({ id: "auth.verifying" }) : formatMessage({ id: "auth.verified-title" }); + + return ( + <DialogContainer title={title} {...this.props}> + {verifying ? ( + <FormattedMessage id="auth.verifying" /> + ) : authOrigin === "spoke" ? ( + <FormattedMessage id="auth.spoke-verified" values={{ br: <br /> }} /> + ) : ( + <FormattedMessage id="auth.verified" /> + )} + </DialogContainer> + ); + } +} + +export default injectIntl(AuthDialog); diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js index 81b30d6057b436a296b9856f742b1bb907514495..0c024655d46b39c77bf7c5f50e87733907cbd790 100644 --- a/src/react-components/avatar-selector.js +++ b/src/react-components/avatar-selector.js @@ -5,9 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; -// TODO: we should make a bundle for avatar picker with it's own geometry, for now just use the indoor part of the meting room -const meetingSpace = "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingSpace1_mesh-d48250ebc6.gltf"; - class AvatarSelector extends Component { static propTypes = { avatars: PropTypes.array, @@ -137,11 +134,7 @@ class AvatarSelector extends Component { const avatarData = this.state.avatarIndices.map(i => [this.props.avatars[i], i]); const avatarEntities = avatarData.map(([avatar, i]) => ( <a-entity key={avatar.id} rotation={`0 ${(360 * -i) / this.props.avatars.length} 0`}> - <a-entity position="0 0 5" gltf-model-plus={`src: #${avatar.id}`} inflate="true"> - <template data-selector=".RootScene"> - <a-entity animation-mixer="" /> - </template> - + <a-entity position="0 0 5" gltf-model-plus={`src: #${avatar.id}; inflate: true`}> <a-animation attribute="rotation" dur="12000" @@ -159,10 +152,7 @@ class AvatarSelector extends Component { return ( <div className="avatar-selector"> <a-scene vr-mode-ui="enabled: false" ref={sce => (this.scene = sce)}> - <a-assets> - {avatarAssets} - <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src={meetingSpace} /> - </a-assets> + <a-assets>{avatarAssets}</a-assets> <a-entity rotation={`0 ${initialRotation} 0`}> <a-animation @@ -185,7 +175,6 @@ class AvatarSelector extends Component { position="0 5 -15" /> <a-entity hide-when-quality="low" light="type: ambient; color: #FFF" /> - <a-entity id="meeting-space" gltf-model-plus="src: #meeting-space1-mesh" position="0 0 0" /> </a-scene> <button className="avatar-selector__previous-button" onClick={this.emitChangeToPrevious}> <FontAwesomeIcon icon={faAngleLeft} /> diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index caf31f378b62aaeb5103721a36f182a42a5c8d07..5ccfe2ce692a234fff6e76d8657516c1ac1d8816 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -1,11 +1,12 @@ import React, { Component } from "react"; -import "aframe"; import PropTypes from "prop-types"; import giphyLogo from "../assets/images/giphy_logo.png"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPaperclip, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip"; +import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; import styles from "../assets/stylesheets/create-object-dialog.scss"; import cx from "classnames"; +import DialogContainer from "./dialog-container.js"; const attributionHostnames = { "giphy.com": giphyLogo, @@ -35,8 +36,8 @@ export default class CreateObjectDialog extends Component { }; static propTypes = { - onCreateObject: PropTypes.func, - onCloseDialog: PropTypes.func + onCreate: PropTypes.func, + onClose: PropTypes.func }; componentDidMount() { @@ -66,12 +67,6 @@ export default class CreateObjectDialog extends Component { }); }; - onCreateClicked = e => { - e.preventDefault(); - this.props.onCreateObject(this.state.file || this.state.url || DEFAULT_OBJECT_URL); - this.props.onCloseDialog(); - }; - reset = e => { e.preventDefault(); this.setState({ @@ -82,7 +77,15 @@ export default class CreateObjectDialog extends Component { this.fileInput.value = null; }; + onCreateClicked = e => { + e.preventDefault(); + this.props.onCreate(this.state.file || this.state.url || DEFAULT_OBJECT_URL); + this.props.onClose(); + }; + render() { + const { onCreate, onClose, ...other } = this.props; // eslint-disable-line no-unused-vars + const cancelButton = ( <label className={cx(styles.smallButton, styles.cancelIcon)} onClick={this.reset}> <FontAwesomeIcon icon={faTimes} /> @@ -105,34 +108,36 @@ export default class CreateObjectDialog extends Component { ); return ( - <div> - {isMobile ? mobileInstructions : desktopInstructions} - <form onSubmit={this.onCreateClicked}> - <div className={styles.addMediaForm}> - <input - id={fileInputId} - ref={f => (this.fileInput = f)} - className={styles.hideFileInput} - type="file" - onChange={this.onFileChange} - /> - <div className={styles.inputBorder}> - {this.state.file ? filenameLabel : urlInput} - {this.state.url || this.state.fileName ? cancelButton : uploadButton} - </div> - <div className={styles.buttons}> - <button className={styles.actionButton}> - <span>create</span> - </button> - </div> - {this.state.attributionImage ? ( - <div> - <img src={this.state.attributionImage} /> + <DialogContainer title="Create Object" onClose={onClose} {...other}> + <div> + {isMobile ? mobileInstructions : desktopInstructions} + <form onSubmit={this.onCreateClicked}> + <div className={styles.addMediaForm}> + <input + id={fileInputId} + ref={f => (this.fileInput = f)} + className={styles.hideFileInput} + type="file" + onChange={this.onFileChange} + /> + <div className={styles.inputBorder}> + {this.state.file ? filenameLabel : urlInput} + {this.state.url || this.state.fileName ? cancelButton : uploadButton} + </div> + <div className={styles.buttons}> + <button className={styles.actionButton}> + <span>Create</span> + </button> </div> - ) : null} - </div> - </form> - </div> + {this.state.attributionImage ? ( + <div> + <img src={this.state.attributionImage} /> + </div> + ) : null} + </div> + </form> + </div> + </DialogContainer> ); } } diff --git a/src/react-components/create-room-dialog.js b/src/react-components/create-room-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..756cc9bf43736059999a1a66b2fdfe5d856a4e96 --- /dev/null +++ b/src/react-components/create-room-dialog.js @@ -0,0 +1,59 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import DialogContainer from "./dialog-container.js"; + +const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; + +export default class CreateObjectDialog extends Component { + static propTypes = { + onCustomScene: PropTypes.func, + onClose: PropTypes.func + }; + + state = { + customRoomName: "", + customSceneUrl: "" + }; + + render() { + const { onCustomScene, onClose, ...other } = this.props; + const onCustomSceneClicked = () => { + onCustomScene(this.state.customRoomName, this.state.customSceneUrl); + onClose(); + }; + + return ( + <DialogContainer title="Create a Room" onClose={onClose} {...other}> + <div> + <div>Choose a name and GLTF URL for your room's scene:</div> + <form onSubmit={onCustomSceneClicked}> + <div className="custom-scene-form"> + <input + type="text" + placeholder="Room name" + className="custom-scene-form__link_field" + value={this.state.customRoomName} + pattern={HUB_NAME_PATTERN} + title="Invalid name, limited to 4 to 64 characters and limited symbols." + onChange={e => this.setState({ customRoomName: e.target.value })} + required + /> + <input + type="url" + placeholder="URL to Scene GLTF or GLB (Optional)" + className="custom-scene-form__link_field" + value={this.state.customSceneUrl} + onChange={e => this.setState({ customSceneUrl: e.target.value })} + /> + <div className="custom-scene-form__buttons"> + <button className="custom-scene-form__action-button"> + <span>create</span> + </button> + </div> + </div> + </form> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/dialog-container.js b/src/react-components/dialog-container.js new file mode 100644 index 0000000000000000000000000000000000000000..18c37956c7408150d59d2385e251817ad9747cb7 --- /dev/null +++ b/src/react-components/dialog-container.js @@ -0,0 +1,57 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +export default class DialogContainer extends Component { + static propTypes = { + title: PropTypes.node, + children: PropTypes.node.isRequired, + onClose: PropTypes.func + }; + + constructor(props) { + super(props); + this.onKeyDown = this.onKeyDown.bind(this); + this.onContainerClicked = this.onContainerClicked.bind(this); + } + + componentDidMount() { + window.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + window.removeEventListener("keydown", this.onKeyDown); + } + + onKeyDown(e) { + if (e.key === "Escape") { + this.props.onClose(); + } + } + + onContainerClicked = e => { + if (e.currentTarget === e.target) { + this.props.onClose(); + } + }; + + render() { + return ( + <div className="dialog-overlay"> + <div className="dialog" onClick={this.onContainerClicked}> + <div className="dialog__box"> + <div className="dialog__box__contents"> + {this.props.onClose && ( + <button className="dialog__box__contents__close" onClick={this.props.onClose}> + <span>×</span> + </button> + )} + <div className="dialog__box__contents__title">{this.props.title}</div> + <div className="dialog__box__contents__body">{this.props.children}</div> + <div className="dialog__box__contents__button-container" /> + </div> + </div> + </div> + </div> + ); + } +} diff --git a/src/react-components/help-dialog.js b/src/react-components/help-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..abb04a1c9e107323f9993d0e160b352c39753fa2 --- /dev/null +++ b/src/react-components/help-dialog.js @@ -0,0 +1,45 @@ +import React, { Component } from "react"; +import { FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; + +export default class HelpDialog extends Component { + render() { + return ( + <DialogContainer title="Getting Started" {...this.props}> + <div className="info-dialog__help"> + <p style={{ textAlign: "center" }}> + Join the Hubs community on{" "} + <a target="_blank" rel="noopener noreferrer" href="https://discord.gg/XzrGUY8"> + Discord + </a>! + </p> + <p>When in a room, other avatars can see and hear you.</p> + <p> + Use your controller's action button to teleport from place to place. If it has a trigger, use it to + pick up objects. + </p> + <p> + In VR, <b>look up</b> to find your menu. + </p> + <p> + The <b>Mic Toggle</b> mutes your mic. + </p> + <p> + The <b>Pause/Resume Toggle</b> pauses all other avatars and lets you block others or remove objects. + </p> + <p className="dialog__box__contents__links"> + <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> + <FormattedMessage id="profile.terms_of_use" /> + </a> + <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"> + <FormattedMessage id="profile.privacy_notice" /> + </a> + <a target="_blank" rel="noopener noreferrer" href="/?report"> + <FormattedMessage id="help.report_issue" /> + </a> + </p> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 80df12ea336bfe0c35fae69ef05e38b95f88fde6..8dfc8bd5c652b162b8518651107770d0a41554ca 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -4,62 +4,139 @@ import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; import { lang, messages } from "../utils/i18n"; +import { playVideoWithStopOnBlur } from "../utils/video-utils.js"; import homeVideoWebM from "../assets/video/home.webm"; import homeVideoMp4 from "../assets/video/home.mp4"; +import hubLogo from "../assets/images/hub-preview-light-no-shadow.png"; +import mozLogo from "../assets/images/moz-logo-black.png"; import classNames from "classnames"; import { ENVIRONMENT_URLS } from "../assets/environments/environments"; +import { connectToReticulum } from "../utils/phoenix-utils"; import styles from "../assets/stylesheets/index.scss"; import HubCreatePanel from "./hub-create-panel.js"; -import InfoDialog from "./info-dialog.js"; +import AuthDialog from "./auth-dialog.js"; +import ReportDialog from "./report-dialog.js"; +import JoinUsDialog from "./join-us-dialog.js"; +import UpdatesDialog from "./updates-dialog.js"; +import DialogContainer from "./dialog-container.js"; addLocaleData([...en]); class HomeRoot extends Component { static propTypes = { intl: PropTypes.object, - dialogType: PropTypes.symbol, + sceneId: PropTypes.string, + authVerify: PropTypes.bool, + authTopic: PropTypes.string, + authToken: PropTypes.string, + authOrigin: PropTypes.string, + listSignup: PropTypes.bool, + report: PropTypes.bool, initialEnvironment: PropTypes.string }; state = { environments: [], - dialogType: null, + dialog: null, mailingListEmail: "", mailingListPrivacy: false }; componentDidMount() { - this.loadEnvironments(); - this.setState({ dialogType: this.props.dialogType }); + this.closeDialog = this.closeDialog.bind(this); + if (this.props.authVerify) { + this.showAuthDialog(true); + this.verifyAuth().then(this.showAuthDialog); + return; + } + if (this.props.sceneId) { + this.loadEnvironmentFromScene(); + } else { + this.loadEnvironments(); + } this.loadHomeVideo(); + if (this.props.listSignup) { + this.showUpdatesDialog(); + } else if (this.props.report) { + this.showReportDialog(); + } + } + + async verifyAuth() { + const socket = connectToReticulum(); + const channel = socket.channel(this.props.authTopic); + await new Promise((resolve, reject) => + channel + .join() + .receive("ok", resolve) + .receive("error", reject) + ); + channel.push("auth_verified", { token: this.props.authToken }); } + showAuthDialog = verifying => { + this.setState({ dialog: <AuthDialog verifying={verifying} authOrigin={this.props.authOrigin} /> }); + }; + loadHomeVideo = () => { const videoEl = document.querySelector("#background-video"); videoEl.playbackRate = 0.9; - function toggleVideo() { - // Play the video if the window/tab is visible. - if (document.hasFocus()) { - videoEl.play(); - } else { - videoEl.pause(); - } - } - if ("hasFocus" in document) { - document.addEventListener("visibilitychange", toggleVideo); - window.addEventListener("focus", toggleVideo); - window.addEventListener("blur", toggleVideo); - } + playVideoWithStopOnBlur(videoEl); }; - showDialog = dialogType => { - return e => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ dialogType }); - }; + closeDialog() { + this.setState({ dialog: null }); + } + + showJoinUsDialog() { + this.setState({ dialog: <JoinUsDialog onClose={this.closeDialog} /> }); + } + + showReportDialog() { + this.setState({ dialog: <ReportDialog onClose={this.closeDialog} /> }); + } + + showUpdatesDialog() { + this.setState({ + dialog: <UpdatesDialog onClose={this.closeDialog} onSubmittedEmail={() => this.showEmailSubmittedDialog()} /> + }); + } + + showEmailSubmittedDialog() { + this.setState({ + dialog: ( + <DialogContainer onClose={this.closeDialog}> + Great! Please check your e-mail to confirm your subscription. + </DialogContainer> + ) + }); + } + + loadEnvironmentFromScene = async () => { + let sceneUrlBase = "/api/v1/scenes"; + if (process.env.RETICULUM_SERVER) { + sceneUrlBase = `https://${process.env.RETICULUM_SERVER}${sceneUrlBase}`; + } + const sceneInfoUrl = `${sceneUrlBase}/${this.props.sceneId}`; + const resp = await fetch(sceneInfoUrl).then(r => r.json()); + const scene = resp.scenes[0]; + const attribution = scene.attribution && scene.attribution.split("\n").join(", "); + const authors = attribution && [{ organization: { name: attribution } }]; + // Transform the scene info into a an environment bundle structure. + this.setState({ + environments: [ + { + scene_id: this.props.sceneId, + meta: { + title: scene.name, + authors, + images: [{ type: "preview-thumbnail", srcset: scene.screenshot_url }] + } + } + ] + }); }; loadEnvironments = () => { @@ -77,34 +154,38 @@ class HomeRoot extends Component { Promise.all(environmentLoads).then(() => this.setState({ environments })); }; + onDialogLinkClicked = trigger => { + return e => { + e.preventDefault(); + e.stopPropagation(); + trigger(); + }; + }; + render() { const mainContentClassNames = classNames({ [styles.mainContent]: true, - [styles.noninteractive]: !!this.state.dialogType + [styles.noninteractive]: !!this.state.dialog }); - const dialogTypes = InfoDialog.dialogTypes; return ( <IntlProvider locale={lang} messages={messages}> <div className={styles.home}> <div className={mainContentClassNames}> <div className={styles.headerContent}> - <div className={styles.titleAndNav} onClick={() => (document.location = "/")}> - <div className={styles.hubs}>hubs</div> - <div className={styles.preview}>preview</div> + <div className={styles.titleAndNav}> <div className={styles.links}> <a href="https://github.com/mozilla/hubs" rel="noreferrer noopener"> <FormattedMessage id="home.source_link" /> </a> - <a - href="https://blog.mozvr.com/introducing-hubs-a-new-way-to-get-together-online/" - rel="noreferrer noopener" - > - <FormattedMessage id="home.about_link" /> + <a href="https://discord.gg/XzrGUY8" rel="noreferrer noopener"> + <FormattedMessage id="home.community_link" /> + </a> + <a href="/spoke" rel="noreferrer noopener"> + Spoke </a> </div> </div> - <div className={styles.ident} /> </div> <div className={styles.heroContent}> <div className={styles.attribution}> @@ -118,21 +199,31 @@ class HomeRoot extends Component { </a> </div> <div className={styles.container}> + <img className={styles.logo} src={hubLogo} /> <div className={styles.title}> <FormattedMessage id="home.hero_title" /> </div> - <div className={styles.subtitle}> - <FormattedMessage id="home.hero_subtitle" /> - </div> + {this.state.environments.length === 0 && ( + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> + )} </div> <div className={styles.create}> - {this.state.environments.length > 0 && ( - <HubCreatePanel - initialEnvironment={this.props.initialEnvironment} - environments={this.state.environments} - /> - )} + <HubCreatePanel + initialEnvironment={this.props.initialEnvironment} + environments={this.state.environments} + /> </div> + {this.state.environments.length > 1 && ( + <div className={styles.joinButton}> + <a href="/link"> + <FormattedMessage id="home.join_room" /> + </a> + </div> + )} </div> <div className={styles.footerContent}> <div className={styles.links}> @@ -141,7 +232,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.slack)} + onClick={this.onDialogLinkClicked(this.showJoinUsDialog.bind(this))} > <FormattedMessage id="home.join_us" /> </a> @@ -149,7 +240,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.updates)} + onClick={this.onDialogLinkClicked(this.showUpdatesDialog.bind(this))} > <FormattedMessage id="home.get_updates" /> </a> @@ -157,7 +248,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.report)} + onClick={this.onDialogLinkClicked(this.showReportDialog.bind(this))} > <FormattedMessage id="home.report_issue" /> </a> @@ -177,12 +268,8 @@ class HomeRoot extends Component { > <FormattedMessage id="home.privacy_notice" /> </a> - </div> - <div className={styles.bottom}> - <div> - <FormattedMessage id="home.made_with_love" /> - <span style={{ fontWeight: "bold", color: "white" }}>Mozilla</span> - </div> + + <img className={styles.mozLogo} src={mozLogo} /> </div> </div> </div> @@ -191,13 +278,7 @@ class HomeRoot extends Component { <source src={homeVideoWebM} type="video/webm" /> <source src={homeVideoMp4} type="video/mp4" /> </video> - {this.state.dialogType && ( - <InfoDialog - dialogType={this.state.dialogType} - onCloseDialog={() => this.setState({ dialogType: null })} - onSubmittedEmail={() => this.setState({ dialogType: dialogTypes.email_submitted })} - /> - )} + {this.state.dialog} </div> </IntlProvider> ); diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 1abb749612baa6f8b8104cdd034a4d9b9531536e..8a9ca35e28edb3d99c3685e483e3f031c755c557 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,13 +6,13 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { resolveURL, extractUrlBase } from "../utils/resolveURL"; -import InfoDialog from "./info-dialog.js"; +import { getReticulumFetchUrl } from "../utils/phoenix-utils"; +import CreateRoomDialog from "./create-room-dialog.js"; import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png"; import styles from "../assets/stylesheets/hub-create.scss"; const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; -const dialogTypes = InfoDialog.dialogTypes; class HubCreatePanel extends Component { static propTypes = { @@ -62,11 +62,18 @@ class HubCreatePanel extends Component { const thumbnailImage = meta.images.find(i => i.type === "preview-thumbnail"); if (thumbnailImage) { - const baseURL = new URL(extractUrlBase(environment.bundle_url), window.location.href); - - environmentThumbnail = { - srcset: resolveURL(thumbnailImage.srcset, baseURL) - }; + // TODO kill bundles + if (environment.bundle_url) { + const baseURL = new URL(extractUrlBase(environment.bundle_url), window.location.href); + + environmentThumbnail = { + srcset: resolveURL(thumbnailImage.srcset, baseURL) + }; + } else { + environmentThumbnail = { + srcset: thumbnailImage.srcset + }; + } } } @@ -79,18 +86,20 @@ class HubCreatePanel extends Component { } const environment = this.props.environments[this.state.environmentIndex]; - const sceneUrl = this.state.customSceneUrl || environment.bundle_url; const payload = { - hub: { name: this.state.name, default_environment_gltf_bundle_url: sceneUrl } + hub: { name: this.state.name } }; - let createUrl = "/api/v1/hubs"; - - if (process.env.RETICULUM_SERVER) { - createUrl = `https://${process.env.RETICULUM_SERVER}${createUrl}`; + if (!this.state.customSceneUrl && environment.scene_id) { + payload.hub.scene_id = environment.scene_id; + } else { + const sceneUrl = this.state.customSceneUrl || environment.bundle_url; + payload.hub.default_environment_gltf_bundle_url = sceneUrl; } + const createUrl = getReticulumFetchUrl("/api/v1/hubs"); + const res = await fetch(createUrl, { body: JSON.stringify(payload), headers: { "content-type": "application/json" }, @@ -153,7 +162,7 @@ class HubCreatePanel extends Component { if (!this.state.ready) return null; if (this.props.environments.length == 0) { - return <div />; + return <div className={styles.placeholder} />; } const environment = this.props.environments[this.state.environmentIndex]; @@ -173,13 +182,7 @@ class HubCreatePanel extends Component { <img className={styles.image} srcSet={environmentThumbnail.srcset} /> <div className={styles.labels}> <div className={styles.header}> - {meta.url ? ( - <a href={meta.url} rel="noopener noreferrer" className={styles.title}> - {environmentTitle} - </a> - ) : ( - <span className={styles.itle}>environmentTitle</span> - )} + <span className={styles.title}>{environmentTitle}</span> {environmentAuthor && environmentAuthor.name && (environmentAuthor.url ? ( @@ -227,20 +230,12 @@ class HubCreatePanel extends Component { <FormattedMessage id="home.room_create_button" /> </button> </div> - <div className={styles.linkCode}> - <div> - <a className={styles.link} href="/link" rel="nofollow"> - <FormattedMessage id="home.have_entry_code" /> - </a> - </div> - </div> </div> </div> </form> {this.state.showCustomSceneDialog && ( - <InfoDialog - dialogType={dialogTypes.custom_scene} - onCloseDialog={() => this.setState({ showCustomSceneDialog: false })} + <CreateRoomDialog + onClose={() => this.setState({ showCustomSceneDialog: false })} onCustomScene={(name, url) => { this.setState({ showCustomSceneDialog: false, name: name, customSceneUrl: url }, () => this.createHub()); }} diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js deleted file mode 100644 index 5130e54a3de6e20e68f4b79ba052aa98d993d94e..0000000000000000000000000000000000000000 --- a/src/react-components/info-dialog.js +++ /dev/null @@ -1,396 +0,0 @@ -import React, { Component } from "react"; -import copy from "copy-to-clipboard"; -import PropTypes from "prop-types"; -import { FormattedMessage } from "react-intl"; -import formurlencoded from "form-urlencoded"; -import LinkDialog from "./link-dialog.js"; -import CreateObjectDialog from "./create-object-dialog.js"; -const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; - -// TODO i18n - -class InfoDialog extends Component { - static dialogTypes = { - slack: Symbol("slack"), - email_submitted: Symbol("email_submitted"), - invite: Symbol("invite"), - safari: Symbol("safari"), - updates: Symbol("updates"), - report: Symbol("report"), - help: Symbol("help"), - link: Symbol("link"), - webvr_recommend: Symbol("webvr_recommend"), - create_object: Symbol("create_object"), - custom_scene: Symbol("custom_scene") - }; - static propTypes = { - dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)), - onCloseDialog: PropTypes.func, - onSubmittedEmail: PropTypes.func, - onCreateObject: PropTypes.func, - onCustomScene: PropTypes.func, - linkCode: PropTypes.string - }; - - constructor(props) { - super(props); - - const loc = document.location; - this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; - this.onKeyDown = this.onKeyDown.bind(this); - this.onContainerClicked = this.onContainerClicked.bind(this); - } - - componentDidMount() { - window.addEventListener("keydown", this.onKeyDown); - } - - componentWillUnmount() { - window.removeEventListener("keydown", this.onKeyDown); - } - - onKeyDown(e) { - if (e.key === "Escape") { - this.props.onCloseDialog(); - } - } - - onContainerClicked = e => { - if (e.currentTarget === e.target) { - this.props.onCloseDialog(); - } - }; - - onCustomSceneClicked = () => { - this.props.onCustomScene(this.state.customRoomName, this.state.customSceneUrl); - this.props.onCloseDialog(); - }; - - shareLinkClicked = () => { - navigator.share({ - title: document.title, - url: this.shareLink - }); - }; - - copyLinkClicked = link => { - copy(link); - this.setState({ copyLinkButtonText: "copied!" }); - }; - - state = { - mailingListEmail: "", - mailingListPrivacy: false, - copyLinkButtonText: "copy", - createObjectUrl: "", - customRoomName: "", - customSceneUrl: "" - }; - - signUpForMailingList = async e => { - e.preventDefault(); - e.stopPropagation(); - if (!this.state.mailingListPrivacy) return; - - const url = "https://www.mozilla.org/en-US/newsletter/"; - - const payload = { - email: this.state.mailingListEmail, - newsletters: "hubs", - privacy: true, - fmt: "H", - source_url: document.location.href - }; - - await fetch(url, { - body: formurlencoded(payload), - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" } - }).then(this.props.onSubmittedEmail); - }; - - render() { - if (!this.props.dialogType) { - return <div />; - } - - let dialogTitle = null; - let dialogBody = null; - - switch (this.props.dialogType) { - // TODO i18n, FormattedMessage doesn't play nicely with links - case InfoDialog.dialogTypes.slack: - dialogTitle = "Get in Touch"; - dialogBody = ( - <span> - <p>Want to join the conversation?</p> - <p> - Join us on the{" "} - <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> - WebVR Slack - </a>{" "} - in the{" "} - <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> - #social - </a>{" "} - channel.<br /> - VR meetups every Friday at noon PDT! - </p> - <p> - Or, tweet at{" "} - <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer"> - @mozillareality - </a>{" "} - on Twitter. - </p> - </span> - ); - break; - case InfoDialog.dialogTypes.email_submitted: - dialogTitle = ""; - dialogBody = "Great! Please check your e-mail to confirm your subscription."; - break; - case InfoDialog.dialogTypes.invite: - dialogTitle = "Invite Others"; - dialogBody = ( - <div> - <div>Just share the link and they'll join you:</div> - <div className="invite-form"> - <input - type="text" - readOnly - onFocus={e => e.target.select()} - value={this.shareLink} - className="invite-form__link_field" - /> - <div className="invite-form__buttons"> - {navigator.share && ( - <button className="invite-form__action-button" onClick={this.shareLinkClicked}> - <span>share</span> - </button> - )} - <button - className="invite-form__action-button" - onClick={this.copyLinkClicked.bind(this, this.shareLink)} - > - <span>{this.state.copyLinkButtonText}</span> - </button> - </div> - </div> - </div> - ); - break; - case InfoDialog.dialogTypes.safari: - dialogTitle = "Open in Safari"; - dialogBody = ( - <div> - <div>Hubs does not support your current browser on iOS. Copy and paste this link directly in Safari.</div> - <div className="invite-form"> - <input - type="text" - readOnly - onFocus={e => e.target.select()} - value={document.location} - className="invite-form__link_field" - /> - <div className="invite-form__buttons"> - <button - className="invite-form__action-button" - onClick={this.copyLinkClicked.bind(this, document.location)} - > - <span>{this.state.copyLinkButtonText}</span> - </button> - </div> - </div> - </div> - ); - break; - case InfoDialog.dialogTypes.create_object: - dialogTitle = "Create Object"; - dialogBody = ( - <CreateObjectDialog onCreateObject={this.props.onCreateObject} onCloseDialog={this.props.onCloseDialog} /> - ); - break; - case InfoDialog.dialogTypes.custom_scene: - dialogTitle = "Create a Room"; - dialogBody = ( - <div> - <div>Choose a name and GLTF URL for your room's scene:</div> - <form onSubmit={this.onCustomSceneClicked}> - <div className="custom-scene-form"> - <input - type="text" - placeholder="Room name" - className="custom-scene-form__link_field" - value={this.state.customRoomName} - pattern={HUB_NAME_PATTERN} - title="Invalid name, limited to 4 to 64 characters and limited symbols." - onChange={e => this.setState({ customRoomName: e.target.value })} - required - /> - <input - type="url" - placeholder="URL to Scene GLTF or GLB (Optional)" - className="custom-scene-form__link_field" - value={this.state.customSceneUrl} - onChange={e => this.setState({ customSceneUrl: e.target.value })} - /> - <div className="custom-scene-form__buttons"> - <button className="custom-scene-form__action-button"> - <span>create</span> - </button> - </div> - </div> - </form> - </div> - ); - break; - case InfoDialog.dialogTypes.updates: - dialogTitle = ""; - dialogBody = ( - <span> - <p>Sign up to get updates about new features in Hubs.</p> - <form onSubmit={this.signUpForMailingList}> - <div className="mailing-list-form"> - <input - type="email" - value={this.state.mailingListEmail} - onChange={e => this.setState({ mailingListEmail: e.target.value })} - className="mailing-list-form__email_field" - required - placeholder="Your email here" - /> - <label className="mailing-list-form__privacy"> - <input - className="mailing-list-form__privacy_checkbox" - type="checkbox" - required - value={this.state.mailingListPrivacy} - onChange={e => this.setState({ mailingListPrivacy: e.target.checked })} - /> - <span className="mailing-list-form__privacy_label"> - <FormattedMessage id="mailing_list.privacy_label" />{" "} - <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/"> - <FormattedMessage id="mailing_list.privacy_link" /> - </a> - </span> - </label> - <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" /> - </div> - </form> - </span> - ); - break; - case InfoDialog.dialogTypes.report: - dialogTitle = "Report an Issue"; - dialogBody = ( - <span> - <p>Need to report a problem?</p> - <p> - You can file a{" "} - <a href="https://github.com/mozilla/hubs/issues" target="_blank" rel="noopener noreferrer"> - GitHub Issue - </a>{" "} - or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. - </p> - <p> - You can also find us in{" "} - <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> - #social - </a>{" "} - on the{" "} - <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> - WebVR Slack - </a>. - </p> - </span> - ); - break; - case InfoDialog.dialogTypes.help: - dialogTitle = "Getting Started"; - dialogBody = ( - <div className="info-dialog__help"> - <p>When in a room, other avatars can see and hear you.</p> - <p> - Use your controller's action button to teleport from place to place. If it has a trigger, use it to - pick up objects. - </p> - <p style={{ textAlign: "center" }}> - In VR, <b>look up</b> to find your menu: - <img - className="info-dialog__help__hud" - src="../assets/images/help-hud.png" - srcSet="../assets/images/help-hud@2x.png 2x" - /> - </p> - <p> - The <b>Mic Toggle</b> mutes your mic. - </p> - <p> - The <b>Pause/Resume Toggle</b> pauses all other avatars. You can then block them from having further - interactions with you. - </p> - <p> - The <b>Bubble Toggle</b> hides avatars that enter your personal space. - </p> - <p className="dialog__box__contents__links"> - <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> - <FormattedMessage id="profile.terms_of_use" /> - </a> - <a - target="_blank" - rel="noopener noreferrer" - href="https://github.com/mozilla/hubs/blob/master/PRIVACY.md" - > - <FormattedMessage id="profile.privacy_notice" /> - </a> - <a target="_blank" rel="noopener noreferrer" href="/?report"> - <FormattedMessage id="help.report_issue" /> - </a> - </p> - </div> - ); - break; - case InfoDialog.dialogTypes.webvr_recommend: - dialogTitle = "Enter in VR"; - dialogBody = ( - <div> - <p>To enter Hubs with Oculus or SteamVR, you can use Firefox.</p> - <a className="info-dialog--action-button" href="https://www.mozilla.org/firefox"> - Download Firefox - </a> - <p style={{ fontSize: "0.8em" }}> - For a full list of browsers with experimental VR support, visit{" "} - <a href="https://webvr.rocks" target="_blank" rel="noopener noreferrer"> - WebVR Rocks - </a>. - </p> - </div> - ); - break; - case InfoDialog.dialogTypes.link: - dialogTitle = "Open on Headset"; - dialogBody = <LinkDialog linkCode={this.props.linkCode} />; - break; - } - - return ( - <div className="dialog-overlay"> - <div className="dialog" onClick={this.onContainerClicked}> - <div className="dialog__box"> - <div className="dialog__box__contents"> - <button className="dialog__box__contents__close" onClick={this.props.onCloseDialog}> - <span>×</span> - </button> - <div className="dialog__box__contents__title">{dialogTitle}</div> - <div className="dialog__box__contents__body">{dialogBody}</div> - <div className="dialog__box__contents__button-container" /> - </div> - </div> - </div> - </div> - ); - } -} - -export default InfoDialog; diff --git a/src/react-components/invite-dialog.js b/src/react-components/invite-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..414194081d417d6185cd1041bdb1ed1a7be355ab --- /dev/null +++ b/src/react-components/invite-dialog.js @@ -0,0 +1,100 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import copy from "copy-to-clipboard"; +import classNames from "classnames"; +import { FormattedMessage } from "react-intl"; + +import styles from "../assets/stylesheets/invite-dialog.scss"; + +function pad(num, size) { + let s = `${num}`; + while (s.length < size) s = `0${s}`; + return s; +} + +export default class InviteDialog extends Component { + static propTypes = { + entryCode: PropTypes.number, + hubId: PropTypes.string, + allowShare: PropTypes.bool, + onClose: PropTypes.func + }; + + state = { + copyButtonActive: false, + shareButtonActive: false + }; + + shareClicked = link => { + this.setState({ shareButtonActive: true }); + setTimeout(() => this.setState({ shareButtonActive: false }), 5000); + + navigator.share({ title: "Join me now in #hubs!", url: link }); + }; + + copyClicked = link => { + this.setState({ copyButtonActive: true }); + setTimeout(() => this.setState({ copyButtonActive: false }), 5000); + + copy(link); + }; + + render() { + const { entryCode } = this.props; + + const entryCodeString = pad(entryCode, 6); + const shortLinkText = `hub.link/${this.props.hubId}`; + const shortLink = "https://" + shortLinkText; + + const tweetText = `Join me now in #hubs!`; + const tweetLink = `https://twitter.com/share?url=${encodeURIComponent(shortLink)}&text=${encodeURIComponent( + tweetText + )}`; + + return ( + <div className={styles.dialog}> + <div className={styles.attachPoint} /> + <div className={styles.close} onClick={() => this.props.onClose()}> + <span>×</span> + </div> + <div> + <FormattedMessage id="invite.enter_via" /> + <a href="https://hub.link" target="_blank" className={styles.hubLinkLink} rel="noopener noreferrer"> + hub.link + </a> + <FormattedMessage id="invite.and_enter_code" /> + </div> + <div className={styles.code}> + {entryCodeString.split("").map((d, i) => ( + <div className={classNames({ [styles.digit]: true, [styles[`digit_${i}`]]: true })} key={`link_code_${i}`}> + {d} + </div> + ))} + </div> + <div> + <FormattedMessage id="invite.or_visit" /> + </div> + <div className={styles.domain}> + <input type="text" readOnly onFocus={e => e.target.select()} value={shortLinkText} /> + </div> + <div className={styles.buttons}> + <button className={styles.linkButton} onClick={this.copyClicked.bind(this, shortLink)}> + <span>{this.state.copyButtonActive ? "copied!" : "copy"}</span> + </button> + {this.props.allowShare && + navigator.share && ( + <button className={styles.linkButton} onClick={this.shareClicked.bind(this, shortLink)}> + <span>{this.state.shareButtonActive ? "sharing..." : "share"}</span> + </button> + )} + {this.props.allowShare && + !navigator.share && ( + <a href={tweetLink} className={styles.linkButton} target="_blank" rel="noopener noreferrer"> + <FormattedMessage id="invite.tweet" /> + </a> + )} + </div> + </div> + ); + } +} diff --git a/src/react-components/invite-team-dialog.js b/src/react-components/invite-team-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..a2b3bcd50c40331487642e0d103b402d464bd5f9 --- /dev/null +++ b/src/react-components/invite-team-dialog.js @@ -0,0 +1,42 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; +import PropTypes from "prop-types"; + +export default class InviteTeamDialog extends Component { + static propTypes = { + hubChannel: PropTypes.object + }; + + state = { + inviteButtonText: "invite a hubs team member" + }; + + constructor(props) { + super(props); + } + + inviteClicked = () => { + this.setState({ inviteButtonText: "OK! We'll be there shortly." }); + this.props.hubChannel.requestSupport(); + }; + + render() { + return ( + <DialogContainer title="Let's Chat!" {...this.props}> + <div> + <div> + <p>Have an idea for Hubs? Having trouble?</p> + <p>We'd love to chat. Someone from our team will join you in just a few minutes.</p> + </div> + <div className="invite-team-form"> + <div className="invite-team-form__buttons"> + <button className="invite-team-form__action-button" onClick={this.inviteClicked}> + <span>{this.state.inviteButtonText}</span> + </button> + </div> + </div> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/join-us-dialog.js b/src/react-components/join-us-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..7773f27b2e89f3c310df59def91e56d3ff8ead0b --- /dev/null +++ b/src/react-components/join-us-dialog.js @@ -0,0 +1,28 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class JoinUsDialog extends Component { + render() { + return ( + <DialogContainer title="Join Us" {...this.props}> + <span> + <p> + Join us in the{" "} + <a href="https://discord.gg/XzrGUY8" target="_blank" rel="noopener noreferrer"> + Hubs community + </a>{" "} + on Discord. + <br /> + </p> + <p>VR meetups every Friday at noon PDT!</p> + <p> + You can also follow us on Twitter at{" "} + <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer"> + @mozillareality + </a>. + </p> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js index 80ab360d50d54da5738a9c3c771b090b0d4bc9fa..d3f45051d33736e65ecde5805eee3bb465959e07 100644 --- a/src/react-components/link-dialog.js +++ b/src/react-components/link-dialog.js @@ -2,53 +2,70 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import { FormattedMessage } from "react-intl"; +import LinkDialogHeader from "../assets/images/link_dialog_header.svg"; import styles from "../assets/stylesheets/link-dialog.scss"; -class LinkDialog extends Component { +export default class LinkDialog extends Component { static propTypes = { - linkCode: PropTypes.string + linkCode: PropTypes.string, + onClose: PropTypes.func }; render() { - if (!this.props.linkCode) { - return ( - <div> - <div className={classNames("loading-panel", styles.codeLoadingPanel)}> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> - </div> - </div> - </div> - </div> - ); - } + const { linkCode } = this.props; return ( - <div> - <div> - <FormattedMessage id="link.in_your_browser" /> + <div className={styles.dialog}> + <div className={styles.close} onClick={() => this.props.onClose()}> + <span>×</span> </div> - <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer"> - hub.link - </a> <div> - <FormattedMessage id="link.enter_code" /> - </div> - <div className={styles.code}> - {this.props.linkCode.split("").map((d, i) => ( - <span className={styles.digit} key={`link_code_${i}`}> - {d} - </span> - ))} - </div> - <div className={styles.keepOpen}> - <FormattedMessage id="link.do_not_close" /> + {!linkCode && ( + <div> + <div className={classNames("loading-panel", styles.codeLoadingPanel)}> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> + </div> + </div> + )} + {linkCode && ( + <div className={styles.contents}> + <img className={styles.imageHeader} src={LinkDialogHeader} /> + <div className={styles.header}> + <FormattedMessage id="link.connect_headset" /> + </div> + <div> + <FormattedMessage id="link.in_your_browser" /> + </div> + <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer"> + hub.link + </a> + <div> + <FormattedMessage id="link.enter_code" /> + </div> + {linkCode && ( + <div className={styles.code}> + {linkCode.split("").map((d, i) => ( + <span className={styles.digit} key={`link_code_${i}`}> + {d} + </span> + ))} + </div> + )} + <div className={styles.keepOpen}> + <FormattedMessage id="link.do_not_close" /> + </div> + <button className={styles.closeButton} onClick={() => this.props.onClose()}> + <FormattedMessage id="link.cancel" /> + </button> + </div> + )} </div> </div> ); } } - -export default LinkDialog; diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js index 84b7101637357253da095a0c69bd0f20382318e4..e06442749445204c858846428f548e526a38c3d8 100644 --- a/src/react-components/link-root.js +++ b/src/react-components/link-root.js @@ -7,21 +7,26 @@ import { lang, messages } from "../utils/i18n"; import classNames from "classnames"; import styles from "../assets/stylesheets/link.scss"; import { disableiOSZoom } from "../utils/disable-ios-zoom"; +import HeadsetIcon from "../assets/images/generic_vr_entry.svg"; -const MAX_DIGITS = 4; +const MAX_DIGITS = 6; +const MAX_LETTERS = 4; addLocaleData([...en]); disableiOSZoom(); +const hasTouchEvents = "ontouchstart" in document.documentElement; class LinkRoot extends Component { static propTypes = { intl: PropTypes.object, store: PropTypes.object, - linkChannel: PropTypes.object + linkChannel: PropTypes.object, + showHeadsetLinkOption: PropTypes.bool }; state = { - enteredDigits: "", + entered: "", + isAlphaMode: false, failedAtLeastOnce: false }; @@ -35,34 +40,47 @@ class LinkRoot extends Component { handleKeyDown = e => { // Number keys 0-9 - if (e.keyCode < 48 || e.keyCode > 57) { + if ((e.keyCode < 48 || e.keyCode > 57) && !this.state.isAlphaMode) { + return; + } + + // Alpha keys A-I + if ((e.keyCode < 65 || e.keyCode > 73) && this.state.isAlphaMode) { return; } e.preventDefault(); e.stopPropagation(); - this.addDigit(e.keyCode - 48); + if (this.state.isAlphaMode) { + this.addToEntry("IHGFEDCBA"[73 - e.keyCode]); + } else { + this.addToEntry(e.keyCode - 48); + } + }; + + maxAllowedChars = () => { + return this.state.isAlphaMode ? MAX_LETTERS : MAX_DIGITS; }; - addDigit = digit => { - if (this.state.enteredDigits.length >= MAX_DIGITS) return; - const newDigits = `${this.state.enteredDigits}${digit}`; + addToEntry = ch => { + if (this.state.entered.length >= this.maxAllowedChars()) return; + const newChars = `${this.state.entered}${ch}`; - if (newDigits.length === MAX_DIGITS) { - this.attemptLink(newDigits); + if (newChars.length === this.maxAllowedChars()) { + this.attemptLookup(newChars); } - this.setState({ enteredDigits: newDigits }); + this.setState({ entered: newChars }); }; - removeDigit = () => { - const enteredDigits = this.state.enteredDigits; - if (enteredDigits.length === 0) return; - this.setState({ enteredDigits: enteredDigits.substring(0, enteredDigits.length - 1) }); + removeChar = () => { + const entered = this.state.entered; + if (entered.length === 0) return; + this.setState({ entered: entered.substring(0, entered.length - 1) }); }; - attemptLink = code => { + attemptLink = async code => { this.props.linkChannel .attemptLink(code) .then(response => { @@ -80,7 +98,7 @@ class LinkRoot extends Component { } }) .catch(e => { - this.setState({ failedAtLeastOnce: true, enteredDigits: "" }); + this.setState({ failedAtLeastOnce: true, entered: "" }); if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) { throw e; @@ -88,6 +106,31 @@ class LinkRoot extends Component { }); }; + attemptEntry = async code => { + const url = "/link/" + code; + const res = await fetch(url); + + if (res.status >= 400) { + this.setState({ failedAtLeastOnce: true, entered: "" }); + } else { + document.location = url; + } + }; + + attemptLookup = async code => { + if (this.state.isAlphaMode) { + // Headset link code + this.attemptLink(code); + } else { + // Room entry code + this.attemptEntry(code); + } + }; + + toggleMode = () => { + this.setState({ isAlphaMode: !this.state.isAlphaMode, entered: "", failedAtLeastOnce: false }); + }; + render() { // Note we use type "tel" for the input due to https://bugzilla.mozilla.org/show_bug.cgi?id=1005603 @@ -95,7 +138,10 @@ class LinkRoot extends Component { <IntlProvider locale={lang} messages={messages}> <div className={styles.link}> <div className={styles.linkContents}> - {this.state.enteredDigits.length === MAX_DIGITS && ( + <div className={styles.logo}> + <img src="../assets/images/hub-preview-light-no-shadow.png" /> + </div> + {this.state.entered.length === this.maxAllowedChars() && ( <div className={classNames("loading-panel", styles.codeLoadingPanel)}> <div className="loader-wrap"> <div className="loader"> @@ -107,72 +153,116 @@ class LinkRoot extends Component { <div className={styles.enteredContents}> <div className={styles.header}> - <FormattedMessage id={this.state.failedAtLeastOnce ? "link.try_again" : "link.link_page_header"} /> + <FormattedMessage + id={ + this.state.failedAtLeastOnce + ? "link.try_again" + : "link.link_page_header_" + (!this.state.isAlphaMode ? "entry" : "headset") + } + /> </div> - <div className={styles.enteredDigits}> + <div className={styles.entered}> <input - className={styles.digitInput} - type="tel" - pattern="[0-9]*" - value={this.state.enteredDigits} + className={styles.charInput} + type={this.state.isAlphaMode ? "text" : "tel"} + pattern="[0-9A-I]*" + value={this.state.entered} onChange={ev => { - this.setState({ enteredDigits: ev.target.value }); + if (!this.state.isAlphaMode && ev.target.value.match(/[a-z]/i)) { + this.setState({ isAlphaMode: true }); + } + + this.setState({ entered: ev.target.value.toUpperCase() }); }} - placeholder="- - - -" /> </div> <div className={styles.enteredFooter}> - <span> - <FormattedMessage id="link.dont_have_a_code" /> - </span>{" "} - <span> - <a href="/"> - <FormattedMessage id="link.create_a_room" /> - </a> - </span> - <img className={styles.entryFooterImage} src="../assets/images/logo.svg" /> + {!this.state.isAlphaMode && + this.props.showHeadsetLinkOption && ( + <img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} /> + )} + {!this.state.isAlphaMode && + this.props.showHeadsetLinkOption && ( + <span> + <a href="#" onClick={() => this.toggleMode()}> + <FormattedMessage id="link.linking_a_headset" /> + </a> + </span> + )} </div> </div> <div className={styles.keypad}> - {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((d, i) => ( + {(this.state.isAlphaMode + ? ["A", "B", "C", "D", "E", "F", "G", "H", "I"] + : [1, 2, 3, 4, 5, 6, 7, 8, 9] + ).map((d, i) => ( <button - disabled={this.state.enteredDigits.length === MAX_DIGITS} - key={`digit_${i}`} + disabled={this.state.entered.length === this.maxAllowedChars()} + key={`char_${i}`} className={styles.keypadButton} - onClick={() => this.addDigit(d)} + onClick={() => { + if (!hasTouchEvents) this.addToEntry(d); + }} + onTouchStart={() => this.addToEntry(d)} > {d} </button> ))} + {this.props.showHeadsetLinkOption ? ( + <button + className={classNames(styles.keypadButton, styles.keypadToggleMode)} + onTouchStart={() => this.toggleMode()} + onClick={() => { + if (!hasTouchEvents) this.toggleMode(); + }} + > + {this.state.isAlphaMode ? "123" : "ABC"} + </button> + ) : ( + <div /> + )} + {!this.state.isAlphaMode && ( + <button + disabled={this.state.entered.length === this.maxAllowedChars()} + className={classNames(styles.keypadButton, styles.keypadZeroButton)} + onTouchStart={() => this.addToEntry(0)} + onClick={() => { + if (!hasTouchEvents) this.addToEntry(0); + }} + > + 0 + </button> + )} <button - disabled={this.state.enteredDigits.length === MAX_DIGITS} - className={classNames(styles.keypadButton, styles.keypadZeroButton)} - onClick={() => this.addDigit(0)} - > - 0 - </button> - <button - disabled={this.state.enteredDigits.length === 0 || this.state.enteredDigits.length === MAX_DIGITS} + disabled={this.state.entered.length === 0 || this.state.entered.length === this.maxAllowedChars()} className={classNames(styles.keypadButton, styles.keypadBackspace)} - onClick={() => this.removeDigit()} + onTouchStart={() => this.removeChar()} + onClick={() => { + if (!hasTouchEvents) this.removeChar(); + }} > ⌫ </button> </div> <div className={styles.footer}> - <span> - <FormattedMessage id="link.dont_have_a_code" /> - </span>{" "} - <span> - <a href="/"> - <FormattedMessage id="link.create_a_room" /> - </a> - </span> - <img className={styles.footerImage} src="../assets/images/logo.svg" alt="Logo" /> + {!this.state.isAlphaMode && + this.props.showHeadsetLinkOption && ( + <div + className={styles.linkHeadsetFooterLink} + style={{ visibility: this.state.isAlphaMode ? "hidden" : "visible" }} + > + <img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} /> + <span> + <a href="#" onClick={() => this.toggleMode()}> + <FormattedMessage id="link.linking_a_headset" /> + </a> + </span> + </div> + )} </div> </div> </div> diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js new file mode 100644 index 0000000000000000000000000000000000000000..9770da2fdbd950b442314d6fbb39f181bdb75ed2 --- /dev/null +++ b/src/react-components/presence-list.js @@ -0,0 +1,61 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import styles from "../assets/stylesheets/presence-list.scss"; +import classNames from "classnames"; +import PhoneImage from "../assets/images/presence_phone.png"; +import DesktopImage from "../assets/images/presence_desktop.png"; +import HMDImage from "../assets/images/presence_vr.png"; +import { FormattedMessage } from "react-intl"; + +export default class PresenceList extends Component { + static propTypes = { + presences: PropTypes.object, + sessionId: PropTypes.string + }; + + domForPresence = ([sessionId, data]) => { + const meta = data.metas[0]; + const context = meta.context; + const profile = meta.profile; + + const image = context && context.mobile ? PhoneImage : context && context.hmd ? HMDImage : DesktopImage; + + return ( + <div className={styles.row} key={sessionId}> + <div className={styles.device}> + <img src={image} /> + </div> + <div + className={classNames({ + [styles.displayName]: true, + [styles.selfDisplayName]: sessionId === this.props.sessionId + })} + > + {profile && profile.displayName} + </div> + <div className={styles.presence}> + <FormattedMessage id={`presence.in_${meta.presence}`} /> + </div> + </div> + ); + }; + + render() { + // Draw self first + return ( + <div className={styles.presenceList}> + <div className={styles.attachPoint} /> + <div className={styles.contents}> + <div className={styles.rows}> + {Object.entries(this.props.presences || {}) + .filter(([k]) => k === this.props.sessionId) + .map(this.domForPresence)} + {Object.entries(this.props.presences || {}) + .filter(([k]) => k !== this.props.sessionId) + .map(this.domForPresence)} + </div> + </div> + </div> + ); + } +} diff --git a/src/react-components/presence-log.js b/src/react-components/presence-log.js new file mode 100644 index 0000000000000000000000000000000000000000..bf9a7b86cb2324546eba0d75c3c152d0b01db4c5 --- /dev/null +++ b/src/react-components/presence-log.js @@ -0,0 +1,63 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import styles from "../assets/stylesheets/presence-log.scss"; +import classNames from "classnames"; +import Linkify from "react-linkify"; +import { toArray as toEmojis } from "react-emoji-render"; +import { FormattedMessage } from "react-intl"; + +export default class PresenceLog extends Component { + static propTypes = { + entries: PropTypes.array, + inRoom: PropTypes.bool + }; + + constructor(props) { + super(props); + } + + domForEntry = e => { + const entryClasses = { + [styles.presenceLogEntry]: true, + [styles.expired]: !!e.expired + }; + + switch (e.type) { + case "join": + case "entered": + return ( + <div key={e.key} className={classNames(entryClasses)}> + <b>{e.name}</b> <FormattedMessage id={`presence.${e.type}_${e.presence}`} /> + </div> + ); + case "leave": + return ( + <div key={e.key} className={classNames(entryClasses)}> + <b>{e.name}</b> <FormattedMessage id={`presence.${e.type}`} /> + </div> + ); + case "display_name_changed": + return ( + <div key={e.key} className={classNames(entryClasses)}> + <b>{e.oldName}</b> <FormattedMessage id="presence.name_change" /> <b>{e.newName}</b>. + </div> + ); + case "message": + return ( + <div key={e.key} className={classNames(entryClasses)}> + <b>{e.name}</b>:{" "} + <Linkify properties={{ target: "_blank", rel: "noopener referrer" }}>{toEmojis(e.body)}</Linkify> + </div> + ); + } + }; + + render() { + const presenceClasses = { + [styles.presenceLog]: true, + [styles.presenceLogInRoom]: this.props.inRoom + }; + + return <div className={classNames(presenceClasses)}>{this.props.entries.map(this.domForEntry)}</div>; + } +} diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 32a1223e8105ec4ba301b5c3422787c6b38be4eb..307ae73b0dac97d5b50908d5773f28bc2953aec1 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -2,6 +2,9 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { injectIntl, FormattedMessage } from "react-intl"; import { SCHEMA } from "../storage/store"; +import styles from "../assets/stylesheets/profile.scss"; +import classNames from "classnames"; +import hubLogo from "../assets/images/hub-preview-white.png"; class ProfileEntryPanel extends Component { static propTypes = { @@ -72,27 +75,25 @@ class ProfileEntryPanel extends Component { const { formatMessage } = this.props.intl; return ( - <div className="profile-entry"> - <form onSubmit={this.saveStateAndFinish} className="profile-entry__form"> - <div className="profile-entry__box profile-entry__box--darkened"> - <label htmlFor="#profile-entry-display-name" className="profile-entry__subtitle"> + <div className={styles.profileEntry}> + <form onSubmit={this.saveStateAndFinish} className={styles.form}> + <div className={classNames([styles.box, styles.darkened])}> + <label htmlFor="#profile-entry-display-name" className={styles.title}> <FormattedMessage id="profile.header" /> </label> - <label> - <input - id="profile-entry-display-name" - className="profile-entry__form-field-text" - value={this.state.displayName} - onFocus={e => e.target.select()} - onChange={e => this.setState({ displayName: e.target.value })} - required - spellCheck="false" - pattern={SCHEMA.definitions.profile.properties.displayName.pattern} - title={formatMessage({ id: "profile.display_name.validation_warning" })} - ref={inp => (this.nameInput = inp)} - /> - </label> - <div className="profile-entry__avatar-selector-container"> + <input + id="profile-entry-display-name" + className={styles.formFieldText} + value={this.state.displayName} + onFocus={e => e.target.select()} + onChange={e => this.setState({ displayName: e.target.value })} + required + spellCheck="false" + pattern={SCHEMA.definitions.profile.properties.displayName.pattern} + title={formatMessage({ id: "profile.display_name.validation_warning" })} + ref={inp => (this.nameInput = inp)} + /> + <div className={styles.avatarSelectorContainer}> <div className="loading-panel"> <div className="loader-wrap"> <div className="loader"> @@ -101,13 +102,13 @@ class ProfileEntryPanel extends Component { </div> </div> <iframe - className="profile-entry__avatar-selector" + className={styles.avatarSelector} src={`/avatar-selector.html#avatar_id=${this.state.avatarId}`} ref={ifr => (this.avatarSelector = ifr)} /> </div> - <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" })} /> - <div className="profile-entry__box__links"> + <input className={styles.formSubmit} type="submit" value={formatMessage({ id: "profile.save" })} /> + <div className={styles.links}> <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> <FormattedMessage id="profile.terms_of_use" /> </a> @@ -121,6 +122,7 @@ class ProfileEntryPanel extends Component { </div> </div> </form> + <img className={styles.logo} src={hubLogo} /> </div> ); } diff --git a/src/react-components/profile-info-header.js b/src/react-components/profile-info-header.js deleted file mode 100644 index 7afb16b5491c4ffccfbe39ed2674c821d28c0ed5..0000000000000000000000000000000000000000 --- a/src/react-components/profile-info-header.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; - -export const ProfileInfoHeader = props => ( - <div className="profile-info-header"> - <div className="profile-info-header__menu-buttons"> - <button className="profile-info-header__menu-buttons__menu-button" onClick={props.onClickHelp}> - <i className="profile-info-header__menu-buttons__menu-button__icon"> - <FontAwesomeIcon icon={faQuestion} /> - </i> - </button> - </div> - <div className="profile-info-header__profile_display_name"> - <img src="../assets/images/account.svg" onClick={props.onClickName} className="profile-info-header__icon" /> - <div onClick={props.onClickName} title={props.name}> - {props.name} - </div> - </div> - </div> -); - -ProfileInfoHeader.propTypes = { - onClickName: PropTypes.func, - onClickHelp: PropTypes.func, - name: PropTypes.string -}; diff --git a/src/react-components/report-dialog.js b/src/react-components/report-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..36072eed617957090fc2942a5f17047b3ea533ba --- /dev/null +++ b/src/react-components/report-dialog.js @@ -0,0 +1,31 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class ReportDialog extends Component { + render() { + return ( + <DialogContainer title="Report an Issue" {...this.props}> + <span> + <p>Need to report a problem?</p> + <p> + You can file a{" "} + <a href="https://github.com/mozilla/hubs/issues" target="_blank" rel="noopener noreferrer"> + GitHub Issue + </a>{" "} + or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. + </p> + <p> + You can also find us in{" "} + <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> + #social + </a>{" "} + on the{" "} + <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> + WebVR Slack + </a>. + </p> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/safari-dialog.js b/src/react-components/safari-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..de96bfbf4545e6496e97a3e0dab81593adad7519 --- /dev/null +++ b/src/react-components/safari-dialog.js @@ -0,0 +1,39 @@ +import React, { Component } from "react"; +import copy from "copy-to-clipboard"; +import DialogContainer from "./dialog-container.js"; + +export default class SafariDialog extends Component { + state = { + copyLinkButtonText: "copy" + }; + + copyLinkClicked = link => { + copy(link); + this.setState({ copyLinkButtonText: "copied!" }); + }; + + render() { + const onCopyClicked = this.copyLinkClicked.bind(this, document.location); + return ( + <DialogContainer title="Open in Safari" {...this.props}> + <div> + <div>Hubs does not support your current browser on iOS. Copy and paste this link directly in Safari.</div> + <div className="invite-form"> + <input + type="text" + readOnly + onFocus={e => e.target.select()} + value={document.location} + className="invite-form__link_field" + /> + <div className="invite-form__buttons"> + <button className="invite-form__action-button" onClick={onCopyClicked}> + <span>{this.state.copyLinkButtonText}</span> + </button> + </div> + </div> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js new file mode 100644 index 0000000000000000000000000000000000000000..2db97a04c5a0d56824f8b4677474b6ddb97540d9 --- /dev/null +++ b/src/react-components/scene-ui.js @@ -0,0 +1,123 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import en from "react-intl/locale-data/en"; +import styles from "../assets/stylesheets/scene-ui.scss"; +import hubLogo from "../assets/images/hub-preview-white.png"; +import spokeLogo from "../assets/images/spoke_logo_black.png"; +import { getReticulumFetchUrl } from "../utils/phoenix-utils"; +import { generateHubName } from "../utils/name-generation"; + +import { lang, messages } from "../utils/i18n"; + +addLocaleData([...en]); + +class SceneUI extends Component { + static propTypes = { + scene: PropTypes.object, + sceneLoaded: PropTypes.bool, + sceneId: PropTypes.string, + sceneName: PropTypes.string, + sceneDescription: PropTypes.string, + sceneAttribution: PropTypes.string, + sceneScreenshotURL: PropTypes.string + }; + + state = { + showScreenshot: false + }; + + constructor(props) { + super(props); + + // Show screenshot if scene isn't loaded in 5 seconds + setTimeout(() => { + if (!this.props.sceneLoaded) { + this.setState({ showScreenshot: true }); + } + }, 5000); + } + + componentDidMount() { + this.props.scene.addEventListener("loaded", this.onSceneLoaded); + } + + componentWillUnmount() { + this.props.scene.removeEventListener("loaded", this.onSceneLoaded); + } + + createRoom = async () => { + const payload = { hub: { name: generateHubName(), scene_id: this.props.sceneId } }; + const createUrl = getReticulumFetchUrl("/api/v1/hubs"); + + const res = await fetch(createUrl, { + body: JSON.stringify(payload), + headers: { "content-type": "application/json" }, + method: "POST" + }); + + const hub = await res.json(); + + if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) { + document.location = hub.url; + } else { + document.location = `/hub.html?hub_id=${hub.hub_id}`; + } + }; + + render() { + const sceneUrl = [location.protocol, "//", location.host, location.pathname].join(""); + const tweetText = `${this.props.sceneName} in #hubs`; + const tweetLink = `https://twitter.com/share?url=${encodeURIComponent(sceneUrl)}&text=${encodeURIComponent( + tweetText + )}`; + + return ( + <IntlProvider locale={lang} messages={messages}> + <div className={styles.ui}> + <div + className={classNames({ + [styles.screenshot]: true, + [styles.screenshotHidden]: this.props.sceneLoaded + })} + > + {this.state.showScreenshot && <img src={this.props.sceneScreenshotURL} />} + </div> + <div className={styles.whiteOverlay} /> + <div className={styles.grid}> + <div className={styles.mainPanel}> + <a href="/" className={styles.logo}> + <img src={hubLogo} /> + </a> + <div className={styles.logoTagline}> + <FormattedMessage id="scene.logo_tagline" /> + </div> + <button onClick={this.createRoom}> + <FormattedMessage id="scene.create_button" /> + </button> + <a href={tweetLink} rel="noopener noreferrer" target="_blank" className={styles.tweetButton}> + <img src="../assets/images/twitter.svg" /> + <div> + <FormattedMessage id="scene.tweet_button" /> + </div> + </a> + </div> + </div> + <div className={styles.info}> + <div className={styles.name}>{this.props.sceneName}</div> + <div className={styles.attribution}>{this.props.sceneAttribution}</div> + </div> + <div className={styles.spoke}> + <div className={styles.madeWith}>made with</div> + <a href="/spoke"> + <img src={spokeLogo} /> + </a> + </div> + </div> + </IntlProvider> + ); + } +} + +export default SceneUI; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 36b5434714a0bca8af2fc7dc4f2f38d094952f3e..c38b79d5edd9c66345dce6db5c598ca70dc3082e 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1,6 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; +import copy from "copy-to-clipboard"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; @@ -18,19 +19,29 @@ import { DaydreamEntryButton, SafariEntryButton } from "./entry-buttons.js"; -import { ProfileInfoHeader } from "./profile-info-header.js"; import ProfileEntryPanel from "./profile-entry-panel"; -import InfoDialog from "./info-dialog.js"; +import HelpDialog from "./help-dialog.js"; +import SafariDialog from "./safari-dialog.js"; +import WebVRRecommendDialog from "./webvr-recommend-dialog.js"; +import InviteTeamDialog from "./invite-team-dialog.js"; +import InviteDialog from "./invite-dialog.js"; +import LinkDialog from "./link-dialog.js"; +import CreateObjectDialog from "./create-object-dialog.js"; +import PresenceLog from "./presence-log.js"; +import PresenceList from "./presence-list.js"; import TwoDHUD from "./2d-hud"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; +import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp"; addLocaleData([...en]); const ENTRY_STEPS = { start: "start", + device: "device", mic_grant: "mic_grant", mic_granted: "mic_granted", audio_setup: "audio_setup", @@ -58,6 +69,7 @@ class UIRoot extends Component { static propTypes = { enterScene: PropTypes.func, exitScene: PropTypes.func, + onSendMessage: PropTypes.func, concurrentLoadDetector: PropTypes.object, disableAutoExitOnConcurrentLoad: PropTypes.bool, forcedVREntryType: PropTypes.string, @@ -65,29 +77,38 @@ class UIRoot extends Component { isBotMode: PropTypes.bool, store: PropTypes.object, scene: PropTypes.object, + hubChannel: PropTypes.object, linkChannel: PropTypes.object, - showProfileEntry: PropTypes.bool, + hubEntryCode: PropTypes.number, availableVREntryTypes: PropTypes.object, - initialEnvironmentLoaded: PropTypes.bool, + environmentSceneLoaded: PropTypes.bool, roomUnavailableReason: PropTypes.string, platformUnsupportedReason: PropTypes.string, hubId: PropTypes.string, hubName: PropTypes.string, - occupantCount: PropTypes.number + isSupportAvailable: PropTypes.bool, + presenceLogEntries: PropTypes.array, + presences: PropTypes.object, + sessionId: PropTypes.string }; state = { entryStep: ENTRY_STEPS.start, enterInVR: false, - infoDialogType: null, + dialog: null, + showInviteDialog: false, + showLinkDialog: false, + showPresenceList: false, linkCode: null, linkCodeCancel: null, + miniInviteActivated: false, shareScreen: false, requestedScreen: false, mediaStream: null, videoTrack: null, audioTrack: null, + entryPanelCollapsed: false, toneInterval: null, tonePlaying: false, @@ -108,14 +129,10 @@ class UIRoot extends Component { exited: false, - showProfileEntry: false + showProfileEntry: false, + pendingMessage: "" }; - constructor(props) { - super(props); - this.state.showProfileEntry = this.props.showProfileEntry; - } - componentDidMount() { this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad); this.micLevelMovingAverage = MovingAverage(100); @@ -130,12 +147,6 @@ class UIRoot extends Component { this.props.scene.removeEventListener("exit", this.exit); } - componentDidUpdate(prevProps) { - if (this.props.availableVREntryTypes && prevProps.availableVREntryTypes !== this.props.availableVREntryTypes) { - this.handleForcedVREntryType(); - } - } - onSceneLoaded = () => { this.setState({ sceneLoaded: true }); }; @@ -160,10 +171,20 @@ class UIRoot extends Component { this.props.scene.emit("action_space_bubble"); }; - handleForcedVREntryType = () => { - if (!this.props.forcedVREntryType) return; + spawnPen = () => { + this.props.scene.emit("spawn_pen"); + }; + + handleStartEntry = () => { + const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName; - if (this.props.forcedVREntryType.startsWith("daydream")) { + if (promptForNameAndAvatarBeforeEntry) { + this.setState({ showProfileEntry: true }); + } + + if (!this.props.forcedVREntryType) { + this.goToEntryStep(ENTRY_STEPS.device); + } else if (this.props.forcedVREntryType.startsWith("daydream")) { this.enterDaydream(); } else if (this.props.forcedVREntryType.startsWith("vr")) { this.enterVR(); @@ -172,6 +193,10 @@ class UIRoot extends Component { } }; + goToEntryStep = entryStep => { + this.setState({ entryStep: entryStep, showInviteDialog: false }); + }; + playTestTone = () => { const toneClip = document.querySelector("#test-tone"); toneClip.currentTime = 0; @@ -256,7 +281,7 @@ class UIRoot extends Component { await this.setMediaStreamToDefault(); this.beginOrSkipAudioSetup(); } else { - this.setState({ entryStep: ENTRY_STEPS.mic_grant }); + this.goToEntryStep(ENTRY_STEPS.mic_grant); } }; @@ -264,36 +289,16 @@ class UIRoot extends Component { await this.performDirectEntryFlow(false); }; - linkSafari = async () => { - this.setState({ infoDialogType: InfoDialog.dialogTypes.safari }); - }; - enterVR = async () => { if (this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) { await this.performDirectEntryFlow(true); } else { - this.setState({ infoDialogType: InfoDialog.dialogTypes.webvr_recommend }); + this.showWebVRRecommendDialog(); } }; enterDaydream = async () => { - if (this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe) { - this.exit(); - - // We are not in mobile chrome, so launch into chrome via an Intent URL - const location = window.location; - const qs = new URLSearchParams(location.search); - qs.set("vr_entry_type", "daydream"); // Auto-choose 'daydream' after landing in chrome - - const intentUrl = - `intent://${location.host}${location.pathname}?` + - `${qs}#Intent;scheme=${location.protocol.replace(":", "")};` + - `action=android.intent.action.VIEW;package=com.android.chrome;end;`; - - window.location = intentUrl; - } else { - await this.performDirectEntryFlow(true); - } + await this.performDirectEntryFlow(true); }; micDeviceChanged = async ev => { @@ -417,7 +422,7 @@ class UIRoot extends Component { const { hasAudio } = await this.setMediaStreamToDefault(); if (hasAudio) { - this.setState({ entryStep: ENTRY_STEPS.mic_granted }); + this.goToEntryStep(ENTRY_STEPS.mic_granted); } else { this.beginOrSkipAudioSetup(); } @@ -428,11 +433,12 @@ class UIRoot extends Component { onProfileFinished = () => { this.setState({ showProfileEntry: false }); + this.props.hubChannel.sendProfileUpdate(); }; beginOrSkipAudioSetup = () => { if (!this.props.forcedVREntryType || !this.props.forcedVREntryType.endsWith("_now")) { - this.setState({ entryStep: ENTRY_STEPS.audio_setup }); + this.goToEntryStep(ENTRY_STEPS.audio_setup); } else { setTimeout(this.onAudioReadyButton, 3000); // Need to wait otherwise input doesn't work :/ } @@ -508,381 +514,574 @@ class UIRoot extends Component { clearInterval(this.state.micUpdateInterval); } - this.setState({ entryStep: ENTRY_STEPS.finished }); + this.goToEntryStep(ENTRY_STEPS.finished); }; attemptLink = async () => { - this.setState({ infoDialogType: InfoDialog.dialogTypes.link }); + this.setState({ showLinkDialog: true }); const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); this.setState({ linkCode: code, linkCodeCancel: cancel }); - onFinished.then(this.handleCloseDialog); + onFinished.then(() => this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null })); }; - handleCloseDialog = async () => { - if (this.state.linkCodeCancel) { - this.state.linkCodeCancel(); - } + showInviteDialog = () => { + this.setState({ showInviteDialog: true }); + }; - this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null }); + toggleInviteDialog = async () => { + this.setState({ showInviteDialog: !this.state.showInviteDialog }); }; - handleCreateObject = url => { - this.props.scene.emit("add_media", url); + createObject = media => { + this.props.scene.emit("add_media", media); }; - render() { - if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { - let subtitle = null; - if (this.props.roomUnavailableReason === "closed") { - // TODO i18n, due to links and markup - subtitle = ( - <div> - Sorry, this room is no longer available. - <p /> - A room may be closed if we receive reports that it violates our{" "} - <a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> - Terms of Use - </a>. - <br /> - If you have questions, contact us at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. - <p /> - If you'd like to run your own server, hubs's source code is available on{" "} - <a href="https://github.com/mozilla/hubs">GitHub</a>. - </div> - ); - } else if (this.props.platformUnsupportedReason === "no_data_channels") { - // TODO i18n, due to links and markup - subtitle = ( - <div> - Your browser does not support{" "} - <a - href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#Browser_compatibility" - rel="noreferrer noopener" - > - WebRTC Data Channels - </a>, which is required to use Hubs.<br />If you"d like to use Hubs with Oculus or SteamVR, you can{" "} - <a href="https://www.mozilla.org/firefox" rel="noreferrer noopener"> - Download Firefox - </a>. - </div> - ); - } else { - const reason = this.props.roomUnavailableReason || this.props.platformUnsupportedReason; - const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : reason}`; - subtitle = ( - <div> - <FormattedMessage id={exitSubtitleId} /> - <p /> - {this.props.roomUnavailableReason && ( - <div> - You can also <a href="/">create a new room</a>. - </div> - )} - </div> - ); - } + closeDialog = () => { + this.setState({ dialog: null }); + }; - return ( - <IntlProvider locale={lang} messages={messages}> - <div className="exited-panel"> - <img className="exited-panel__logo" src="../assets/images/logo.svg" /> - <div className="exited-panel__subtitle">{subtitle}</div> - </div> - </IntlProvider> - ); + showHelpDialog = () => { + this.setState({ dialog: <HelpDialog onClose={this.closeDialog} /> }); + }; + + showSafariDialog = () => { + this.setState({ dialog: <SafariDialog onClose={this.closeDialog} /> }); + }; + + showInviteTeamDialog = () => { + this.setState({ dialog: <InviteTeamDialog hubChannel={this.props.hubChannel} onClose={this.closeDialog} /> }); + }; + + showCreateObjectDialog = () => { + this.setState({ dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} /> }); + }; + + showWebVRRecommendDialog = () => { + this.setState({ dialog: <WebVRRecommendDialog onClose={this.closeDialog} /> }); + }; + + onMiniInviteClicked = () => { + const link = "https://hub.link/" + this.props.hubId; + + this.setState({ miniInviteActivated: true }); + setTimeout(() => { + this.setState({ miniInviteActivated: false }); + }, 5000); + + if (navigator.share) { + navigator.share({ title: document.title, url: link }); + } else { + copy(link); } + }; - if (this.props.isBotMode) { - return ( - <div className="loading-panel"> - <img className="loading-panel__logo" src="../assets/images/logo.svg" /> - <input type="file" id="bot-audio-input" accept="audio/*" /> - <input type="file" id="bot-data-input" accept="application/json" /> + sendMessage = e => { + e.preventDefault(); + this.props.onSendMessage(this.state.pendingMessage); + this.setState({ pendingMessage: "" }); + }; + + occupantCount = () => { + return this.props.presences ? Object.entries(this.props.presences).length : 0; + }; + + renderExitedPane = () => { + let subtitle = null; + if (this.props.roomUnavailableReason === "closed") { + // TODO i18n, due to links and markup + subtitle = ( + <div> + Sorry, this room is no longer available. + <p /> + A room may be closed if we receive reports that it violates our{" "} + <a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> + Terms of Use + </a>. + <br /> + If you have questions, contact us at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. + <p /> + If you'd like to run your own server, hubs's source code is available on{" "} + <a href="https://github.com/mozilla/hubs">GitHub</a>. </div> ); - } - - if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.hubId) { - return ( - <IntlProvider locale={lang} messages={messages}> - <div className="loading-panel"> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> - </div> + } else if (this.props.platformUnsupportedReason === "no_data_channels") { + // TODO i18n, due to links and markup + subtitle = ( + <div> + Your browser does not support{" "} + <a + href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#Browser_compatibility" + rel="noreferrer noopener" + > + WebRTC Data Channels + </a>, which is required to use Hubs.<br />If you"d like to use Hubs with Oculus or SteamVR, you can{" "} + <a href="https://www.mozilla.org/firefox" rel="noreferrer noopener"> + Download Firefox + </a>. + </div> + ); + } else { + const reason = this.props.roomUnavailableReason || this.props.platformUnsupportedReason; + const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : reason}`; + subtitle = ( + <div> + <FormattedMessage id={exitSubtitleId} /> + <p /> + {this.props.roomUnavailableReason && ( + <div> + You can also <a href="/">create a new room</a>. </div> - - <img className="loading-panel__logo" src="../assets/images/logo.svg" /> - </div> - </IntlProvider> + )} + </div> ); } - // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and - // will attempt to share your webcam instead! - const screenSharingCheckbox = this.props.enableScreenSharing && - !AFRAME.utils.device.isMobile() && - /firefox/i.test(navigator.userAgent) && ( - <label className={entryStyles.screenSharing}> - <input - className={entryStyles.checkbox} - type="checkbox" - value={this.state.shareScreen} - onChange={this.setStateAndRequestScreen} - /> - <FormattedMessage id="entry.enable-screen-sharing" /> - </label> - ); + return ( + <IntlProvider locale={lang} messages={messages}> + <div className="exited-panel"> + <img className="exited-panel__logo" src="../assets/images/logo.svg" /> + <div className="exited-panel__subtitle">{subtitle}</div> + </div> + </IntlProvider> + ); + }; - const entryPanel = - this.state.entryStep === ENTRY_STEPS.start ? ( - <div className={entryStyles.entryPanel}> - <div className={entryStyles.buttonContainer}> - {false /* TODO */ && ( - <div className={entryStyles.presenceInfo}> - <span className={entryStyles.people}>2 people</span> have joined - </div> - )} - {this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && ( - <TwoDEntryButton onClick={this.enter2D} /> - )} - {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( - <SafariEntryButton onClick={this.linkSafari} /> - )} - {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( - <GenericEntryButton onClick={this.enterVR} /> - )} - {this.props.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( - <DaydreamEntryButton - onClick={this.enterDaydream} - subtitle={ - this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe - ? "entry.daydream-via-chrome" - : null - } - /> - )} - <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} /> - {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( - <div className={entryStyles.secondary} onClick={this.enterVR}> - <FormattedMessage id="entry.cardboard" /> - </div> - )} - {screenSharingCheckbox} - <button - className={entryStyles.inviteButton} - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })} - > - <FormattedMessage id="entry.invite-others" /> - </button> + renderBotMode = () => { + return ( + <div className="loading-panel"> + <img className="loading-panel__logo" src="../assets/images/logo.svg" /> + <input type="file" id="bot-audio-input" accept="audio/*" /> + <input type="file" id="bot-data-input" accept="application/json" /> + </div> + ); + }; + + renderLoader = () => { + return ( + <IntlProvider locale={lang} messages={messages}> + <div className="loading-panel"> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> </div> + + <img className="loading-panel__logo" src="../assets/images/hub-preview-light-no-shadow.png" /> </div> - ) : null; + </IntlProvider> + ); + }; - const micPanel = - this.state.entryStep === ENTRY_STEPS.mic_grant || this.state.entryStep === ENTRY_STEPS.mic_granted ? ( - <div className="mic-grant-panel"> - <div className="mic-grant-panel__grant-container"> - <div className="mic-grant-panel__title"> - <FormattedMessage - id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-title" : "audio.granted-title"} - /> - </div> - <div className="mic-grant-panel__subtitle"> - <FormattedMessage - id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"} + renderEntryStartPanel = () => { + return ( + <div className={entryStyles.entryPanel}> + <div className={entryStyles.name}>{this.props.hubName}</div> + + <div className={entryStyles.center}> + <div onClick={() => this.setState({ showProfileEntry: true })} className={entryStyles.profileName}> + <img src="../assets/images/account.svg" className={entryStyles.profileIcon} /> + <div title={this.props.store.state.profile.displayName}>{this.props.store.state.profile.displayName}</div> + </div> + + <form onSubmit={this.sendMessage}> + <div className={styles.messageEntry}> + <input + className={styles.messageEntryInput} + value={this.state.pendingMessage} + onFocus={e => e.target.select()} + onChange={e => this.setState({ pendingMessage: e.target.value })} + placeholder="Send a message..." /> + <input className={styles.messageEntrySubmit} type="submit" value="send" /> </div> - <div className="mic-grant-panel__button-container"> - {this.state.entryStep == ENTRY_STEPS.mic_grant ? ( - <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> - <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" /> - </button> - ) : ( - <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> - <img src="../assets/images/mic_granted.png" srcSet="../assets/images/mic_granted@2x.png 2x" /> - </button> - )} + </form> + </div> + + <div className={entryStyles.buttonContainer}> + <button + className={classNames([entryStyles.actionButton, entryStyles.wideButton])} + onClick={() => this.handleStartEntry()} + > + <FormattedMessage id="entry.enter-room" /> + </button> + </div> + </div> + ); + }; + + renderDevicePanel = () => { + // Only screen sharing in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and will attempt to share your webcam instead! + const isFireFox = /firefox/i.test(navigator.userAgent); + const isNonMobile = !AFRAME.utils.device.isMobile(); + + const screenSharingCheckbox = + this.props.enableScreenSharing && isNonMobile && isFireFox && this.renderScreensharing(); + + return ( + <div className={entryStyles.entryPanel}> + <div className={entryStyles.title}> + <FormattedMessage id="entry.choose-device" /> + </div> + + <div className={entryStyles.buttonContainer}> + {this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && ( + <TwoDEntryButton onClick={this.enter2D} /> + )} + {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( + <SafariEntryButton onClick={this.showSafariDialog} /> + )} + {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( + <GenericEntryButton onClick={this.enterVR} /> + )} + {this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && ( + <DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} /> + )} + <DeviceEntryButton onClick={() => this.attemptLink()} isInHMD={this.props.availableVREntryTypes.isInHMD} /> + {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( + <div className={entryStyles.secondary} onClick={this.enterVR}> + <FormattedMessage id="entry.cardboard" /> </div> + )} + {screenSharingCheckbox} + </div> + </div> + ); + }; + + renderScreensharing = () => { + return ( + <label className={entryStyles.screenSharing}> + <input + className={entryStyles.checkbox} + type="checkbox" + value={this.state.shareScreen} + onChange={this.setStateAndRequestScreen} + /> + <FormattedMessage id="entry.enable-screen-sharing" /> + </label> + ); + }; + + renderMicPanel = () => { + return ( + <div className="mic-grant-panel"> + <div className="mic-grant-panel__grant-container"> + <div className="mic-grant-panel__title"> + <FormattedMessage + id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-title" : "audio.granted-title"} + /> </div> - <div className="mic-grant-panel__next-container"> - <button className={classNames("mic-grant-panel__next")} onClick={this.onMicGrantButton}> - <FormattedMessage id="audio.granted-next" /> - </button> + <div className="mic-grant-panel__subtitle"> + <FormattedMessage + id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"} + /> + </div> + <div className="mic-grant-panel__button-container"> + {this.state.entryStep == ENTRY_STEPS.mic_grant ? ( + <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> + <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" /> + </button> + ) : ( + <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> + <img src="../assets/images/mic_granted.png" srcSet="../assets/images/mic_granted@2x.png 2x" /> + </button> + )} </div> </div> - ) : null; + <div className="mic-grant-panel__next-container"> + <button className={classNames("mic-grant-panel__next")} onClick={this.onMicGrantButton}> + <FormattedMessage id="audio.granted-next" /> + </button> + </div> + </div> + ); + }; + renderAudioSetupPanel = () => { const maxLevelHeight = 111; const micClip = { clip: `rect(${maxLevelHeight - Math.floor(this.state.micLevel * maxLevelHeight)}px, 111px, 111px, 0px)` }; const speakerClip = { clip: `rect(${this.state.tonePlaying ? 0 : maxLevelHeight}px, 111px, 111px, 0px)` }; const subtitleId = AFRAME.utils.device.isMobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop"; - const audioSetupPanel = - this.state.entryStep === ENTRY_STEPS.audio_setup ? ( - <div className="audio-setup-panel"> - <div> - <div className="audio-setup-panel__title"> - <FormattedMessage id="audio.title" /> - </div> - <div className="audio-setup-panel__subtitle"> - {(AFRAME.utils.device.isMobile() || this.state.enterInVR) && <FormattedMessage id={subtitleId} />} - </div> - <div className="audio-setup-panel__levels"> - <div className="audio-setup-panel__levels__icon"> - <img - src="../assets/images/level_background.png" - srcSet="../assets/images/level_background@2x.png 2x" - className="audio-setup-panel__levels__icon-part" - /> - <img - src="../assets/images/level_fill.png" - srcSet="../assets/images/level_fill@2x.png 2x" - className="audio-setup-panel__levels__icon-part" - style={micClip} - /> - {this.state.audioTrack ? ( - <img - src="../assets/images/mic_level.png" - srcSet="../assets/images/mic_level@2x.png 2x" - className="audio-setup-panel__levels__icon-part" - /> - ) : ( - <img - src="../assets/images/mic_denied.png" - srcSet="../assets/images/mic_denied@2x.png 2x" - className="audio-setup-panel__levels__icon-part" - /> - )} - </div> - <div className="audio-setup-panel__levels__icon" onClick={this.playTestTone}> - <img - src="../assets/images/level_background.png" - srcSet="../assets/images/level_background@2x.png 2x" - className="audio-setup-panel__levels__icon-part" - /> + return ( + <div className="audio-setup-panel"> + <div> + <div className="audio-setup-panel__title"> + <FormattedMessage id="audio.title" /> + </div> + <div className="audio-setup-panel__subtitle"> + {(AFRAME.utils.device.isMobile() || this.state.enterInVR) && <FormattedMessage id={subtitleId} />} + </div> + <div className="audio-setup-panel__levels"> + <div className="audio-setup-panel__levels__icon"> + <img + src="../assets/images/level_background.png" + srcSet="../assets/images/level_background@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + /> + <img + src="../assets/images/level_fill.png" + srcSet="../assets/images/level_fill@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + style={micClip} + /> + {this.state.audioTrack ? ( <img - src="../assets/images/level_fill.png" - srcSet="../assets/images/level_fill@2x.png 2x" + src="../assets/images/mic_level.png" + srcSet="../assets/images/mic_level@2x.png 2x" className="audio-setup-panel__levels__icon-part" - style={speakerClip} /> + ) : ( <img - src="../assets/images/speaker_level.png" - srcSet="../assets/images/speaker_level@2x.png 2x" + src="../assets/images/mic_denied.png" + srcSet="../assets/images/mic_denied@2x.png 2x" className="audio-setup-panel__levels__icon-part" /> - </div> + )} + </div> + <div className="audio-setup-panel__levels__icon" onClick={this.playTestTone}> + <img + src="../assets/images/level_background.png" + srcSet="../assets/images/level_background@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + /> + <img + src="../assets/images/level_fill.png" + srcSet="../assets/images/level_fill@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + style={speakerClip} + /> + <img + src="../assets/images/speaker_level.png" + srcSet="../assets/images/speaker_level@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + /> </div> - {this.state.audioTrack && ( - <div className="audio-setup-panel__device-chooser"> - <select - className="audio-setup-panel__device-chooser__dropdown" - value={this.selectedMicDeviceId()} - onChange={this.micDeviceChanged} - > - {this.state.micDevices.map(d => ( - <option key={d.deviceId} value={d.deviceId}> - {d.label} - </option> - ))} - </select> - <img - className="audio-setup-panel__device-chooser__mic-icon" - src="../assets/images/mic_small.png" - srcSet="../assets/images/mic_small@2x.png 2x" - /> - <img - className="audio-setup-panel__device-chooser__dropdown-arrow" - src="../assets/images/dropdown_arrow.png" - srcSet="../assets/images/dropdown_arrow@2x.png 2x" - /> - </div> - )} - {this.shouldShowHmdMicWarning() && ( - <div className="audio-setup-panel__hmd-mic-warning"> - <img - src="../assets/images/warning_icon.png" - srcSet="../assets/images/warning_icon@2x.png 2x" - className="audio-setup-panel__hmd-mic-warning__icon" - /> - <span className="audio-setup-panel__hmd-mic-warning__label"> - <FormattedMessage id="audio.hmd-mic-warning" /> - </span> - </div> - )} - </div> - <div className="audio-setup-panel__enter-button-container"> - <button className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}> - <FormattedMessage id="audio.enter-now" /> - </button> </div> + {this.state.audioTrack && ( + <div className="audio-setup-panel__device-chooser"> + <select + className="audio-setup-panel__device-chooser__dropdown" + value={this.selectedMicDeviceId()} + onChange={this.micDeviceChanged} + > + {this.state.micDevices.map(d => ( + <option key={d.deviceId} value={d.deviceId}> + {d.label} + </option> + ))} + </select> + <img + className="audio-setup-panel__device-chooser__mic-icon" + src="../assets/images/mic_small.png" + srcSet="../assets/images/mic_small@2x.png 2x" + /> + <img + className="audio-setup-panel__device-chooser__dropdown-arrow" + src="../assets/images/dropdown_arrow.png" + srcSet="../assets/images/dropdown_arrow@2x.png 2x" + /> + </div> + )} + {this.shouldShowHmdMicWarning() && ( + <div className="audio-setup-panel__hmd-mic-warning"> + <img + src="../assets/images/warning_icon.png" + srcSet="../assets/images/warning_icon@2x.png 2x" + className="audio-setup-panel__hmd-mic-warning__icon" + /> + <span className="audio-setup-panel__hmd-mic-warning__label"> + <FormattedMessage id="audio.hmd-mic-warning" /> + </span> + </div> + )} + </div> + <div className="audio-setup-panel__enter-button-container"> + <button className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}> + <FormattedMessage id="audio.enter-now" /> + </button> </div> - ) : null; - - const dialogContents = this.isWaitingForAutoExit() ? ( - <AutoExitWarning secondsRemaining={this.state.secondsRemainingBeforeAutoExit} onCancel={this.endAutoExitTimer} /> - ) : ( - <div className={entryStyles.entryDialog}> - <ProfileInfoHeader - name={this.props.store.state.profile.displayName} - onClickName={() => this.setState({ showProfileEntry: true })} - onClickHelp={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} - /> - {entryPanel} - {micPanel} - {audioSetupPanel} </div> ); + }; + + render() { + const isExited = this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason; + const isLoading = !this.props.environmentSceneLoaded || !this.props.availableVREntryTypes || !this.props.hubId; + + if (isExited) return this.renderExitedPane(); + if (isLoading) return this.renderLoader(); + if (this.props.isBotMode) return this.renderBotMode(); + + const startPanel = this.state.entryStep === ENTRY_STEPS.start && this.renderEntryStartPanel(); + const devicePanel = this.state.entryStep === ENTRY_STEPS.device && this.renderDevicePanel(); - const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.infoDialogType, "ui-dialog-box": true }); + const micPanel = + (this.state.entryStep === ENTRY_STEPS.mic_grant || this.state.entryStep === ENTRY_STEPS.mic_granted) && + this.renderMicPanel(); + + const audioSetupPanel = this.state.entryStep === ENTRY_STEPS.audio_setup && this.renderAudioSetupPanel(); + + // Dialog is empty if coll + let dialogContents = null; + + if (this.state.entryPanelCollapsed && !this.isWaitingForAutoExit()) { + dialogContents = ( + <div className={entryStyles.entryDialog}> + <div> </div> + <button onClick={() => this.setState({ entryPanelCollapsed: false })} className={entryStyles.expand}> + <i> + <FontAwesomeIcon icon={faChevronUp} /> + </i> + </button> + </div> + ); + } else { + dialogContents = this.isWaitingForAutoExit() ? ( + <AutoExitWarning + secondsRemaining={this.state.secondsRemainingBeforeAutoExit} + onCancel={this.endAutoExitTimer} + /> + ) : ( + <div className={entryStyles.entryDialog}> + {!this.state.entryPanelCollapsed && ( + <button onClick={() => this.setState({ entryPanelCollapsed: true })} className={entryStyles.collapse}> + <i> + <FontAwesomeIcon icon={faChevronDown} /> + </i> + </button> + )} + {startPanel} + {devicePanel} + {micPanel} + {audioSetupPanel} + </div> + ); + } const dialogBoxContentsClassNames = classNames({ - "ui-dialog-box-contents": true, - "ui-dialog-box-contents--backgrounded": this.state.showProfileEntry + [styles.uiInteractive]: !this.state.dialog, + [styles.uiDialogBoxContents]: true, + [styles.backgrounded]: this.state.showProfileEntry }); + const entryFinished = this.state.entryStep === ENTRY_STEPS.finished; + const showVREntryButton = entryFinished && this.props.availableVREntryTypes.isInHMD; + return ( <IntlProvider locale={lang} messages={messages}> - <div className="ui"> - <InfoDialog - dialogType={this.state.infoDialogType} - linkCode={this.state.linkCode} - onSubmittedEmail={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.email_submitted })} - onCloseDialog={this.handleCloseDialog} - onCreateObject={this.handleCreateObject} - /> - - {this.state.entryStep === ENTRY_STEPS.finished && ( - <button - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} - className="ui__help-icon" - > - <i className="ui__help-icon__icon"> - <FontAwesomeIcon icon={faQuestion} /> - </i> - </button> + <div className={styles.ui}> + {this.state.dialog} + + {this.state.showProfileEntry && ( + <ProfileEntryPanel finished={this.onProfileFinished} store={this.props.store} /> )} - {this.state.entryStep === ENTRY_STEPS.finished && ( - <div className={styles.presenceInfo}> - <FontAwesomeIcon icon={faUsers} /> - <span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span> + {(!entryFinished || this.isWaitingForAutoExit()) && ( + <div className={styles.uiDialog}> + <PresenceLog entries={this.props.presenceLogEntries || []} /> + <div className={dialogBoxContentsClassNames}>{dialogContents}</div> </div> )} - <div className="ui-dialog"> - {(this.state.entryStep !== ENTRY_STEPS.finished || this.isWaitingForAutoExit()) && ( - <div className={dialogBoxClassNames}> - <div className={dialogBoxContentsClassNames}>{dialogContents}</div> - - {this.state.showProfileEntry && ( - <ProfileEntryPanel finished={this.onProfileFinished} store={this.props.store} /> - )} + {entryFinished && <PresenceLog inRoom={true} entries={this.props.presenceLogEntries || []} />} + {entryFinished && ( + <form onSubmit={this.sendMessage}> + <div className={styles.messageEntryInRoom}> + <input + className={classNames([styles.messageEntryInput, styles.messageEntryInputInRoom])} + value={this.state.pendingMessage} + onFocus={e => e.target.select()} + onChange={e => { + e.stopPropagation(); + this.setState({ pendingMessage: e.target.value }); + }} + placeholder="Send a message..." + /> + <input + className={classNames([styles.messageEntrySubmit, styles.messageEntrySubmitInRoom])} + type="submit" + value="send" + /> </div> + </form> + )} + + <div + className={classNames({ + [styles.inviteContainer]: true, + [styles.inviteContainerBelowHud]: entryFinished, + [styles.inviteContainerInverted]: this.state.showInviteDialog + })} + > + {!showVREntryButton && ( + <button + className={classNames({ [styles.hideSmallScreens]: this.occupantCount() > 1 && entryFinished })} + onClick={() => this.toggleInviteDialog()} + > + <FormattedMessage id="entry.invite-others-nag" /> + </button> + )} + {!showVREntryButton && + this.occupantCount() > 1 && + entryFinished && ( + <button onClick={this.onMiniInviteClicked} className={styles.inviteMiniButton}> + <span> + {this.state.miniInviteActivated + ? navigator.share + ? "sharing..." + : "copied!" + : "hub.link/" + this.props.hubId} + </span> + </button> + )} + {showVREntryButton && ( + <button onClick={() => this.props.scene.enterVR()}> + <FormattedMessage id="entry.return-to-vr" /> + </button> + )} + {this.state.showInviteDialog && ( + <InviteDialog + allowShare={!this.props.availableVREntryTypes.isInHMD} + entryCode={this.props.hubEntryCode} + hubId={this.props.hubId} + onClose={() => this.setState({ showInviteDialog: false })} + /> )} </div> + + {this.state.showLinkDialog && ( + <LinkDialog + linkCode={this.state.linkCode} + onClose={() => { + this.state.linkCodeCancel(); + this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null }); + }} + /> + )} + + <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}> + <i> + <FontAwesomeIcon icon={faQuestion} /> + </i> + </button> + + <div + onClick={() => this.setState({ showPresenceList: !this.state.showPresenceList })} + className={classNames({ + [styles.presenceInfo]: true, + [styles.presenceInfoSelected]: this.state.showPresenceList + })} + > + <FontAwesomeIcon icon={faUsers} /> + <span className={styles.occupantCount}>{this.occupantCount()}</span> + </div> + + {this.state.showPresenceList && ( + <PresenceList presences={this.props.presences} sessionId={this.props.sessionId} /> + )} + {this.state.entryStep === ENTRY_STEPS.finished ? ( <div> <TwoDHUD.TopHUD @@ -892,25 +1091,23 @@ class UIRoot extends Component { onToggleMute={this.toggleMute} onToggleFreeze={this.toggleFreeze} onToggleSpaceBubble={this.toggleSpaceBubble} + onSpawnPen={this.spawnPen} + onSpawnCamera={() => this.props.scene.emit("action_spawn_camera")} /> - {!this.props.availableVREntryTypes.isInHMD && - this.props.occupantCount <= 1 && ( - <div className={styles.nagButton}> - <button onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })}> - <FormattedMessage id="entry.invite-others-nag" /> - </button> - </div> - )} - {this.props.availableVREntryTypes.isInHMD && ( - <div className={styles.nagButton}> - <button onClick={() => this.props.scene.enterVR()}> - <FormattedMessage id="entry.return-to-vr" /> + {this.props.isSupportAvailable && ( + <div className={styles.nagCornerButton}> + <button onClick={() => this.showInviteTeamDialog()}> + <FormattedMessage id="entry.invite-team-nag" /> </button> </div> )} - <TwoDHUD.BottomHUD - onCreateObject={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.create_object })} - /> + {!this.isWaitingForAutoExit() && ( + <TwoDHUD.BottomHUD + onCreateObject={() => this.showCreateObjectDialog()} + showPhotoPicker={AFRAME.utils.device.isMobile()} + onMediaPicked={this.createObject} + /> + )} </div> ) : null} </div> diff --git a/src/react-components/updates-dialog.js b/src/react-components/updates-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..71d732c2404a366a7bb5e858ba0609bc6d241473 --- /dev/null +++ b/src/react-components/updates-dialog.js @@ -0,0 +1,77 @@ +import React, { Component } from "react"; +import { FormattedMessage } from "react-intl"; +import PropTypes from "prop-types"; +import formurlencoded from "form-urlencoded"; +import DialogContainer from "./dialog-container.js"; + +export default class UpdatesDialog extends Component { + static propTypes = { + onSubmittedEmail: PropTypes.func + }; + + state = { + mailingListEmail: "", + mailingListPrivacy: false + }; + + render() { + const { onSubmittedEmail, ...other } = this.props; + const signUpForMailingList = async e => { + e.preventDefault(); + e.stopPropagation(); + if (!this.state.mailingListPrivacy) return; + + const url = "https://www.mozilla.org/en-US/newsletter/"; + + const payload = { + email: this.state.mailingListEmail, + newsletters: "hubs", + privacy: true, + fmt: "H", + source_url: document.location.href + }; + + await fetch(url, { + body: formurlencoded(payload), + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" } + }).then(onSubmittedEmail); + }; + + return ( + <DialogContainer {...other}> + <span> + <p>Sign up to get updates about new features in Hubs.</p> + <form onSubmit={signUpForMailingList}> + <div className="mailing-list-form"> + <input + type="email" + value={this.state.mailingListEmail} + onChange={e => this.setState({ mailingListEmail: e.target.value })} + className="mailing-list-form__email_field" + required + placeholder="Your email here" + /> + <label className="mailing-list-form__privacy"> + <input + className="mailing-list-form__privacy_checkbox" + type="checkbox" + required + value={this.state.mailingListPrivacy} + onChange={e => this.setState({ mailingListPrivacy: e.target.checked })} + /> + <span className="mailing-list-form__privacy_label"> + <FormattedMessage id="mailing_list.privacy_label" />{" "} + <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/"> + <FormattedMessage id="mailing_list.privacy_link" /> + </a> + </span> + </label> + <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" /> + </div> + </form> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/webvr-recommend-dialog.js b/src/react-components/webvr-recommend-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..264245b273f69e3ba6c17a893362d3f0f715c2a7 --- /dev/null +++ b/src/react-components/webvr-recommend-dialog.js @@ -0,0 +1,23 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class WebVRRecommendDialog extends Component { + render() { + return ( + <DialogContainer title="Enter in VR" {...this.props}> + <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}> + <p>To enter Hubs with Oculus or SteamVR, you can use Firefox.</p> + <a className="info-dialog--action-button" href="https://www.mozilla.org/firefox"> + Download Firefox + </a> + <p style={{ fontSize: "0.8em" }}> + For a full list of browsers with experimental VR support, visit{" "} + <a href="https://webvr.rocks" target="_blank" rel="noopener noreferrer"> + WebVR Rocks + </a>. + </p> + </div> + </DialogContainer> + ); + } +} diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..b39159838952cb60917d471b4d31ce65f0569c3b --- /dev/null +++ b/src/scene-entry-manager.js @@ -0,0 +1,299 @@ +import qsTruthy from "./utils/qs_truthy"; +import screenfull from "screenfull"; +import { inGameActions } from "./input-mappings"; +import nextTick from "./utils/next-tick"; + +const playerHeight = 1.6; +const isBotMode = qsTruthy("bot"); +const isMobile = AFRAME.utils.device.isMobile(); +const isDebug = qsTruthy("debug"); +const qs = new URLSearchParams(location.search); +const aframeInspectorUrl = require("file-loader?name=assets/js/[name]-[hash].[ext]!aframe-inspector/dist/aframe-inspector.min.js"); + +import { addMedia } from "./utils/media-utils"; +import { ObjectContentOrigins } from "./object-types"; + +function requestFullscreen() { + if (screenfull.enabled && !screenfull.isFullscreen) screenfull.request(); +} + +export default class SceneEntryManager { + constructor(hubChannel) { + this.hubChannel = hubChannel; + this.store = window.APP.store; + this.scene = document.querySelector("a-scene"); + this.cursorController = document.querySelector("#cursor-controller"); + this.playerRig = document.querySelector("#player-rig"); + this._entered = false; + } + + init = () => { + this.whenSceneLoaded(() => { + this.cursorController.components["cursor-controller"].disable(); + }); + }; + + hasEntered = () => { + return this._entered; + }; + + enterScene = async (mediaStream, enterInVR) => { + const playerCamera = document.querySelector("#player-camera"); + playerCamera.removeAttribute("scene-preview-camera"); + playerCamera.object3D.position.set(0, playerHeight, 0); + + // Get aframe inspector url using the webpack file-loader. + // Set the aframe-inspector url to our hosted copy. + this.scene.setAttribute("inspector", { url: aframeInspectorUrl }); + + if (isDebug) { + NAF.connection.adapter.session.options.verbose = true; + } + + if (enterInVR) { + this.scene.enterVR(); + } else if (AFRAME.utils.device.isMobile()) { + document.body.addEventListener("touchend", requestFullscreen); + } + + AFRAME.registerInputActions(inGameActions, "default"); + + if (isMobile || qsTruthy("mobile")) { + this.playerRig.setAttribute("virtual-gamepad-controls", {}); + } + + this._setupPlayerRig(); + this._setupScreensharing(mediaStream); + this._setupBlocking(); + this._setupMedia(); + this._setupCamera(); + + if (qsTruthy("offline")) return; + + this._spawnAvatar(); + + if (isBotMode) { + this._runBot(mediaStream); + return; + } + + if (mediaStream) { + NAF.connection.adapter.setLocalMediaStream(mediaStream); + } + + this.scene.classList.remove("hand-cursor"); + this.scene.classList.add("no-cursor"); + + const cursor = this.cursorController.components["cursor-controller"]; + cursor.enable(); + cursor.setCursorVisibility(true); + this._entered = true; + + // Delay sending entry event telemetry until VR display is presenting. + (async () => { + while (enterInVR && !(await navigator.getVRDisplays()).find(d => d.isPresenting)) { + await nextTick(); + } + + this.hubChannel.sendEntryEvent().then(() => { + this.store.update({ activity: { lastEnteredAt: new Date().toISOString() } }); + }); + })(); + }; + + whenSceneLoaded = callback => { + if (this.scene.hasLoaded) { + callback(); + } else { + this.scene.addEventListener("loaded", callback); + } + }; + + enterSceneWhenLoaded = (mediaStream, enterInVR) => { + this.whenSceneLoaded(() => this.enterScene(mediaStream, enterInVR)); + }; + + exitScene = () => { + if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { + NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop()); + } + if (this.hubChannel) { + this.hubChannel.disconnect(); + } + if (this.scene.renderer) { + this.scene.renderer.setAnimationLoop(null); // Stop animation loop, TODO A-Frame should do this + } + document.body.removeChild(this.scene); + document.body.removeEventListener("touchend", requestFullscreen); + }; + + _setupPlayerRig = () => { + this._updatePlayerRigWithProfile(); + this.store.addEventListener("statechanged", this._updatePlayerRigWithProfile); + + const avatarScale = parseInt(qs.get("avatar_scale"), 10); + + if (avatarScale) { + this.playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); + } + }; + + _updatePlayerRigWithProfile = () => { + const displayName = this.store.state.profile.displayName; + this.playerRig.setAttribute("player-info", { + displayName, + avatarSrc: "#" + (this.store.state.profile.avatarId || "botdefault") + }); + const hudController = this.playerRig.querySelector("[hud-controller]"); + hudController.setAttribute("hud-controller", { showTip: !this.store.state.activity.hasFoundFreeze }); + this.scene.emit("username-changed", { username: displayName }); + }; + + _setupScreensharing = mediaStream => { + const videoTracks = mediaStream ? mediaStream.getVideoTracks() : []; + let sharingScreen = videoTracks.length > 0; + + const screenEntityId = `${NAF.clientId}-screen`; + let screenEntity = document.getElementById(screenEntityId); + + if (screenEntity) { + screenEntity.setAttribute("visible", sharingScreen); + } else if (sharingScreen) { + screenEntity = document.createElement("a-entity"); + screenEntity.id = screenEntityId; + screenEntity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: "0 0 -2", + on: "action_share_screen" + }); + screenEntity.setAttribute("networked", { template: "#video-template" }); + this.scene.appendChild(screenEntity); + } + + this.scene.addEventListener("action_share_screen", () => { + sharingScreen = !sharingScreen; + if (sharingScreen) { + for (const track of videoTracks) { + mediaStream.addTrack(track); + } + } else { + for (const track of mediaStream.getVideoTracks()) { + mediaStream.removeTrack(track); + } + } + NAF.connection.adapter.setLocalMediaStream(mediaStream); + screenEntity.setAttribute("visible", sharingScreen); + }); + }; + + _setupBlocking = () => { + document.body.addEventListener("blocked", ev => { + NAF.connection.entities.removeEntitiesOfClient(ev.detail.clientId); + }); + + document.body.addEventListener("unblocked", ev => { + NAF.connection.entities.completeSync(ev.detail.clientId); + }); + }; + + _setupMedia = () => { + const offset = { x: 0, y: 0, z: -1.5 }; + const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { + const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true, true); + + orientation.then(or => { + entity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset, + orientation: or + }); + }); + }; + + this.scene.addEventListener("add_media", e => { + const contentOrigin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL; + + spawnMediaInfrontOfPlayer(e.detail, contentOrigin); + }); + + this.scene.addEventListener("object_spawned", e => { + this.hubChannel.sendObjectSpawnedEvent(e.detail.objectType); + }); + + document.addEventListener("paste", e => { + if (e.target.nodeName === "INPUT" && document.activeElement === e.target) return; + + const url = e.clipboardData.getData("text"); + const files = e.clipboardData.files && e.clipboardData.files; + if (url) { + spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); + } else { + for (const file of files) { + spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.CLIPBOARD); + } + } + }); + + document.addEventListener("dragover", e => e.preventDefault()); + + document.addEventListener("drop", e => { + e.preventDefault(); + const url = e.dataTransfer.getData("url"); + const files = e.dataTransfer.files; + if (url) { + spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); + } else { + for (const file of files) { + spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.FILE); + } + } + }); + }; + + _setupCamera = () => { + this.scene.addEventListener("action_spawn_camera", () => { + const entity = document.createElement("a-entity"); + entity.setAttribute("networked", { template: "#interactable-camera" }); + entity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: { x: 0, y: 0, z: -1.5 } + }); + this.scene.appendChild(entity); + }); + }; + + _spawnAvatar = () => { + this.playerRig.setAttribute("networked", "template: #remote-avatar-template; attachTemplateToLocal: false;"); + this.playerRig.setAttribute("networked-avatar", ""); + this.playerRig.emit("entered"); + }; + + _runBot = async mediaStream => { + console.log("Running bot"); + + this.playerRig.setAttribute("avatar-replay", { + camera: "#player-camera", + leftController: "#player-left-controller", + rightController: "#player-right-controller" + }); + + const audioEl = document.createElement("audio"); + const audioInput = document.querySelector("#bot-audio-input"); + audioInput.onchange = () => { + audioEl.loop = true; + audioEl.muted = true; + audioEl.crossorigin = "anonymous"; + audioEl.src = URL.createObjectURL(audioInput.files[0]); + document.body.appendChild(audioEl); + }; + const dataInput = document.querySelector("#bot-data-input"); + dataInput.onchange = () => { + const url = URL.createObjectURL(dataInput.files[0]); + this.playerRig.setAttribute("avatar-replay", { recordingUrl: url }); + }; + await new Promise(resolve => audioEl.addEventListener("canplay", resolve)); + mediaStream.addTrack(audioEl.captureStream().getAudioTracks()[0]); + NAF.connection.adapter.setLocalMediaStream(mediaStream); + audioEl.play(); + }; +} diff --git a/src/scene.html b/src/scene.html new file mode 100644 index 0000000000000000000000000000000000000000..1cfeab1b7d46cd42b71edd2af6e0db0307a9af32 --- /dev/null +++ b/src/scene.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + +<head> + <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS --> + + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + + <link rel="shortcut icon" type="image/png" href="/favicon.ico"> + <title>Scene | Hubs by Mozilla</title> + <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700" rel="stylesheet"> +</head> + +<body> + <a-scene + renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;" + vr-mode-ui="enabled: false" + gamma-factor + > + <a-entity + id="scene-root" + static-body="shape: none;" + ></a-entity> + + <a-camera id="camera" fov="80" look-controls="enabled: false" wasd-controls="enabled: false"></a-camera> + </a-scene> + + <div id="ui-root"></div> +</body> + +</html> diff --git a/src/scene.js b/src/scene.js new file mode 100644 index 0000000000000000000000000000000000000000..6f2f0732d0da231b0bebef56b8ce526326aa5ea4 --- /dev/null +++ b/src/scene.js @@ -0,0 +1,110 @@ +console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`); + +import "./assets/stylesheets/scene.scss"; + +import "aframe"; +import "./utils/logging"; +import { patchWebGLRenderingContext } from "./utils/webgl"; +patchWebGLRenderingContext(); + +import "three/examples/js/loaders/GLTFLoader"; + +import "./components/scene-components"; +import "./components/debug"; +import "./systems/nav"; + +import { getReticulumFetchUrl } from "./utils/phoenix-utils"; + +import ReactDOM from "react-dom"; +import React from "react"; +import SceneUI from "./react-components/scene-ui"; +import { disableiOSZoom } from "./utils/disable-ios-zoom"; + +import "./gltf-component-mappings"; + +import { App } from "./App"; + +window.APP = new App(); + +const qs = new URLSearchParams(location.search); +const isMobile = AFRAME.utils.device.isMobile(); + +window.APP.quality = qs.get("quality") || isMobile ? "low" : "high"; + +import "aframe-physics-system"; +import "aframe-physics-extras"; +import "./components/event-repeater"; +import "./components/controls-shape-offset"; + +import registerTelemetry from "./telemetry"; + +registerTelemetry(); + +disableiOSZoom(); + +function mountUI(scene, props = {}) { + ReactDOM.render( + <SceneUI + {...{ + scene, + ...props + }} + />, + document.getElementById("ui-root") + ); +} + +const onReady = async () => { + const scene = document.querySelector("a-scene"); + window.APP.scene = scene; + + const sceneId = qs.get("scene_id") || document.location.pathname.substring(1).split("/")[1]; + console.log(`Scene ID: ${sceneId}`); + + let uiProps = { sceneId: sceneId }; + + mountUI(scene); + + const remountUI = props => { + uiProps = { ...uiProps, ...props }; + mountUI(scene, uiProps); + }; + + const sceneRoot = document.querySelector("#scene-root"); + const sceneModelEntity = document.createElement("a-entity"); + const gltfEl = document.createElement("a-entity"); + const camera = document.getElementById("camera"); + + sceneModelEntity.addEventListener("scene-loaded", () => { + remountUI({ sceneLoaded: true }); + const previewCamera = gltfEl.object3D.getObjectByName("scene-preview-camera"); + + if (previewCamera) { + camera.object3D.position.copy(previewCamera.position); + camera.object3D.rotation.copy(previewCamera.rotation); + camera.object3D.updateMatrix(); + } + + camera.setAttribute("scene-preview-camera", ""); + }); + + const res = await fetch(getReticulumFetchUrl(`/api/v1/scenes/${sceneId}`)).then(r => r.json()); + const sceneInfo = res.scenes[0]; + + const modelUrl = sceneInfo.model_url; + console.log(`Scene Model URL: ${modelUrl}`); + + gltfEl.setAttribute("gltf-model-plus", { src: modelUrl, useCache: false, inflate: true }); + gltfEl.addEventListener("model-loaded", () => sceneModelEntity.emit("scene-loaded")); + sceneModelEntity.appendChild(gltfEl); + sceneRoot.appendChild(sceneModelEntity); + + remountUI({ + sceneName: sceneInfo.name, + sceneDescription: sceneInfo.description, + sceneAttribution: sceneInfo.attribution, + sceneScreenshotURL: sceneInfo.screenshot_url + }); +}; + +document.addEventListener("DOMContentLoaded", onReady); diff --git a/src/spoke.html b/src/spoke.html new file mode 100644 index 0000000000000000000000000000000000000000..4cdea285a2a48fe96ef7ae28b8bc8826f328bd4b --- /dev/null +++ b/src/spoke.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <meta property="og:url" content="https://hubs.mozilla.com/spoke"> + <meta property="og:title" content="Spoke by Mozilla"> + <meta property="og:description" content="Create custom social VR scenes, right in your browser."> + <meta property="og:image" content="https://hubs.mozilla.com/spoke-preview.png"> + <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:domain" value="hubs.mozilla.com"> + <meta name="twitter:title" value="Spoke by Mozilla"> + <meta name="twitter:description" content="Create custom social VR scenes, right in your browser."> + <meta property="twitter:image" content="https://hubs.mozilla.com/spoke-preview.png"> + <meta name="twitter:url" value="https://hubs.mozilla.com/spoke"> + <link rel="shortcut icon" type="image/png" href="/favicon-spoke.ico"> + <title>Spoke by Mozilla</title> + <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700" rel="stylesheet"> +</head> + +<body> + <div id="ui-root"></div> +</body> + +</html> diff --git a/src/spoke.js b/src/spoke.js new file mode 100644 index 0000000000000000000000000000000000000000..c82a3c898a3c4080af29f8046f20f09a7ca3a9f6 --- /dev/null +++ b/src/spoke.js @@ -0,0 +1,238 @@ +import ReactDOM from "react-dom"; +import React, { Component } from "react"; +//import PropTypes from "prop-types"; +//import classNames from "classnames"; +import { playVideoWithStopOnBlur } from "./utils/video-utils.js"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import styles from "./assets/stylesheets/spoke.scss"; +import spokeLogo from "./assets/images/spoke_logo.png"; +import spokeVideoMp4 from "./assets/video/spoke.mp4"; +import spokeVideoWebm from "./assets/video/spoke.webm"; +import YouTube from "react-youtube"; + +//const qs = new URLSearchParams(location.search); + +import registerTelemetry from "./telemetry"; + +registerTelemetry(); + +import en from "react-intl/locale-data/en"; +import { lang, messages } from "./utils/i18n"; + +addLocaleData([...en]); + +function getPlatform() { + const platform = window.navigator.platform; + + if (["Macintosh", "MacIntel", "MacPPC", "Mac68K"].indexOf(platform) >= 0) { + return "macos"; + } else if (["Win32", "Win64", "Windows"].indexOf(platform) >= 0) { + return "win"; + } else if (/Linux/.test(platform) && !/\WAndroid\W/.test(navigator.userAgent)) { + return "linux"; + } + + return "unsupported"; +} + +class SpokeLanding extends Component { + static propTypes = {}; + + constructor(props) { + super(props); + this.state = { + platform: getPlatform(), + downloadClicked: false, + downloadLinkForCurrentPlatform: {}, + showPlayer: false + }; + } + + componentDidMount() { + this.loadVideo(); + this.fetchReleases(); + } + + tryGetJson = async request => { + const text = await request.text(); + try { + return JSON.parse(text); + } catch (e) { + console.log(`JSON error parsing response from ${request.url} "${text}"`, e); + } + }; + + getDownloadUrlForPlatform = (assets, platform) => { + return assets.find(asset => asset.name.includes(platform)).downloadUrl; + }; + + fetchReleases = async () => { + // Read-only, public access token. + const token = "de8cbfb4cc0281c7b731c891df431016c29b0ace"; + const result = await fetch("https://api.github.com/graphql", { + timeout: 5000, + method: "POST", + headers: { authorization: `bearer ${token}` }, + body: JSON.stringify({ + query: ` + { + repository(owner: "mozillareality", name: "spoke") { + releases( + orderBy: { field: CREATED_AT, direction: DESC }, + first: 5 + ) { + nodes { + isPrerelease, + isDraft, + tag { name }, + releaseAssets(last: 3) { + nodes { name, downloadUrl } + } + }, + pageInfo { endCursor, hasNextPage } + } + } + } + ` + }) + }).then(this.tryGetJson); + + if (!result || !result.data) { + this.setState({ platform: "unsupported" }); + return; + } + + const releases = result.data.repository.releases; + const release = releases.nodes.find(release => /*!release.isPrerelease && */ !release.isDraft); + + if (!release) { + this.setState({ platform: "unsupported" }); + return; + } + + this.setState({ + downloadLinkForCurrentPlatform: this.getDownloadUrlForPlatform(release.releaseAssets.nodes, this.state.platform), + spokeVersion: release.tag.name + }); + }; + + loadVideo() { + const videoEl = document.querySelector("#preview-video"); + playVideoWithStopOnBlur(videoEl); + } + + render() { + const platform = this.state.platform; + const releasesLink = "https://github.com/MozillaReality/Spoke/releases/latest"; + const downloadLink = platform === "unsupported" ? releasesLink : this.state.downloadLinkForCurrentPlatform; + + return ( + <IntlProvider locale={lang} messages={messages}> + <div className={styles.ui}> + <div className={styles.header}> + <div className={styles.headerLinks}> + <a href="https://github.com/mozillareality/spoke" rel="noopener noreferrer"> + <FormattedMessage id="home.source_link" /> + </a> + <a href="https://discord.gg/XzrGUY8" rel="noreferrer noopener"> + <FormattedMessage id="home.community_link" /> + </a> + <a href="/" rel="noreferrer noopener"> + Hubs + </a> + </div> + </div> + <div className={styles.content}> + <div className={styles.heroPane}> + <div className={styles.heroMessage}> + <div className={styles.spokeLogo}> + <img src={spokeLogo} /> + <div className={styles.primaryTagline}> + <FormattedMessage id="spoke.primary_tagline" /> + </div> + </div> + <div className={styles.secondaryTagline}> + <FormattedMessage id="spoke.secondary_tagline" /> + <a style={{ fontWeight: "bold" }} href="/"> + Hubs + </a> + </div> + <div className={styles.actionButtons}> + {!this.state.downloadClicked ? ( + <a + href={downloadLink} + onClick={() => this.setState({ downloadClicked: platform !== "unsupported" })} + className={styles.downloadButton} + > + <div> + <FormattedMessage id={"spoke.download_" + this.state.platform} /> + </div> + {platform !== "unsupported" && ( + <div className={styles.version}>{this.state.spokeVersion} Beta</div> + )} + </a> + ) : ( + <div className={styles.thankYou}> + <p> + <FormattedMessage id="spoke.thank_you" /> + </p> + + <p> + You can also <a href="https://discord.gg/XzrGUY8/">join our community</a> on Discord. + </p> + </div> + )} + + {platform !== "unsupported" && + !this.state.downloadClicked && ( + <a href={releasesLink} className={styles.browseVersions}> + <FormattedMessage id="spoke.browse_all_versions" /> + </a> + )} + <button className={styles.playButton} onClick={() => this.setState({ showPlayer: true })}> + <FormattedMessage id="spoke.play_button" /> + </button> + </div> + </div> + <div className={styles.heroVideo}> + <video playsInline muted loop autoPlay className={styles.previewVideo} id="preview-video"> + <source src={spokeVideoMp4} type="video/mp4" /> + <source src={spokeVideoWebm} type="video/webm" /> + </video> + <div className={styles.attribution}>Low Poly Campfire by Minzkraut</div> + </div> + </div> + </div> + <div className={styles.bg} /> + {this.state.showPlayer && ( + <div className={styles.playerOverlay}> + <div className={styles.playerContent}> + <YouTube + className={styles.playerVideo} + opts={{ rel: 0 }} + videoId="WmQKZJPhV7s" + onReady={e => e.target.playVideo()} + /> + {platform !== "unsupported" && ( + <a href={downloadLink} className={styles.downloadButton}> + <div> + <FormattedMessage id={"spoke.download_" + this.state.platform} /> + </div> + <div className={styles.version}>{this.state.spokeVersion} Beta</div> + </a> + )} + <a onClick={() => this.setState({ showPlayer: false })} className={styles.closeVideo}> + <FormattedMessage id="spoke.close" /> + </a> + </div> + </div> + )} + </div> + </IntlProvider> + ); + } +} + +document.addEventListener("DOMContentLoaded", () => { + ReactDOM.render(<SpokeLanding />, document.getElementById("ui-root")); +}); diff --git a/src/systems/tunnel-effect.js b/src/systems/tunnel-effect.js new file mode 100644 index 0000000000000000000000000000000000000000..8375964060a8c85977fe999ee4f73086f74b75d8 --- /dev/null +++ b/src/systems/tunnel-effect.js @@ -0,0 +1,163 @@ +import "../utils/postprocessing/EffectComposer"; +import "../utils/postprocessing/RenderPass"; +import "../utils/postprocessing/ShaderPass"; +import "../utils/postprocessing/MaskPass"; +import "../utils/shaders/CopyShader"; +import "../utils/shaders/VignetteShader"; +import qsTruthy from "../utils/qs_truthy"; + +const disabledByQueryString = qsTruthy("disableTunnel"); +const CLAMP_SPEED = 0.01; +const CLAMP_RADIUS = 0.001; +const NO_TUNNEL_RADIUS = 10.0; +const NO_TUNNEL_SOFTNESS = 0.0; + +function lerp(start, end, t) { + return (1 - t) * start + t * end; +} + +function f(t) { + const x = t - 1; + return 1 + x * x * x * x * x; +} + +AFRAME.registerSystem("tunneleffect", { + schema: { + targetComponent: { type: "string", default: "character-controller" }, + radius: { type: "number", default: 1.0, min: 0.25 }, + minRadius: { type: "number", default: 0.25, min: 0.1 }, + maxSpeed: { type: "number", default: 0.5, min: 0.1 }, + softest: { type: "number", default: 0.1, min: 0.0 }, + opacity: { type: "number", default: 1, min: 0.0 } + }, + + init: function() { + this.scene = this.el; + this.isMoving = false; + this.isVR = false; + this.dt = 0; + this.isPostProcessingReady = false; + this.characterEl = document.querySelector(`a-entity[${this.data.targetComponent}]`); + if (this.characterEl) { + this._initPostProcessing = this._initPostProcessing.bind(this); + this.characterEl.addEventListener("componentinitialized", this._initPostProcessing); + } else { + console.warn("Could not find target component."); + } + this._enterVR = this._enterVR.bind(this); + this._exitVR = this._exitVR.bind(this); + this.scene.addEventListener("enter-vr", this._enterVR); + this.scene.addEventListener("exit-vr", this._exitVR); + }, + + pause: function() { + if (!this.characterEl) { + return; + } + this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); + this.scene.removeEventListener("enter-vr", this._enterVR); + this.scene.removeEventListener("exit-vr", this._exitVR); + }, + + play: function() { + this.scene.addEventListener("enter-vr", this._enterVR); + this.scene.addEventListener("exit-vr", this._exitVR); + }, + + tick: function(t, dt) { + this.dt = dt; + + if (disabledByQueryString || !this.isPostProcessingReady || !this.isVR) { + return; + } + + const { maxSpeed, minRadius, softest } = this.data; + const characterSpeed = this.characterComponent.velocity.length(); + const shaderRadius = this.vignettePass.uniforms["radius"].value || NO_TUNNEL_RADIUS; + if (!this.enabled && characterSpeed > CLAMP_SPEED) { + this.enabled = true; + this._bindRenderFunc(); + } else if ( + this.enabled && + characterSpeed < CLAMP_SPEED && + Math.abs(NO_TUNNEL_RADIUS - shaderRadius) < CLAMP_RADIUS + ) { + this.enabled = false; + this._exitTunnel(); + } + if (this.enabled) { + const clampedSpeed = characterSpeed > maxSpeed ? maxSpeed : characterSpeed; + const speedRatio = clampedSpeed / maxSpeed; + this.targetRadius = lerp(NO_TUNNEL_RADIUS, minRadius, f(speedRatio)); + this.targetSoftness = lerp(NO_TUNNEL_SOFTNESS, softest, f(speedRatio)); + this._updateVignettePass(this.targetRadius, this.targetSoftness, this.data.opacity); + } + }, + + _exitTunnel: function() { + this.scene.renderer.render = this.originalRenderFunc; + this.isMoving = false; + }, + + _initPostProcessing: function(event) { + if (event.detail.name === this.data.targetComponent) { + this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); + this.characterComponent = this.characterEl.components[this.data.targetComponent]; + this._initComposer(); + } + }, + + _enterVR: function() { + this.isVR = true; //TODO: This is called in 2D mode when you press "f", which is bad + }, + + _exitVR: function() { + this._exitTunnel(); + this.isVR = false; + }, + + _initComposer: function() { + this.renderer = this.scene.renderer; + this.camera = this.scene.camera; + this.originalRenderFunc = this.scene.renderer.render; + this.isDigest = false; + const render = this.scene.renderer.render; + const system = this; + this.postProcessingRenderFunc = function() { + if (system.isDigest) { + render.apply(this, arguments); + } else { + system.isDigest = true; + system.composer.render(system.dt); + system.isDigest = false; + } + }; + this.composer = new THREE.EffectComposer(this.renderer); + this.composer.resize(); + this.scenePass = new THREE.RenderPass(this.scene.object3D, this.camera); + this.vignettePass = new THREE.ShaderPass(THREE.VignetteShader); + this._updateVignettePass(this.data.radius, this.data.softness, this.data.opacity); + this.composer.addPass(this.scenePass); + this.composer.addPass(this.vignettePass); + this.isPostProcessingReady = true; + }, + + _updateVignettePass: function(radius, softness, opacity) { + const { width, height } = this.renderer.getSize(); + const pixelRatio = this.renderer.getPixelRatio(); + this.vignettePass.uniforms["radius"].value = radius; + this.vignettePass.uniforms["softness"].value = softness; + this.vignettePass.uniforms["opacity"].value = opacity; + this.vignettePass["resolution"] = new THREE.Uniform(new THREE.Vector2(width * pixelRatio, height * pixelRatio)); + if (!this.vignettePass.renderToScreen) { + this.vignettePass.renderToScreen = true; + } + }, + + /** + * use the render func of the effect composer when we need the postprocessing + */ + _bindRenderFunc: function() { + this.scene.renderer.render = this.postProcessingRenderFunc; + } +}); diff --git a/src/utils/action-event-handler.js b/src/utils/action-event-handler.js index 6288c34e002af618390ab63185a47830067f39f1..472dd813a75ce4bbd11036057550d51105b9be2b 100644 --- a/src/utils/action-event-handler.js +++ b/src/utils/action-event-handler.js @@ -1,29 +1,47 @@ +const VERTICAL_SCROLL_TIMEOUT = 150; +const HORIZONTAL_SCROLL_TIMEOUT = 150; +const SCROLL_THRESHOLD = 0.05; +const SCROLL_MODIFIER = 0.1; + export default class ActionEventHandler { constructor(scene, cursor) { this.scene = scene; this.cursor = cursor; + this.cursorHand = this.cursor.data.cursor.components["super-hands"]; this.isCursorInteracting = false; - this.isCursorInteractingOnGrab = false; this.isTeleporting = false; this.handThatAlsoDrivesCursor = null; this.hovered = false; + this.gotPrimaryDown = false; + this.onPrimaryDown = this.onPrimaryDown.bind(this); this.onPrimaryUp = this.onPrimaryUp.bind(this); - this.onGrab = this.onGrab.bind(this); - this.onRelease = this.onRelease.bind(this); + this.onSecondaryDown = this.onSecondaryDown.bind(this); + this.onSecondaryUp = this.onSecondaryUp.bind(this); + this.onPrimaryGrab = this.onPrimaryGrab.bind(this); + this.onPrimaryRelease = this.onPrimaryRelease.bind(this); + this.onSecondaryGrab = this.onSecondaryGrab.bind(this); + this.onSecondaryRelease = this.onSecondaryRelease.bind(this); this.onCardboardButtonDown = this.onCardboardButtonDown.bind(this); this.onCardboardButtonUp = this.onCardboardButtonUp.bind(this); - this.onMoveDuck = this.onMoveDuck.bind(this); + this.onScrollMove = this.onScrollMove.bind(this); this.addEventListeners(); + + this.lastVerticalScrollTime = 0; + this.lastHorizontalScrollTime = 0; } addEventListeners() { this.scene.addEventListener("action_primary_down", this.onPrimaryDown); this.scene.addEventListener("action_primary_up", this.onPrimaryUp); - this.scene.addEventListener("action_grab", this.onGrab); - this.scene.addEventListener("action_release", this.onRelease); - this.scene.addEventListener("move_duck", this.onMoveDuck); + this.scene.addEventListener("action_secondary_down", this.onSecondaryDown); + this.scene.addEventListener("action_secondary_up", this.onSecondaryUp); + this.scene.addEventListener("primary_action_grab", this.onPrimaryGrab); + this.scene.addEventListener("primary_action_release", this.onPrimaryRelease); + this.scene.addEventListener("secondary_action_grab", this.onSecondaryGrab); + this.scene.addEventListener("secondary_action_release", this.onSecondaryRelease); + this.scene.addEventListener("scroll_move", this.onScrollMove); this.scene.addEventListener("cardboardbuttondown", this.onCardboardButtonDown); // TODO: These should be actions this.scene.addEventListener("cardboardbuttonup", this.onCardboardButtonUp); } @@ -31,97 +49,168 @@ export default class ActionEventHandler { tearDown() { this.scene.removeEventListener("action_primary_down", this.onPrimaryDown); this.scene.removeEventListener("action_primary_up", this.onPrimaryUp); - this.scene.removeEventListener("action_grab", this.onGrab); - this.scene.removeEventListener("action_release", this.onRelease); - this.scene.removeEventListener("move_duck", this.onMoveDuck); + this.scene.removeEventListener("action_secondary_down", this.onSecondaryDown); + this.scene.removeEventListener("action_secondary_up", this.onSecondaryUp); + this.scene.removeEventListener("primary_action_grab", this.onPrimaryGrab); + this.scene.removeEventListener("primary_action_release", this.onPrimaryRelease); + this.scene.removeEventListener("secondary_action_grab", this.onSecondaryGrab); + this.scene.removeEventListener("secondary_action_release", this.onSecondaryRelease); + this.scene.removeEventListener("scroll_move", this.onScrollMove); this.scene.removeEventListener("cardboardbuttondown", this.onCardboardButtonDown); this.scene.removeEventListener("cardboardbuttonup", this.onCardboardButtonUp); } - onMoveDuck(e) { - this.cursor.changeDistanceMod(-e.detail.axis[1] / 8); + onScrollMove(e) { + let scrollY = e.detail.axis[1] * SCROLL_MODIFIER; + scrollY = Math.abs(scrollY) > SCROLL_THRESHOLD ? scrollY : 0; + const changed = this.cursor.changeDistanceMod(-scrollY); //TODO: don't negate this for certain controllers + + let scrollX = e.detail.axis[0] * SCROLL_MODIFIER; + scrollX = Math.abs(scrollX) > SCROLL_THRESHOLD ? scrollX : 0; + + this.isCursorInteracting = this.cursor.isInteracting(); + + if ( + Math.abs(scrollY) > 0 && + (this.lastVerticalScrollTime === 0 || this.lastVerticalScrollTime + VERTICAL_SCROLL_TIMEOUT < Date.now()) + ) { + if (!changed && this.isCursorInteracting && this.isHandThatAlsoDrivesCursor(e.target)) { + this.cursorHand.el.emit(scrollY < 0 ? "scroll_up" : "scroll_down"); + this.cursorHand.el.emit("vertical_scroll_release"); + } else { + e.target.emit(scrollY < 0 ? "scroll_up" : "scroll_down"); + e.target.emit("vertical_scroll_release"); + } + this.lastVerticalScrollTime = Date.now(); + } + + if ( + Math.abs(scrollX) > 0 && + (this.lastHorizontalScrollTime === 0 || this.lastHorizontalScrollTime + HORIZONTAL_SCROLL_TIMEOUT < Date.now()) + ) { + if (this.isCursorInteracting && this.isHandThatAlsoDrivesCursor(e.target)) { + this.cursorHand.el.emit(scrollX < 0 ? "scroll_left" : "scroll_right"); + this.cursorHand.el.emit("horizontal_scroll_release"); + } else { + e.target.emit(scrollX < 0 ? "scroll_left" : "scroll_right"); + e.target.emit("horizontal_scroll_release"); + } + this.lastHorizontalScrollTime = Date.now(); + } } setHandThatAlsoDrivesCursor(handThatAlsoDrivesCursor) { this.handThatAlsoDrivesCursor = handThatAlsoDrivesCursor; } - onGrab(e) { - if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { - if (this.isCursorInteracting) { - return; - } else if (e.target.components["super-hands"].state.has("hover-start")) { - e.target.emit("hand_grab"); - return; + isToggle(el) { + return el && el.matches(".toggle, .toggle *"); + } + + isHandThatAlsoDrivesCursor(el) { + return this.handThatAlsoDrivesCursor === el; + } + + onGrab(e, event) { + event = event || e.type; + const superHand = e.target.components["super-hands"]; + const isCursorHand = this.isHandThatAlsoDrivesCursor(e.target); + this.isCursorInteracting = this.cursor.isInteracting(); + if (isCursorHand && !this.isCursorInteracting) { + if (superHand.state.has("hover-start") || superHand.state.get("grab-start")) { + e.target.emit(event); } else { this.isCursorInteracting = this.cursor.startInteraction(); - if (this.isCursorInteracting) { - this.isCursorInteractingOnGrab = true; - } - return; } + } else if (isCursorHand && this.isCursorInteracting) { + this.cursorHand.el.emit(event); } else { - e.target.emit("hand_grab"); - return; + e.target.emit(event); } } - onRelease(e) { + onRelease(e, event) { + event = event || e.type; + const isCursorHand = this.isHandThatAlsoDrivesCursor(e.target); + if (this.isCursorInteracting && isCursorHand) { + //need to check both grab-start and hover-start in the case that the spawner is being grabbed this frame + if (this.isToggle(this.cursorHand.state.get("grab-start") || this.cursorHand.state.get("hover-start"))) { + this.cursorHand.el.emit(event); + this.isCursorInteracting = this.cursor.isInteracting(); + } else { + this.isCursorInteracting = false; + this.cursor.endInteraction(); + } + } else { + e.target.emit(event); + } + } + + onPrimaryGrab(e) { + this.onGrab(e, "primary_hand_grab"); + } + + onPrimaryRelease(e) { + this.onRelease(e, "primary_hand_release"); + } + + onSecondaryGrab(e) { + this.onGrab(e, "secondary_hand_grab"); + } + + onSecondaryRelease(e) { + this.onRelease(e, "secondary_hand_release"); + } + + onDown(e, event) { + this.onGrab(e, event); + if ( - this.isCursorInteracting && - this.isCursorInteractingOnGrab && - this.handThatAlsoDrivesCursor && - this.handThatAlsoDrivesCursor === e.target + this.isHandThatAlsoDrivesCursor(e.target) && + !this.isCursorInteracting && + !this.cursorHand.state.get("grab-start") ) { - this.isCursorInteracting = false; - this.isCursorInteractingOnGrab = false; - this.cursor.endInteraction(); + this.cursor.setCursorVisibility(false); + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "down"); + this.isTeleporting = true; + } + } + + onUp(e, event) { + if (this.isTeleporting && this.isHandThatAlsoDrivesCursor(e.target)) { + const superHand = e.target.components["super-hands"]; + this.cursor.setCursorVisibility(!superHand.state.has("hover-start")); + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "up"); + this.isTeleporting = false; } else { - e.target.emit("hand_release"); + this.onRelease(e, event); } } onPrimaryDown(e) { - if (this.isCursorInteractingOnGrab) return; - if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { - if (this.isCursorInteracting) { - return; - } else if (e.target.components["super-hands"].state.has("hover-start")) { - e.target.emit("hand_grab"); - return; - } else { - this.isCursorInteracting = this.cursor.startInteraction(); - if (this.isCursorInteracting) return; - } + if (!this.gotPrimaryDown) { + this.onDown(e, "primary_hand_grab"); + this.gotPrimaryDown = true; } - - this.cursor.setCursorVisibility(false); - const button = e.target.components["teleport-controls"].data.button; - e.target.emit(button + "down"); - this.isTeleporting = true; } onPrimaryUp(e) { - if (this.isCursorInteractingOnGrab) return; - const isCursorHand = this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target; - if (this.isCursorInteracting && isCursorHand) { - this.isCursorInteracting = false; - this.cursor.endInteraction(); - return; + if (this.gotPrimaryDown) { + this.onUp(e, "primary_hand_release"); + } else if (this.isToggle(this.cursorHand.state.get("grab-start") || this.cursorHand.state.get("hover-start"))) { + this.onUp(e, "secondary_hand_release"); } + this.gotPrimaryDown = false; + } - const state = e.target.components["super-hands"].state; - if (state.has("grab-start")) { - e.target.emit("hand_release"); - return; - } + onSecondaryDown(e) { + this.onDown(e, "secondary_hand_grab"); + } - if (isCursorHand) { - this.cursor.setCursorVisibility(!state.has("hover-start")); - } - const button = e.target.components["teleport-controls"].data.button; - e.target.emit(button + "up"); - this.isTeleporting = false; + onSecondaryUp(e) { + this.onUp(e, "secondary_hand_release"); } onCardboardButtonDown(e) { diff --git a/src/utils/animation.js b/src/utils/animation.js new file mode 100644 index 0000000000000000000000000000000000000000..99152fece6b41f1fe8df7cc27c28c4a649d835e1 --- /dev/null +++ b/src/utils/animation.js @@ -0,0 +1,9 @@ +export function addAnimationComponents(modelEl) { + if (!modelEl.components["animation-mixer"]) { + return; + } + + if (!modelEl.querySelector("[loop-animation]")) { + modelEl.setAttribute("loop-animation", ""); + } +} diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 86940a0912406cf0b6bd536440606807feaf38f8..328a76a36e46dd2578bc0595ea0280ccfb93eb4d 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -87,6 +87,19 @@ export default class HubChannel { this.channel.push("events:object_spawned", spawnEvent); }; + sendProfileUpdate = () => { + this.channel.push("events:profile_updated", { profile: this.store.state.profile }); + }; + + sendMessage = body => { + if (body === "") return; + this.channel.push("message", { body }); + }; + + requestSupport = () => { + this.channel.push("events:request_support", {}); + }; + disconnect = () => { if (this.channel) { this.channel.socket.disconnect(); diff --git a/src/utils/link-channel.js b/src/utils/link-channel.js index 4da4e1eb2795c248cd7b4ddeb134549706d1ac0a..172a327ade77fd434f2c70612c14ca9298cfb444 100644 --- a/src/utils/link-channel.js +++ b/src/utils/link-channel.js @@ -24,9 +24,8 @@ export default class LinkChannel { return new Promise(resolve => { const onFinished = new Promise(finished => { const step = () => { - const code = Math.floor(Math.random() * 9999) - .toString() - .padStart(4, "0"); + const getLetter = () => "ABCDEFGHI"[Math.floor(Math.random() * 9)]; + const code = `${getLetter()}${getLetter()}${getLetter()}${getLetter()}`; // Only respond to one link_request in this channel. let readyToSend = false; @@ -109,7 +108,7 @@ export default class LinkChannel { if (numOccupants === 1) { // Great, only sender is in topic, request link channel.push("link_request", { - reply_to_session_id: this.socket.params.session_id, + reply_to_session_id: this.socket.params().session_id, public_key: publicKeyString }); diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 83a5f788ea841c1f3343c2068293b97bf9f53f3e..d72a453fa31707371dced4fb5a0151ee5577634e 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -1,47 +1,53 @@ import { objectTypeForOriginAndContentType } from "../object-types"; -let mediaAPIEndpoint = "/api/v1/media"; +import { getReticulumFetchUrl } from "./phoenix-utils"; +const mediaAPIEndpoint = getReticulumFetchUrl("/api/v1/media"); -if (process.env.RETICULUM_SERVER) { - mediaAPIEndpoint = `https://${process.env.RETICULUM_SERVER}${mediaAPIEndpoint}`; -} - -const fetchContentType = async url => { - return fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type")); +const commonKnownContentTypes = { + gltf: "model/gltf", + glb: "model/gltf-binary", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + pdf: "application/pdf", + mp4: "video/mp4", + mp3: "audio/mpeg" }; -const contentIndexCache = new Map(); -export const fetchMaxContentIndex = async (documentUrl, pageUrl) => { - if (contentIndexCache.has(documentUrl)) return contentIndexCache.get(documentUrl); - const maxIndex = await fetch(pageUrl).then(r => parseInt(r.headers.get("x-max-content-index"))); - contentIndexCache.set(documentUrl, maxIndex); - return maxIndex; +// thanks to https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding +function b64EncodeUnicode(str) { + // first we use encodeURIComponent to get percent-encoded UTF-8, then we convert the percent-encodings + // into raw bytes which can be fed into btoa. + const CHAR_RE = /%([0-9A-F]{2})/g; + return btoa(encodeURIComponent(str).replace(CHAR_RE, (_, p1) => String.fromCharCode("0x" + p1))); +} + +export const proxiedUrlFor = (url, index) => { + // farspark doesn't know how to read '=' base64 padding characters + const base64Url = b64EncodeUnicode(url).replace(/=+$/g, ""); + // translate base64 + to - and / to _ for URL safety + const encodedUrl = base64Url.replace(/\+/g, "-").replace(/\//g, "_"); + const method = index != null ? "extract" : "raw"; + return `https://${process.env.FARSPARK_SERVER}/0/${method}/0/0/0/${index || 0}/${encodedUrl}`; }; -const resolveMediaCache = new Map(); -export const resolveMedia = async (url, skipContentType, index) => { - const parsedUrl = new URL(url); +const resolveUrlCache = new Map(); +export const resolveUrl = async (url, index) => { const cacheKey = `${url}|${index}`; - if (resolveMediaCache.has(cacheKey)) return resolveMediaCache.get(cacheKey); - - const isHttpOrHttps = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; - const resolved = !isHttpOrHttps - ? { raw: url, origin: url } - : await fetch(mediaAPIEndpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ media: { url, index } }) - }).then(r => r.json()); - - if (isHttpOrHttps && !skipContentType) { - const contentType = - (resolved.meta && resolved.meta.expected_content_type) || (await fetchContentType(resolved.raw)); - resolved.contentType = contentType; - } - - resolveMediaCache.set(cacheKey, resolved); + if (resolveUrlCache.has(cacheKey)) return resolveUrlCache.get(cacheKey); + const resolved = await fetch(mediaAPIEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ media: { url, index } }) + }).then(r => r.json()); + resolveUrlCache.set(cacheKey, resolved); return resolved; }; +export const guessContentType = url => { + const extension = new URL(url).pathname.split(".").pop(); + return commonKnownContentTypes[extension]; +}; + export const upload = file => { const formData = new FormData(); formData.append("media", file); @@ -51,32 +57,82 @@ export const upload = file => { }).then(r => r.json()); }; +// https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603 +function getOrientation(file, callback) { + const reader = new FileReader(); + reader.onload = function(e) { + const view = new DataView(e.target.result); + if (view.getUint16(0, false) != 0xffd8) { + return callback(-2); + } + const length = view.byteLength; + let offset = 2; + while (offset < length) { + if (view.getUint16(offset + 2, false) <= 8) return callback(-1); + const marker = view.getUint16(offset, false); + offset += 2; + if (marker == 0xffe1) { + if (view.getUint32((offset += 2), false) != 0x45786966) { + return callback(-1); + } + + const little = view.getUint16((offset += 6), false) == 0x4949; + offset += view.getUint32(offset + 4, little); + const tags = view.getUint16(offset, little); + offset += 2; + for (let i = 0; i < tags; i++) { + if (view.getUint16(offset + i * 12, little) == 0x0112) { + return callback(view.getUint16(offset + i * 12 + 8, little)); + } + } + } else if ((marker & 0xff00) != 0xff00) { + break; + } else { + offset += view.getUint16(offset, false); + } + } + return callback(-1); + }; + reader.readAsArrayBuffer(file); +} + let interactableId = 0; -export const addMedia = (src, contentOrigin, resize = false) => { +export const addMedia = (src, template, contentOrigin, resolve = false, resize = false) => { const scene = AFRAME.scenes[0]; const entity = document.createElement("a-entity"); entity.id = "interactable-media-" + interactableId++; - entity.setAttribute("networked", { template: "#interactable-media" }); - entity.setAttribute("media-loader", { resize, src: typeof src === "string" ? src : "" }); + entity.setAttribute("networked", { template: template }); + entity.setAttribute("media-loader", { resize, resolve, src: typeof src === "string" ? src : "" }); scene.appendChild(entity); + const orientation = new Promise(function(resolve) { + if (src instanceof File) { + getOrientation(src, x => { + resolve(x); + }); + } else { + resolve(1); + } + }); if (src instanceof File) { upload(src) .then(response => { const srcUrl = new URL(response.raw); srcUrl.searchParams.set("token", response.meta.access_token); - entity.setAttribute("media-loader", { src: srcUrl.href }); + entity.setAttribute("media-loader", { resolve: false, src: srcUrl.href }); }) .catch(() => { entity.setAttribute("media-loader", { src: "error" }); }); } - entity.addEventListener("media_resolved", ({ detail }) => { - const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType); - scene.emit("object_spawned", { objectType }); - }); + if (contentOrigin) { + entity.addEventListener("media_resolved", ({ detail }) => { + const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType); + scene.emit("object_spawned", { objectType }); + }); + } - return entity; + return { entity, orientation }; }; diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js index 0fdb330c6558e397e395908e4b0c09fa8c15d1f0..510cb0cd25dd0ca792c294a73e544c60beb1b084 100644 --- a/src/utils/mouse-events-handler.js +++ b/src/utils/mouse-events-handler.js @@ -1,10 +1,14 @@ // TODO: Make look speed adjustable by the user const HORIZONTAL_LOOK_SPEED = 0.1; const VERTICAL_LOOK_SPEED = 0.06; +const VERTICAL_SCROLL_TIMEOUT = 50; +const HORIZONTAL_SCROLL_TIMEOUT = 50; export default class MouseEventsHandler { constructor(cursor, cameraController) { this.cursor = cursor; + const cursorController = this.cursor.el.getAttribute("cursor-controller"); + this.superHand = cursorController.cursor.components["super-hands"]; this.cameraController = cameraController; this.isLeftButtonDown = false; this.isLeftButtonHandledByCursor = false; @@ -16,6 +20,9 @@ export default class MouseEventsHandler { this.onMouseWheel = this.onMouseWheel.bind(this); this.addEventListeners(); + + this.lastVerticalScrollTime = 0; + this.lastHorizontalScrollTime = 0; } tearDown() { @@ -43,46 +50,83 @@ export default class MouseEventsHandler { } onMouseDown(e) { - const isLeftButton = e.button === 0; - const isRightButton = e.button === 2; - if (isLeftButton) { - this.onLeftButtonDown(); - } else if (isRightButton) { - this.onRightButtonDown(); + switch (e.button) { + case 0: //left button + this.onLeftButtonDown(); + break; + case 1: //middle/scroll button + //TODO: rotation? scaling? + break; + case 2: //right button + this.onRightButtonDown(); + break; } } onLeftButtonDown() { this.isLeftButtonDown = true; + if (this.isToggle(this.superHand.state.get("grab-start"))) { + this.superHand.el.emit("secondary-cursor-grab"); + } this.isLeftButtonHandledByCursor = this.cursor.startInteraction(); } onRightButtonDown() { - if (this.isPointerLocked) { - document.exitPointerLock(); - this.isPointerLocked = false; - } else { - document.body.requestPointerLock(); - this.isPointerLocked = true; + this.isLeftButtonHandledByCursor = this.cursor.isInteracting(); + if (!this.isLeftButtonHandledByCursor) { + if (this.isPointerLocked) { + document.exitPointerLock(); + this.isPointerLocked = false; + } else { + document.body.requestPointerLock(); + this.isPointerLocked = true; + } } } onMouseWheel(e) { - switch (e.deltaMode) { - case e.DOM_DELTA_PIXEL: - this.cursor.changeDistanceMod(e.deltaY / 500); - break; - case e.DOM_DELTA_LINE: - this.cursor.changeDistanceMod(e.deltaY / 10); - break; - case e.DOM_DELTA_PAGE: - this.cursor.changeDistanceMod(e.deltaY / 2); - break; + let changed = true; + if (!e.altKey && !e.shiftKey) { + changed = this.cursor.changeDistanceMod(this.getScrollMod(e.deltaY, e.deltaMode)); + } + + if ( + (!changed || e.shiftKey) && + (this.lastVerticalScrollTime === 0 || this.lastVerticalScrollTime + VERTICAL_SCROLL_TIMEOUT < Date.now()) + ) { + this.superHand.el.emit(e.deltaY > 0 ? "scroll_up" : "scroll_down"); + this.superHand.el.emit("vertical_scroll_release"); + this.lastVerticalScrollTime = Date.now(); + } + + const delta = e.altKey ? e.deltaY : e.deltaX; + if ( + Math.abs(delta) > 0 && + (this.lastHorizontalScrollTime === 0 || this.lastHorizontalScrollTime + HORIZONTAL_SCROLL_TIMEOUT < Date.now()) + ) { + this.superHand.el.emit(delta < 0 ? "scroll_left" : "scroll_right"); + this.superHand.el.emit("horizontal_scroll_release"); + this.lastHorizontalScrollTime = Date.now(); + } + + if (e.altKey) e.preventDefault(); //prevent forward/back on firefox + } + + getScrollMod(delta, deltaMode) { + switch (deltaMode) { + case WheelEvent.DOM_DELTA_PIXEL: + return delta / 500; + case WheelEvent.DOM_DELTA_LINE: + return delta / 10; + case WheelEvent.DOM_DELTA_PAGE: + return delta / 2; } } onMouseMove(e) { - const shouldLook = this.isPointerLocked || (this.isLeftButtonDown && !this.isLeftButtonHandledByCursor); + const shouldLook = + this.isPointerLocked || + (!this.superHand.state.get("grab-start") && this.isLeftButtonDown && !this.isLeftButtonHandledByCursor); if (shouldLook) { this.look(e); } @@ -91,14 +135,30 @@ export default class MouseEventsHandler { } onMouseUp(e) { - const isLeftButton = e.button === 0; - if (!isLeftButton) return; - - if (this.isLeftButtonHandledByCursor) { - this.cursor.endInteraction(); + switch (e.button) { + case 0: //left button + if (this.isToggle(this.superHand.state.get("grab-start"))) { + this.superHand.el.emit("secondary-cursor-release"); + } else { + this.endInteraction(); + } + this.isLeftButtonDown = false; + break; + case 1: //middle/scroll button + break; + case 2: //right button + this.endInteraction(); + break; } + } + + endInteraction() { + this.cursor.endInteraction(); this.isLeftButtonHandledByCursor = false; - this.isLeftButtonDown = false; + } + + isToggle(el) { + return el && el.matches(".toggle, .toggle *"); } look(e) { diff --git a/src/utils/next-tick.js b/src/utils/next-tick.js new file mode 100644 index 0000000000000000000000000000000000000000..85f66b7121e9245a6b9349ba0b8e9260a60ea057 --- /dev/null +++ b/src/utils/next-tick.js @@ -0,0 +1,5 @@ +export default function nextTick() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +} diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index b1b1e129ac1926b776b9b632f0626a14e3b22ff7..19619599af4803c4d537b764888fb4de6b21491f 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -1,7 +1,7 @@ import uuid from "uuid/v4"; import { Socket } from "phoenix"; -export function connectToReticulum() { +export function connectToReticulum(debug = false) { const qs = new URLSearchParams(location.search); const socketProtocol = qs.get("phx_protocol") || (document.location.protocol === "https:" ? "wss:" : "ws:"); @@ -20,8 +20,26 @@ export function connectToReticulum() { const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; console.log(`Phoenix Socket URL: ${socketUrl}`); - const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); + const socketSettings = { + params: { session_id: uuid() } + }; + + if (debug) { + socketSettings.logger = (kind, msg, data) => { + console.log(`${kind}: ${msg}`, data); + }; + } + + const socket = new Socket(socketUrl, socketSettings); socket.connect(); return socket; } + +export function getReticulumFetchUrl(path) { + if (process.env.RETICULUM_SERVER) { + return `https://${process.env.RETICULUM_SERVER}${path}`; + } else { + return path; + } +} diff --git a/src/utils/postprocessing/EffectComposer.js b/src/utils/postprocessing/EffectComposer.js new file mode 100644 index 0000000000000000000000000000000000000000..11fe7447817d002e86973b14856a5f2bc4a85de9 --- /dev/null +++ b/src/utils/postprocessing/EffectComposer.js @@ -0,0 +1,167 @@ +THREE.EffectComposer = function(renderer, renderTarget) { + this.renderer = renderer; + this.delta = 0; + window.addEventListener("vrdisplaypresentchange", this.resize.bind(this)); + + if (renderTarget === undefined) { + const parameters = { + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + format: THREE.RGBAFormat, + stencilBuffer: false + }; + + const size = renderer.getDrawingBufferSize(); + renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, parameters); + renderTarget.texture.name = "EffectComposer.rt1"; + } + + this.renderTarget1 = renderTarget; + this.renderTarget2 = renderTarget.clone(); + this.renderTarget2.texture.name = "EffectComposer.rt2"; + + this.writeBuffer = this.renderTarget1; + this.readBuffer = this.renderTarget2; + + this.passes = []; + this.maskActive = false; + + // dependencies + + if (THREE.CopyShader === undefined) { + console.error("THREE.EffectComposer relies on THREE.CopyShader"); + } + + if (THREE.ShaderPass === undefined) { + console.error("THREE.EffectComposer relies on THREE.ShaderPass"); + } + + this.copyPass = new THREE.ShaderPass(THREE.CopyShader); +}; + +Object.assign(THREE.EffectComposer.prototype, { + swapBuffers: function(pass) { + if (pass.needsSwap) { + if (this.maskActive) { + const context = this.renderer.context; + context.stencilFunc(context.NOTEQUAL, 1, 0xffffffff); + this.copyPass.render(this.renderer, this.writeBuffer, this.readBuffer, this.delta); + context.stencilFunc(context.EQUAL, 1, 0xffffffff); + } + + const tmp = this.readBuffer; + this.readBuffer = this.writeBuffer; + this.writeBuffer = tmp; + } + + if (THREE.MaskPass !== undefined) { + if (pass instanceof THREE.MaskPass) { + this.maskActive = true; + } else if (pass instanceof THREE.ClearMaskPass) { + this.maskActive = false; + } + } + }, + + addPass: function(pass) { + this.passes.push(pass); + const size = this.renderer.getDrawingBufferSize(); + pass.setSize(size.width, size.height); + }, + + insertPass: function(pass, index) { + this.passes.splice(index, 0, pass); + }, + + render: function(delta, starti) { + const maskActive = this.maskActive; + let pass; + let i; + const il = this.passes.length; + const scope = this; + let currentOnAfterRender; + this.delta = delta; + + for (i = starti || 0; i < il; i++) { + pass = this.passes[i]; + if (pass.enabled === false) continue; + + // If VR mode is enabled and rendering the whole scene is required. + // The pass renders the scene and and postprocessing is resumed before + // submitting the frame to the headset by using the onAfterRender callback. + if (this.renderer.vr.enabled && pass.scene) { + currentOnAfterRender = pass.scene.onAfterRender; + pass.scene.onAfterRender = function() { + // Disable stereo rendering when doing postprocessing + // on a render target. + scope.renderer.vr.enabled = false; + scope.render(delta, i + 1, maskActive); + + // Renable vr mode. + scope.renderer.vr.enabled = true; + }; + + pass.render(this.renderer, this.writeBuffer, this.readBuffer); + + // Restore onAfterRender + pass.scene.onAfterRender = currentOnAfterRender; + this.swapBuffers(pass); + return; + } + + pass.render(this.renderer, this.writeBuffer, this.readBuffer); + this.swapBuffers(pass); + } + }, + + reset: function(renderTarget) { + if (renderTarget === undefined) { + const size = this.renderer.getDrawingBufferSize(); + renderTarget = this.renderTarget1.clone(); + renderTarget.setSize(size.width, size.height); + } + + this.renderTarget1.dispose(); + this.renderTarget2.dispose(); + this.renderTarget1 = renderTarget; + this.renderTarget2 = renderTarget.clone(); + + this.writeBuffer = this.renderTarget1; + this.readBuffer = this.renderTarget2; + }, + + setSize: function(width, height) { + this.renderTarget1.setSize(width, height); + this.renderTarget2.setSize(width, height); + for (let i = 0; i < this.passes.length; i++) { + this.passes[i].setSize(width, height); + } + }, + + resize: function() { + const rendererSize = this.renderer.getDrawingBufferSize(); + this.setSize(rendererSize.width, rendererSize.height); + } +}); + +THREE.Pass = function() { + // if set to true, the pass is processed by the composer + this.enabled = true; + + // if set to true, the pass indicates to swap read and write buffer after rendering + this.needsSwap = true; + + // if set to true, the pass clears its buffer before rendering + this.clear = false; + + // if set to true, the result of the pass is rendered to screen + this.renderToScreen = false; +}; + +Object.assign(THREE.Pass.prototype, { + setSize: function() {}, + + render: function() { + console.error("THREE.Pass: .render() must be implemented in derived pass."); + } +}); diff --git a/src/utils/postprocessing/MaskPass.js b/src/utils/postprocessing/MaskPass.js new file mode 100644 index 0000000000000000000000000000000000000000..ddc5aa98a39be86dc224c282a9e02837e9aa7f14 --- /dev/null +++ b/src/utils/postprocessing/MaskPass.js @@ -0,0 +1,80 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.MaskPass = function(scene, camera) { + THREE.Pass.call(this); + + this.scene = scene; + this.camera = camera; + + this.clear = true; + this.needsSwap = false; + + this.inverse = false; +}; + +THREE.MaskPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.MaskPass, + + render: function(renderer, writeBuffer, readBuffer) { + const context = renderer.context; + const state = renderer.state; + + // don't update color or depth + + state.buffers.color.setMask(false); + state.buffers.depth.setMask(false); + + // lock buffers + + state.buffers.color.setLocked(true); + state.buffers.depth.setLocked(true); + + // set up stencil + + let writeValue, clearValue; + + if (this.inverse) { + writeValue = 0; + clearValue = 1; + } else { + writeValue = 1; + clearValue = 0; + } + + state.buffers.stencil.setTest(true); + state.buffers.stencil.setOp(context.REPLACE, context.REPLACE, context.REPLACE); + state.buffers.stencil.setFunc(context.ALWAYS, writeValue, 0xffffffff); + state.buffers.stencil.setClear(clearValue); + + // draw into the stencil buffer + + renderer.render(this.scene, this.camera, readBuffer, this.clear); + renderer.render(this.scene, this.camera, writeBuffer, this.clear); + + // unlock color and depth buffer for subsequent rendering + + state.buffers.color.setLocked(false); + state.buffers.depth.setLocked(false); + + // only render where stencil is set to 1 + + state.buffers.stencil.setFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 + state.buffers.stencil.setOp(context.KEEP, context.KEEP, context.KEEP); + } +}); + +THREE.ClearMaskPass = function() { + THREE.Pass.call(this); + + this.needsSwap = false; +}; + +THREE.ClearMaskPass.prototype = Object.create(THREE.Pass.prototype); + +Object.assign(THREE.ClearMaskPass.prototype, { + render: function(renderer) { + renderer.state.buffers.stencil.setTest(false); + } +}); diff --git a/src/utils/postprocessing/README.md b/src/utils/postprocessing/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1385db1d2b5b53f0325fe39020b0de2f4d91144d --- /dev/null +++ b/src/utils/postprocessing/README.md @@ -0,0 +1,7 @@ +These files +- EffectComposer.js +- MaskPass.js +- RenderPass.js +- ShaderPass.js +are copied from https://github.com/mrdoob/three.js/blob/dev/examples/js/postprocessing/EffectComposer.js + diff --git a/src/utils/postprocessing/RenderPass.js b/src/utils/postprocessing/RenderPass.js new file mode 100644 index 0000000000000000000000000000000000000000..e7dd98af6f12876c0c179147fb1b3e20836772a8 --- /dev/null +++ b/src/utils/postprocessing/RenderPass.js @@ -0,0 +1,52 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.RenderPass = function(scene, camera, overrideMaterial, clearColor, clearAlpha) { + THREE.Pass.call(this); + + this.scene = scene; + this.camera = camera; + + this.overrideMaterial = overrideMaterial; + + this.clearColor = clearColor; + this.clearAlpha = clearAlpha !== undefined ? clearAlpha : 0; + + this.clear = true; + this.clearDepth = false; + this.needsSwap = false; +}; + +THREE.RenderPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.RenderPass, + + render: function(renderer, writeBuffer, readBuffer) { + const oldAutoClear = renderer.autoClear; + renderer.autoClear = false; + + this.scene.overrideMaterial = this.overrideMaterial; + + let oldClearColor, oldClearAlpha; + + if (this.clearColor) { + oldClearColor = renderer.getClearColor().getHex(); + oldClearAlpha = renderer.getClearAlpha(); + + renderer.setClearColor(this.clearColor, this.clearAlpha); + } + + if (this.clearDepth) { + renderer.clearDepth(); + } + + renderer.render(this.scene, this.camera, this.renderToScreen ? null : readBuffer, this.clear); + + if (this.clearColor) { + renderer.setClearColor(oldClearColor, oldClearAlpha); + } + + this.scene.overrideMaterial = null; + renderer.autoClear = oldAutoClear; + } +}); diff --git a/src/utils/postprocessing/ShaderPass.js b/src/utils/postprocessing/ShaderPass.js new file mode 100644 index 0000000000000000000000000000000000000000..d6d25800f86e19df01c8e39c490b3763ecd7ef77 --- /dev/null +++ b/src/utils/postprocessing/ShaderPass.js @@ -0,0 +1,44 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.ShaderPass = function(shader, textureID) { + THREE.Pass.call(this); + this.textureID = textureID !== undefined ? textureID : "tDiffuse"; + if (shader instanceof THREE.ShaderMaterial) { + this.uniforms = shader.uniforms; + this.material = shader; + } else if (shader) { + this.uniforms = THREE.UniformsUtils.clone(shader.uniforms); + this.material = new THREE.ShaderMaterial({ + defines: Object.assign({}, shader.defines), + uniforms: this.uniforms, + vertexShader: shader.vertexShader, + fragmentShader: shader.fragmentShader + }); + } + + this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + this.scene = new THREE.Scene(); + + this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null); + this.quad.frustumCulled = false; // Avoid getting clipped + this.scene.add(this.quad); +}; + +THREE.ShaderPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.ShaderPass, + render: function(renderer, writeBuffer, readBuffer) { + if (this.uniforms[this.textureID]) { + this.uniforms[this.textureID].value = readBuffer.texture; + } + + this.quad.material = this.material; + + if (this.renderToScreen) { + renderer.render(this.scene, this.camera); + } else { + renderer.render(this.scene, this.camera, writeBuffer, this.clear); + } + } +}); diff --git a/src/utils/scene-graph.js b/src/utils/scene-graph.js new file mode 100644 index 0000000000000000000000000000000000000000..0be2cc7216d0093966c841cf8d979e9517be3c6e --- /dev/null +++ b/src/utils/scene-graph.js @@ -0,0 +1,6 @@ +export function findAncestorWithComponent(entity, componentName) { + while (entity && !(entity.components && entity.components[componentName])) { + entity = entity.parentNode; + } + return entity; +} diff --git a/src/utils/shaders/CopyShader.js b/src/utils/shaders/CopyShader.js new file mode 100644 index 0000000000000000000000000000000000000000..f970d4a92df1ae3653b45906596963c3c6b7611d --- /dev/null +++ b/src/utils/shaders/CopyShader.js @@ -0,0 +1,38 @@ +/** + * @author alteredq / http://alteredqualia.com/ + * + * Full-screen textured quad shader + */ + +THREE.CopyShader = { + uniforms: { + tDiffuse: { value: null }, + opacity: { value: 1.0 } + }, + + vertexShader: [ + "varying vec2 vUv;", + + "void main() {", + + "vUv = uv;", + "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", + + "}" + ].join("\n"), + + fragmentShader: [ + "uniform float opacity;", + + "uniform sampler2D tDiffuse;", + + "varying vec2 vUv;", + + "void main() {", + + "vec4 texel = texture2D( tDiffuse, vUv );", + "gl_FragColor = opacity * texel;", + + "}" + ].join("\n") +}; diff --git a/src/utils/shaders/README.md b/src/utils/shaders/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d4b23f4a3954768617953bbea2a897ded60ded5 --- /dev/null +++ b/src/utils/shaders/README.md @@ -0,0 +1,5 @@ +These files +- CopyShader.js +- VignetteShader.js +were copied from https://github.com/mrdoob/three.js/blob/dev/examples/js/shaders/CopyShader.js + diff --git a/src/utils/shaders/VignetteShader.js b/src/utils/shaders/VignetteShader.js new file mode 100644 index 0000000000000000000000000000000000000000..9a424139bc72101aea8f7251f741c9790f851bae --- /dev/null +++ b/src/utils/shaders/VignetteShader.js @@ -0,0 +1,46 @@ +THREE.VignetteShader = { + uniforms: { + tDiffuse: { value: null }, + radius: { value: 0.65 }, + opacity: { value: 0.9 }, + softness: { value: 0.2 }, + resolution: new THREE.Uniform(new THREE.Vector2(1920, 1080)) + }, + + vertexShader: [ + "varying vec2 vUv;", + "void main() {", + "vUv = uv;", + "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", + "}" + ].join("\n"), + + fragmentShader: [ + "uniform sampler2D tDiffuse;", + "uniform float radius;", + "uniform float opacity;", + "uniform float softness;", + "uniform vec2 resolution;", + + "varying vec2 vUv;", + + "void main() {", + "vec4 texel = texture2D( tDiffuse, vUv);", + "float ratio = resolution.x / resolution.y;", + "float centerXOffset = radius / ratio;", + "float leftX = (0.3 + centerXOffset >= 0.5) ? 0.5 - centerXOffset : 0.3;", + "float rightX = (0.7 - centerXOffset <= 0.5) ? 0.5 + centerXOffset : 0.7;", + "vec2 uvLeft = (vUv.xy) - vec2(leftX, 0.5);", + "vec2 uvRight = (vUv.xy) - vec2(rightX, 0.5);", + "uvLeft.x *= ratio;", + "uvRight.x *= ratio;", + "float lenLeft = length(uvLeft);", + "float lenRight = length(uvRight);", + "float vignetteLeft = smoothstep(radius, radius-softness, lenLeft);", + "float vignetteRight = smoothstep(radius, radius-softness, lenRight);", + "float vignette = vignetteLeft + vignetteRight;", + "vec3 final = mix (texel.rgb, texel.rgb * vignette, opacity);", + "gl_FragColor = vec4(final.rgb, 1.0);", + "}" + ].join("\n") +}; diff --git a/src/utils/sharedbuffergeometry.js b/src/utils/sharedbuffergeometry.js new file mode 100644 index 0000000000000000000000000000000000000000..aeebd45ed273e67e2abc22ab00288053fb321781 --- /dev/null +++ b/src/utils/sharedbuffergeometry.js @@ -0,0 +1,117 @@ +export default class SharedBufferGeometry { + constructor(material, primitiveMode, maxBufferSize) { + this.material = material; + this.primitiveMode = primitiveMode; + + console.log("maxBufferSize", maxBufferSize); + this.maxBufferSize = maxBufferSize; + this.geometries = []; + this.current = null; + this.drawing = new THREE.Object3D(); + this.addBuffer(); + } + + restartPrimitive() { + if (this.idx.position >= this.current.attributes.position.count) { + console.error("maxBufferSize limit exceeded"); + } else if (this.idx.position !== 0) { + let prev = (this.idx.position - 1) * 3; + const position = this.current.attributes.position.array; + this.addVertex(position[prev++], position[prev++], position[prev++]); + + this.idx.color++; + this.idx.normal++; + } + } + + remove(prevIdx, idx) { + // Loop through all the attributes: position, color, normal,... + if (this.idx.position > idx.position) { + for (const key in this.idx) { + const componentSize = 3; + let pos = prevIdx[key] * componentSize; + const start = (idx[key] + 1) * componentSize; + const end = this.idx[key] * componentSize; + for (let i = start; i < end; i++) { + this.current.attributes[key].array[pos++] = this.current.attributes[key].array[i]; + } + const diff = idx[key] - prevIdx[key] + 1; + this.idx[key] -= diff; + } + } else { + for (const key in this.idx) { + const diff = idx[key] - prevIdx[key]; + this.idx[key] -= diff; + } + } + + this.update(); + } + + undo(prevIdx) { + this.idx = prevIdx; + this.update(); + } + + addBuffer() { + const geometry = new THREE.BufferGeometry(); + + const vertices = new Float32Array(this.maxBufferSize * 3); + const normals = new Float32Array(this.maxBufferSize * 3); + const colors = new Float32Array(this.maxBufferSize * 3); + + const mesh = new THREE.Mesh(geometry, this.material); + + mesh.drawMode = this.primitiveMode; + + mesh.frustumCulled = false; + mesh.vertices = vertices; + + const object3D = new THREE.Object3D(); + this.drawing.add(object3D); + object3D.add(mesh); + + geometry.setDrawRange(0, 0); + geometry.addAttribute("position", new THREE.BufferAttribute(vertices, 3).setDynamic(true)); + geometry.addAttribute("normal", new THREE.BufferAttribute(normals, 3).setDynamic(true)); + geometry.addAttribute("color", new THREE.BufferAttribute(colors, 3).setDynamic(true)); + + this.previous = null; + if (this.geometries.length > 0) { + this.previous = this.current; + } + + this.idx = { + position: 0, + normal: 0, + color: 0 + }; + + this.geometries.push(geometry); + this.current = geometry; + } + + addColor(r, g, b) { + this.current.attributes.color.setXYZ(this.idx.color++, r, g, b); + } + + addNormal(x, y, z) { + this.current.attributes.normal.setXYZ(this.idx.normal++, x, y, z); + } + + addVertex(x, y, z) { + const buffer = this.current.attributes.position; + if (this.idx.position === buffer.count) { + console.error("maxBufferSize limit exceeded"); + } + buffer.setXYZ(this.idx.position++, x, y, z); + } + + update() { + this.current.setDrawRange(0, this.idx.position); + + this.current.attributes.color.needsUpdate = true; + this.current.attributes.normal.needsUpdate = true; + this.current.attributes.position.needsUpdate = true; + } +} diff --git a/src/utils/sharedbuffergeometrymanager.js b/src/utils/sharedbuffergeometrymanager.js new file mode 100644 index 0000000000000000000000000000000000000000..a3f86d44e73cfa8b6f86cb712d4b9c00d4d5d822 --- /dev/null +++ b/src/utils/sharedbuffergeometrymanager.js @@ -0,0 +1,15 @@ +import SharedBufferGeometry from "./sharedbuffergeometry"; + +export default class SharedBufferGeometryManager { + constructor() { + this.sharedBuffers = {}; + } + + addSharedBuffer(name, material, primitiveMode, maxBufferSize) { + this.sharedBuffers[name] = new SharedBufferGeometry(material, primitiveMode, maxBufferSize); + } + + getSharedBuffer(name) { + return this.sharedBuffers[name]; + } +} diff --git a/src/utils/video-utils.js b/src/utils/video-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..74fdb626f52901b4b636f9fce7ccf9106bce20a4 --- /dev/null +++ b/src/utils/video-utils.js @@ -0,0 +1,15 @@ +export function playVideoWithStopOnBlur(videoEl) { + function toggleVideo() { + // Play the video if the window/tab is visible. + if (document.hasFocus()) { + videoEl.play(); + } else { + videoEl.pause(); + } + } + if ("hasFocus" in document) { + document.addEventListener("visibilitychange", toggleVideo); + window.addEventListener("focus", toggleVideo); + window.addEventListener("blur", toggleVideo); + } +} diff --git a/src/utils/vr-caps-detect.js b/src/utils/vr-caps-detect.js index 3294820125788e0343eb5f70277eefc48b75a2c2..fc2737ad41519798326d1f2ce6cec57e5a8e5b23 100644 --- a/src/utils/vr-caps-detect.js +++ b/src/utils/vr-caps-detect.js @@ -22,6 +22,11 @@ function isMaybeDaydreamCompatibleDevice(ua) { // that can be entered into as a "generic" entry flow. const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i]; +export function detectInHMD() { + const isOculusBrowser = /Oculus/.test(navigator.userAgent); + return isOculusBrowser; +} + // Tries to determine VR entry compatibility regardless of the current browser. // // For each VR "entry type", returns VR_DEVICE_AVAILABILITY.yes if that type can be launched into directly from this browser @@ -45,7 +50,6 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i]; export async function getAvailableVREntryTypes() { const ua = navigator.userAgent; const isSamsungBrowser = browser.name === "chrome" && /SamsungBrowser/.test(ua); - const isOculusBrowser = /Oculus/.test(ua); // This needs to be kept up-to-date with the latest browsers that can support VR and Hubs. // Checking for navigator.getVRDisplays always passes b/c of polyfill. @@ -63,8 +67,9 @@ export async function getAvailableVREntryTypes() { : VR_DEVICE_AVAILABILITY.no; const displays = isWebVRCapableBrowser ? await navigator.getVRDisplays() : []; - const isFirefoxReality = window.orientation === 0 && "buildID" in navigator && displays.length > 0; - const isInHMD = isOculusBrowser || isFirefoxReality; + + const isOculusBrowser = /Oculus/.test(ua); + const isInHMD = detectInHMD(); const screen = isInHMD ? VR_DEVICE_AVAILABILITY.no diff --git a/webpack.config.js b/webpack.config.js index 3e481d53ccfc6cbdc15365ef51d6c11978a5f6ee..e6567532eb15a79f243b7e8a5df1b9064e5eeefd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,7 +64,9 @@ module.exports = (env, argv) => ({ entry: { index: path.join(__dirname, "src", "index.js"), hub: path.join(__dirname, "src", "hub.js"), + scene: path.join(__dirname, "src", "scene.js"), link: path.join(__dirname, "src", "link.js"), + spoke: path.join(__dirname, "src", "spoke.js"), "avatar-selector": path.join(__dirname, "src", "avatar-selector.js") }, output: { @@ -157,11 +159,11 @@ module.exports = (env, argv) => ({ optimization: { // necessary due to https://github.com/visionmedia/debug/issues/547 - minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: { collapse_vars: false } } })], + minimizer: [new UglifyJsPlugin({ sourceMap: true, uglifyOptions: { compress: { collapse_vars: false } } })], splitChunks: { cacheGroups: { engine: { - test: /[\\/]node_modules[\\/](aframe|cannon|three\.js)/, + test: /([\\/]src[\\/]workers|[\\/]node_modules[\\/](aframe|cannon|three\.js))/, priority: 100, name: "engine", chunks: "all" @@ -180,7 +182,7 @@ module.exports = (env, argv) => ({ new HTMLWebpackPlugin({ filename: "index.html", template: path.join(__dirname, "src", "index.html"), - chunks: ["vendor", "engine", "index"] + chunks: ["vendor", "index"] }), new HTMLWebpackPlugin({ filename: "hub.html", @@ -196,11 +198,30 @@ module.exports = (env, argv) => ({ } ] }), + new HTMLWebpackPlugin({ + filename: "scene.html", + template: path.join(__dirname, "src", "scene.html"), + chunks: ["vendor", "engine", "scene"], + inject: "head", + meta: [ + { + "http-equiv": "origin-trial", + "data-feature": "WebVR (For Chrome M62+)", + "data-expires": process.env.ORIGIN_TRIAL_EXPIRES, + content: process.env.ORIGIN_TRIAL_TOKEN + } + ] + }), new HTMLWebpackPlugin({ filename: "link.html", template: path.join(__dirname, "src", "link.html"), chunks: ["vendor", "link"] }), + new HTMLWebpackPlugin({ + filename: "spoke.html", + template: path.join(__dirname, "src", "spoke.html"), + chunks: ["vendor", "spoke"] + }), new HTMLWebpackPlugin({ filename: "avatar-selector.html", template: path.join(__dirname, "src", "avatar-selector.html"), @@ -230,6 +251,7 @@ module.exports = (env, argv) => ({ NODE_ENV: argv.mode, JANUS_SERVER: process.env.JANUS_SERVER, RETICULUM_SERVER: process.env.RETICULUM_SERVER, + FARSPARK_SERVER: process.env.FARSPARK_SERVER, ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER, EXTRA_ENVIRONMENTS: process.env.EXTRA_ENVIRONMENTS, BUILD_VERSION: process.env.BUILD_VERSION