diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss index 3485ea3de016e7bc7f895a4232fa9136dd1e04f1..21553781a0612dc2d17d3fa2d7a627409a6b8d47 100644 --- a/src/assets/stylesheets/avatar-selector.scss +++ b/src/assets/stylesheets/avatar-selector.scss @@ -1,5 +1,4 @@ @import 'shared'; -@import 'loader'; #selector-root { height: 100%; diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index 2ed8fde235a1f75fcbb77bddf41bce2815d94cee..5b9e58bdc7a999b091323d20b7b6c418c31b1da8 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -9,11 +9,19 @@ display: flex; pointer-events: auto; + &__avatar-selector-container { + flex: 1; + position: relative; + margin-bottom: 0.5em; + width: 95%; + } + &__avatar-selector { border: none; width: 95%; height: 100%; - margin: 1em 0; + z-index: 1; + position: relative; } &__form { diff --git a/src/components/virtual-gamepad-controls.css b/src/components/virtual-gamepad-controls.css index 572e6169f6a29c911d0fa89e37382b10838da3c0..4270c36ad7be261997f7ab77218e9ea817269620 100644 --- a/src/components/virtual-gamepad-controls.css +++ b/src/components/virtual-gamepad-controls.css @@ -13,3 +13,32 @@ left: 50%; right: 0; } + +:local(.mockJoystickContainer) { + position: absolute; + height: 20vh; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: space-around; +} + +:local(.mockJoystick) { + display: flex; + align-items: center; + justify-content: center; + width: 100px; + height: 100px; + background-color: rgba(255,255,255,0.5); + border-top-left-radius: 50%; + border-top-right-radius: 50%; + border-bottom-right-radius: 50%; + border-bottom-left-radius: 50%; +} + +:local(.mockJoystick.inner) { + width: 50px; + height: 50px; +} diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js index f92b7d4534f8e45e499edf6bcf1345f9e0f33374..87433f427c00fc64054405e5efea91f841dc579a 100644 --- a/src/components/virtual-gamepad-controls.js +++ b/src/components/virtual-gamepad-controls.js @@ -5,42 +5,62 @@ AFRAME.registerComponent("virtual-gamepad-controls", { schema: {}, init() { + this.onEnterVr = this.onEnterVr.bind(this); + this.onExitVr = this.onExitVr.bind(this); + this.onFirstInteraction = this.onFirstInteraction.bind(this); + this.onMoveJoystickChanged = this.onMoveJoystickChanged.bind(this); + this.onMoveJoystickEnd = this.onMoveJoystickEnd.bind(this); + this.onLookJoystickChanged = this.onLookJoystickChanged.bind(this); + this.onLookJoystickEnd = this.onLookJoystickEnd.bind(this); + + this.mockJoystickContainer = document.createElement("div"); + this.mockJoystickContainer.classList.add(styles.mockJoystickContainer); + const leftMock = document.createElement("div"); + leftMock.classList.add(styles.mockJoystick); + const leftMockSmall = document.createElement("div"); + leftMockSmall.classList.add(styles.mockJoystick, styles.inner); + leftMock.appendChild(leftMockSmall); + this.mockJoystickContainer.appendChild(leftMock); + const rightMock = document.createElement("div"); + rightMock.classList.add(styles.mockJoystick); + const rightMockSmall = document.createElement("div"); + rightMockSmall.classList.add(styles.mockJoystick, styles.inner); + rightMock.appendChild(rightMockSmall); + this.mockJoystickContainer.appendChild(rightMock); + document.body.appendChild(this.mockJoystickContainer); + // Setup gamepad elements const leftTouchZone = document.createElement("div"); leftTouchZone.classList.add(styles.touchZone, styles.left); document.body.appendChild(leftTouchZone); - const rightTouchZone = document.createElement("div"); - rightTouchZone.classList.add(styles.touchZone, styles.right); - document.body.appendChild(rightTouchZone); + this.leftTouchZone = leftTouchZone; - const leftStick = nipplejs.create({ - zone: leftTouchZone, + this.leftStick = nipplejs.create({ + zone: this.leftTouchZone, color: "white", fadeTime: 0 }); - const rightStick = nipplejs.create({ - zone: rightTouchZone, - color: "white", - fadeTime: 0 - }); + this.leftStick.on("start", this.onFirstInteraction); + this.leftStick.on("move", this.onMoveJoystickChanged); + this.leftStick.on("end", this.onMoveJoystickEnd); - this.onMoveJoystickChanged = this.onMoveJoystickChanged.bind(this); - this.onMoveJoystickEnd = this.onMoveJoystickEnd.bind(this); - this.onLookJoystickChanged = this.onLookJoystickChanged.bind(this); - this.onLookJoystickEnd = this.onLookJoystickEnd.bind(this); + const rightTouchZone = document.createElement("div"); + rightTouchZone.classList.add(styles.touchZone, styles.right); + document.body.appendChild(rightTouchZone); - leftStick.on("move", this.onMoveJoystickChanged); - leftStick.on("end", this.onMoveJoystickEnd); + this.rightTouchZone = rightTouchZone; - rightStick.on("move", this.onLookJoystickChanged); - rightStick.on("end", this.onLookJoystickEnd); + this.rightStick = nipplejs.create({ + zone: this.rightTouchZone, + color: "white", + fadeTime: 0 + }); - this.leftTouchZone = leftTouchZone; - this.rightTouchZone = rightTouchZone; - this.leftStick = leftStick; - this.rightStick = rightStick; + this.rightStick.on("start", this.onFirstInteraction); + this.rightStick.on("move", this.onLookJoystickChanged); + this.rightStick.on("end", this.onLookJoystickEnd); this.inVr = false; this.moving = false; @@ -53,12 +73,16 @@ AFRAME.registerComponent("virtual-gamepad-controls", { value: 0 }; - this.onEnterVr = this.onEnterVr.bind(this); - this.onExitVr = this.onExitVr.bind(this); this.el.sceneEl.addEventListener("enter-vr", this.onEnterVr); this.el.sceneEl.addEventListener("exit-vr", this.onExitVr); }, + onFirstInteraction() { + this.leftStick.off("start", this.onFirstInteraction); + this.rightStick.off("start", this.onFirstInteraction); + document.body.removeChild(this.mockJoystickContainer); + }, + onMoveJoystickChanged(event, joystick) { const angle = joystick.angle.radian; const force = joystick.force < 1 ? joystick.force : 1; diff --git a/src/hub.html b/src/hub.html index 34f642b64adec241532b4ef9ea9964d809675a8b..f3cf9e74cafbe3534906c63ab4480b08135088bd 100644 --- a/src/hub.html +++ b/src/hub.html @@ -5,11 +5,10 @@ <!-- DO NOT REMOVE/EDIT THIS COMMENT - HUB_META_TAGS --> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>"> - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> - <link rel="shortcut icon" type="image/png" href="/favicon.ico"/> + <link rel="shortcut icon" type="image/png" href="/favicon.ico"> <title>Get together | Hubs by Mozilla</title> <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js index 266278013074732ae19c8d3ba4bbe3681c3a12ed..4495819a83aca79aaddaf63deb66efecb4f49dcb 100644 --- a/src/react-components/avatar-selector.js +++ b/src/react-components/avatar-selector.js @@ -15,11 +15,20 @@ class AvatarSelector extends Component { onChange: PropTypes.func }; - getAvatarIndex = (direction = 0) => { - const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.props.avatarId); - const numAvatars = this.props.avatars.length; - return ((currAvatarIndex + direction) % numAvatars + numAvatars) % numAvatars; + static getAvatarIndex = (props, offset = 0) => { + const currAvatarIndex = props.avatars.findIndex(avatar => avatar.id === props.avatarId); + const numAvatars = props.avatars.length; + return ((currAvatarIndex + offset) % numAvatars + numAvatars) % numAvatars; }; + static nextAvatarIndex = props => AvatarSelector.getAvatarIndex(props, -1); + static previousAvatarIndex = props => AvatarSelector.getAvatarIndex(props, 1); + + state = { + initialAvatarIndex: 0, + avatarIndices: [] + }; + + getAvatarIndex = (offset = 0) => AvatarSelector.getAvatarIndex(this.props, offset); nextAvatarIndex = () => this.getAvatarIndex(-1); previousAvatarIndex = () => this.getAvatarIndex(1); @@ -33,6 +42,64 @@ class AvatarSelector extends Component { this.props.onChange(previousAvatarId); }; + constructor(props) { + super(props); + this.state.initialAvatarIndex = AvatarSelector.getAvatarIndex(props); + this.state.avatarIndices = [ + AvatarSelector.nextAvatarIndex(props), + this.state.initialAvatarIndex, + AvatarSelector.previousAvatarIndex(props) + ]; + } + + componentWillReceiveProps(nextProps) { + // Push new avatar indices onto the array if necessary. + this.setState(state => { + const numAvatars = nextProps.avatars.length; + if (state.avatarIndices.length === numAvatars) return; + + const lastIndex = numAvatars - 1; + const currAvatarIndex = this.getAvatarIndex(); + const nextAvatarIndex = AvatarSelector.getAvatarIndex(nextProps); + const avatarIndices = Array.from(state.avatarIndices); + const increasing = currAvatarIndex - nextAvatarIndex < 0; + + let direction = -1; + let push = false; + + if (nextAvatarIndex === 0) { + if (currAvatarIndex === lastIndex) { + direction = 1; + push = avatarIndices.indexOf(lastIndex) !== 0; + } else { + direction = -1; + push = avatarIndices.indexOf(1) !== 0; + } + } else if (nextAvatarIndex === lastIndex) { + if (currAvatarIndex === 0) { + direction = -1; + push = avatarIndices.indexOf(0) === 0; + } else { + direction = 1; + push = avatarIndices.indexOf(lastIndex - 1) !== 0; + } + } else { + direction = increasing ? 1 : -1; + push = increasing; + } + + const addIndex = AvatarSelector.getAvatarIndex(nextProps, direction); + if (avatarIndices.includes(addIndex)) return; + + if (push) { + avatarIndices.push(addIndex); + } else { + avatarIndices.unshift(addIndex); + } + return { avatarIndices }; + }); + } + componentDidUpdate(prevProps) { if (this.props.avatarId !== prevProps.avatarId) { // HACK - a-animation ought to restart the animation when the `to` attribute changes, but it doesn't @@ -59,10 +126,10 @@ class AvatarSelector extends Component { const avatarAssets = this.props.avatars.map(avatar => ( <a-asset-item id={avatar.id} key={avatar.id} response-type="arraybuffer" src={`${avatar.model}`} /> )); - - const avatarEntities = this.props.avatars.map((avatar, i) => ( - <a-entity key={avatar.id} position="0 0 0" rotation={`0 ${360 * -i / this.props.avatars.length} 0`}> - <a-entity position="0 0 5" rotation="0 0 0" gltf-model-plus={`src: #${avatar.id}`} inflate="true"> + const avatarData = this.state.avatarIndices.map(i => [this.props.avatars[i], i]); + const avatarEntities = avatarData.map(([avatar, i]) => ( + <a-entity key={avatar.id} rotation={`0 ${360 * -i / this.props.avatars.length} 0`}> + <a-entity position="0 0 5" gltf-model-plus={`src: #${avatar.id}`} inflate="true"> <template data-selector=".RootScene"> <a-entity animation-mixer /> </template> @@ -77,33 +144,32 @@ class AvatarSelector extends Component { </a-entity> )); + const rotationFromIndex = index => (360 * index / this.props.avatars.length + 180) % 360; + const initialRotation = rotationFromIndex(this.state.initialAvatarIndex); + const toRotation = rotationFromIndex(this.getAvatarIndex()); + return ( <div className="avatar-selector"> - <div className="loading-panel"> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> - </div> - </div> - </div> <a-scene vr-mode-ui="enabled: false" ref={sce => (this.scene = sce)}> <a-assets> {avatarAssets} <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src={meetingSpace} /> </a-assets> - <a-entity> + <a-entity rotation={`0 ${initialRotation} 0`}> <a-animation ref={anm => (this.animation = anm)} attribute="rotation" dur="2000" easing="ease-out" - to={`0 ${(360 * this.getAvatarIndex() / this.props.avatars.length + 180) % 360} 0`} + to={`0 ${toRotation} 0`} /> {avatarEntities} </a-entity> - <a-entity position="0 1.5 -5.6" rotation="-10 180 0" camera /> + <a-entity position="0 1.5 -5.6" rotation="-10 180 0"> + <a-entity camera /> + </a-entity> <a-entity hide-when-quality="low" diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 8d1c341ad3b0337ca71663629ce5705f55297ae8..0e7b22c0042f0e05aad3634b55bc43f91bd4a1dd 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -93,11 +93,20 @@ class ProfileEntryPanel extends Component { ref={inp => (this.nameInput = inp)} /> </label> - <iframe - className="profile-entry__avatar-selector" - src={`/${this.props.htmlPrefix}avatar-selector.html#avatar_id=${this.state.avatarId}`} - ref={ifr => (this.avatarSelector = ifr)} - /> + <div className="profile-entry__avatar-selector-container"> + <div className="loading-panel"> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> + </div> + <iframe + className="profile-entry__avatar-selector" + src={`/${this.props.htmlPrefix}avatar-selector.html#avatar_id=${this.state.avatarId}`} + ref={ifr => (this.avatarSelector = ifr)} + /> + </div> <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" })} /> <div className="profile-entry__box__links"> <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> diff --git a/yarn.lock b/yarn.lock index a9e1e2e95bf3783fdc9550bdcce76c190a646d44..fad002f47b96807bab71374126c6e0e129ec86fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5458,7 +5458,7 @@ neo-async@^2.5.0: "nipplejs@https://github.com/mozillareality/nipplejs#mr-social-client/master": version "0.6.8" - resolved "https://github.com/mozillareality/nipplejs#2ee0f479b66182aec2f338f2961f1eaeeccaeb1c" + resolved "https://github.com/mozillareality/nipplejs#7b5f953f75df28d42689e96c6a8342ab0a3cb595" no-case@^2.2.0: version "2.3.2"