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/.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..ca13a7681168060b9a2b5bdc199177cb518dbd70 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -49,7 +49,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/README.md b/README.md index 503057f930fe9fc7cfe37c01a18990860274f5a7..4e96bc332b06bf6b6ae5f502dd36fb9b893f00c7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,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 @@ -72,3 +73,4 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca * [Hubs-Ops](https://github.com/mozilla/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS. [](http://waffle.io/mozilla/socialmr) + 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 035aba4c5b879a87ec30fcb837f219d94bc5e61f..89f73fe4c6e6fbecf2d06e51f2c27b72284ae991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -513,8 +513,8 @@ } }, "aframe": { - "version": "github:aframevr/aframe#1be48d9204f0919d6362ef4c4dfa955e4ef64439", - "from": "aframe@github:aframevr/aframe#1be48d9204f0919d6362ef4c4dfa955e4ef64439", + "version": "github:mozillareality/aframe#b9d11e68fdeaa75ae0c6893103b54bc149a2e38f", + "from": "github:mozillareality/aframe#bugfix/oculus-go-controller-reconnect-pre-e0c8ff7", "requires": { "@tweenjs/tween.js": "^16.8.0", "browserify-css": "^0.8.2", @@ -527,19 +527,19 @@ "present": "0.0.6", "promise-polyfill": "^3.1.0", "style-attr": "^1.0.2", - "three": "0.93.0", + "three": "0.94.0", "three-bmfont-text": "^2.1.0", "webvr-polyfill": "^0.10.5" }, "dependencies": { "debug": { "version": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a", - "from": "debug@github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a" + "from": "github:ngokevin/debug#noTimestamp" }, "three": { - "version": "0.93.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.93.0.tgz", - "integrity": "sha1-P9bDZ+9FVKu7bhataZNig+iVwSM=" + "version": "0.94.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.94.0.tgz", + "integrity": "sha1-TObbfyv795wtc0RKpuPPwIoy12I=" } } }, @@ -552,14 +552,54 @@ "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" }, "aframe-physics-extras": { - "version": "0.1.3", - "resolved": "https://registry.yarnpkg.com/aframe-physics-extras/-/aframe-physics-extras-0.1.3.tgz", - "integrity": "sha1-gD4hZPuWwKgPLRqBRY8yd/JisTA=" + "version": "github:mozillareality/aframe-physics-extras#3a00539a9c9f259df3a55c70648b1ec0648b5479", + "from": "github:mozillareality/aframe-physics-extras#bugfix/physics-collider-world" }, "aframe-physics-system": { "version": "github:mozillareality/aframe-physics-system#ecc5c9c533d6d9c71f8d6453ab961ed074d44b1c", @@ -584,6 +624,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", @@ -748,6 +793,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", @@ -769,6 +819,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", @@ -858,8 +913,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", @@ -1956,6 +2010,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", @@ -1998,12 +2057,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", @@ -2020,6 +2089,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", @@ -2029,8 +2106,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", @@ -2038,6 +2114,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", @@ -2063,7 +2144,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", @@ -2080,8 +2160,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==" } } }, @@ -2362,8 +2441,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", @@ -2449,6 +2527,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", @@ -2763,6 +2846,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", @@ -2978,11 +3071,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", @@ -3025,6 +3127,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", @@ -3061,8 +3195,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", @@ -3073,8 +3206,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", @@ -3146,8 +3278,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", @@ -3224,6 +3355,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", @@ -3361,6 +3502,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", @@ -3404,7 +3550,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" } @@ -3622,6 +3767,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", @@ -3631,8 +3781,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", @@ -3693,6 +3842,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", @@ -3756,7 +3910,7 @@ }, "document-register-element": { "version": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90", - "from": "document-register-element@github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90" + "from": "github:dmarcos/document-register-element#8ccc532b7" }, "dom-converter": { "version": "0.1.4", @@ -3767,6 +3921,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", @@ -3896,8 +4061,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", @@ -3941,8 +4105,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", @@ -3961,6 +4124,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", @@ -3972,6 +4219,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", @@ -4089,8 +4341,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", @@ -4304,8 +4555,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", @@ -4381,6 +4631,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", @@ -4503,8 +4797,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", @@ -4963,7 +5256,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" }, @@ -4972,7 +5264,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" } @@ -5080,14 +5371,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", @@ -5097,24 +5386,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", @@ -5123,13 +5408,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" @@ -5138,34 +5421,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" @@ -5174,25 +5451,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" @@ -5201,13 +5474,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", @@ -5223,7 +5494,6 @@ "glob": { "version": "7.1.2", "bundled": true, - "dev": true, "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -5237,13 +5507,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" @@ -5252,7 +5520,6 @@ "ignore-walk": { "version": "3.0.1", "bundled": true, - "dev": true, "optional": true, "requires": { "minimatch": "^3.0.4" @@ -5261,7 +5528,6 @@ "inflight": { "version": "1.0.6", "bundled": true, - "dev": true, "optional": true, "requires": { "once": "^1.3.0", @@ -5270,19 +5536,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" } @@ -5290,26 +5553,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" @@ -5318,7 +5577,6 @@ "minizlib": { "version": "1.1.0", "bundled": true, - "dev": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -5327,7 +5585,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "dev": true, "requires": { "minimist": "0.0.8" } @@ -5335,13 +5592,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", @@ -5352,7 +5607,6 @@ "node-pre-gyp": { "version": "0.10.0", "bundled": true, - "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -5370,7 +5624,6 @@ "nopt": { "version": "4.0.1", "bundled": true, - "dev": true, "optional": true, "requires": { "abbrev": "1", @@ -5380,13 +5633,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", @@ -5396,7 +5647,6 @@ "npmlog": { "version": "4.1.2", "bundled": true, - "dev": true, "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -5407,19 +5657,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" } @@ -5427,19 +5674,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", @@ -5449,19 +5693,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", @@ -5473,7 +5714,6 @@ "minimist": { "version": "1.2.0", "bundled": true, - "dev": true, "optional": true } } @@ -5481,7 +5721,6 @@ "readable-stream": { "version": "2.3.6", "bundled": true, - "dev": true, "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -5496,7 +5735,6 @@ "rimraf": { "version": "2.6.2", "bundled": true, - "dev": true, "optional": true, "requires": { "glob": "^7.0.5" @@ -5504,43 +5742,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", @@ -5550,7 +5781,6 @@ "string_decoder": { "version": "1.1.1", "bundled": true, - "dev": true, "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -5559,7 +5789,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5567,13 +5796,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", @@ -5588,13 +5815,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" @@ -5602,13 +5827,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "dev": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "dev": true + "bundled": true } } }, @@ -5798,7 +6021,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", @@ -5981,6 +6203,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", @@ -6086,12 +6316,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", @@ -6398,7 +6641,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", @@ -6416,7 +6658,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", @@ -6566,8 +6807,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", @@ -6796,7 +7036,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" } @@ -7120,8 +7359,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", @@ -7385,8 +7623,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", @@ -7494,6 +7731,150 @@ } } }, + "karma": { + "version": "0.13.22", + "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", + "integrity": "sha1-B3ULG9Bj1+fnuRvNLmNU2PKqh0Q=", + "requires": { + "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", @@ -7733,15 +8114,15 @@ } }, "load-bmfont": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.3.0.tgz", - "integrity": "sha1-u358cQ3mvK/LE8s7jIHgwBMey8k=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.3.1.tgz", + "integrity": "sha512-lQkEawgez06lM2iw1vQEEOtVLJXyMzFcUqbwWMrB0g6zwhdUs/+e0KNd1zEJ7OFBbMVz0tbzQyjgjtTB47+PBg==", "requires": { "buffer-equal": "0.0.1", "mime": "^1.3.4", "parse-bmfont-ascii": "^1.0.3", "parse-bmfont-binary": "^1.0.5", - "parse-bmfont-xml": "^1.1.0", + "parse-bmfont-xml": "^1.1.4", "xhr": "^2.0.1", "xtend": "^4.0.0" }, @@ -7836,8 +8217,7 @@ "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.mergewith": { "version": "4.6.1", @@ -7878,6 +8258,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", @@ -7937,7 +8344,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" @@ -7946,8 +8352,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=" } } }, @@ -8051,8 +8456,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", @@ -8255,14 +8659,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" } @@ -8457,9 +8859,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.12.0", + "resolved": "https://registry.npmjs.org/naf-janus-adapter/-/naf-janus-adapter-0.12.0.tgz", + "integrity": "sha512-Zt2QC/7o2haXgaqerby+DCaDYth8CDtsbMBZettsQg4wufDKbK41xhej/KM9Wwgkfopu6YiK6CK4G1kcOgL/RA==", "requires": { "debug": "^3.1.0", "minijanus": "0.6.2", @@ -8479,8 +8881,7 @@ "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", @@ -8547,8 +8948,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", @@ -8915,6 +9315,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", @@ -8989,7 +9394,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" } @@ -9023,6 +9427,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", @@ -9037,6 +9462,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", @@ -9110,8 +9540,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", @@ -9250,9 +9679,9 @@ "integrity": "sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY=" }, "parse-bmfont-xml": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.3.tgz", - "integrity": "sha1-1rZqNxr9OcUAfZ8O6yYqTyzOe3w=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz", + "integrity": "sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==", "requires": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.4.5" @@ -9306,17 +9735,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", @@ -9822,8 +10274,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", @@ -9878,8 +10329,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", @@ -9929,8 +10379,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", @@ -10032,7 +10481,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", @@ -10043,14 +10491,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", @@ -10061,14 +10507,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=" } } }, @@ -10094,6 +10538,22 @@ "prop-types": "^15.6.0" } }, + "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", @@ -10105,6 +10565,16 @@ "invariant": "^2.1.1" } }, + "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" + } + }, "read-chunk": { "version": "2.1.0", "resolved": "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz", @@ -10171,7 +10641,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", @@ -10185,14 +10654,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" } @@ -10203,7 +10670,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", @@ -10563,8 +11029,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", @@ -10643,7 +11108,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" } @@ -10696,8 +11160,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", @@ -10874,6 +11337,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", @@ -10966,8 +11434,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", @@ -10989,8 +11456,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", @@ -11142,6 +11608,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", @@ -11373,8 +11961,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", @@ -11931,8 +12518,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", @@ -12087,15 +12674,24 @@ "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==" + }, "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", @@ -12269,7 +12865,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" @@ -12351,6 +12946,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", @@ -12490,8 +13090,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", @@ -12651,6 +13250,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", @@ -12684,8 +13292,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", @@ -12802,6 +13409,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", @@ -13294,6 +13906,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", @@ -13350,6 +13976,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", @@ -13479,6 +14110,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", diff --git a/package.json b/package.json index 2fcd8dd780347a943a5c939048eadeff76c90258..b0f7710f6b8bdcf1c4606b5ca7bc5e44bf815e90 100644 --- a/package.json +++ b/package.json @@ -19,17 +19,19 @@ "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", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.0", - "aframe": "github:aframevr/aframe#1be48d9204f0919d6362ef4c4dfa955e4ef64439", + "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": "^0.1.3", + "aframe-physics-extras": "github:mozillareality/aframe-physics-extras#bugfix/physics-collider-world", "aframe-physics-system": "github:mozillareality/aframe-physics-system#ecc5c9c533d6d9c71f8d6453ab961ed074d44b1c", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", @@ -43,7 +45,7 @@ "jsonschema": "^1.2.2", "jszip": "^3.1.5", "moving-average": "^1.0.0", - "naf-janus-adapter": "^0.11.0", + "naf-janus-adapter": "^0.12.0", "networked-aframe": "github:mozillareality/networked-aframe#master", "nipplejs": "github:mozillareality/nipplejs#mr-social-client/master", "phoenix": "^1.3.0", @@ -52,7 +54,7 @@ "react-dom": "^16.1.1", "react-intl": "^2.4.0", "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-to-cannon": "1.3.0", 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..687f27f8598764a9384dc7023e523bf1e9be6288 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`; @@ -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..1380235bd31dc889dece549791c550791ec0f178 100755 --- a/scripts/hab-build-and-push.sh +++ b/scripts/hab-build-and-push.sh @@ -30,3 +30,4 @@ 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/hud/spawn_pen-hover.png b/src/assets/hud/spawn_pen-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..eb298cd91498432098056cd69213919574364dee 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..62cfbb279046e04e070e55a14c62399ec22b7be7 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 100755 index 0000000000000000000000000000000000000000..178e75cf5d2ca06e39885eccd93475f96f3175f5 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 100755 index 0000000000000000000000000000000000000000..00473bb8faa20e958effb45ee17b651d730510dc Binary files /dev/null and b/src/assets/hud/spawn_photo.png differ diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss index edc8b391990a9c81dd67ca2579ffc2991a169ac3..f8b56e42545b757b425135a2e7fb1a21abf29a62 100644 --- a/src/assets/stylesheets/2d-hud.scss +++ b/src/assets/stylesheets/2d-hud.scss @@ -1,23 +1,38 @@ @import 'shared'; +:local(.unselectable) { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + :local(.container) { position: absolute; display: flex; justify-content: center; align-items: center; - height: 80px; width: 100%; - user-select: none; &:local(.top) { top: 10px; + height: 80px; } - &:local(.bottom) { + &:local(.column) { + flex-direction: column; bottom: 20px; } } +:local(.bottom) { + margin-bottom: 20px; +} + +:local(.hide) { + display: none; +} + :local(.panel) { display: flex; justify-content: space-around; @@ -42,6 +57,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 +113,13 @@ 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.freeze) { background-image: url(../hud/freeze_off.png); } @@ -109,3 +139,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/translations.data.json b/src/assets/translations.data.json index 857c1840a2d5de3ac814f3f19835af1bb0aed8c3..0748867c4cafa14758b02010af6be0e61390c502 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -1,5 +1,9 @@ { "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.screen-prefix": "Enter on ", "entry.desktop-screen": "Screen", "entry.mobile-screen": "Phone", 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 e898bf82af0b429a7531a2083b2d6def5e9f306d..625fc4c3536a1660b4a908d4d8d6e47e36377f44 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -2,7 +2,7 @@ import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js"; 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"; @@ -279,6 +279,7 @@ AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, contentType: { type: "string" }, + useCache: { default: true }, inflate: { default: false } }, @@ -300,6 +301,17 @@ 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. @@ -319,11 +331,7 @@ AFRAME.registerComponent("gltf-model-plus", { return; } - if (!GLTFCache[src]) { - GLTFCache[src] = loadGLTF(src, contentType, this.preferredTechnique); - } - - const model = cloneGltf(await GLTFCache[src]); + const model = 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 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/in-world-hud.js b/src/components/in-world-hud.js index a4fe8c9655ecffe3161a86b09fa3b6f6cdbdb19b..1bee398de9521f9a12e7fd0eb12b2af035a89041 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -11,23 +11,23 @@ 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.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.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 +39,8 @@ 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"); }; }, @@ -50,7 +50,7 @@ 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); }, pause() { @@ -59,6 +59,6 @@ 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); } }); 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/media-loader.js b/src/components/media-loader.js index 802d582c5a1b1dfac3961420dcf8bfde6d0619b6..da20ee178906bd68a9aa7a23496e208e5676e37f 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -20,6 +20,8 @@ AFRAME.registerComponent("media-loader", { 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 +29,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 +46,7 @@ AFRAME.registerComponent("media-loader", { shape: "box", halfExtents: halfExtents }); + this.shapeAdded = true; } }, @@ -72,6 +76,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; }, @@ -146,6 +151,7 @@ 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); }, { once: true } 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/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..425b1c834da9b7a75b54cad155224c33ec8edf7a 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,12 @@ 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: "" }, + + /** + * 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 +32,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 +52,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 +75,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 +108,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).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 +150,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).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..3a5ed3c9c6aeb7ebda7a3b8f4cee610af2a78a57 100644 --- a/src/components/virtual-gamepad-controls.css +++ b/src/components/virtual-gamepad-controls.css @@ -6,11 +6,11 @@ :local(.touchZone.left) { left: 0; - right: 50%; + right: 55%; } :local(.touchZone.right) { - left: 50%; + left: 55%; right: 0; } diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 71b9cbff5b20df5eb8935664f1ae45f5b28bb478..5dde948e65abc8b1ce5ea461a4ac38994047b88c 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -1,5 +1,6 @@ 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"); diff --git a/src/hub.html b/src/hub.html index 9b9161d2a27f6505f9d105b0bd3a1d9dde1831a5..787984a1889fecaf5f2aaa53ad3efe53c539e8c4 100644 --- a/src/hub.html +++ b/src/hub.html @@ -56,6 +56,8 @@ <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"> <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> @@ -69,7 +71,6 @@ <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> @@ -156,6 +157,8 @@ 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> @@ -165,6 +168,46 @@ </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:Delete; 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 +242,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 +262,16 @@ <!-- 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="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,17 +279,21 @@ 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 --> @@ -260,7 +318,7 @@ <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-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: Spawn Pen; activeTooltipText: Spawn 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-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> @@ -272,7 +330,7 @@ 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 +387,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 @@ -376,6 +435,14 @@ static-body="shape: none;" ></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> <div id="ui-root"></div> diff --git a/src/hub.js b/src/hub.js index 8db01eaddef8c92fa5dd4a8da040fa2473615b56..6bc7382c84b8ec3cf3af71914a592ca853f76d1b 100644 --- a/src/hub.js +++ b/src/hub.js @@ -7,6 +7,7 @@ import "./utils/logging"; import { patchWebGLRenderingContext } from "./utils/webgl"; patchWebGLRenderingContext(); +import screenfull from "screenfull"; import "three/examples/js/loaders/GLTFLoader"; import "networked-aframe/src/index"; import "naf-janus-adapter"; @@ -126,12 +127,18 @@ 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"; @@ -139,6 +146,7 @@ import registerTelemetry from "./telemetry"; import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; + import qsTruthy from "./utils/qs_truthy"; const isBotMode = qsTruthy("bot"); @@ -189,6 +197,10 @@ function mountUI(scene, props = {}) { ); } +function requestFullscreen() { + if (screenfull.enabled && !screenfull.isFullscreen) screenfull.request(); +} + const onReady = async () => { const scene = document.querySelector("a-scene"); const hubChannel = new HubChannel(store); @@ -233,10 +245,17 @@ const onReady = async () => { } document.body.removeChild(scene); } + document.body.removeEventListener("touchend", requestFullscreen); }; const enterScene = async (mediaStream, enterInVR, hubId) => { const scene = document.querySelector("a-scene"); + + // Get aframe inspector url using the webpack file-loader. + const aframeInspectorUrl = require("file-loader?name=assets/js/[name]-[hash].[ext]!aframe-inspector/dist/aframe-inspector.min.js"); + // Set the aframe-inspector url to our hosted copy. + scene.setAttribute("inspector", { url: aframeInspectorUrl }); + if (!isBotMode) { scene.classList.add("no-cursor"); } @@ -246,6 +265,8 @@ const onReady = async () => { if (enterInVR) { scene.enterVR(); + } else if (AFRAME.utils.device.isMobile()) { + document.body.addEventListener("touchend", requestFullscreen); } AFRAME.registerInputActions(inGameActions, "default"); @@ -305,12 +326,16 @@ const onReady = async () => { }); const offset = { x: 0, y: 0, z: -1.5 }; + const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { - const entity = addMedia(src, contentOrigin, true); + const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true); - entity.setAttribute("offset-relative-to", { - target: "#player-camera", - offset + orientation.then(or => { + entity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset, + orientation: or + }); }); }; @@ -536,7 +561,7 @@ const onReady = async () => { 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.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true }); gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded")); initialEnvironmentEl.appendChild(gltfEl); } else { diff --git a/src/index.js b/src/index.js index 7fddba12131a6d46fea444a42b2001acfe7afeb7..fa64e8923edfb8f103f8ac0d98342e759ad59304 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/network-schemas.js b/src/network-schemas.js index 4c8ac07e05625edffe916a549d96ea0a95bc297f..e639aa899a1e931df1b97b25761a4c7080d55a83 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -72,10 +72,12 @@ function registerNetworkSchemas() { template: "#video-template", components: [ { - component: "position" + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { - component: "rotation" + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, "visible" ] @@ -104,6 +106,48 @@ function registerNetworkSchemas() { } ] }); + + 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: "#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" + } + ] + }); } export default registerNetworkSchemas; diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 15168e999bd4576f80b0f4242ed60675f4557699..052817a81d6aac00a1b2881df7eb73a5268fc79c 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -4,8 +4,8 @@ import cx from "classnames"; import styles from "../assets/stylesheets/2d-hud.scss"; -const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onToggleSpaceBubble }) => ( - <div className={cx(styles.container, styles.top)}> +const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) => ( + <div className={cx(styles.container, styles.top, styles.unselectable)}> <div className={cx("ui-interactive", styles.panel, styles.left)}> <div className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} @@ -19,11 +19,7 @@ const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onTo 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(styles.iconButton, styles.spawn_pen)} title={"Drawing Pen"} onClick={onSpawnPen} /> </div> </div> ); @@ -31,24 +27,48 @@ 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 }; -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("ui-interactive", 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("ui-interactive", 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/create-object-dialog.js b/src/react-components/create-object-dialog.js index caf31f378b62aaeb5103721a36f182a42a5c8d07..2636230326838dc4bcabbb73e8d38a8674b2f4da 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..dc0ba425a3f4e9cd12db05c503280fc1c19ce3d7 --- /dev/null +++ b/src/react-components/help-dialog.js @@ -0,0 +1,48 @@ +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>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> + </DialogContainer> + ); + } +} diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 80df12ea336bfe0c35fae69ef05e38b95f88fde6..450a4bcf2defd7285080bc9583f855f5ac14fdd7 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -8,34 +8,75 @@ import homeVideoWebM from "../assets/video/home.webm"; import homeVideoMp4 from "../assets/video/home.mp4"; 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 SlackDialog from "./slack-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; @@ -54,12 +95,55 @@ class HomeRoot extends Component { } }; - showDialog = dialogType => { - return e => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ dialogType }); - }; + closeDialog() { + this.setState({ dialog: null }); + } + + showSlackDialog() { + this.setState({ dialog: <SlackDialog 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]; + // Transform the scene info into a an environment bundle structure. + this.setState({ + environments: [ + { + // Environment loading doesn't check the content-type, so we force a .glb extension here. + bundle_url: `${scene.model_url}.glb`, + meta: { + title: scene.name, + images: [{ type: "preview-thumbnail", srcset: scene.screenshot_url }] + } + } + ] + }); }; loadEnvironments = () => { @@ -77,12 +161,19 @@ 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}> @@ -141,7 +232,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.slack)} + onClick={this.onDialogLinkClicked(this.showSlackDialog.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> @@ -191,13 +282,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..a39a6d9b492e56d7179c996d5b940eb910f57e25 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,13 +6,12 @@ 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 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 = { @@ -178,7 +177,7 @@ class HubCreatePanel extends Component { {environmentTitle} </a> ) : ( - <span className={styles.itle}>environmentTitle</span> + <span className={styles.title}>{environmentTitle}</span> )} {environmentAuthor && environmentAuthor.name && @@ -238,9 +237,8 @@ class HubCreatePanel extends Component { </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..e01fc1282cf835e8e5a65363b3dfc6ee0f4e891f --- /dev/null +++ b/src/react-components/invite-dialog.js @@ -0,0 +1,56 @@ +import React, { Component } from "react"; +import copy from "copy-to-clipboard"; +import DialogContainer from "./dialog-container.js"; + +export default class InviteDialog extends Component { + state = { + copyLinkButtonText: "copy" + }; + + constructor(props) { + super(props); + const loc = document.location; + this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; + } + + copyLinkClicked = link => { + copy(link); + this.setState({ copyLinkButtonText: "copied!" }); + }; + + shareLinkClicked = () => { + navigator.share({ + title: document.title, + url: this.shareLink + }); + }; + + render() { + return ( + <DialogContainer title="Invite Others" {...this.props}> + <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> + </DialogContainer> + ); + } +} diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js index 80ab360d50d54da5738a9c3c771b090b0d4bc9fa..456090494e4cc9e00799d697530adbe460ae4bd2 100644 --- a/src/react-components/link-dialog.js +++ b/src/react-components/link-dialog.js @@ -2,53 +2,57 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import { FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; import styles from "../assets/stylesheets/link-dialog.scss"; -class LinkDialog extends Component { +export default class LinkDialog extends Component { static propTypes = { linkCode: PropTypes.string }; render() { - if (!this.props.linkCode) { + const { linkCode, ...other } = this.props; + if (!linkCode) { return ( - <div> - <div className={classNames("loading-panel", styles.codeLoadingPanel)}> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> + <DialogContainer title="Open on Headset" {...other}> + <div> + <div className={classNames("loading-panel", styles.codeLoadingPanel)}> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> </div> </div> </div> - </div> + </DialogContainer> ); } return ( - <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> + <DialogContainer title="Open on Headset" {...other}> <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" /> + <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> + <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> </div> - </div> + </DialogContainer> ); } } - -export default LinkDialog; 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/slack-dialog.js b/src/react-components/slack-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..5a024855ba2a206ebcc4a8a80e92e1460daa4b7a --- /dev/null +++ b/src/react-components/slack-dialog.js @@ -0,0 +1,33 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class SlackDialog extends Component { + render() { + return ( + <DialogContainer title="Get in Touch" {...this.props}> + <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> + </DialogContainer> + ); + } +} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 36b5434714a0bca8af2fc7dc4f2f38d094952f3e..43ec3dba9dab07a3b127b8275fce5355bc4808e6 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -20,7 +20,12 @@ import { } 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 InviteDialog from "./invite-dialog.js"; +import LinkDialog from "./link-dialog.js"; +import CreateObjectDialog from "./create-object-dialog.js"; import TwoDHUD from "./2d-hud"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; @@ -79,7 +84,7 @@ class UIRoot extends Component { state = { entryStep: ENTRY_STEPS.start, enterInVR: false, - infoDialogType: null, + dialog: null, linkCode: null, linkCodeCancel: null, @@ -160,6 +165,10 @@ class UIRoot extends Component { this.props.scene.emit("action_space_bubble"); }; + spawnPen = () => { + this.props.scene.emit("spawn_pen"); + }; + handleForcedVREntryType = () => { if (!this.props.forcedVREntryType) return; @@ -264,15 +273,11 @@ 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(); } }; @@ -512,24 +517,49 @@ class UIRoot extends Component { }; attemptLink = async () => { - this.setState({ infoDialogType: InfoDialog.dialogTypes.link }); + this.showLinkDialog(); const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); this.setState({ linkCode: code, linkCodeCancel: cancel }); - onFinished.then(this.handleCloseDialog); + this.showLinkDialog(); + onFinished.then(this.closeDialog); }; - handleCloseDialog = async () => { + closeDialog = async () => { if (this.state.linkCodeCancel) { this.state.linkCodeCancel(); } - this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null }); + this.setState({ dialog: null, linkCode: null, linkCodeCancel: null }); }; - handleCreateObject = url => { - this.props.scene.emit("add_media", url); + createObject = media => { + this.props.scene.emit("add_media", media); }; + showHelpDialog() { + this.setState({ dialog: <HelpDialog onClose={this.closeDialog} /> }); + } + + showSafariDialog() { + this.setState({ dialog: <SafariDialog onClose={this.closeDialog} /> }); + } + + showInviteDialog() { + this.setState({ dialog: <InviteDialog onClose={this.closeDialog} /> }); + } + + showCreateObjectDialog() { + this.setState({ dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} /> }); + } + + showLinkDialog() { + this.setState({ dialog: <LinkDialog linkCode={this.state.linkCode} onClose={this.closeDialog} /> }); + } + + showWebVRRecommendDialog() { + this.setState({ dialog: <WebVRRecommendDialog onClose={this.closeDialog} /> }); + } + render() { if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { let subtitle = null; @@ -647,7 +677,7 @@ class UIRoot extends Component { <TwoDEntryButton onClick={this.enter2D} /> )} {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( - <SafariEntryButton onClick={this.linkSafari} /> + <SafariEntryButton onClick={this.showSafariDialog} /> )} {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( <GenericEntryButton onClick={this.enterVR} /> @@ -669,10 +699,7 @@ class UIRoot extends Component { </div> )} {screenSharingCheckbox} - <button - className={entryStyles.inviteButton} - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })} - > + <button className={entryStyles.inviteButton} onClick={() => this.showInviteDialog()}> <FormattedMessage id="entry.invite-others" /> </button> </div> @@ -828,7 +855,7 @@ class UIRoot extends Component { <ProfileInfoHeader name={this.props.store.state.profile.displayName} onClickName={() => this.setState({ showProfileEntry: true })} - onClickHelp={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} + onClickHelp={() => this.showHelpDialog()} /> {entryPanel} {micPanel} @@ -836,7 +863,7 @@ class UIRoot extends Component { </div> ); - const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.infoDialogType, "ui-dialog-box": true }); + const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.dialog, "ui-dialog-box": true }); const dialogBoxContentsClassNames = classNames({ "ui-dialog-box-contents": true, @@ -846,19 +873,10 @@ class UIRoot extends Component { 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.dialog} {this.state.entryStep === ENTRY_STEPS.finished && ( - <button - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} - className="ui__help-icon" - > + <button onClick={() => this.showHelpDialog()} className="ui__help-icon"> <i className="ui__help-icon__icon"> <FontAwesomeIcon icon={faQuestion} /> </i> @@ -892,11 +910,12 @@ class UIRoot extends Component { onToggleMute={this.toggleMute} onToggleFreeze={this.toggleFreeze} onToggleSpaceBubble={this.toggleSpaceBubble} + onSpawnPen={this.spawnPen} /> {!this.props.availableVREntryTypes.isInHMD && this.props.occupantCount <= 1 && ( <div className={styles.nagButton}> - <button onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })}> + <button onClick={() => this.showInviteDialog()}> <FormattedMessage id="entry.invite-others-nag" /> </button> </div> @@ -909,7 +928,9 @@ class UIRoot extends Component { </div> )} <TwoDHUD.BottomHUD - onCreateObject={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.create_object })} + onCreateObject={() => this.showCreateObjectDialog()} + showPhotoPicker={AFRAME.utils.device.isMobile()} + onMediaPicked={this.createObject} /> </div> ) : null} 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..cd7d9434498a16ac219686d8fb5c264a962abd9e --- /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> + <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/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/media-utils.js b/src/utils/media-utils.js index 83a5f788ea841c1f3343c2068293b97bf9f53f3e..829d00de9709d3d2ce5214c884edf63b1dac6a35 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -51,16 +51,64 @@ 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, 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("networked", { template: template }); entity.setAttribute("media-loader", { resize, 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 => { @@ -78,5 +126,5 @@ export const addMedia = (src, contentOrigin, resize = false) => { 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/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/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/vr-caps-detect.js b/src/utils/vr-caps-detect.js index 3294820125788e0343eb5f70277eefc48b75a2c2..d5ad87eff808d1cb528c76b068de0c855d0bb3a0 100644 --- a/src/utils/vr-caps-detect.js +++ b/src/utils/vr-caps-detect.js @@ -63,8 +63,7 @@ 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 isInHMD = isOculusBrowser; const screen = isInHMD ? VR_DEVICE_AVAILABILITY.no diff --git a/webpack.config.js b/webpack.config.js index 3e481d53ccfc6cbdc15365ef51d6c11978a5f6ee..9531ceb26e70881f4bd4b5af7eb4367e2141f007 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -157,11 +157,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 +180,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",