diff --git a/README.md b/README.md index 5f0bf883f79df7b447d8c9fbd3b4102072a8efbc..a3e7026c4e4d15a72fbdef4629ddc756f579f112 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ yarn build - `mobile` - Force mobile mode - `no_stats` - Disable performance stats - `vr_entry_type` - Either "gearvr" or "daydream". Used internally to force a VR entry type +- `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. ## Additional Resources diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000000000000000000000000000000000000..65c927b352e2d6517be04c98065013326846cfd2 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,581 @@ + +# Component Docs +- Systems + - [app-mode](#systems/app-mode) + - [exit-on-blur](#systems/exit-on-blur) + - [personal-space-bubble](#systems/personal-space-bubble) +- Components + - [app-mode](#components/app-mode) + - [app-mode-toggle-playing](#components/app-mode/app-mode-toggle-playing) + - [app-mode-toggle-attribute](#components/app-mode/app-mode-toggle-attribute) + - [app-mode-input-mappings](#components/app-mode/app-mode-input-mappings) + - [avatar](#components/avatar) + - [networked-audio-analyser](#components/avatar/networked-audio-analyser) + - [scale-audio-feedback](#components/avatar/scale-audio-feedback) + - [avatar-replay](#components/avatar/avatar-replay) + - [bone-mute-state-indicator](#components/avatar/bone-mute-state-indicator) + - [bone-visibility](#components/avatar/bone-visibility) + - [character-controller](#components/avatar/character-controller) + - [hand-pose](#components/avatar/hand-pose) + - [hand-pose-controller](#components/avatar/hand-pose-controller) + - [ik-root](#components/avatar/ik-root) + - [ik-controller](#components/avatar/ik-controller) + - [networked-avatar](#components/avatar/networked-avatar) + - [player-info](#components/avatar/player-info) + - [spawn-controller](#components/avatar/spawn-controller) + - [avatar/personal-space-bubble](#components/avatar/personal-space-bubble) + - [space-invader-mesh](#components/avatar/personal-space-bubble/space-invader-mesh) + - [personal-space-invader](#components/avatar/personal-space-bubble/personal-space-invader) + - [personal-space-bubble](#components/avatar/personal-space-bubble/personal-space-bubble) + - [environment](#components/environment) + - [hide-when-quality](#components/environment/hide-when-quality) + - [layers](#components/environment/layers) + - [nav-mesh-helper](#components/environment/nav-mesh-helper) + - [scene-shadow](#components/environment/scene-shadow) + - [spawn-point](#components/environment/spawn-point) + - [gltf](#components/gltf) + - [gltf-bundle](#components/gltf/gltf-bundle) + - [gltf-model-plus](#components/gltf/gltf-model-plus) + - [misc](#components/misc) + - [animation-mixer](#components/misc/animation-mixer) + - [matcolor-audio-feedback](#components/misc/matcolor-audio-feedback) + - [css-class](#components/misc/css-class) + - [duck](#components/misc/duck) + - [event-repeater](#components/misc/event-repeater) + - [loop-animation](#components/misc/loop-animation) + - [offset-relative-to](#components/misc/offset-relative-to) + - [network](#components/network) + - [block-button](#components/network/block-button) + - [freeze-controller](#components/network/freeze-controller) + - [mute-mic](#components/network/mute-mic) + - [networked-counter](#components/network/networked-counter) + - [networked-video-player](#components/network/networked-video-player) + - [super-networked-interactable](#components/network/super-networked-interactable) + - [super-spawner](#components/network/super-spawner) + - [ui](#components/ui) + - [hud-controller](#components/ui/hud-controller) + - [icon-button](#components/ui/icon-button) + - [in-world-hud](#components/ui/in-world-hud) + - [text-button](#components/ui/text-button) + - [visible-while-frozen](#components/ui/visible-while-frozen) + - [ui-class-while-frozen](#components/ui/ui-class-while-frozen) + - [user-input](#components/user-input) + - [cardboard-controls](#components/user-input/cardboard-controls) + - [controls-shape-offset](#components/user-input/controls-shape-offset) + - [cursor-controller](#components/user-input/cursor-controller) + - [hand-controls2](#components/user-input/hand-controls2) + - [haptic-feedback](#components/user-input/haptic-feedback) + - [virtual-gamepad-controls](#components/user-input/virtual-gamepad-controls) + - [wasd-to-analog2d](#components/user-input/wasd-to-analog2d) + - [vr-mode](#components/vr-mode) + - [vr-mode-toggle-visibility](#components/vr-mode/vr-mode-toggle-visibility) + - [vr-mode-toggle-playing](#components/vr-mode/vr-mode-toggle-playing) + +## Systems + +<a name="systems/app-mode"></a> +#### app-mode + +Simple system for keeping track of a modal app state + +`src/systems/app-mode.js` + + +<a name="systems/exit-on-blur"></a> +#### exit-on-blur + +Emits an "exit" event when a user has stopped using the app for a certain period of time + +`src/systems/exit-on-blur.js` + + +<a name="systems/personal-space-bubble"></a> +#### personal-space-bubble + +Iterates through bubbles and invaders on every tick and sets invader state accordingly. testing multiline things + +`src/systems/personal-space-bubble.js` + + +## Components + +<a name="components/misc"></a> +### misc + +<a name="components/misc/animation-mixer"></a> +#### animation-mixer + +Instantiates and updates a THREE.AnimationMixer on an entity. + +`src/components/animation-mixer.js` + + +<a name="components/misc/matcolor-audio-feedback"></a> +#### matcolor-audio-feedback + +Sets an entity's color base on audioFrequencyChange events. + +`src/components/audio-feedback.js` + + +<a name="components/misc/css-class"></a> +#### css-class + +Sets the CSS class on an entity. + +`src/components/css-class.js` + + +<a name="components/misc/duck"></a> +#### duck + +Floats a duck based on its scale. + +`src/components/duck.js` + + +<a name="components/misc/event-repeater"></a> +#### event-repeater + +Listens to events from an event source and re-emits them on this entity + +`src/components/event-repeater.js` + + +<a name="components/misc/loop-animation"></a> +#### loop-animation + +Loops the given clip using this entity's animation mixer + +`src/components/loop-animation.js` + + +<a name="components/misc/offset-relative-to"></a> +#### offset-relative-to + +Positions an entity relative to a given target when the given event is fired. + +`src/components/offset-relative-to.js` + + + +<a name="components/avatar"></a> +### avatar + +<a name="components/avatar/networked-audio-analyser"></a> +#### networked-audio-analyser + +Emits audioFrequencyChange events based on a networked audio source + +`src/components/audio-feedback.js` + + +<a name="components/avatar/scale-audio-feedback"></a> +#### scale-audio-feedback + +Sets an entity's scale base on audioFrequencyChange events. + +`src/components/audio-feedback.js` + + +<a name="components/avatar/avatar-replay"></a> +#### avatar-replay + +Replays a recorded motion capture with the given avatar body parts + +`src/components/avatar-replay.js` + + +<a name="components/avatar/bone-mute-state-indicator"></a> +#### bone-mute-state-indicator + +Toggles the position of 2 bones into "on" and "off" positions to indicate mute state. + +`src/components/bone-mute-state-indicator.js` + + +<a name="components/avatar/bone-visibility"></a> +#### bone-visibility + +Scales an object to near-zero if the object is invisible. Useful for bones representing avatar body parts. + +`src/components/bone-visibility.js` + + +<a name="components/avatar/character-controller"></a> +#### character-controller + +Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly. The controller accounts for playspace offset and orientation and depends on the nav mesh system for translation. + +`src/components/character-controller.js` + + +<a name="components/avatar/hand-pose"></a> +#### hand-pose + +Animates between poses based on networked pose state using an animation mixer. + +`src/components/hand-poses.js` + + +<a name="components/avatar/hand-pose-controller"></a> +#### hand-pose-controller + +Sets the networked hand pose state based on hand-pose events. + +`src/components/hand-poses.js` + + +<a name="components/avatar/ik-root"></a> +#### ik-root + +Provides access to the end effectors for IK. + +`src/components/ik-controller.js` + + +<a name="components/avatar/ik-controller"></a> +#### ik-controller + +Performs IK on a hip-rooted skeleton to align the hip, head and hands with camera and controller inputs. + +`src/components/ik-controller.js` + + +<a name="components/avatar/networked-avatar"></a> +#### networked-avatar + +Stores networked avatar state. + +`src/components/networked-avatar.js` + + +<a name="components/avatar/player-info"></a> +#### player-info + +Sets player info state, including avatar choice and display name. + +`src/components/player-info.js` + + +<a name="components/avatar/spawn-controller"></a> +#### spawn-controller + +Used on a player-rig to move the player to a random spawn point on entry. + +`src/components/spawn-controller.js` + + + +<a name="components/network"></a> +### network + +<a name="components/network/block-button"></a> +#### block-button + +Registers a click handler and invokes the block method on the NAF adapter for the owner associated with its entity. + +`src/components/block-button.js` + + +<a name="components/network/freeze-controller"></a> +#### freeze-controller + +Toggles freezing of network traffic on the given event. + +`src/components/freeze-controller.js` + + +<a name="components/network/mute-mic"></a> +#### mute-mic + +Toggles the microphone on the current network connection based on the given events. + +`src/components/mute-mic.js` + + +<a name="components/network/networked-counter"></a> +#### networked-counter + +Limits networked interactables to a maximum number at any given time + +`src/components/networked-counter.js` + + +<a name="components/network/networked-video-player"></a> +#### networked-video-player + +Instantiates and plays a network video stream, setting the video as the source material for this entity. + +`src/components/networked-video-player.js` + + +<a name="components/network/super-networked-interactable"></a> +#### super-networked-interactable + +Manages ownership and haptics on an interatable + +`src/components/super-networked-interactable.js` + + +<a name="components/network/super-spawner"></a> +#### super-spawner + +Spawns networked objects when grabbed. + +`src/components/super-spawner.js` + + + +<a name="components/user-input"></a> +### user-input + +<a name="components/user-input/cardboard-controls"></a> +#### cardboard-controls + +Polls the Gamepad API for Cardboard Button input and emits cardboardbutton events. + +`src/components/cardboard-controls.js` + + +<a name="components/user-input/controls-shape-offset"></a> +#### controls-shape-offset + +Sets the offset of the aframe-physics shape on this entity based on the current VR controller type + +`src/components/controls-shape-offset.js` + + +<a name="components/user-input/cursor-controller"></a> +#### cursor-controller + +Controls virtual cursor behavior in various modalities to affect teleportation, interatables and UI. + +`src/components/cursor-controller.js` + + +<a name="components/user-input/hand-controls2"></a> +#### hand-controls2 + +Converts events from various 6DoF and 3DoF controllers into hand-pose events. + +`src/components/hand-controls2.js` + + +<a name="components/user-input/haptic-feedback"></a> +#### haptic-feedback + +Listens for haptic events and actuates hardware controllers accordingly + +`src/components/haptic-feedback.js` + + +<a name="components/user-input/virtual-gamepad-controls"></a> +#### virtual-gamepad-controls + +Instantiates 2D virtual gamepads and emits associated events. + +`src/components/virtual-gamepad-controls.js` + + +<a name="components/user-input/wasd-to-analog2d"></a> +#### wasd-to-analog2d + +Converts WASD keyboard inputs to simulated analog inputs. + +`src/components/wasd-to-analog2d.js` + + + +<a name="components/gltf"></a> +### gltf + +<a name="components/gltf/gltf-bundle"></a> +#### gltf-bundle + +Instantiates GLTF models as specified in a bundle JSON. + +`src/components/gltf-bundle.js` + + +<a name="components/gltf/gltf-model-plus"></a> +#### gltf-model-plus + +Loads a GLTF model, optionally recursively "inflates" the child nodes of a model into a-entities and sets whitelisted components on them if defined in the node's extras. + +`src/components/gltf-model-plus.js` + + + +<a name="components/environment"></a> +### environment + +<a name="components/environment/hide-when-quality"></a> +#### hide-when-quality + +Hides entities based on the scene's quality mode + +`src/components/hide-when-quality.js` + + +<a name="components/environment/layers"></a> +#### layers + +Sets layer flags on the underlying Object3D + +`src/components/layers.js` + + +<a name="components/environment/nav-mesh-helper"></a> +#### nav-mesh-helper + +Initializes teleport-controls when the environment bundle has loaded. + +`src/components/nav-mesh-helper.js` + + +<a name="components/environment/scene-shadow"></a> +#### scene-shadow + +For use in environment gltf bundles to set scene shadow properties. + +`src/components/scene-shadow.js` + + +<a name="components/environment/spawn-point"></a> +#### spawn-point + +Marks an entity as a potential spawn point. + +`src/components/spawn-controller.js` + + + +<a name="components/ui"></a> +### ui + +<a name="components/ui/hud-controller"></a> +#### hud-controller + +Positions the HUD and toggles app mode based on where the user is looking + +`src/components/hud-controller.js` + + +<a name="components/ui/icon-button"></a> +#### icon-button + +A button with an image, tooltip, hover states and haptics. + +`src/components/icon-button.js` + + +<a name="components/ui/in-world-hud"></a> +#### in-world-hud + +HUD panel for muting, freezing, and space bubble controls. + +`src/components/in-world-hud.js` + + +<a name="components/ui/text-button"></a> +#### text-button + +A button with text and haptics + +`src/components/text-button.js` + + +<a name="components/ui/visible-while-frozen"></a> +#### visible-while-frozen + +Toggles the visibility of this entity when the scene is frozen. + +`src/components/visible-while-frozen.js` + + +<a name="components/ui/ui-class-while-frozen"></a> +#### ui-class-while-frozen + +Toggles the interactivity of a UI entity while the scene is frozen. + +`src/components/visible-while-frozen.js` + + + +<a name="components/app-mode"></a> +### app-mode + +<a name="components/app-mode/app-mode-toggle-playing"></a> +#### app-mode-toggle-playing + +Toggle the isPlaying state of a component based on app mode + +`src/systems/app-mode.js` + + +<a name="components/app-mode/app-mode-toggle-attribute"></a> +#### app-mode-toggle-attribute + +Toggle a boolean property of a component based on app mode + +`src/systems/app-mode.js` + + +<a name="components/app-mode/app-mode-input-mappings"></a> +#### app-mode-input-mappings + +Toggle aframe input mappings action set based on app mode + +`src/systems/app-mode.js` + + + +<a name="components/vr-mode"></a> +### vr-mode + +<a name="components/vr-mode/vr-mode-toggle-visibility"></a> +#### vr-mode-toggle-visibility + +Toggle visibility of an entity based on if the user is in vr mode or not + +`src/systems/app-mode.js` + + +<a name="components/vr-mode/vr-mode-toggle-playing"></a> +#### vr-mode-toggle-playing + +Toggle the isPlaying state of a component based on app mode + +`src/systems/app-mode.js` + + + +<a name="components/avatar/personal-space-bubble"></a> +### avatar/personal-space-bubble + +<a name="components/avatar/personal-space-bubble/space-invader-mesh"></a> +#### space-invader-mesh + +Specifies a mesh associated with an invader. + +`src/systems/personal-space-bubble.js` + + +<a name="components/avatar/personal-space-bubble/personal-space-invader"></a> +#### personal-space-invader + +Represents an entity that can invade a personal space bubble + +`src/systems/personal-space-bubble.js` + + +<a name="components/avatar/personal-space-bubble/personal-space-bubble"></a> +#### personal-space-bubble + +Represents a personal space bubble on an entity. + +`src/systems/personal-space-bubble.js` + + + \ No newline at end of file diff --git a/package.json b/package.json index bc629bbadb51a4d066346ab64eaf07ba30651a88..4bdb9af46d4d0ec2b3e0392a482d66706b83547d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "postinstall": "node ./scripts/postinstall.js", "start": "cross-env NODE_ENV=development webpack-dev-server", "build": "rimraf ./public && cross-env NODE_ENV=production webpack --mode=production", + "doc": "node ./scripts/doc/build.js", "prettier": "prettier --write '*.js' 'src/**/*.js'", "lint:js": "eslint '*.js' 'scripts/**/*.js' 'src/**/*.js'", "lint:html": "node ./scripts/lint-html.js 'src/**/*.html'", diff --git a/scripts/bot/run-bot.js b/scripts/bot/run-bot.js index 2e37ccc3d62a5d6bea59efa64a8b11d9984ee1e0..cadf6aaf2a1a2a444b290fc64cb2ad1cba3a895a 100644 --- a/scripts/bot/run-bot.js +++ b/scripts/bot/run-bot.js @@ -4,9 +4,10 @@ Usage: ./run-bot.js [options] Options: - -h --host=<host> Hubs host [default: localhost:8080] - -r --room=<room> Room id [default: 234234]. - -h --help Show this screen. + -u --url=<url> URL + -o --host=<host> Hubs host if URL is not specified [default: localhost:8080] + -r --room=<room> Room id + -h --help Show this screen `; const docopt = require("docopt").docopt; @@ -19,34 +20,55 @@ const querystring = require("query-string"); const browser = await puppeteer.launch({ ignoreHTTPSErrors: true }); const page = await browser.newPage(); page.on("console", msg => console.log("PAGE: ", msg.text())); - page.on("error", err => console.error("ERROR: ", err)); - page.on("pageerror", err => console.error("PAGE ERROR: ", err)); + page.on("error", err => console.error("ERROR: ", err.toString().split("\n")[0])); + page.on("pageerror", err => console.error("PAGE ERROR: ", err.toString().split("\n")[0])); + + const baseUrl = options["--url"] || `https://${options["--host"]}/hub.html`; const params = { - room: options["--room"], - bot: true + bot: true, + allow_multi: true }; - console.log(params); - const url = `https://${options["--host"]}/hub.html?${querystring.stringify(params)}`; + const roomOption = options["--room"]; + if (roomOption) { + params.room = roomOption; + } + + const url = `${baseUrl}?${querystring.stringify(params)}`; + console.log(url); const navigate = async () => { try { + console.log("Spawning bot..."); await page.goto(url); - await page.evaluate(() => { - console.log(navigator.userAgent); - }); - // Interact with the page so that audio can play. - await page.mouse.click(100, 100); - // Signal that the page has been interacted with. - // If the interacted function has not been defined yet, this will error and restart the process with the - // setTimeout below. - await page.evaluate(() => window.interacted()); + await page.evaluate(() => console.log(navigator.userAgent)); + let retryCount = 5; + let backoff = 1000; + const interact = async () => { + try { + // Interact with the page so that audio can play. + await page.mouse.click(100, 100); + // Signal that the page has been interacted with. + await page.evaluate(() => window.interacted()); + console.log("Interacted."); + } catch (e) { + console.log("Interaction error", e.message); + if (retryCount-- < 0) { + // If retries failed, throw and restart navigation. + throw new Error("Retries failed"); + } + console.log("Retrying..."); + backoff *= 2; + // Retry interaction to start audio playback + setTimeout(interact, backoff); + } + }; + await interact(); } catch (e) { - console.log("Navigation error", e.toString()); + console.log("Navigation error", e.message); setTimeout(navigate, 1000); } }; - console.log("Spawning bot..."); navigate(); })(); diff --git a/scripts/bot/run-bot.sh b/scripts/bot/run-bot.sh index 7bd9b986a2463a2864403b81599a0e161206d58e..aa693b25a11eca48bb83d4cc51115fdf6138f2eb 100755 --- a/scripts/bot/run-bot.sh +++ b/scripts/bot/run-bot.sh @@ -9,7 +9,6 @@ yarn echo 'Building Hubs' yarn build > /dev/null -# install run-bot.js dependencies cd $script_directory echo 'Installing bot dependencies' yarn diff --git a/scripts/build_local_reticulum.sh b/scripts/build_local_reticulum.sh index f883958f8f18e028a32916f50c6af38002938893..3f5e3a00136a84adf99b68926ee0dd1b17a09af3 100755 --- a/scripts/build_local_reticulum.sh +++ b/scripts/build_local_reticulum.sh @@ -4,4 +4,4 @@ if [ ! -e ../reticulum ]; then echo "This script assumes reticulum is checked out in a sibling to this folder." fi -rm -rf ../reticulum/priv/static ; GENERATE_SMOKE_TESTS=true BASE_ASSETS_PATH=http://localhost:4000/ yarn build -- --output-path ../reticulum/priv/static +rm -rf ../reticulum/priv/static ; GENERATE_SMOKE_TESTS=true BASE_ASSETS_PATH=https://localhost:4000/ yarn build -- --output-path ../reticulum/priv/static diff --git a/scripts/doc/build.js b/scripts/doc/build.js new file mode 100644 index 0000000000000000000000000000000000000000..2e910117fcdc94239dab4177b94307a746daec42 --- /dev/null +++ b/scripts/doc/build.js @@ -0,0 +1,60 @@ +const fs = require("fs"); +const { promisify } = require("util"); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); +const shell = require("shelljs"); +const flatten = require("lodash/flatten"); +const indexTemplate = require("./index.js"); + +async function extractDocs(file) { + const contents = (await readFile(file)).toString(); + // Find all the doc strings in the file. + const matches = contents.match(/\/\*\*.+?\*\//gs); + if (matches) { + return matches.map(match => { + return { doc: match, file }; + }); + } else { + return null; + } +} + +function parseDocs(doc) { + const _doc = doc; + // Capture the description and tags in the doc string + const matches = _doc.doc.match(/\/\*\*([^@]+)(.+)\*\//s); + // Trim whitespace and asterisks from a line + const trimLine = line => line.trim().replace(/^\*\s*|\s*\*$/g, ""); + + _doc.doc = { + desc: matches[1] + .split("\n") + .map(trimLine) + .filter(x => x) + .join(" "), + tags: matches[2] + .split(/[\r\n]/) + .map(trimLine) + .filter(x => x.startsWith("@")) + .reduce((a, x) => { + const tag = x.split(" "); + a[tag[0].substring(1)] = tag.slice(1).join(); + return a; + }, {}) + }; + return _doc; +} + +function aframeDocs(doc) { + const keys = Object.keys(doc.doc.tags); + return keys.includes("component") || keys.includes("system"); +} + +(async function() { + const files = shell.ls("src/components/*.js", "src/systems/*.js"); + const parsedDocs = flatten(await Promise.all(files.map(extractDocs))) + .filter(x => x) + .map(parseDocs) + .filter(aframeDocs); + writeFile("doc/index.md", indexTemplate(parsedDocs)); +})(); diff --git a/scripts/doc/index.js b/scripts/doc/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bb6f0d1592cda812f50d3da16aa41ef1bfad2578 --- /dev/null +++ b/scripts/doc/index.js @@ -0,0 +1,69 @@ +module.exports = function(docs) { + const systems = docs.filter(doc => Object.keys(doc.doc.tags).includes("system")); + const components = docs.filter(doc => Object.keys(doc.doc.tags).includes("component")).reduce((acc, doc) => { + const namespace = doc.doc.tags.namespace || "misc"; + if (!acc[namespace]) { + acc[namespace] = []; + } + acc[namespace].push(doc); + return acc; + }, {}); + return ` +# Component Docs +- Systems +${systems + .map(system => { + return ` - [${system.doc.tags.system}](#systems/${system.doc.tags.system})`; + }) + .join("\n")} +- Components +${Object.entries(components) + .sort((a, b) => a[0] > b[0]) + .map(([namespace, components]) => { + return ` - [${namespace}](#components/${namespace}) +${components + .map( + component => + ` - [${component.doc.tags.component}](#components/${namespace}/${component.doc.tags.component})` + ) + .join("\n")}`; + }) + .join("\n")} + +## Systems +${systems + .map(system => { + return ` +<a name="systems/${system.doc.tags.system}"></a> +#### ${system.doc.tags.system} + +${system.doc.desc} + +\`${system.file}\` + `; + }) + .join("\n")} + +## Components +${Object.entries(components) + .map(([namespace, components]) => { + return ` +<a name="components/${namespace}"></a> +### ${namespace} + ${components + .map( + component => ` +<a name="components/${namespace}/${component.doc.tags.component}"></a> +#### ${component.doc.tags.component} + +${component.doc.desc} + +\`${component.file}\` + ` + ) + .join("\n")} + `; + }) + .join("\n")} + `; +}; diff --git a/src/assets/environments/environments.js b/src/assets/environments/environments.js index 7c20ce61da763de912493c4e8d35385d09347b5a..9bf05b6d13b8cdf97ae6cfff3d7d77def96a2536 100644 --- a/src/assets/environments/environments.js +++ b/src/assets/environments/environments.js @@ -1,7 +1,8 @@ export const ENVIRONMENT_URLS = [ process.env.ASSET_BUNDLE_SERVER + "/rooms/meetingroom/MeetingRoom.bundle.json", process.env.ASSET_BUNDLE_SERVER + "/rooms/atrium/Atrium.bundle.json", - process.env.ASSET_BUNDLE_SERVER + "/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json" + process.env.ASSET_BUNDLE_SERVER + "/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json", + process.env.ASSET_BUNDLE_SERVER + "/rooms/rooftopbuilding1/RooftopBuilding1.bundle.json" ]; export const DEFAULT_ENVIRONMENT_URL = ENVIRONMENT_URLS[0]; diff --git a/src/assets/images/device_entry.svg b/src/assets/images/device_entry.svg new file mode 100755 index 0000000000000000000000000000000000000000..f406f0c8058b65ca14a04aee952f1cccc071ae6d --- /dev/null +++ b/src/assets/images/device_entry.svg @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="94" height="94"> + <g fill="none"> + <path fill="#D8D8D8" fill-opacity=".01" fill-rule="evenodd" d="M47 92c24.853 0 45-20.147 45-45S71.853 2 47 2 2 22.147 2 47s20.147 45 45 45z" clip-rule="evenodd"/> + <path stroke="#fff" stroke-width="3" d="M47 92c24.853 0 45-20.147 45-45S71.853 2 47 2 2 22.147 2 47s20.147 45 45 45z"/> + <path fill="#C4C4C4" d="M59.5 39c1.792.29 5.44 1.047 6.43 1.549C65.525 28.767 59.048 26.819 55.5 29c4.222 1.121 4.102 7.456 4 10z"/> + <path fill="#C4C4C4" d="M59.5 39c1.792.29 5.44 1.047 6.43 1.549C65.525 28.767 59.048 26.819 55.5 29c4.222 1.121 4.102 7.456 4 10z"/> + <path fill="#fff" fill-rule="evenodd" stroke="#fff" d="M49.52 43.729a3 3 0 0 0 3.16-1.846 49.424 49.424 0 0 1 1.577-3.51c1.206.038 2.299.077 3.243.127 5.107.27 8.46 1.781 10.542 3.82-1.43.633-3.15 1.481-4.801 2.328a238.752 238.752 0 0 0-6.04 3.212l-.41.225-.147.081a1.5 1.5 0 0 0-.76 1.499l.01 8.465c.06.474.34.89.757 1.123.422.24.918.178 1.4.034l.13-.06a120.01 120.01 0 0 0 1.345-.628c.269-.127.567-.268.889-.422l.242-.116c1.51-.72 4.234-2.02 6.072-2.974a95.31 95.31 0 0 0 3.542-1.926c-.385 1.003-.908 1.845-1.501 2.425L51.5 65c-1.417-1.043-.933-1.604-.068-2.603 1.538-1.778 4.278-4.946-.35-14.69-5.62-4.425-18.234-4.507-23.614-4.543-1.739-.01-2.722-.017-2.468-.164 1.715-.99 12.942-4.158 16.658-5.076a149.626 149.626 0 0 1-.37.794 3 3 0 0 0 2.332 4.258l5.9.753z" clip-rule="evenodd"/> + <path fill="#fff" d="M44 40l5.9.753c7.82-19.229 15.691-11.946 16.2.548-.05-8.196-2.693-14.35-10.397-14.3C49.539 27.04 46.127 35.5 44 40z"/> + <mask id="a" fill="#fff"> + <path d="M17.038 1.115C4.277 2.707.865 1.717 0 14.395c0 13.783 7.5 9.45 19.16 7.594 8.824-1.89 13.932-1.037 13.932-13.732C32.243-2.03 29.087-.39 17.038 1.115z"/> + </mask> + <g mask="url(#a)" transform="matrix(-1 0 0 1 55.092 41.876)"> + <path fill="#fff" d="M0 14.395l-3.99-.272-.01.136v.136h4zm17.038-13.28l-.495-3.97.495 3.97zm16.054 7.142h4v-.165l-.014-.164-3.986.329zM19.159 21.989l.63 3.95.104-.016.104-.023-.838-3.911zM3.991 14.668c.436-6.386 1.492-7.26 2.128-7.642.579-.348 1.58-.681 3.606-.994 2.071-.32 4.408-.524 7.808-.948l-.99-7.939c-2.98.372-5.856.643-8.04.98-2.23.345-4.529.856-6.506 2.044C-2.479 2.86-3.56 7.83-3.99 14.123l7.982.545zm13.542-9.584c3.216-.401 5.411-.754 7.384-.955 1.975-.202 2.849-.133 3.247-.02.035.01.005-.093.144.198.276.582.593 1.798.797 4.279l7.973-.658c-.22-2.663-.615-5.099-1.546-7.056-1.068-2.247-2.8-3.776-5.166-4.453-2.003-.574-4.23-.456-6.262-.249-2.034.208-4.752.625-7.561.975l.99 7.939zm11.559 3.173c0 6.022-1.247 7.164-1.898 7.605-.552.373-1.418.71-3.007 1.065-.775.174-1.624.332-2.624.517-.978.181-2.076.384-3.241.634l1.675 7.822c1.04-.223 2.033-.406 3.023-.59.969-.18 1.967-.364 2.911-.575 1.852-.413 3.917-1.01 5.745-2.247 4.109-2.78 5.416-7.559 5.416-14.231h-8zM18.53 18.039c-2.982.475-5.911 1.147-8.04 1.557-2.36.456-3.825.61-4.797.49-.676-.082-.754-.224-.892-.459C4.465 19.057 4 17.62 4 14.395h-8c0 3.667.473 6.856 1.908 9.293 1.632 2.77 4.158 4.014 6.814 4.34 2.362.288 4.958-.128 7.283-.576 2.556-.494 4.937-1.06 7.783-1.513l-1.258-7.9z"/> + </g> + <path fill="#fff" d="M58.692 56v-5.897C62.741 48.19 67.26 45.81 70 44.5c1.86-.889 3.371-.179 3.74 1.69.26 1.31-.586 2.684-1.74 3.31-1.154.626-13.308 6.5-13.308 6.5z"/> + </g> +</svg> diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 014ca0a7a7f7f32e20db86d9a4725da7beb5d011..6b13dabd4f325a0884a47d221e7297430df7d4f6 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -2,6 +2,8 @@ @import 'hub-create'; @import 'info-dialog'; +$header-section-width: 350px; + * { box-sizing: border-box; } @@ -53,20 +55,24 @@ body { .header-content { padding: 1.5em 2.5em 1.5em 2.5em; - background-color: rgba(0, 0, 0, 0.85); + background-color: $darkest-transparent; min-height: 90px; height: 90px; display: flex; - border-bottom: 2px solid #242424; + border-bottom: 1px solid $darkest-grey; &__title { - flex: 10; display: flex; + width: $header-section-width; @media (max-width: 768px) { justify-content: center; } + @media (max-width: 1024px) { + flex: 1 1 $header-section-width; + } + &__name { width: 200px; } @@ -77,12 +83,32 @@ body { } } + &__entry-code { + @media (max-width: 1024px) { + display: none; + } + + flex: 10; + text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + &__link { + color: white; + text-decoration-color: $light-grey; + } + } + &__experiment { text-align: right; - flex: 1 1 350px; + flex: 1 1 $header-section-width; + width: $header-section-width; color: $grey-text; font-size: 1.0em; font-weight: lighter; + white-space: nowrap; @media (max-width: 768px) { display: none; @@ -117,6 +143,26 @@ body { } } +.header-subtitle { + @media (min-width: 1024px) { + display: none; + } + + padding: 8px; + background-color: $darkest-transparent; + text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-bottom: 2px solid $darkest-grey; + + &__link { + color: white; + text-decoration-color: $light-grey; + } +} + .hero-content { flex: 10; min-height: 740px; @@ -208,7 +254,7 @@ body { .footer-content { padding: 1em 2.25em 1em 2.25em; - background-color: rgba(0, 0, 0, 0.85); + background-color: $darkest-transparent; min-height: 80px; display: flex; border-top: 2px solid #242424; diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index 47b06a43623b109a9fa27060d83f7da44889e59c..690db56c83a0fee301b0bb95952f6a1622ec9140 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -182,3 +182,17 @@ } } } + + +.info-dialog--action-button { + @extend %bottom-button; + margin-left: 6px; + margin-right: 6px; + appearance: none; + width: 168px; + text-align: center; + -moz-appearance: none; + -webkit-appearance: none; + margin: auto; + text-decoration: none; +} diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss new file mode 100644 index 0000000000000000000000000000000000000000..aafcac281e33a362fc69871f9474fff32ddd73b7 --- /dev/null +++ b/src/assets/stylesheets/link-dialog.scss @@ -0,0 +1,29 @@ +:local(.domain) , :local(.code) { + color: white; + font-family: monospace; + font-weight: bold; + text-decoration: none; +} + +:local(.domain) { + font-size: 3em; + padding: 14px; + display: block; +} + +:local(.code) { + font-size: 4.0em; + padding: 8px; +} + +:local(.keep-open) { + font-size: 0.8em; +} + +:local(.digit) { + padding: 0 8px; +} + +:local(.code-loading-panel) { + background: none; +} diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss new file mode 100644 index 0000000000000000000000000000000000000000..b94325c5039062d7848a4d254df9d27b88a7ceb4 --- /dev/null +++ b/src/assets/stylesheets/link.scss @@ -0,0 +1,182 @@ +@import 'shared'; +@import 'loader'; + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + background-color: black; + color: white; +} + +a { + color: white; +} + +.link-root { + @extend %default-font; + + width: 100%; + height: 100%; + margin: 0; + padding: 0; + + position: absolute; +} + +:local(.link) { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +:local(.link-contents) { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + color: $grey-text; + font-size: 1.4em; + + @media (max-width: 690px) { + flex-direction: column; + } +} + +:local(.entered-footer) { + margin: 16px; + font-size: 0.8em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + @media (max-width: 690px) { + display: none; + } +} + +:local(.entered-contents) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +:local(.code-loading-panel) { + background: rgba(0.4, 0.4, 0.4, 0.85); +} + +:local(.entry-footer-image) { + width: 200px; + margin: 12px; +} + + +:local(.footer-image) { + width: 200px; + margin: 12px; + + @media (max-height: 719px) { + display: none; + } +} + +:local(.header) { + margin: 16px; +} + +:local(.footer) { + margin: 16px; + font-size: 0.8em; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + @media (min-width: 690px) , (max-height: 650px) { + display: none; + } +} + +:local(.keypad) { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + text-align: center; +} + +:local(.keypad-button) { + @extend %big-icon-button; + font-size: 1.8em; + font-family: sans-serif; + border: 4px $light-grey solid; + border-radius: 128px; + min-width: 80px; + min-height: 80px; + cursor: pointer; + line-height: 68px; + margin: 8px; +} + +:local(.keypad-button):active { + background-color: $darker-grey; +} + +:local(.keypad-zero-button) { + grid-column: 2; +} + +:local(.keypad-button):disabled { + color: $light-grey; + border: 6px $dark-grey solid; +} + +:local(.keypad-backspace) , :local(.keypad-backspace):disabled , :local(.keypad-backspace):active { + border: none; +} + +:local(.keypad-backspace):active { + background-color: transparent; + color: $light-grey; +} + +:local(.entered-digits) { + font-face: monospace; + height: 100px; + width: 300px; + text-align: center; + font-size: 3.0em; + color: white; + display: flex; + justify-content: center; +} + +:local(.digit) { + margin: 8px; +} + +:local(.digit-input) { + outline-style: none; + appearance:textfield; + -moz-appearance:textfield; + -webkit-appearance:textfield; + background: transparent; + color: white; + margin: 0; + font-size: 64pt; + border: 0; + width: 295px; + letter-spacing: 0.08em; + text-align: center; +} + +:local(.digit-input::placeholder) { + letter-spacing: 0; +} + diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index 18944a4080dd096f4f6d917e61d3834cb05ab04a..e54447f58f853687b1f3f3d2be6d136d95df01c1 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -1,11 +1,12 @@ $dark-transparent: rgba(0, 0, 0, 0.4); $darker-transparent: rgba(0, 0, 0, 0.6); -$darkest-transparent: rgba(0, 0, 0, 0.95); +$darkest-transparent: rgba(0, 0, 0, 0.9); $grey-text: rgba(192, 192, 192, 1.0); $light-text: rgba(240, 240, 240, 1.0); $light-grey: lightgrey; $dark-grey: rgba(128, 128, 128, 1.0); $darker-grey: rgba(64, 64, 64, 1.0); +$darkest-grey: rgba(32, 32, 32, 1.0); %default-font { font-family: 'Zilla Slab', sans-serif; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 7dcd32f8f81d394ed06aa11ee117823a721b0f1f..0201cf0fca4a81f60217ab97679f6f2e7316fc43 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -5,15 +5,20 @@ "entry.mobile-screen": "Phone", "entry.generic-prefix": "Enter in ", "entry.generic-medium": "VR", + "entry.generic-subtitle-desktop": "Oculus or SteamVR", "entry.gearvr-prefix": "Enter on ", - "entry.gearvr-medium": "GearVR", + "entry.gearvr-medium": "Gear VR", + "entry.device-prefix-desktop": "Send to ", + "entry.device-prefix-mobile": "Enter on ", + "entry.device-medium": "Device", + "entry.device-subtitle-desktop": "Standalone Headset or Phone", + "entry.device-subtitle-mobile": "Mobile Headset or PC", + "entry.device-subtitle-vr": "Phone or PC", "entry.cardboard": "Enter on Google Cardboard", "entry.daydream-prefix": "Enter on ", "entry.daydream-medium": "Daydream", "entry.daydream-via-chrome": "Using Google Chrome", "entry.enable-screen-sharing": "Share my desktop", - "entry.webvr-link-preamble": "New to WebVR?", - "entry.webvr-link": "Learn more", "profile.save": "SAVE", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Your display name:", @@ -55,7 +60,15 @@ "home.made_with_love": "made with 🦆 by ", "home.environment_author_by": " by ", "home.dialog.close": "CLOSE", + "home.have_entry_code": "Have a link code?", "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in", - "mailing_list.privacy_link": "this Privacy Notice" + "mailing_list.privacy_link": "this Privacy Notice", + "link.in_your_browser": "In your device's browser, go to:", + "link.enter_code": "Then, enter this link code:", + "link.do_not_close": "Keep this dialog open to use this code.", + "link.link_page_header": "Enter your code:", + "link.dont_have_a_code": "Don't have a code?", + "link.create_a_room": "Create a Room", + "link.try_again": "We couldn't find that code. Please try again." } } diff --git a/src/behaviours/msft-mr-axis-with-deadzone.js b/src/behaviours/msft-mr-axis-with-deadzone.js new file mode 100644 index 0000000000000000000000000000000000000000..c2b86fe8b5a0b645572a6bf27a2dfceb0449187b --- /dev/null +++ b/src/behaviours/msft-mr-axis-with-deadzone.js @@ -0,0 +1,26 @@ +function msft_mr_axis_with_deadzone(el, outputPrefix) { + this.el = el; + this.outputPrefix = outputPrefix; + this.deadzone = 0.1; + this.emitAxisMoveWithDeadzone = this.emitAxisMoveWithDeadzone.bind(this); +} + +msft_mr_axis_with_deadzone.prototype = { + addEventListeners: function() { + this.el.addEventListener("axismove", this.emitAxisMoveWithDeadzone); + }, + removeEventListeners: function() { + this.el.removeEventListener("axismove", this.emitAxisMoveWithDeadzone); + }, + emitAxisMoveWithDeadzone: function(event) { + const axis = event.detail.axis; + if (Math.abs(axis[0]) < this.deadzone && Math.abs(axis[1]) < this.deadzone) { + return; + } + // Reverse y + axis[1] = -axis[1]; + this.el.emit("axisMoveWithDeadzone", event.detail); + } +}; + +export default msft_mr_axis_with_deadzone; diff --git a/src/components/animation-mixer.js b/src/components/animation-mixer.js index 791854f1eab2b6f30f20f094b9fb9a35c3ecb9f2..480a4de234751aa4dd2088d9f497f043f3c46b85 100644 --- a/src/components/animation-mixer.js +++ b/src/components/animation-mixer.js @@ -1,3 +1,7 @@ +/** + * Instantiates and updates a THREE.AnimationMixer on an entity. + * @component animation-mixer + */ AFRAME.registerComponent("animation-mixer", { init() { this.mixer = null; diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js index a72ec196ece7d4f66f5cc0e85b7f9a1edaeff916..7edf3ec3f654eb9ee7b45c35c58d2afee2c56373 100644 --- a/src/components/audio-feedback.js +++ b/src/components/audio-feedback.js @@ -1,3 +1,8 @@ +/** + * Emits audioFrequencyChange events based on a networked audio source + * @namespace avatar + * @component networked-audio-analyser + */ AFRAME.registerComponent("networked-audio-analyser", { schema: {}, async init() { @@ -27,6 +32,10 @@ AFRAME.registerComponent("networked-audio-analyser", { } }); +/** + * Sets an entity's color base on audioFrequencyChange events. + * @component matcolor-audio-feedback + */ AFRAME.registerComponent("matcolor-audio-feedback", { schema: { analyserSrc: { type: "selector" } @@ -49,6 +58,11 @@ AFRAME.registerComponent("matcolor-audio-feedback", { } }); +/** + * Sets an entity's scale base on audioFrequencyChange events. + * @namespace avatar + * @component scale-audio-feedback + */ AFRAME.registerComponent("scale-audio-feedback", { schema: { analyserSrc: { type: "selector" }, diff --git a/src/components/avatar-replay.js b/src/components/avatar-replay.js index bbcb397cf4127778c2b4ffb3c45a78894c0e2bbd..260744549a7f3eb60c210bb90f7ec063c454e888 100644 --- a/src/components/avatar-replay.js +++ b/src/components/avatar-replay.js @@ -11,6 +11,11 @@ const controlsBlacklist = [ "gearvr-controls" ]; +/** + * Replays a recorded motion capture with the given avatar body parts + * @namespace avatar + * @component avatar-replay + */ AFRAME.registerComponent("avatar-replay", { schema: { camera: { type: "selector" }, diff --git a/src/components/block-button.js b/src/components/block-button.js index 9dc23988c351d8483de8ea2e6546579d9e25d5b0..603de3bc6dc649e90165cf65b323b66431836839 100644 --- a/src/components/block-button.js +++ b/src/components/block-button.js @@ -1,3 +1,8 @@ +/** + * Registers a click handler and invokes the block method on the NAF adapter for the owner associated with its entity. + * @namespace network + * @component block-button + */ AFRAME.registerComponent("block-button", { init() { this.onClick = () => { diff --git a/src/components/bone-mute-state-indicator.js b/src/components/bone-mute-state-indicator.js index 92a2590e2cda9c2d0c048b0849d1e7bdcab35849..cf7305576e1df88fb5a713e577e93d6cbd0170ca 100644 --- a/src/components/bone-mute-state-indicator.js +++ b/src/components/bone-mute-state-indicator.js @@ -1,5 +1,7 @@ /** * Toggles the position of 2 bones into "on" and "off" positions to indicate mute state. + * @namespace avatar + * @component bone-mute-state-indicator */ AFRAME.registerComponent("bone-mute-state-indicator", { schema: { diff --git a/src/components/bone-visibility.js b/src/components/bone-visibility.js index 6f6f1a53e6f3eb464561ea91741d6d09e33a093f..84016d5984a53ccac41ab8089e2ec26dd541d2b4 100644 --- a/src/components/bone-visibility.js +++ b/src/components/bone-visibility.js @@ -1,3 +1,8 @@ +/** + * Scales an object to near-zero if the object is invisible. Useful for bones representing avatar body parts. + * @namespace avatar + * @component bone-visibility + */ AFRAME.registerComponent("bone-visibility", { tick() { const { visible } = this.el.object3D; diff --git a/src/components/cardboard-controls.js b/src/components/cardboard-controls.js index 766fda015a2b9d3ccf1e392ba9c3613b420cb904..792fa1bd0b22f8162e316f7782e4f361f0aebabe 100644 --- a/src/components/cardboard-controls.js +++ b/src/components/cardboard-controls.js @@ -1,5 +1,10 @@ const CARDBOARD_BUTTON_GAMEPAD_ID = "Cardboard Button"; +/** + * Polls the Gamepad API for Cardboard Button input and emits cardboardbutton events. + * @namespace user-input + * @component cardboard-controls + */ module.exports = AFRAME.registerComponent("cardboard-controls", { init: function() { this.buttons = {}; diff --git a/src/components/character-controller.js b/src/components/character-controller.js index 174148d9c0109bde06e52f4cd68a89c798331208..a079671e29c24733a11681c867a5ad0b7af10041 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -2,7 +2,12 @@ const CLAMP_VELOCITY = 0.01; const MAX_DELTA = 0.2; const EPS = 10e-6; -// Does not have any type of collisions yet. +/** + * Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly. + * The controller accounts for playspace offset and orientation and depends on the nav mesh system for translation. + * @namespace avatar + * @component character-controller + */ AFRAME.registerComponent("character-controller", { schema: { groundAcc: { default: 5.5 }, @@ -136,7 +141,8 @@ AFRAME.registerComponent("character-controller", { // Reapply playspace (player rig) translation root.applyMatrix(trans); - // TODO: the above matrix trnsfomraitons introduce some floating point erros in scale, this reverts them to avoid spamming network with fake scale updates + // TODO: the above matrix trnsfomraitons introduce some floating point errors in scale, this reverts them to + // avoid spamming network with fake scale updates root.scale.copy(startScale); this.pendingSnapRotationMatrix.identity(); // Revert to identity @@ -184,7 +190,15 @@ AFRAME.registerComponent("character-controller", { velocity.y -= velocity.y * data.easing * dt; } - // Clamp velocity easing. + const dvx = data.groundAcc * dt * this.accelerationInput.x; + const dvz = data.groundAcc * dt * -this.accelerationInput.z; + velocity.x += dvx; + velocity.z += dvz; + + const decay = 0.7; + this.accelerationInput.x = this.accelerationInput.x * decay; + this.accelerationInput.z = this.accelerationInput.z * decay; + if (Math.abs(velocity.x) < CLAMP_VELOCITY) { velocity.x = 0; } @@ -194,14 +208,5 @@ AFRAME.registerComponent("character-controller", { if (Math.abs(velocity.z) < CLAMP_VELOCITY) { velocity.z = 0; } - - const dvx = data.groundAcc * dt * this.accelerationInput.x; - const dvz = data.groundAcc * dt * -this.accelerationInput.z; - velocity.x += dvx; - velocity.z += dvz; - - const decay = 0.7; - this.accelerationInput.x = this.accelerationInput.x * decay; - this.accelerationInput.z = this.accelerationInput.z * decay; } }); diff --git a/src/components/controls-shape-offset.js b/src/components/controls-shape-offset.js index 7f47c498bc287a0dbd5c8f8126b724d320c95ca7..d234b971ac69d3379c740d414e84831d0ba9cf90 100644 --- a/src/components/controls-shape-offset.js +++ b/src/components/controls-shape-offset.js @@ -1,5 +1,10 @@ import { CONTROLLER_OFFSETS } from "./hand-controls2.js"; +/** + * Sets the offset of the aframe-physics shape on this entity based on the current VR controller type + * @namespace user-input + * @component controls-shape-offset + */ AFRAME.registerComponent("controls-shape-offset", { schema: { additionalOffset: { type: "vec3", default: { x: 0, y: -0.03, z: -0.04 } } diff --git a/src/components/css-class.js b/src/components/css-class.js index 1528ed4d450aea61f63b7426524229ed63609044..77882e9bce9e2e00c6d02f02677b9388b8110d14 100644 --- a/src/components/css-class.js +++ b/src/components/css-class.js @@ -1,3 +1,7 @@ +/** + * Sets the CSS class on an entity. + * @component css-class + */ AFRAME.registerComponent("css-class", { schema: { type: "string" diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index 7f3b1bbc42fad4f891b1eef4aaaa7047f8156b60..3f81d7ea0869a4481e003a8aeba3cf418621ac3c 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -2,7 +2,13 @@ const TARGET_TYPE_NONE = 1; const TARGET_TYPE_INTERACTABLE = 2; const TARGET_TYPE_UI = 4; const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI; +const virtualJoystickCutoff = 0.8; +/** + * Controls virtual cursor behavior in various modalities to affect teleportation, interatables and UI. + * @namespace user-input + * @component cursor-controller + */ AFRAME.registerComponent("cursor-controller", { dependencies: ["raycaster", "line"], schema: { @@ -38,9 +44,13 @@ AFRAME.registerComponent("cursor-controller", { this.origin = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.controllerQuaternion = new THREE.Quaternion(); + this.activeTouch = null; this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); + this._handleTouchStart = this._handleTouchStart.bind(this); + this._handleTouchMove = this._handleTouchMove.bind(this); + this._handleTouchEnd = this._handleTouchEnd.bind(this); this._handleMouseDown = this._handleMouseDown.bind(this); this._handleMouseMove = this._handleMouseMove.bind(this); this._handleMouseUp = this._handleMouseUp.bind(this); @@ -54,17 +64,11 @@ AFRAME.registerComponent("cursor-controller", { this._handleControllerConnected = this._handleControllerConnected.bind(this); this._handleControllerDisconnected = this._handleControllerDisconnected.bind(this); - this._handleTouchStart = this._handleTouchStart.bind(this); - this._updateRaycasterIntersections = this._updateRaycasterIntersections.bind(this); - this._handleTouchMove = this._handleTouchMove.bind(this); - this._handleTouchEnd = this._handleTouchEnd.bind(this); - - this.el.sceneEl.renderer.sortObjects = true; - this.data.cursor.addEventListener("loaded", this.cursorLoadedListener); + this.data.cursor.addEventListener("loaded", this._handleCursorLoaded); }, remove: function() { - this.data.cursor.removeEventListener("loaded", this._cursorLoadedListener); + this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); }, update: function(oldData) { @@ -81,6 +85,7 @@ AFRAME.registerComponent("cursor-controller", { document.addEventListener("touchstart", this._handleTouchStart); document.addEventListener("touchmove", this._handleTouchMove); document.addEventListener("touchend", this._handleTouchEnd); + document.addEventListener("touchcancel", this._handleTouchEnd); document.addEventListener("mousedown", this._handleMouseDown); document.addEventListener("mousemove", this._handleMouseMove); document.addEventListener("mouseup", this._handleMouseUp); @@ -106,6 +111,7 @@ AFRAME.registerComponent("cursor-controller", { document.removeEventListener("touchstart", this._handleTouchStart); document.removeEventListener("touchmove", this._handleTouchMove); document.removeEventListener("touchend", this._handleTouchEnd); + document.removeEventListener("touchcancel", this._handleTouchEnd); document.removeEventListener("mousedown", this._handleMouseDown); document.removeEventListener("mousemove", this._handleMouseMove); document.removeEventListener("mouseup", this._handleMouseUp); @@ -259,50 +265,38 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchStart: function(e) { - if (!this.isMobile || this.hasPointingDevice) return; + if (!this.isMobile || this.hasPointingDevice || this.activeTouch) return; - const touch = e.touches[0]; - if (touch.clientY / window.innerHeight >= 0.8) return true; - this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); - this._updateRaycasterIntersections(); - - // update cursor position - if (!this.isGrabbing) { - const intersections = this.el.components.raycaster.intersections; - if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - const intersection = intersections[0]; - this.data.cursor.object3D.position.copy(intersection.point); - this.currentDistance = intersections[0].distance; - this.currentDistanceMod = 0; - } else { - this.currentDistance = this.data.maxDistance; + for (let i = e.touches.length - 1; i >= 0; i--) { + const touch = e.touches[i]; + if (touch.clientY / window.innerHeight < virtualJoystickCutoff) { + this.activeTouch = touch; + break; } } + if (!this.activeTouch) return; - this._setLookControlsEnabled(false); - - // Set timeout because if I don't, the duck moves is picked up at the - // the wrong offset from the cursor: If the cursor started below and - // to the left, the duck lifts above and to the right by the same amount. - // I don't understand exactly why this is, since I am setting the - // cursor object's position manually in this function, but something else - // must happen before cursor-grab ends up doing the right thing. - // TODO : Figure this out. - window.setTimeout(() => { - this.data.cursor.emit("cursor-grab", {}); - }, 40); - - this.lastTouch = touch; - }, - - _updateRaycasterIntersections: function() { - const raycaster = this.el.components.raycaster.raycaster; + // Update the ray and cursor positions + const raycasterComp = this.el.components.raycaster; + const raycaster = raycasterComp.raycaster; const camera = this.data.camera.components.camera.camera; + const cursor = this.data.cursor; + this.mousePos.set( + this.activeTouch.clientX / window.innerWidth * 2 - 1, + -(this.activeTouch.clientY / window.innerHeight) * 2 + 1 + ); raycaster.setFromCamera(this.mousePos, camera); - this.origin = raycaster.ray.origin; - this.direction = raycaster.ray.direction; - this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction }); - this.el.components.raycaster.checkIntersections(); + this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction }); + raycasterComp.checkIntersections(); + const intersections = raycasterComp.intersections; + if (intersections.length === 0 || intersections[0].distance >= this.data.maxDistance) { + this.activeTouch = null; + return; + } + cursor.object3D.position.copy(intersections[0].point); + // Cursor position must be synced to physics before constraint is created + cursor.components["static-body"].syncToPhysics(); + cursor.emit("cursor-grab", {}); }, _handleTouchMove: function(e) { @@ -310,28 +304,28 @@ AFRAME.registerComponent("cursor-controller", { for (let i = 0; i < e.touches.length; i++) { const touch = e.touches[i]; - if (touch.clientY / window.innerHeight >= 0.8) return true; - this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); - this.lastTouch = touch; + if ( + (!this.activeTouch && touch.clientY / window.innerHeight < virtualJoystickCutoff) || + (this.activeTouch && touch.identifier === this.activeTouch.identifier) + ) { + this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1); + return; + } } }, _handleTouchEnd: function(e) { - if (!this.isMobile || this.hasPointingDevice) return; - - for (let i = 0; i < e.changedTouches.length; i++) { - const touch = e.changedTouches[i]; - if (this.lastTouch) { - const thisTouchDidNotDriveMousePos = - Math.abs(touch.clientX - this.lastTouch.clientX) > 0.1 && - Math.abs(touch.clientY - this.lastTouch.clientY) > 0.1; - if (thisTouchDidNotDriveMousePos) { - return; - } - } + if ( + !this.isMobile || + this.hasPointingDevice || + !this.activeTouch || + Array.prototype.some.call(e.touches, touch => touch.identifier === this.activeTouch.identifier) + ) { + return; } - this._setLookControlsEnabled(true); + this.data.cursor.emit("cursor-release", {}); + this.activeTouch = null; }, _handleMouseDown: function() { @@ -399,8 +393,8 @@ AFRAME.registerComponent("cursor-controller", { _handlePrimaryUp: function(e) { if (e.target === this.controller || e.target === this.data.playerRig) { + this.grabStarting = false; if (this._isGrabbing() || this._isTargetOfType(TARGET_TYPE_UI)) { - this.grabStarting = false; this.data.cursor.emit("cursor-release", e.detail); } else if (e.type !== this.data.releaseEvent) { this._endTeleport(); @@ -413,7 +407,7 @@ AFRAME.registerComponent("cursor-controller", { }, _handleCursorLoaded: function() { - this.data.cursor.object3DMap.mesh.renderOrder = 1; + this.data.cursor.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR; }, _handleControllerConnected: function(e) { diff --git a/src/components/duck.js b/src/components/duck.js index 59172942074173c393c7d86e9fd9e121737ae128..2b18f32c56cf62cef7baf23d378375d84a66be74 100644 --- a/src/components/duck.js +++ b/src/components/duck.js @@ -1,4 +1,8 @@ /* global CANNON */ +/** + * Floats a duck based on its scale. + * @component duck + */ AFRAME.registerComponent("duck", { schema: { initialForce: { default: 0 }, diff --git a/src/components/event-repeater.js b/src/components/event-repeater.js index e5e5ffd192b2b170a4efeac2e0c30e9ee43dd92a..10061132520ee0a7102255052f0108729ecf6090 100644 --- a/src/components/event-repeater.js +++ b/src/components/event-repeater.js @@ -1,3 +1,7 @@ +/** + * Listens to events from an event source and re-emits them on this entity + * @component event-repeater + */ AFRAME.registerComponent("event-repeater", { schema: { eventSource: { type: "selector" }, diff --git a/src/components/freeze-controller.js b/src/components/freeze-controller.js index 3cb64d76d91a8823c6d14ca8922cf388c7f83948..b7305ea720660644837192a89d85d27c3072397e 100644 --- a/src/components/freeze-controller.js +++ b/src/components/freeze-controller.js @@ -1,3 +1,8 @@ +/** + * Toggles freezing of network traffic on the given event. + * @namespace network + * @component freeze-controller + */ AFRAME.registerComponent("freeze-controller", { schema: { toggleEvent: { type: "string" } diff --git a/src/components/gltf-bundle.js b/src/components/gltf-bundle.js index ba6f7b3dc6d78aa08646352e49b6e3f91031056a..72163ec542922ea21c5c7612b54b193f5c256a97 100644 --- a/src/components/gltf-bundle.js +++ b/src/components/gltf-bundle.js @@ -1,3 +1,8 @@ +/** + * Instantiates GLTF models as specified in a bundle JSON. + * @namespace gltf + * @component gltf-bundle + */ AFRAME.registerComponent("gltf-bundle", { schema: { src: { default: "" } diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index cc803235164240994ddd47d7a3be2809d609bbe3..55d62b10e997a943d39c6e593cb35256c67b0e6c 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -194,6 +194,12 @@ function cachedLoadGLTF(src, preferredTechnique, onProgress) { }); } +/** + * Loads a GLTF model, optionally recursively "inflates" the child nodes of a model into a-entities and sets + * whitelisted components on them if defined in the node's extras. + * @namespace gltf + * @component gltf-model-plus + */ AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js index d99d5fc063d9a2c487c60490616b959b79c297ea..961790fead81073fcf066ad907079c2497461cd8 100644 --- a/src/components/hand-controls2.js +++ b/src/components/hand-controls2.js @@ -28,6 +28,11 @@ export const CONTROLLER_OFFSETS = { "gearvr-controls": new THREE.Matrix4() }; +/** + * Converts events from various 6DoF and 3DoF controllers into hand-pose events. + * @namespace user-input + * @component hand-controls2 + */ AFRAME.registerComponent("hand-controls2", { schema: { default: "left" }, diff --git a/src/components/hand-poses.js b/src/components/hand-poses.js index 7a0f9a4d26ddb022fbf5e5e51ee26fb01eef1772..98c1282cb714ae3b5a2d915ff5a5e237b127ca87 100644 --- a/src/components/hand-poses.js +++ b/src/components/hand-poses.js @@ -11,6 +11,11 @@ const POSES = { const NETWORK_POSES = ["allOpen", "thumbDown", "indexDown", "mrpDown", "thumbsUp", "point", "allGrip", "pinch"]; +/** + * Animates between poses based on networked pose state using an animation mixer. + * @namespace avatar + * @component hand-pose + */ AFRAME.registerComponent("hand-pose", { multiple: true, @@ -66,6 +71,11 @@ AFRAME.registerComponent("hand-pose", { } }); +/** + * Sets the networked hand pose state based on hand-pose events. + * @namespace avatar + * @component hand-pose-controller + */ AFRAME.registerComponent("hand-pose-controller", { multiple: true, schema: { diff --git a/src/components/haptic-feedback.js b/src/components/haptic-feedback.js index 9a0a2337de1ef895bdbd3bb699259f3063187cf2..72ce54ad0228317976183a7dfefcd85427023ec7 100644 --- a/src/components/haptic-feedback.js +++ b/src/components/haptic-feedback.js @@ -4,6 +4,11 @@ const strengthForIntensity = { high: 1 }; +/** + * Listens for haptic events and actuates hardware controllers accordingly + * @namespace user-input + * @component haptic-feedback + */ AFRAME.registerComponent("haptic-feedback", { schema: { hapticEventName: { default: "haptic_pulse" } diff --git a/src/components/hide-when-quality.js b/src/components/hide-when-quality.js index 93e238d61b74eff66d3becdabca314d13641a97c..8f13e0a32486e510be47e33df3e03132f094b541 100644 --- a/src/components/hide-when-quality.js +++ b/src/components/hide-when-quality.js @@ -1,3 +1,8 @@ +/** + * Hides entities based on the scene's quality mode + * @namespace environment + * @component hide-when-quality + */ AFRAME.registerComponent("hide-when-quality", { schema: { type: "string", default: "low" }, diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js index 67f9c4616285d38efe31e84e63475879463d84fb..1f94a319b6abb0a370a05014d7c1d8ba3795fd76 100644 --- a/src/components/hud-controller.js +++ b/src/components/hud-controller.js @@ -8,6 +8,8 @@ function deltaAngle(a, b) { /** * Positions the HUD and toggles app mode based on where the user is looking + * @namespace ui + * @component hud-controller */ AFRAME.registerComponent("hud-controller", { schema: { @@ -81,10 +83,8 @@ AFRAME.registerComponent("hud-controller", { const AppModeSystem = sceneEl.systems["app-mode"]; if (pitch > lookCutoff && AppModeSystem.mode !== AppModes.HUD) { AppModeSystem.setMode(AppModes.HUD); - sceneEl.renderer.sortObjects = true; } else if (pitch < lookCutoff && AppModeSystem.mode === AppModes.HUD) { AppModeSystem.setMode(AppModes.DEFAULT); - sceneEl.renderer.sortObjects = false; } } }); diff --git a/src/components/icon-button.js b/src/components/icon-button.js index 88f5e7303822ff103d7e67479ab99b3d7bddba4f..0f0e21a1dfc0205d4c4f4d4fc957a0af08e645dd 100644 --- a/src/components/icon-button.js +++ b/src/components/icon-button.js @@ -1,3 +1,8 @@ +/** + * A button with an image, tooltip, hover states and haptics. + * @namespace ui + * @component icon-button + */ AFRAME.registerComponent("icon-button", { schema: { image: { type: "string" }, diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js index 23f2dbf799f59558fd261eaa615f1bd2d37a66d2..3b2f3026a56ce64afb9460c6c6de0f37ccc6d969 100644 --- a/src/components/ik-controller.js +++ b/src/components/ik-controller.js @@ -1,4 +1,9 @@ const { Vector3, Quaternion, Matrix4, Euler } = THREE; +/** + * Provides access to the end effectors for IK. + * @namespace avatar + * @component ik-root + */ AFRAME.registerComponent("ik-root", { schema: { camera: { type: "string", default: ".camera" }, @@ -27,6 +32,11 @@ function findIKRoot(entity) { return entity && entity.components["ik-root"]; } +/** + * Performs IK on a hip-rooted skeleton to align the hip, head and hands with camera and controller inputs. + * @namespace avatar + * @component ik-controller + */ AFRAME.registerComponent("ik-controller", { schema: { leftEye: { type: "string", default: ".LeftEye" }, @@ -150,7 +160,8 @@ AFRAME.registerComponent("ik-controller", { // Compute the head position such that the hmd position would be in line with the middleEye headTransform.multiplyMatrices(cameraForward, invMiddleEyeToHead); - // Then position the hips such that the head is aligned with headTransform (which positions middleEye in line with the hmd) + // Then position the hips such that the head is aligned with headTransform + // (which positions middleEye in line with the hmd) hips.object3D.position.setFromMatrixPosition(headTransform).add(invHipsToHeadVector); // Animate the hip rotation to follow the Y rotation of the camera with some damping. @@ -165,7 +176,8 @@ AFRAME.registerComponent("ik-controller", { this.data.rotationSpeed * dt / 1000 ); - // Take the head orientation computed from the hmd, remove the Y rotation already applied to it by the hips, and apply it to the head + // Take the head orientation computed from the hmd, remove the Y rotation already applied to it by the hips, + // and apply it to the head invHipsQuaternion.copy(hips.object3D.quaternion).inverse(); head.object3D.quaternion.setFromRotationMatrix(headTransform).premultiply(invHipsQuaternion); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index df42d283b956bfa2b90da54977a716ee1f072849..a4fe8c9655ecffe3161a86b09fa3b6f6cdbdb19b 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -1,3 +1,8 @@ +/** + * HUD panel for muting, freezing, and space bubble controls. + * @namespace ui + * @component in-world-hud + */ AFRAME.registerComponent("in-world-hud", { schema: { haptic: { type: "selector" }, @@ -7,6 +12,12 @@ AFRAME.registerComponent("in-world-hud", { this.mic = this.el.querySelector(".mic"); this.freeze = this.el.querySelector(".freeze"); this.bubble = this.el.querySelector(".bubble"); + 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.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND; this.updateButtonStates = () => { this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted")); diff --git a/src/components/layers.js b/src/components/layers.js index 2838cf754efd2c525cfa5508246f230449f5c895..28a9e7c1d3da2477aa3bba09d1aef76f47998c1f 100644 --- a/src/components/layers.js +++ b/src/components/layers.js @@ -3,6 +3,11 @@ export const Layers = { reflection: 3 }; +/** + * Sets layer flags on the underlying Object3D + * @namespace environment + * @component layers + */ AFRAME.registerComponent("layers", { schema: { reflection: { type: "boolean", default: false } diff --git a/src/components/loop-animation.js b/src/components/loop-animation.js index 09a9e9dafabf8c05c7f13a02a9ecb820018ef573..76d3a0ec2135d174abf6c014363d3ea8561bd97e 100644 --- a/src/components/loop-animation.js +++ b/src/components/loop-animation.js @@ -1,3 +1,7 @@ +/** + * Loops the given clip using this entity's animation mixer + * @component loop-animation + */ AFRAME.registerComponent("loop-animation", { dependencies: ["animation-mixer"], schema: { diff --git a/src/components/mute-mic.js b/src/components/mute-mic.js index 00729ba1904002a88729e21ed82f69dbb872dda9..9498d90a421701fe033696d8f2660ef908204587 100644 --- a/src/components/mute-mic.js +++ b/src/components/mute-mic.js @@ -17,6 +17,11 @@ const unbindAllEvents = function(elements, events, f) { } }; +/** + * Toggles the microphone on the current network connection based on the given events. + * @namespace network + * @component mute-mic + */ AFRAME.registerComponent("mute-mic", { schema: { eventSrc: { type: "selectorAll" }, diff --git a/src/components/nav-mesh-helper.js b/src/components/nav-mesh-helper.js index 5c1be4ce96946dddb6ba98590dc7da27d403ab10..876f62a8558add90f2093d1430535c792cc732a5 100644 --- a/src/components/nav-mesh-helper.js +++ b/src/components/nav-mesh-helper.js @@ -1,3 +1,8 @@ +/** + * Initializes teleport-controls when the environment bundle has loaded. + * @namespace environment + * @component nav-mesh-helper + */ AFRAME.registerComponent("nav-mesh-helper", { schema: { teleportControls: { type: "selectorAll", default: "[teleport-controls]" } diff --git a/src/components/networked-avatar.js b/src/components/networked-avatar.js index 8820509cd57cae4c359cdbd8235edf6a62548555..04a5fac8e5d097b7daf7d75b5b27923b6c65534a 100644 --- a/src/components/networked-avatar.js +++ b/src/components/networked-avatar.js @@ -1,3 +1,8 @@ +/** + * Stores networked avatar state. + * @namespace avatar + * @component networked-avatar + */ AFRAME.registerComponent("networked-avatar", { schema: { left_hand_pose: { default: 0 }, diff --git a/src/components/networked-counter.js b/src/components/networked-counter.js index 39f76ddb7fb31482e86ddf4630f5d45d14c0d4e0..be9725cf6a50efb822370ab5d4b7196a6bcce3c4 100644 --- a/src/components/networked-counter.js +++ b/src/components/networked-counter.js @@ -1,3 +1,8 @@ +/** + * Limits networked interactables to a maximum number at any given time + * @namespace network + * @component networked-counter + */ AFRAME.registerComponent("networked-counter", { schema: { max: { default: 3 }, diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js index 189a43e393f359a5c2d217b5978132476f6407c6..eb4a93e0acc77173a714ca72aee1a29f744a2ffb 100644 --- a/src/components/networked-video-player.js +++ b/src/components/networked-video-player.js @@ -8,6 +8,11 @@ const nafConnected = function() { }); }; +/** + * Instantiates and plays a network video stream, setting the video as the source material for this entity. + * @namespace network + * @component networked-video-player + */ AFRAME.registerComponent("networked-video-player", { schema: {}, async init() { diff --git a/src/components/offset-relative-to.js b/src/components/offset-relative-to.js index 39361022488971c5b41a160520594020d81f36ac..3cd45b942ed4573ee9b588a467de456af801f62f 100644 --- a/src/components/offset-relative-to.js +++ b/src/components/offset-relative-to.js @@ -1,3 +1,7 @@ +/** + * Positions an entity relative to a given target when the given event is fired. + * @component offset-relative-to + */ AFRAME.registerComponent("offset-relative-to", { schema: { target: { diff --git a/src/components/player-info.js b/src/components/player-info.js index b9455352a104bd71aceb07ed4718a2999a4d08b6..a7e0812f8810c56544f14f2ed0c93602050f2a13 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -1,3 +1,8 @@ +/** + * Sets player info state, including avatar choice and display name. + * @namespace avatar + * @component player-info + */ AFRAME.registerComponent("player-info", { schema: { displayName: { type: "string" }, diff --git a/src/components/scene-shadow.js b/src/components/scene-shadow.js index 14b2e23cc6bbfc4b0a3e930cba4bfab65e74977d..c510604c9ea40785a24853bc341a481f992983ad 100644 --- a/src/components/scene-shadow.js +++ b/src/components/scene-shadow.js @@ -1,4 +1,8 @@ -// For use in environment gltf bundles to set scene shadow properties. +/** + * For use in environment gltf bundles to set scene shadow properties. + * @namespace environment + * @component scene-shadow + */ AFRAME.registerComponent("scene-shadow", { schema: { type: { diff --git a/src/components/spawn-controller.js b/src/components/spawn-controller.js index 1daf01b756e4308f1f9d50ceffd362aeb4b8e399..6476d733a38a4ea183ea46ca641a897d2290b510 100644 --- a/src/components/spawn-controller.js +++ b/src/components/spawn-controller.js @@ -1,3 +1,8 @@ +/** + * Used on a player-rig to move the player to a random spawn point on entry. + * @namespace avatar + * @component spawn-controller + */ AFRAME.registerComponent("spawn-controller", { schema: { target: { type: "selector" }, @@ -23,4 +28,9 @@ AFRAME.registerComponent("spawn-controller", { } }); +/** + * Marks an entity as a potential spawn point. + * @namespace environment + * @component spawn-point + */ AFRAME.registerComponent("spawn-point", {}); diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 0d9fc26dea6304e7ea66f165f76a62bd7044b2d3..903302562341289cb972515c3ea9030eec5979fd 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -1,3 +1,8 @@ +/** + * Manages ownership and haptics on an interatable + * @namespace network + * @component super-networked-interactable + */ AFRAME.registerComponent("super-networked-interactable", { schema: { mass: { default: 1 }, diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 7bd554f5a5ae07026896c45f0a837f5fb7467be3..083f81db766a810876b563a77b8de18544daf912 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -1,3 +1,8 @@ +/** + * Spawns networked objects when grabbed. + * @namespace network + * @component super-spawner + */ AFRAME.registerComponent("super-spawner", { schema: { template: { default: "" }, diff --git a/src/components/text-button.js b/src/components/text-button.js index 2668694c01d8efc973e7c8bdfd65596554e1ea7b..67af3653dcc4c9e81b59aec376feab6a00eb7fe4 100644 --- a/src/components/text-button.js +++ b/src/components/text-button.js @@ -1,3 +1,8 @@ +/** + * A button with text and haptics + * @namespace ui + * @component text-button + */ AFRAME.registerComponent("text-button", { schema: { haptic: { type: "selector" }, diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js index 5b45d07cbb197ae144f0450b31b7e29078c368f1..5da2c3c8ebd0c27530d5eb45b546b6dc0558bc2e 100644 --- a/src/components/virtual-gamepad-controls.js +++ b/src/components/virtual-gamepad-controls.js @@ -1,6 +1,11 @@ import nipplejs from "nipplejs"; import styles from "./virtual-gamepad-controls.css"; +/** + * Instantiates 2D virtual gamepads and emits associated events. + * @namespace user-input + * @component virtual-gamepad-controls + */ AFRAME.registerComponent("virtual-gamepad-controls", { schema: {}, diff --git a/src/components/visible-while-frozen.js b/src/components/visible-while-frozen.js index 59448cb6b989034699d879daae0a1ac1690c39af..4acff3d420136fee0393dc84ae32e14c6779553b 100644 --- a/src/components/visible-while-frozen.js +++ b/src/components/visible-while-frozen.js @@ -1,3 +1,8 @@ +/** + * Toggles the visibility of this entity when the scene is frozen. + * @namespace ui + * @component visible-while-frozen + */ AFRAME.registerComponent("visible-while-frozen", { init() { this.onStateChange = evt => { @@ -18,6 +23,11 @@ AFRAME.registerComponent("visible-while-frozen", { } }); +/** + * Toggles the interactivity of a UI entity while the scene is frozen. + * @namespace ui + * @component ui-class-while-frozen + */ AFRAME.registerComponent("ui-class-while-frozen", { init() { this.onStateChange = evt => { diff --git a/src/components/wasd-to-analog2d.js b/src/components/wasd-to-analog2d.js index a86641623b348f67c743e1845f51dd1c6e9ccb7f..a99babe7d65221a7f527e1cdd563cdaad708bbe9 100644 --- a/src/components/wasd-to-analog2d.js +++ b/src/components/wasd-to-analog2d.js @@ -1,3 +1,8 @@ +/** + * Converts WASD keyboard inputs to simulated analog inputs. + * @namespace user-input + * @component wasd-to-analog2d + */ AFRAME.registerComponent("wasd-to-analog2d", { schema: { analog2dOutputAction: { default: "wasd_analog2d" } diff --git a/src/hub.html b/src/hub.html index 62d4984389c245fcbba362f07292d16b23864bd1..c1d26a7ce3d6b6cf8cdcb5172ad45044956d474b 100644 --- a/src/hub.html +++ b/src/hub.html @@ -17,6 +17,10 @@ <% } else { %> <script src="https://cdn.rawgit.com/aframevr/aframe/3e7a4b3/dist/aframe-master.js" integrity="sha384-EaMOuyBOi9ERV/lVDwQgz/yFWBDWPsIju5Co6oCZZHXuvbLBO81yPWn80q0BbBn3" crossorigin="anonymous"></script> <% } %> + + + <!-- HACK: this has to run after A-Frame but before our bundle, since A-Frame blows away the local storage setting --> + <script src="https://cdn.rawgit.com/gfodor/ba8f88d9f34fe9cbe59a01ce3c48420d/raw/03e31f0ef7b9eac5e947bd39e440f34df0701f75/naf-janus-adapter-logging.js" integrity="sha384-4q1V8Q88oeCFriFefFo5uEUtMzbw6K116tFyC9cwbiPr6wEe7050l5HoJUxMvnzj" crossorigin="anonymous"></script> </head> <body data-html-prefix="<%= HTML_PREFIX %>"> diff --git a/src/hub.js b/src/hub.js index 80741c2e11939461503bb57cd99a1cee97a0ee75..a9f63cc3c7f72d02f38ca2fd7b424a1759a14508 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1,13 +1,12 @@ import "./assets/stylesheets/hub.scss"; import moment from "moment-timezone"; -import uuid from "uuid/v4"; import queryString from "query-string"; -import { Socket } from "phoenix"; import { patchWebGLRenderingContext } from "./utils/webgl"; patchWebGLRenderingContext(); import "aframe-xr"; + import "./vendor/GLTFLoader"; import "networked-aframe/src/index"; import "naf-janus-adapter"; @@ -23,6 +22,7 @@ import "./utils/audio-context-fix"; import trackpad_dpad4 from "./behaviours/trackpad-dpad4"; import joystick_dpad4 from "./behaviours/joystick-dpad4"; +import msft_mr_axis_with_deadzone from "./behaviours/msft-mr-axis-with-deadzone"; import { PressedMove } from "./activators/pressedmove"; import { ReverseY } from "./activators/reversey"; import "./activators/shortpress"; @@ -68,6 +68,9 @@ import ReactDOM from "react-dom"; import React from "react"; import UIRoot from "./react-components/ui-root"; import HubChannel from "./utils/hub-channel"; +import LinkChannel from "./utils/link-channel"; +import { connectToReticulum } from "./utils/phoenix-utils"; +import { disableiOSZoom } from "./utils/disable-ios-zoom"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -79,6 +82,11 @@ import { DEFAULT_ENVIRONMENT_URL } from "./assets/environments/environments"; import { App } from "./App"; window.APP = new App(); +window.APP.RENDER_ORDER = { + HUD_BACKGROUND: 1, + HUD_ICONS: 2, + CURSOR: 3 +}; const store = window.APP.store; const qs = queryString.parse(location.search); @@ -112,7 +120,6 @@ import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; -import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"; import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; @@ -122,26 +129,28 @@ function qsTruthy(param) { return val === null || /1|on|true/i.test(val); } -registerTelemetry(); +const isBotMode = qsTruthy("bot"); +const isTelemetryDisabled = qsTruthy("disable_telemetry"); +const isDebug = qsTruthy("debug"); + +if (!isBotMode && !isTelemetryDisabled) { + registerTelemetry(); +} + +disableiOSZoom(); AFRAME.registerInputBehaviour("trackpad_dpad4", trackpad_dpad4); AFRAME.registerInputBehaviour("joystick_dpad4", joystick_dpad4); +AFRAME.registerInputBehaviour("msft_mr_axis_with_deadzone", msft_mr_axis_with_deadzone); AFRAME.registerInputActivator("pressedmove", PressedMove); AFRAME.registerInputActivator("reverseY", ReverseY); AFRAME.registerInputMappings(inputConfig, true); -const isBotMode = qsTruthy("bot"); const concurrentLoadDetector = new ConcurrentLoadDetector(); concurrentLoadDetector.start(); -// Always layer in any new default profile bits -store.update({ activity: {}, settings: {}, profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); - -// Regenerate name to encourage users to change it. -if (!store.state.activity.hasChangedName) { - store.update({ profile: { displayName: generateRandomName() } }); -} +store.init(); function mountUI(scene, props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); @@ -171,13 +180,14 @@ function mountUI(scene, props = {}) { const onReady = async () => { const scene = document.querySelector("a-scene"); const hubChannel = new HubChannel(store); + const linkChannel = new LinkChannel(store); - document.querySelector("a-scene canvas").classList.add("blurred"); + document.querySelector("canvas").classList.add("blurred"); window.APP.scene = scene; registerNetworkSchemas(); - let uiProps = {}; + let uiProps = { linkChannel }; mountUI(scene); @@ -215,8 +225,9 @@ const onReady = async () => { const enterScene = async (mediaStream, enterInVR, hubId) => { const scene = document.querySelector("a-scene"); + scene.renderer.sortObjects = true; const playerRig = document.querySelector("#player-rig"); - document.querySelector("a-scene canvas").classList.remove("blurred"); + document.querySelector("canvas").classList.remove("blurred"); scene.render(); if (enterInVR) { @@ -232,6 +243,10 @@ const onReady = async () => { serverURL: process.env.JANUS_SERVER }); + if (isDebug) { + scene.setAttribute("networked-scene", { debug: true }); + } + scene.setAttribute("stats-plus", false); if (isMobile || qsTruthy("mobile")) { @@ -279,9 +294,11 @@ const onReady = async () => { if (!qsTruthy("offline")) { document.body.addEventListener("connected", () => { - hubChannel.sendEntryEvent().then(() => { - store.update({ activity: { lastEnteredAt: moment().toJSON() } }); - }); + if (!isBotMode) { + hubChannel.sendEntryEvent().then(() => { + store.update({ activity: { lastEnteredAt: moment().toJSON() } }); + }); + } remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 }); }); @@ -300,12 +317,17 @@ const onReady = async () => { scene.components["networked-scene"].connect().catch(connectError => { // hacky until we get return codes const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i); + console.error(connectError); remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" }); exitScene(); return; }); + if (isDebug) { + NAF.connection.adapter.session.options.verbose = true; + } + if (isBotMode) { playerRig.setAttribute("avatar-replay", { camera: "#player-camera", @@ -314,7 +336,7 @@ const onReady = async () => { }); const audio = document.getElementById("bot-recording"); mediaStream.addTrack(audio.captureStream().getAudioTracks()[0]); - // wait for runner script to interact with the page so that we can play audio. + // Wait for runner script to interact with the page so that we can play audio. await new Promise(resolve => { window.interacted = resolve; }); @@ -378,6 +400,11 @@ const onReady = async () => { // Stop rendering while the UI is up. We restart the render loop in enterScene. // Wait a tick plus some margin so that the environments actually render. setTimeout(() => scene.renderer.animate(null), 100); + } else { + const noop = () => {}; + // Replace renderer with a noop renderer to reduce bot resource usage. + scene.renderer = { animate: noop, render: noop }; + document.body.style.display = "none"; } }); environmentRoot.appendChild(initialEnvironmentEl); @@ -408,17 +435,7 @@ const onReady = async () => { const hubId = qs.hub_id || document.location.pathname.substring(1).split("/")[0]; console.log(`Hub ID: ${hubId}`); - const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:"; - const [retHost, retPort] = (process.env.DEV_RETICULUM_SERVER || "").split(":"); - const isProd = process.env.NODE_ENV === "production"; - const socketPort = qs.phx_port || (isProd ? document.location.port : retPort) || "443"; - const socketHost = qs.phx_host || (isProd ? document.location.hostname : retHost) || ""; - const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; - console.log(`Phoenix Channel URL: ${socketUrl}`); - - const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); - socket.connect(); - + const socket = connectToReticulum(); const channel = socket.channel(`hub:${hubId}`, {}); channel @@ -439,6 +456,8 @@ const onReady = async () => { console.error(res); }); + + linkChannel.setSocket(socket); }; document.addEventListener("DOMContentLoaded", onReady); diff --git a/src/input-mappings.js b/src/input-mappings.js index 5413bbac75868938b92986d40c2e2aa21f0effb2..a1c4c06a2d7cf1f8442b81fee5cfa25821af90b5 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -27,7 +27,8 @@ const config = { trackpad: "trackpad_dpad4" }, "windows-motion-controls": { - joystick: "joystick_dpad4" + joystick: "joystick_dpad4", + axisMoveWithDeadzone: "msft_mr_axis_with_deadzone" }, "daydream-controls": { trackpad: "trackpad_dpad4" @@ -106,7 +107,7 @@ const config = { trackpadtouchend: "thumb_up", triggerdown: ["action_grab", "index_down"], triggerup: ["action_release", "index_up"], - "axismove.reverseY": { left: "move" } + axisMoveWithDeadzone: { left: "move" } }, "daydream-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", diff --git a/src/link.html b/src/link.html new file mode 100644 index 0000000000000000000000000000000000000000..954061491faa3b257adedf87df06a26cfc58f79b --- /dev/null +++ b/src/link.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="shortcut icon" type="image/png" href="/favicon.ico"/> + <title>Enter Code | Hubs by Mozilla</title> + <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> +</head> + +<body> + <div id="link-root" class="link-root"></div> +</body> + +</html> diff --git a/src/link.js b/src/link.js new file mode 100644 index 0000000000000000000000000000000000000000..401fe54d9b8b9bd91df1c2140257710502a23add --- /dev/null +++ b/src/link.js @@ -0,0 +1,20 @@ +import "./assets/stylesheets/link.scss"; +import React from "react"; +import ReactDOM from "react-dom"; +import registerTelemetry from "./telemetry"; +import LinkRoot from "./react-components/link-root"; +import LinkChannel from "./utils/link-channel"; +import { connectToReticulum } from "./utils/phoenix-utils"; +import Store from "./storage/store"; + +registerTelemetry(); + +const socket = connectToReticulum(); +const store = new Store(); +store.init(); + +const linkChannel = new LinkChannel(store); + +linkChannel.setSocket(socket); + +ReactDOM.render(<LinkRoot store={store} linkChannel={linkChannel} />, document.getElementById("link-root")); diff --git a/src/network-schemas.js b/src/network-schemas.js index d30176a299f5ff7fd5336509454910325c116e58..9e20a17ca68bf2d669fbeb68d78f665c94de2ed9 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -1,30 +1,46 @@ function registerNetworkSchemas() { + const positionRequiresUpdate = (oldData, newData) => { + return !NAF.utils.almostEqualVec3(oldData, newData, 0.001); + }; + + const rotationRequiresUpdate = (oldData, newData) => { + return !NAF.utils.almostEqualVec3(oldData, newData, 0.5); + }; + NAF.schemas.add({ template: "#remote-avatar-template", components: [ - "position", + { + component: "position", + requiresNetworkUpdate: positionRequiresUpdate + }, { component: "rotation", - lerp: false + lerp: false, + requiresNetworkUpdate: rotationRequiresUpdate }, "scale", "player-info", "networked-avatar", { selector: ".camera", - component: "position" + component: "position", + requiresNetworkUpdate: positionRequiresUpdate }, { selector: ".camera", - component: "rotation" + component: "rotation", + requiresNetworkUpdate: rotationRequiresUpdate }, { selector: ".left-controller", - component: "position" + component: "position", + requiresNetworkUpdate: positionRequiresUpdate }, { selector: ".left-controller", - component: "rotation" + component: "rotation", + requiresNetworkUpdate: rotationRequiresUpdate }, { selector: ".left-controller", @@ -32,11 +48,13 @@ function registerNetworkSchemas() { }, { selector: ".right-controller", - component: "position" + component: "position", + requiresNetworkUpdate: positionRequiresUpdate }, { selector: ".right-controller", - component: "rotation" + component: "rotation", + requiresNetworkUpdate: rotationRequiresUpdate }, { selector: ".right-controller", @@ -47,12 +65,30 @@ function registerNetworkSchemas() { NAF.schemas.add({ template: "#video-template", - components: ["position", "rotation", "visible"] + components: [ + { + component: "position" + }, + { + component: "rotation" + }, + "visible" + ] }); NAF.schemas.add({ template: "#interactable-template", - components: ["position", "rotation", "scale"] + components: [ + { + component: "position", + requiresNetworkUpdate: positionRequiresUpdate + }, + { + component: "rotation", + requiresNetworkUpdate: rotationRequiresUpdate + }, + "scale" + ] }); } diff --git a/src/react-components/entry-buttons.js b/src/react-components/entry-buttons.js index 92d0ef5ef8ccd4992d9913bfb30144fff018c0b9..9e8302b3c7bbf9bd44edbaf6538100f11abf3fb8 100644 --- a/src/react-components/entry-buttons.js +++ b/src/react-components/entry-buttons.js @@ -7,7 +7,8 @@ import MobileScreenEntryImg from "../assets/images/mobile_screen_entry.svg"; import DesktopScreenEntryImg from "../assets/images/desktop_screen_entry.svg"; import GenericVREntryImg from "../assets/images/generic_vr_entry.svg"; import GearVREntryImg from "../assets/images/gearvr_entry.svg"; -import DaydreamEntyImg from "../assets/images/daydream_entry.svg"; +import DaydreamEntryImg from "../assets/images/daydream_entry.svg"; +import DeviceEntryImg from "../assets/images/device_entry.svg"; const mobiledetect = new MobileDetect(navigator.userAgent); @@ -22,7 +23,11 @@ const EntryButton = props => ( <span className="entry-button--bolded"> <FormattedMessage id={props.mediumMessageId} /> </span> - {props.subtitle && <div className="entry-button__subtitle">{props.subtitle}</div>} + {props.subtitle && ( + <div className="entry-button__subtitle"> + <FormattedMessage id={props.subtitle} /> + </div> + )} </div> </div> </button> @@ -33,7 +38,8 @@ EntryButton.propTypes = { iconSrc: PropTypes.string, prefixMessageId: PropTypes.string, mediumMessageId: PropTypes.string, - subtitle: PropTypes.string + subtitle: PropTypes.string, + isInHMD: PropTypes.bool }; export const TwoDEntryButton = props => { @@ -52,7 +58,8 @@ export const GenericEntryButton = props => { ...props, iconSrc: GenericVREntryImg, prefixMessageId: "entry.generic-prefix", - mediumMessageId: "entry.generic-medium" + mediumMessageId: "entry.generic-medium", + subtitle: mobiledetect.mobile() ? null : "entry.generic-subtitle-desktop" }; return <EntryButton {...entryButtonProps} />; @@ -72,10 +79,25 @@ export const GearVREntryButton = props => { export const DaydreamEntryButton = props => { const entryButtonProps = { ...props, - iconSrc: DaydreamEntyImg, + iconSrc: DaydreamEntryImg, prefixMessageId: "entry.daydream-prefix", mediumMessageId: "entry.daydream-medium" }; return <EntryButton {...entryButtonProps} />; }; + +export const DeviceEntryButton = props => { + const entryButtonProps = { + ...props, + iconSrc: DeviceEntryImg, + prefixMessageId: mobiledetect.mobile() ? "entry.device-prefix-mobile" : "entry.device-prefix-desktop", + mediumMessageId: "entry.device-medium" + }; + + entryButtonProps.subtitle = entryButtonProps.isInHMD + ? "entry.device-subtitle-vr" + : mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop"; + + return <EntryButton {...entryButtonProps} />; +}; diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index df85b425646d1b9a0eae4f216ba3c00cae6dfdc8..318fc88d95acd9d95d67adbb507ecd08cce843ee 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -35,29 +35,20 @@ class HomeRoot extends Component { loadHomeVideo = () => { const videoEl = document.querySelector("#background-video"); - function initVideo() { - videoEl.playbackRate = 0.75; - videoEl.play(); - function toggleVideo() { - // Play the video if the window/tab is visible. - if (!("hasFocus" in document)) { - return; - } - if (document.hasFocus()) { - videoEl.play(); - } else { - videoEl.pause(); - } + videoEl.playbackRate = 0.75; + function toggleVideo() { + // Play the video if the window/tab is visible. + if (document.hasFocus()) { + videoEl.play(); + } else { + videoEl.pause(); } + } + if ("hasFocus" in document) { document.addEventListener("visibilitychange", toggleVideo); window.addEventListener("focus", toggleVideo); window.addEventListener("blur", toggleVideo); } - if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) { - initVideo(); - } else { - videoEl.addEventListener("canplay", initVideo); - } }; showDialog = dialogType => { @@ -99,6 +90,11 @@ class HomeRoot extends Component { <img className="header-content__title__name" src="../assets/images/logo.svg" /> <div className="header-content__title__preview">preview</div> </div> + <div className="header-content__entry-code"> + <a className="header-content__entry-code__link" href="/link" rel="nofollow"> + <FormattedMessage id="home.have_entry_code" /> + </a> + </div> <div className="header-content__experiment"> <div className="header-content__experiment__container"> <img src="../assets/images/webvr_cube.svg" className="header-content__experiment__icon" /> @@ -132,6 +128,13 @@ class HomeRoot extends Component { </div> </div> </div> + <div className="header-subtitle"> + <div> + <a className="header-subtitle__link" href="/link" rel="nofollow"> + <FormattedMessage id="home.have_entry_code" /> + </a> + </div> + </div> <div className="hero-content"> <div className="hero-content__attribution"> Medieval Fantasy Book by{" "} @@ -208,7 +211,7 @@ class HomeRoot extends Component { </div> </div> </div> - <video playsInline muted loop className="background-video" id="background-video"> + <video playsInline muted loop autoPlay className="background-video" id="background-video"> <source src={homeVideoWebM} type="video/webm" /> <source src={homeVideoMp4} type="video/mp4" /> </video> diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js index 6e27e39c91262cf7e23a2597d00793f5f172f34b..7a48099b400203abcadced08ac5c5df6cb41552d 100644 --- a/src/react-components/info-dialog.js +++ b/src/react-components/info-dialog.js @@ -4,6 +4,7 @@ import classNames from "classnames"; import PropTypes from "prop-types"; import { FormattedMessage } from "react-intl"; import formurlencoded from "form-urlencoded"; +import LinkDialog from "./link-dialog.js"; // TODO i18n @@ -14,12 +15,15 @@ class InfoDialog extends Component { invite: Symbol("invite"), updates: Symbol("updates"), report: Symbol("report"), - help: Symbol("help") + help: Symbol("help"), + link: Symbol("link"), + webvr_recommend: Symbol("webvr_recommend") }; static propTypes = { dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)), onCloseDialog: PropTypes.func, - onSubmittedEmail: PropTypes.func + onSubmittedEmail: PropTypes.func, + linkCode: PropTypes.string }; constructor(props) { @@ -248,6 +252,27 @@ class InfoDialog extends Component { </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 = "Send Link to Device"; + dialogBody = <LinkDialog linkCode={this.props.linkCode} />; + break; } const dialogClasses = classNames({ diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..80ab360d50d54da5738a9c3c771b090b0d4bc9fa --- /dev/null +++ b/src/react-components/link-dialog.js @@ -0,0 +1,54 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { FormattedMessage } from "react-intl"; + +import styles from "../assets/stylesheets/link-dialog.scss"; + +class LinkDialog extends Component { + static propTypes = { + linkCode: PropTypes.string + }; + + render() { + if (!this.props.linkCode) { + return ( + <div> + <div className={classNames("loading-panel", styles.codeLoadingPanel)}> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> + </div> + </div> + ); + } + + 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> + <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> + </div> + ); + } +} + +export default LinkDialog; diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js new file mode 100644 index 0000000000000000000000000000000000000000..66ab987b6493a0cb99ab0742eb966d074ffe5ad5 --- /dev/null +++ b/src/react-components/link-root.js @@ -0,0 +1,184 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import en from "react-intl/locale-data/en"; + +import { lang, messages } from "../utils/i18n"; +import classNames from "classnames"; +import styles from "../assets/stylesheets/link.scss"; +import { disableiOSZoom } from "../utils/disable-ios-zoom"; + +const MAX_DIGITS = 4; + +addLocaleData([...en]); +disableiOSZoom(); + +class LinkRoot extends Component { + static propTypes = { + intl: PropTypes.object, + store: PropTypes.object, + linkChannel: PropTypes.object + }; + + state = { + enteredDigits: "", + failedAtLeastOnce: false + }; + + componentWillMount = () => { + document.addEventListener("keydown", this.handleKeyDown); + }; + + componentWillUnmount = () => { + document.removeEventListener("keydown", this.handleKeyDown); + }; + + handleKeyDown = e => { + // Number keys 0-9 + if (e.keyCode < 48 || e.keyCode > 57) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.addDigit(e.keyCode - 48); + }; + + addDigit = digit => { + if (this.state.enteredDigits.length >= MAX_DIGITS) return; + const newDigits = `${this.state.enteredDigits}${digit}`; + + if (newDigits.length === MAX_DIGITS) { + this.attemptLink(newDigits); + } + + this.setState({ enteredDigits: newDigits }); + }; + + removeDigit = () => { + const enteredDigits = this.state.enteredDigits; + if (enteredDigits.length === 0) return; + this.setState({ enteredDigits: enteredDigits.substring(0, enteredDigits.length - 1) }); + }; + + attemptLink = code => { + this.props.linkChannel + .attemptLink(code) + .then(response => { + // If there is a profile from the linked device, copy it over if we don't have one yet. + if (response.profile) { + const { hasChangedName } = this.props.store.state.activity; + + if (!hasChangedName) { + this.props.store.update({ activity: { hasChangedName: true }, profile: response.profile }); + } + } + + if (response.path) { + window.location.href = response.path; + } + }) + .catch(e => { + this.setState({ failedAtLeastOnce: true, enteredDigits: "" }); + + if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) { + throw e; + } + }); + }; + + render() { + // Note we use type "tel" for the input due to https://bugzilla.mozilla.org/show_bug.cgi?id=1005603 + + return ( + <IntlProvider locale={lang} messages={messages}> + <div className={styles.link}> + <div className={styles.linkContents}> + {this.state.enteredDigits.length === MAX_DIGITS && ( + <div className={classNames("loading-panel", styles.codeLoadingPanel)}> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> + </div> + )} + + <div className={styles.enteredContents}> + <div className={styles.header}> + <FormattedMessage id={this.state.failedAtLeastOnce ? "link.try_again" : "link.link_page_header"} /> + </div> + + <div className={styles.enteredDigits}> + <input + className={styles.digitInput} + type="tel" + pattern="[0-9]*" + value={this.state.enteredDigits} + onChange={ev => { + this.setState({ enteredDigits: ev.target.value }); + }} + placeholder="- - - -" + /> + </div> + + <div className={styles.enteredFooter}> + <span> + <FormattedMessage id="link.dont_have_a_code" /> + </span>{" "} + <span> + <a href="/"> + <FormattedMessage id="link.create_a_room" /> + </a> + </span> + <img className={styles.entryFooterImage} src="../assets/images/logo.svg" /> + </div> + </div> + + <div className={styles.keypad}> + {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((d, i) => ( + <button + disabled={this.state.enteredDigits.length === MAX_DIGITS} + key={`digit_${i}`} + className={styles.keypadButton} + onClick={() => this.addDigit(d)} + > + {d} + </button> + ))} + <button + disabled={this.state.enteredDigits.length === MAX_DIGITS} + className={classNames(styles.keypadButton, styles.keypadZeroButton)} + onClick={() => this.addDigit(0)} + > + 0 + </button> + <button + disabled={this.state.enteredDigits.length === 0 || this.state.enteredDigits.length === MAX_DIGITS} + className={classNames(styles.keypadButton, styles.keypadBackspace)} + onClick={() => this.removeDigit()} + > + ⌫ + </button> + </div> + + <div className={styles.footer}> + <span> + <FormattedMessage id="link.dont_have_a_code" /> + </span>{" "} + <span> + <a href="/"> + <FormattedMessage id="link.create_a_room" /> + </a> + </span> + <img className={styles.footerImage} src="../assets/images/logo.svg" alt="Logo" /> + </div> + </div> + </div> + </IntlProvider> + ); + } +} + +export default LinkRoot; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 849cd21eb49d0bf27c4eea7c08d7ad7e2de909bc..cb69c63436f8d9d72479fc10e9ce541d5fde2ceb 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -11,7 +11,7 @@ import screenfull from "screenfull"; import { lang, messages } from "../utils/i18n"; import AutoExitWarning from "./auto-exit-warning"; -import { TwoDEntryButton, GenericEntryButton, GearVREntryButton, DaydreamEntryButton } from "./entry-buttons.js"; +import { TwoDEntryButton, DeviceEntryButton, GenericEntryButton, DaydreamEntryButton } from "./entry-buttons.js"; import { ProfileInfoHeader } from "./profile-info-header.js"; import ProfileEntryPanel from "./profile-entry-panel"; import InfoDialog from "./info-dialog.js"; @@ -60,6 +60,7 @@ class UIRoot extends Component { enableScreenSharing: PropTypes.bool, store: PropTypes.object, scene: PropTypes.object, + linkChannel: PropTypes.object, htmlPrefix: PropTypes.string, showProfileEntry: PropTypes.bool, availableVREntryTypes: PropTypes.object, @@ -75,6 +76,8 @@ class UIRoot extends Component { entryStep: ENTRY_STEPS.start, enterInVR: false, infoDialogType: null, + linkCode: null, + linkCodeCancel: null, shareScreen: false, requestedScreen: false, @@ -158,8 +161,6 @@ class UIRoot extends Component { if (this.props.forcedVREntryType === "daydream") { this.enterDaydream(); - } else if (this.props.forcedVREntryType === "gearvr") { - this.enterGearVR(); } else if (this.props.forcedVREntryType === "vr") { this.enterVR(); } else if (this.props.forcedVREntryType === "2d") { @@ -260,25 +261,10 @@ class UIRoot extends Component { }; enterVR = async () => { - await this.performDirectEntryFlow(true); - }; - - enterGearVR = async () => { - if (this.props.availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) { + if (this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) { await this.performDirectEntryFlow(true); } else { - this.exit(); - - // Launch via Oculus Browser - const location = window.location; - const qs = queryString.parse(location.search); - qs.vr_entry_type = "gearvr"; // Auto-choose 'gearvr' after landing in Oculus Browser - - const ovrwebUrl = - `ovrweb://${location.protocol || "http:"}//${location.host}` + - `${location.pathname || ""}?${queryString.stringify(qs)}#${location.hash || ""}`; - - window.location = ovrwebUrl; + this.setState({ infoDialogType: InfoDialog.dialogTypes.webvr_recommend }); } }; @@ -513,6 +499,21 @@ class UIRoot extends Component { this.setState({ entryStep: ENTRY_STEPS.finished }); }; + attemptLink = async () => { + this.setState({ infoDialogType: InfoDialog.dialogTypes.link }); + const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); + this.setState({ linkCode: code, linkCodeCancel: cancel }); + onFinished.then(this.handleCloseDialog); + }; + + handleCloseDialog = async () => { + if (this.state.linkCodeCancel) { + this.state.linkCodeCancel(); + } + + this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null }); + }; + render() { if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { let subtitle = null; @@ -543,7 +544,10 @@ class UIRoot extends Component { rel="noreferrer noopener" > WebRTC Data Channels - </a>, which is required to use Hubs. + </a>, which is required to use Hubs.<br />If you"d like to use Hubs with Oculus or SteamVR, you can{" "} + <a href="https://www.mozilla.org/firefox" rel="noreferrer noopener"> + Download Firefox + </a>. </div> ); } else { @@ -588,8 +592,6 @@ class UIRoot extends Component { ); } - const daydreamMaybeSubtitle = messages["entry.daydream-via-chrome"]; - // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and // will attempt to share your webcam instead! const screenSharingCheckbox = this.props.enableScreenSharing && @@ -610,21 +612,23 @@ class UIRoot extends Component { this.state.entryStep === ENTRY_STEPS.start ? ( <div className="entry-panel"> <div className="entry-panel__button-container"> - <TwoDEntryButton onClick={this.enter2D} /> + {this.props.availableVREntryTypes.screen !== VR_DEVICE_AVAILABILITY.no && ( + <TwoDEntryButton onClick={this.enter2D} /> + )} {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( <GenericEntryButton onClick={this.enterVR} /> )} - {this.props.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && ( - <GearVREntryButton onClick={this.enterGearVR} /> - )} {this.props.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( <DaydreamEntryButton onClick={this.enterDaydream} subtitle={ - this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" + this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe + ? "entry.daydream-via-chrome" + : null } /> )} + <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} /> {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className="entry-panel__secondary" onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> @@ -632,19 +636,6 @@ class UIRoot extends Component { )} {screenSharingCheckbox} </div> - {!mobiledetect.mobile() && ( - <div className="entry-panel__webvr-link-container"> - <FormattedMessage id="entry.webvr-link-preamble" />{" "} - <a - className="entry-panel__webvr-link" - target="_blank" - rel="noopener noreferrer" - href="https://webvr.rocks/" - > - <FormattedMessage id="entry.webvr-link" /> - </a> - </div> - )} </div> ) : null; @@ -675,12 +666,7 @@ class UIRoot extends Component { </div> </div> <div className="mic-grant-panel__next-container"> - <button - className={classNames("mic-grant-panel__next", { - invisible: this.state.entryStep === ENTRY_STEPS.mic_grant - })} - onClick={this.onMicGrantButton} - > + <button className={classNames("mic-grant-panel__next")} onClick={this.onMicGrantButton}> <FormattedMessage id="audio.granted-next" /> </button> </div> @@ -829,8 +815,9 @@ class UIRoot extends Component { <div className="ui"> <InfoDialog dialogType={this.state.infoDialogType} + linkCode={this.state.linkCode} onSubmittedEmail={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.email_submitted })} - onCloseDialog={() => this.setState({ infoDialogType: null })} + onCloseDialog={this.handleCloseDialog} /> {this.state.entryStep === ENTRY_STEPS.finished && ( diff --git a/src/storage/store.js b/src/storage/store.js index 23f3168198f11a6824d0f1a4bdd63d67a0ef5961..e4e509ba3c1f1fc41b45d6816ba31f542f9c09cd 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -5,6 +5,7 @@ const LOCAL_STORE_KEY = "___hubs_store"; const STORE_STATE_CACHE_KEY = Symbol(); const validator = new Validator(); import { EventTarget } from "event-target-shim"; +import { generateDefaultProfile, generateRandomName } from "../utils/identity.js"; // Durable (via local-storage) schema-enforced state that is meant to be consumed via forward data flow. // (Think flux but with way less incidental complexity, at least for now :)) @@ -60,6 +61,20 @@ export default class Store extends EventTarget { } } + // Initializes store with any default bits + init = () => { + this.update({ + activity: {}, + settings: {}, + profile: { ...generateDefaultProfile(), ...(this.state.profile || {}) } + }); + + // Regenerate name to encourage users to change it. + if (!this.state.activity.hasChangedName) { + this.update({ profile: { displayName: generateRandomName() } }); + } + }; + get state() { if (!this.hasOwnProperty(STORE_STATE_CACHE_KEY)) { this[STORE_STATE_CACHE_KEY] = JSON.parse(localStorage.getItem(LOCAL_STORE_KEY)); diff --git a/src/systems/app-mode.js b/src/systems/app-mode.js index 49e8d361bd90de4612fe4548fc0ecb4cc027d53d..6954d3ad5e364e05954ffa1acc6371d5364854f2 100644 --- a/src/systems/app-mode.js +++ b/src/systems/app-mode.js @@ -4,6 +4,7 @@ export const AppModes = Object.freeze({ DEFAULT: "default", HUD: "hud" }); /** * Simple system for keeping track of a modal app state + * @system app-mode */ AFRAME.registerSystem("app-mode", { init() { @@ -20,6 +21,8 @@ AFRAME.registerSystem("app-mode", { /** * Toggle the isPlaying state of a component based on app mode + * @namespace app-mode + * @component app-mode-toggle-playing */ AFRAME.registerComponent("app-mode-toggle-playing", { multiple: true, @@ -44,6 +47,8 @@ AFRAME.registerComponent("app-mode-toggle-playing", { /** * Toggle a boolean property of a component based on app mode + * @namespace app-mode + * @component app-mode-toggle-attribute */ AFRAME.registerComponent("app-mode-toggle-attribute", { multiple: true, @@ -69,6 +74,8 @@ AFRAME.registerComponent("app-mode-toggle-attribute", { /** * Toggle aframe input mappings action set based on app mode + * @namespace app-mode + * @component app-mode-input-mappings */ AFRAME.registerComponent("app-mode-input-mappings", { schema: { @@ -91,6 +98,8 @@ AFRAME.registerComponent("app-mode-input-mappings", { /** * Toggle visibility of an entity based on if the user is in vr mode or not + * @namespace vr-mode + * @component vr-mode-toggle-visibility */ AFRAME.registerComponent("vr-mode-toggle-visibility", { schema: { @@ -120,6 +129,8 @@ AFRAME.registerComponent("vr-mode-toggle-visibility", { /** * Toggle the isPlaying state of a component based on app mode + * @namespace vr-mode + * @component vr-mode-toggle-playing */ AFRAME.registerComponent("vr-mode-toggle-playing", { multiple: true, diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js index c6820a4016cbd3440bab91997e19741b006b6b29..5fe0b6fd6f7f87ae431e6844935cd4bcb44595e2 100644 --- a/src/systems/exit-on-blur.js +++ b/src/systems/exit-on-blur.js @@ -1,3 +1,7 @@ +/** + * Emits an "exit" event when a user has stopped using the app for a certain period of time + * @system exit-on-blur + */ AFRAME.registerSystem("exit-on-blur", { init() { this.onBlur = this.onBlur.bind(this); diff --git a/src/systems/personal-space-bubble.js b/src/systems/personal-space-bubble.js index d89a59cae4ff907f777f98d6327ef704cc1e38e3..0ba3cee5d48a1b5095031db89f9fa2cf6e490429 100644 --- a/src/systems/personal-space-bubble.js +++ b/src/systems/personal-space-bubble.js @@ -1,6 +1,12 @@ const invaderPos = new AFRAME.THREE.Vector3(); const bubblePos = new AFRAME.THREE.Vector3(); +/** + * Iterates through bubbles and invaders on every tick and sets invader state accordingly. + * testing multiline things + * @namespace avatar/personal-space-bubble + * @system personal-space-bubble + */ AFRAME.registerSystem("personal-space-bubble", { schema: { debug: { default: false }, @@ -110,6 +116,11 @@ function createSphereGizmo(radius) { } // TODO: we need to come up with a more generic way of doing this as this is very specific to our avatars. +/** + * Specifies a mesh associated with an invader. + * @namespace avatar/personal-space-bubble + * @component space-invader-mesh + */ AFRAME.registerComponent("space-invader-mesh", { schema: { meshSelector: { type: "string" } @@ -128,6 +139,11 @@ function findInvaderMesh(entity) { const DEBUG_OBJ = "psb-debug"; +/** + * Represents an entity that can invade a personal space bubble + * @namespace avatar/personal-space-bubble + * @component personal-space-invader + */ AFRAME.registerComponent("personal-space-invader", { schema: { radius: { type: "number", default: 0.1 }, @@ -177,6 +193,11 @@ AFRAME.registerComponent("personal-space-invader", { } }); +/** + * Represents a personal space bubble on an entity. + * @namespace avatar/personal-space-bubble + * @component personal-space-bubble + */ AFRAME.registerComponent("personal-space-bubble", { schema: { radius: { type: "number", default: 0.8 }, diff --git a/src/utils/crypto.js b/src/utils/crypto.js new file mode 100644 index 0000000000000000000000000000000000000000..53fd606e98657c896da161edb97f93272a684ef7 --- /dev/null +++ b/src/utils/crypto.js @@ -0,0 +1,76 @@ +// NOTE: We do not use an IV since we generate a new keypair each time we use these routines. + +async function deriveKey(privateKey, publicKey) { + return crypto.subtle.deriveKey( + { name: "ECDH", public: publicKey }, + privateKey, + { name: "AES-CBC", length: 256 }, + true, + ["encrypt", "decrypt"] + ); +} + +async function publicKeyToString(key) { + return JSON.stringify(await crypto.subtle.exportKey("jwk", key)); +} + +async function stringToPublicKey(s) { + return await crypto.subtle.importKey("jwk", JSON.parse(s), { name: "ECDH", namedCurve: "P-256" }, true, []); +} + +function stringToArrayBuffer(s) { + const buf = new Uint8Array(s.length); + + for (let i = 0; i < s.length; i++) { + buf[i] = s.charCodeAt(i); + } + + return buf; +} + +function arrayBufferToString(b) { + const buf = new Uint8Array(b); + let s = ""; + + for (let i = 0; i < buf.byteLength; i++) { + s += String.fromCharCode(buf[i]); + } + + return s; +} + +// This allows a single object to be passed encrypted from a receiver in a req -> response flow + +// Requestor generates a public key and private key, and should send the public key to receiver. +export async function generateKeys() { + const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]); + const publicKeyString = await publicKeyToString(keyPair.publicKey); + return { publicKeyString, privateKey: keyPair.privateKey }; +} + +// Receiver takes the public key from requestor and passes obj to get a response public key and the encrypted data to return. +export async function generatePublicKeyAndEncryptedObject(incomingPublicKeyString, obj) { + const iv = new Uint8Array(16); + const incomingPublicKey = await stringToPublicKey(incomingPublicKeyString); + const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]); + const publicKeyString = await publicKeyToString(keyPair.publicKey); + const secret = await deriveKey(keyPair.privateKey, incomingPublicKey); + + const encryptedData = btoa( + arrayBufferToString( + await crypto.subtle.encrypt({ name: "AES-CBC", iv }, secret, stringToArrayBuffer(JSON.stringify(obj))) + ) + ); + + return { publicKeyString, encryptedData }; +} + +// Requestor then takes the receiver's public key, the private key (returned from generateKeys()), and the data from the receiver. +export async function decryptObject(publicKeyString, privateKey, base64value) { + const iv = new Uint8Array(16); + const publicKey = await stringToPublicKey(publicKeyString); + const secret = await deriveKey(privateKey, publicKey); + const ciphertext = stringToArrayBuffer(atob(base64value)); + const data = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, secret, ciphertext); + return JSON.parse(arrayBufferToString(data)); +} diff --git a/src/utils/disable-ios-zoom.js b/src/utils/disable-ios-zoom.js new file mode 100644 index 0000000000000000000000000000000000000000..c2d17104a6ae4b9b8c99221fe5bec8a8f79cc909 --- /dev/null +++ b/src/utils/disable-ios-zoom.js @@ -0,0 +1,24 @@ +import MobileDetect from "mobile-detect"; +const mobiledetect = new MobileDetect(navigator.userAgent); + +export function disableiOSZoom() { + if (!mobiledetect.is("iPhone") && !mobiledetect.is("iPad")) return; + + let lastTouchAtMs = 0; + + document.addEventListener("touchmove", ev => { + if (ev.scale === 1) return; + + ev.preventDefault(); + }); + + document.addEventListener("touchend", ev => { + const now = new Date().getTime(); + const isDoubleTouch = now - lastTouchAtMs <= 300; + lastTouchAtMs = now; + + if (isDoubleTouch) { + ev.preventDefault(); + } + }); +} diff --git a/src/utils/link-channel.js b/src/utils/link-channel.js new file mode 100644 index 0000000000000000000000000000000000000000..4da4e1eb2795c248cd7b4ddeb134549706d1ac0a --- /dev/null +++ b/src/utils/link-channel.js @@ -0,0 +1,143 @@ +import { generatePublicKeyAndEncryptedObject, generateKeys, decryptObject } from "./crypto"; + +const LINK_ACTION_TIMEOUT = 10000; + +export default class LinkChannel { + constructor(store) { + this.store = store; + } + + setSocket = socket => { + this.socket = socket; + }; + + // Returns a promise that, when resolved, will forward an object with three keys: + // + // code: The code that was made available to use for link. + // + // cancel: A function that the caller can call to cancel the use of the code. + // + // onFinished: A promise that, when resolved, indicates the code is no longer usable, + // because it was either successfully used by the remote device or it has expired + // ("used" or "expired" is passed to the callback). + generateCode = () => { + return new Promise(resolve => { + const onFinished = new Promise(finished => { + const step = () => { + const code = Math.floor(Math.random() * 9999) + .toString() + .padStart(4, "0"); + + // Only respond to one link_request in this channel. + let readyToSend = false; + let leftChannel = false; + + const channel = this.socket.channel(`link:${code}`, { timeout: LINK_ACTION_TIMEOUT }); + + const leave = () => { + if (!leftChannel) channel.leave(); + leftChannel = true; + }; + + const cancel = () => leave(); + + channel.on("link_expired", () => finished("expired")); + + channel.on("presence_state", state => { + if (readyToSend) return; + + if (Object.keys(state).length > 0) { + // Code is in use by someone else, try a new one + step(); + } else { + readyToSend = true; + resolve({ code, cancel, onFinished }); + } + }); + + channel.on("link_request", incoming => { + if (readyToSend) { + const data = { path: location.pathname }; + + // Copy profile data to link'ed device if it's been set. + if (this.store.state.activity.hasChangedName) { + data.profile = { ...this.store.state.profile }; + } + + generatePublicKeyAndEncryptedObject(incoming.public_key, data).then( + ({ publicKeyString, encryptedData }) => { + const payload = { + target_session_id: incoming.reply_to_session_id, + public_key: publicKeyString, + data: encryptedData + }; + + if (!leftChannel) { + channel.push("link_response", payload); + } + + leave(); + + finished("used"); + readyToSend = false; + } + ); + } + }); + + channel.join().receive("error", r => console.error(r)); + }; + + step(); + }); + }); + }; + + // Attempts to receive a link payload from a remote device using the given code. + // + // Promise rejects if the code is invalid or there is a problem with the channel. + // Promise resolves and passes payload of link source on successful link. + attemptLink = code => { + return new Promise((resolve, reject) => { + const channel = this.socket.channel(`link:${code}`, { timeout: LINK_ACTION_TIMEOUT }); + let finished = false; + + generateKeys().then(({ publicKeyString, privateKey }) => { + channel.on("presence_state", state => { + const numOccupants = Object.keys(state).length; + + if (numOccupants === 1) { + // Great, only sender is in topic, request link + channel.push("link_request", { + reply_to_session_id: this.socket.params.session_id, + public_key: publicKeyString + }); + + setTimeout(() => { + if (finished) return; + channel.leave(); + reject(new Error("no_response")); + }, LINK_ACTION_TIMEOUT); + } else if (numOccupants === 0) { + // Nobody in this channel, probably a bad code. + channel.leave(); + reject(new Error("failed")); + } else { + console.warn("link code channel already has 2 or more occupants, something fishy is going on."); + channel.leave(); + reject(new Error("in_use")); + } + }); + + channel.on("link_response", payload => { + finished = true; + channel.leave(); + + decryptObject(payload.public_key, privateKey, payload.data).then(resolve); + }); + + channel.join().receive("error", r => console.error(r)); + }); + }); + }; +} diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..3c20f8db96aa261e4d8dd27d727b89b499728e53 --- /dev/null +++ b/src/utils/phoenix-utils.js @@ -0,0 +1,20 @@ +import queryString from "query-string"; +import uuid from "uuid/v4"; +import { Socket } from "phoenix"; + +export function connectToReticulum() { + const qs = queryString.parse(location.search); + + const socketProtocol = qs.phx_protocol || (document.location.protocol === "https:" ? "wss:" : "ws:"); + const [retHost, retPort] = (process.env.DEV_RETICULUM_SERVER || "").split(":"); + const isProd = process.env.NODE_ENV === "production"; + const socketPort = qs.phx_port || (isProd ? document.location.port : retPort) || "443"; + const socketHost = qs.phx_host || (isProd ? document.location.hostname : retHost) || ""; + const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; + console.log(`Phoenix Socket URL: ${socketUrl}`); + + const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); + socket.connect(); + + return socket; +} diff --git a/src/utils/vr-caps-detect.js b/src/utils/vr-caps-detect.js index 7a577b6ebc03896c54aefb1130e68052f792a3c7..297842342e7937922aa2286ebbae0cd2a53cb2a4 100644 --- a/src/utils/vr-caps-detect.js +++ b/src/utils/vr-caps-detect.js @@ -1,6 +1,9 @@ const { detect } = require("detect-browser"); +import MobileDetect from "mobile-detect"; + const browser = detect(); const deviceDetect = require("device-detect")(); +const mobiledetect = new MobileDetect(navigator.userAgent); // Precision on device detection is fuzzy -- we can sometimes know if a device is definitely // available, or definitely *not* available, and assume it may be available otherwise. @@ -33,21 +36,30 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i]; // Once in a compatible browser, we should assume it will work (if it doesn't, it's because they don't have the headset, // haven't installed the software, our guess about their phone was wrong, etc.) // -// At the time of this writing there are three VR "entry types" that will be validated by this method: +// At the time of this writing there are the following VR "entry types" that will be validated by this method: // +// - screen: Enter "on-screen" in 2D // - generic: Generic WebVR (platform/OS agnostic indicator if a general 'Enter VR' option should be presented.) // - daydream: Google Daydream // - gearvr: Oculus GearVR +// - cardboard: Google Cardboard // +// This function also detects if the user is already in a headset, and returns the isInHMD key to be `true` if so. export async function getAvailableVREntryTypes() { - const isWebVRCapableBrowser = !!navigator.getVRDisplays; const isSamsungBrowser = browser.name === "chrome" && navigator.userAgent.match(/SamsungBrowser/); const isOculusBrowser = navigator.userAgent.match(/Oculus/); + const isInHMD = isOculusBrowser; + + // This needs to be kept up-to-date with the latest browsers that can support VR and Hubs. + // Checking for navigator.getVRDisplays always passes b/c of polyfill. + const isWebVRCapableBrowser = window.hasNativeWebVRImplementation; + const isDaydreamCapableBrowser = !!(isWebVRCapableBrowser && browser.name === "chrome" && !isSamsungBrowser); const isIDevice = ["iPhone", "iPad", "iPod"].indexOf(deviceDetect.device) > -1; const isFirefoxBrowser = browser.name === "firefox"; - let generic = VR_DEVICE_AVAILABILITY.no; + const screen = isInHMD ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.yes; + let generic = mobiledetect.mobile() ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.maybe; let cardboard = VR_DEVICE_AVAILABILITY.no; // We only consider GearVR support as "maybe" and never "yes". The only browser @@ -63,7 +75,8 @@ export async function getAvailableVREntryTypes() { // For daydream detection, we first check if they are on an Android compatible device, and assume they // may support daydream *unless* this browser has WebVR capabilities, in which case we can do better. - let daydream = isMaybeDaydreamCompatibleDevice() ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; + let daydream = + isMaybeDaydreamCompatibleDevice() && !isInHMD ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; if (isWebVRCapableBrowser) { const displays = await navigator.getVRDisplays(); @@ -95,5 +108,5 @@ export async function getAvailableVREntryTypes() { } } - return { generic, gearvr, daydream, cardboard }; + return { screen, generic, gearvr, daydream, cardboard, isInHMD }; } diff --git a/webpack.config.js b/webpack.config.js index b77adbc4c7b14e8a1acd5ed4dfa0ba271c143734..17fbb4f8b666a03a7d1577f07d011040b6125f40 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,6 +78,7 @@ const config = { entry: { index: path.join(__dirname, "src", "index.js"), hub: path.join(__dirname, "src", "hub.js"), + link: path.join(__dirname, "src", "link.js"), "avatar-selector": path.join(__dirname, "src", "avatar-selector.js") }, output: { @@ -194,6 +195,11 @@ const config = { chunks: ["hub"], inject: "head" }), + new HTMLWebpackPlugin({ + filename: "link.html", + template: path.join(__dirname, "src", "link.html"), + chunks: ["link"] + }), new HTMLWebpackPlugin({ filename: "avatar-selector.html", template: path.join(__dirname, "src", "avatar-selector.html"), diff --git a/yarn.lock b/yarn.lock index aba9e254d0dd96d59f84c68ded93b6a4c9960d26..0a82d0444ffe0e2187d968da0136df7d87028c2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -152,10 +152,14 @@ acorn@^4.0.3: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.2.1, acorn@^5.4.0, acorn@^5.4.1: +acorn@^5.0.0, acorn@^5.2.1, acorn@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" +acorn@^5.5.0: + version "5.5.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" + aframe-billboard-component@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/aframe-billboard-component/-/aframe-billboard-component-1.0.0.tgz#10ce2482729eef7386c5844d65917581a62d3adc" @@ -208,7 +212,11 @@ after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" -ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" @@ -219,7 +227,7 @@ ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.3.0: +ajv@^5.2.3, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -228,7 +236,7 @@ ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.0.1, ajv@^6.1.0: +ajv@^6.1.0: version "6.1.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e" dependencies: @@ -249,8 +257,8 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" ansi-escapes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" ansi-html@0.0.7: version "0.0.7" @@ -280,9 +288,9 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: color-convert "^1.9.0" @@ -1608,6 +1616,10 @@ budo@^10.0.3: ws "^1.1.1" xtend "^4.0.0" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -1768,7 +1780,15 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1: +chalk@^2.0.0, chalk@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^2.0.1, chalk@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" dependencies: @@ -2016,8 +2036,8 @@ colormin@^1.0.5: has "^1.0.1" colors@*: - version "1.2.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.3.tgz#1b152a9c4f6c9f74bc4bb96233ad0b7983b79744" + version "1.2.5" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" colors@1.0.3: version "1.0.3" @@ -2101,7 +2121,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@~1.6.0: +concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -2109,6 +2129,15 @@ concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@ readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" @@ -2941,8 +2970,8 @@ eslint-visitor-keys@^1.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" eslint@^4.10.0: - version "4.18.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.1.tgz#b9138440cb1e98b2f44a0d578c6ecf8eae6150b0" + version "4.19.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" @@ -2953,7 +2982,7 @@ eslint@^4.10.0: doctrine "^2.1.0" eslint-scope "^3.7.1" eslint-visitor-keys "^1.0.0" - espree "^3.5.2" + espree "^3.5.4" esquery "^1.0.0" esutils "^2.0.2" file-entry-cache "^2.0.0" @@ -2975,18 +3004,19 @@ eslint@^4.10.0: path-is-inside "^1.0.2" pluralize "^7.0.0" progress "^2.0.0" + regexpp "^1.0.1" require-uncached "^1.0.3" semver "^5.3.0" strip-ansi "^4.0.0" strip-json-comments "~2.0.1" - table "^4.0.1" + table "4.0.2" text-table "~0.2.0" -espree@^3.5.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6" +espree@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: - acorn "^5.4.0" + acorn "^5.5.0" acorn-jsx "^3.0.0" esprima@^2.6.0: @@ -3002,8 +3032,8 @@ esprima@~3.1.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" esquery@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" dependencies: estraverse "^4.0.0" @@ -3200,8 +3230,8 @@ external-editor@^1.1.0: tmp "^0.0.29" external-editor@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" + version "2.2.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" dependencies: chardet "^0.4.0" iconv-lite "^0.4.17" @@ -3699,7 +3729,11 @@ global@^4.3.2: min-document "^2.19.0" process "~0.5.1" -globals@^11.0.1, globals@^11.1.0: +globals@^11.0.1: + version "11.5.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642" + +globals@^11.1.0: version "11.3.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" @@ -4119,10 +4153,16 @@ humps@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" -iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: +iconv-lite@0.4.19, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@^0.4.17: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -4141,7 +4181,11 @@ iferr@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" -ignore@^3.3.3, ignore@^3.3.5: +ignore@^3.3.3: + version "3.3.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" + +ignore@^3.3.5: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" @@ -4491,8 +4535,8 @@ is-path-cwd@^1.0.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" dependencies: is-path-inside "^1.0.0" @@ -4639,8 +4683,8 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -4971,10 +5015,14 @@ lodash@3.7.x: version "3.7.0" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.4: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" +lodash@^4.17.4, lodash@^4.3.0: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + log-symbols@2.1.0, log-symbols@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6" @@ -5027,7 +5075,14 @@ lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" -lru-cache@^4.0.1, lru-cache@^4.1.1: +lru-cache@^4.0.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" dependencies: @@ -5409,8 +5464,8 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" naf-janus-adapter@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.9.3.tgz#2ea71119cb1e51ada28d0a492d9af4a15f062b8a" + version "0.9.5" + resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.9.5.tgz#81fcbf068daf66820892544de4d8357614e0152d" dependencies: debug "^3.1.0" minijanus "0.6.1" @@ -5456,7 +5511,7 @@ neo-async@^2.5.0: "networked-aframe@https://github.com/mozillareality/networked-aframe#mr-social-client/master": version "0.6.1" - resolved "https://github.com/mozillareality/networked-aframe#641b5e44b8514d02925e3efb4289ca36a41c1006" + resolved "https://github.com/mozillareality/networked-aframe#424b41cfdf53db64033885da411c33685644db97" dependencies: easyrtc "1.1.0" express "^4.10.7" @@ -6674,7 +6729,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.9, readable-stream@^2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" dependencies: @@ -6704,6 +6759,18 @@ readable-stream@1.1: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + 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" + readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -6808,6 +6875,10 @@ regex-not@^1.0.0: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexpp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + regexpu-core@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" @@ -7074,16 +7145,24 @@ rxjs@^5.0.0-beta.11: dependencies: symbol-observable "1.0.1" -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" dependencies: ret "~0.1.10" +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + sass-graph@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" @@ -7685,6 +7764,12 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringstream@~0.0.4: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -7787,12 +7872,18 @@ supports-color@^4.4.0: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0: +supports-color@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" dependencies: has-flag "^3.0.0" +supports-color@^5.2.0, supports-color@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -7815,12 +7906,12 @@ syntax-error@^1.1.1: dependencies: acorn-node "^1.2.0" -table@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" +table@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: - ajv "^6.0.1" - ajv-keywords "^3.0.0" + ajv "^5.2.3" + ajv-keywords "^2.1.0" chalk "^2.1.0" lodash "^4.17.4" slice-ansi "1.0.0"