diff --git a/src/App.js b/src/App.js index c542f439f903e0a4426d087cb33d69852b25944d..b814e080d4e1df7ae5f6dbe7649d2548211f4c42 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,10 @@ +import Store from "./storage/store"; + export class App { constructor() { this.scene = null; this.quality = "low"; + this.store = new Store(); } setQuality(quality) { diff --git a/src/assets/stylesheets/2d-hud.css b/src/assets/stylesheets/2d-hud.css index a50436114181d18997248dce77a3cc1d9500363f..d360067e0a24f93ce897d4c2549953d826af0a12 100644 --- a/src/assets/stylesheets/2d-hud.css +++ b/src/assets/stylesheets/2d-hud.css @@ -39,6 +39,13 @@ align-items: center; justify-content: center; z-index: 10; + border-radius: 50%; + border: 2px solid white; + cursor: pointer; +} + +:local(.modeButton.frozen) { + border-color: red; } :local(.panel) { @@ -65,7 +72,6 @@ background-size: 100%; background-image: url(../hud/avatar.png); } - :local(.mic) { display: flex; width: 32px; diff --git a/src/components/freeze-controller.js b/src/components/freeze-controller.js new file mode 100644 index 0000000000000000000000000000000000000000..77e66f91f167d0f01ea4acac406c6f32eac83039 --- /dev/null +++ b/src/components/freeze-controller.js @@ -0,0 +1,27 @@ +AFRAME.registerComponent("freeze-controller", { + schema: { + toggleEvent: { type: "string" } + }, + + init: function() { + this.onToggle = this.onToggle.bind(this); + }, + + play: function() { + this.el.addEventListener(this.data.toggleEvent, this.onToggle); + }, + + pause: function() { + this.el.removeEventListener(this.data.toggleEvent, this.onToggle); + }, + + onToggle: function() { + window.APP.store.update({ profile: { has_found_freeze: true } }); + NAF.connection.adapter.toggleFreeze(); + if (NAF.connection.adapter.frozen) { + this.el.addState("frozen"); + } else { + this.el.removeState("frozen"); + } + } +}); diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js index 4ee274f34ae9daa6f24c8e414eee824b845c49db..d2439b3765d0272ae5b113fa174b1738ab213c2b 100644 --- a/src/components/hud-controller.js +++ b/src/components/hud-controller.js @@ -15,7 +15,8 @@ AFRAME.registerComponent("hud-controller", { offset: { default: 0.7 }, // distance from hud above head, lookCutoff: { default: 20 }, // angle at which the hud should be "on", animRange: { default: 30 }, // degrees over which to animate the hud into view - yawCutoff: { default: 50 } // yaw degrees at wich the hud should reoirent even if the user is looking up + yawCutoff: { default: 50 }, // yaw degrees at wich the hud should reoirent even if the user is looking up + showTip: { type: "bool" } }, init() { this.isYLocked = false; @@ -33,14 +34,34 @@ AFRAME.registerComponent("hud-controller", { const head = this.data.head.object3D; const sceneEl = this.el.sceneEl; - const { offset, lookCutoff, animRange, yawCutoff } = this.data; + const { offset, lookCutoff, animRange, yawCutoff, showTip } = this.data; const pitch = head.rotation.x * THREE.Math.RAD2DEG; const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.Math.RAD2DEG; - // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/3 way animated away + // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle + let t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange; + + // HUD is locked down while showing tooltip + if (showTip) { + t = 1; + } + + // Once the HUD is in place it should stay in place until you look sufficiently far down + if (t === 1) { + this.lockedHeadPositionY = head.position.y; + this.hudLocked = true; + } else if (this.hudLocked && pitch < lookCutoff - animRange / 2) { + this.hudLocked = false; + } + + if (this.hudLocked) { + t = 1; + } + + // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/2 way animated away // TODO: come up with better huristics for this that maybe account for the user turning away from the hud "too far", also animate the position so that it doesnt just snap. - if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 3) { + if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 2) { const lookDir = new THREE.Vector3(0, 0, -1); lookDir.applyQuaternion(head.quaternion); lookDir.add(head.position); @@ -48,18 +69,6 @@ AFRAME.registerComponent("hud-controller", { hud.position.z = lookDir.z; hud.setRotationFromEuler(new THREE.Euler(0, head.rotation.y, 0)); } - - // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle - const t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange; - - // Lock the hud in place relative to a known head position so it doesn't bob up and down - // with the user's head - if (!this.isYLocked && t === 1) { - this.lockedHeadPositionY = head.position.y; - } - const EPSILON = 0.001; - this.isYLocked = t > 1 - EPSILON; - hud.position.y = (this.isYLocked ? this.lockedHeadPositionY : head.position.y) + offset + (1 - t) * offset; hud.rotation.x = (1 - t) * THREE.Math.DEG2RAD * 90; diff --git a/src/hub.html b/src/hub.html index edb7395d4641f10c27994b3399dc580e72f6952b..973461f270e5b2f412421d8f3ba5ed06b4dc3328 100644 --- a/src/hub.html +++ b/src/hub.html @@ -22,6 +22,7 @@ networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" physics mute-mic="eventSrc: a-scene; toggleEvents: action_mute" + freeze-controller="toggleEvent: action_freeze" personal-space-bubble="debug: false;" app-mode-input-mappings="modes: default, hud; actionSets: default, hud;" diff --git a/src/hub.js b/src/hub.js index 75111f9c6b4abdee9ef8ecb771d195e02f17be38..34d2812823ff6b2aa3926dbf5f20afa6c47da577 100644 --- a/src/hub.js +++ b/src/hub.js @@ -49,6 +49,7 @@ import "./components/hand-poses"; import "./components/gltf-model-plus"; import "./components/gltf-bundle"; import "./components/hud-controller"; +import "./components/freeze-controller"; import ReactDOM from "react-dom"; import React from "react"; @@ -63,6 +64,7 @@ import "./gltf-component-mappings"; import { App } from "./App"; window.APP = new App(); +const store = window.APP.store; const qs = queryString.parse(location.search); const isMobile = AFRAME.utils.device.isMobile(); @@ -88,7 +90,6 @@ import "./components/nav-mesh-helper"; import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; -import Store from "./storage/store"; import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; @@ -108,7 +109,6 @@ AFRAME.registerInputActivator("pressedmove", PressedMove); AFRAME.registerInputActivator("reverseY", ReverseY); AFRAME.registerInputMappings(inputConfig, true); -const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); const hubChannel = new HubChannel(store); @@ -135,6 +135,8 @@ function applyProfileFromStore(playerRig) { displayName, avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") }); + const hudController = playerRig.querySelector("[hud-controller]"); + hudController.setAttribute("hud-controller", { showTip: !store.state.profile.has_found_freeze }); document.querySelector("a-scene").emit("username-changed", { username: displayName }); } diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 9679bb9c090af49966c6f5cbf03072201c860f00..8d35fada2457db7ef74728e9547b4123223c3e00 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -4,12 +4,12 @@ import cx from "classnames"; import styles from "../assets/stylesheets/2d-hud.css"; -const TwoDHUD = ({ muted, onToggleMute }) => ( +const TwoDHUD = ({ muted, frozen, onToggleMute, onToggleFreeze }) => ( <div className={styles.container}> <div className={cx("ui-interactive", styles.panel, styles.left)}> <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} /> </div> - <div className={cx("ui-interactive", styles.modeButton)}> + <div className={cx("ui-interactive", styles.modeButton, { [styles.frozen]: frozen })} onClick={onToggleFreeze}> <div className={styles.avatar} /> </div> <div className={cx("ui-interactive", styles.panel, styles.right)}> @@ -20,7 +20,9 @@ const TwoDHUD = ({ muted, onToggleMute }) => ( TwoDHUD.propTypes = { muted: PropTypes.bool, - onToggleMute: PropTypes.func + frozen: PropTypes.bool, + onToggleMute: PropTypes.func, + onToggleFreeze: PropTypes.func }; export default TwoDHUD; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 3e22208dacc6219b59cc9276562c71d43f129f03..bbf19e97eadc58dd0e5367bbd2981130d0b671bc 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -90,6 +90,9 @@ class UIRoot extends Component { autoExitTimerInterval: null, secondsRemainingBeforeAutoExit: Infinity, + muted: false, + frozen: false, + exited: false, showProfileEntry: false @@ -121,9 +124,9 @@ class UIRoot extends Component { // TODO: mute state should probably actually just live in react land onAframeStateChanged = e => { - if (e.detail !== "muted") return; + if (!(e.detail === "muted" || e.detail === "frozen")) return; this.setState({ - muted: this.props.scene.is("muted") + [e.detail]: this.props.scene.is(e.detail) }); }; @@ -131,6 +134,10 @@ class UIRoot extends Component { this.props.scene.emit("action_mute"); }; + toggleFreeze = () => { + this.props.scene.emit("action_freeze"); + }; + handleForcedVREntryType = () => { if (!this.props.forcedVREntryType) return; @@ -758,7 +765,12 @@ class UIRoot extends Component { )} </div> {this.state.entryStep === ENTRY_STEPS.finished ? ( - <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} /> + <TwoDHUD + muted={this.state.muted} + frozen={this.state.frozen} + onToggleMute={this.toggleMute} + onToggleFreeze={this.toggleFreeze} + /> ) : null} </div> </IntlProvider> diff --git a/src/storage/store.js b/src/storage/store.js index 4351ebeda99e9f9665fcbbf395858296cd2e56a4..b117e1849bf6a20cc28d770c7ef66112bb8888ba 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -19,6 +19,7 @@ export const SCHEMA = { properties: { has_agreed_to_terms: { type: "boolean" }, has_changed_name: { type: "boolean" }, + has_found_freeze: { type: "boolean" }, display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" }, avatar_id: { type: "string" } } diff --git a/src/utils/identity.js b/src/utils/identity.js index db78b027e3e851aa254532f264438e362062576c..4cce7fa3729e376be6d28d57eb3120715bceee8b 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -103,6 +103,7 @@ export function generateDefaultProfile() { return { has_agreed_to_terms: false, has_changed_name: false, + has_found_freeze: false, avatar_id: selectRandom(avatarIds) }; }