diff --git a/package.json b/package.json index a86b58dbedf0f72813f92978b5120294ea8d4c40..123aaec21dbee756dbaa10b5229c8be8f0816315 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "prettier": "prettier --write src/**/*.js" }, "dependencies": { + "@fortawesome/fontawesome": "^1.1.5", + "@fortawesome/fontawesome-free-solid": "^5.0.9", + "@fortawesome/react-fontawesome": "^0.0.18", "aframe-billboard-component": "^1.0.0", "aframe-extras": "^3.12.4", "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array", diff --git a/src/assets/avatars/BotBobo_Avatar.glb b/src/assets/avatars/BotBobo_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..66862adb28f35fb3bb1ca4954d840a1f9a91bb96 Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar.glb differ diff --git a/src/assets/avatars/BotBobo_Avatar_Unlit.glb b/src/assets/avatars/BotBobo_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..e4a939a242fc930dc52c7d60aa145b7b86023330 Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb index 9c72e3d2fc170d6b0782d741eca441a62780daad..098a042ead7b919170c6c4c6a63ba40f62a1b81b 100644 Binary files a/src/assets/avatars/BotDom_Avatar.glb and b/src/assets/avatars/BotDom_Avatar.glb differ diff --git a/src/assets/avatars/BotDom_Avatar_Unlit.glb b/src/assets/avatars/BotDom_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..88c830ae9f73581683050705ad75f321bdeecb37 Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotGreg_Avatar.glb b/src/assets/avatars/BotGreg_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..33ca3ee66249ed267688353ef0ab82fd97dfc8bc Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar.glb differ diff --git a/src/assets/avatars/BotGreg_Avatar_Unlit.glb b/src/assets/avatars/BotGreg_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..ed0a5291790f79029b6c7e624a45253c5f6caeb3 Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotGuest_Avatar.glb b/src/assets/avatars/BotGuest_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..e0be8e9cbad1a1d6167c0c0ccf333285d0857472 Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar.glb differ diff --git a/src/assets/avatars/BotGuest_Avatar_Unlit.glb b/src/assets/avatars/BotGuest_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..3bc50b18e3ffbf9f9007a6f7790a8512284949a7 Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotJim_Avatar.glb b/src/assets/avatars/BotJim_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..34c1a8cf274d0ed62b2bc22b51d573f6bafc1728 Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar.glb differ diff --git a/src/assets/avatars/BotJim_Avatar_Unlit.glb b/src/assets/avatars/BotJim_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..534356f80deb17b8808bc778db2db048811baf6a Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotKev_Avatar.glb b/src/assets/avatars/BotKev_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..a20a54bcc42926a79b623e360a4ed9ddb2380695 Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar.glb differ diff --git a/src/assets/avatars/BotKev_Avatar_Unlit.glb b/src/assets/avatars/BotKev_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..d70196b7a3f83f25445cef27cf0fbc7493805cd2 Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotPinky_Avatar.glb b/src/assets/avatars/BotPinky_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..da14ccd6ada339e7d28c1dfcd636a0810d45907c Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar.glb differ diff --git a/src/assets/avatars/BotPinky_Avatar_Unlit.glb b/src/assets/avatars/BotPinky_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..4029c8c6feb3dfe137d932ff0e7eb9223c60b6d1 Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotRobert_Avatar.glb b/src/assets/avatars/BotRobert_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..68d28e5a19616eae6e5e5c8694182efe02643c65 Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar.glb differ diff --git a/src/assets/avatars/BotRobert_Avatar_Unlit.glb b/src/assets/avatars/BotRobert_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..505eae50b5604bdbe3a7076421dabd1847e561f1 Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotWoody_Avatar.glb b/src/assets/avatars/BotWoody_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..0a205ee9311b5c73f96f5fdf562970bd362b1a21 Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar.glb differ diff --git a/src/assets/avatars/BotWoody_Avatar_Unlit.glb b/src/assets/avatars/BotWoody_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..bd330d47f269ccaae436fbb3a33aa85b51eee6fd Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/avatars.json b/src/assets/avatars/avatars.json new file mode 100644 index 0000000000000000000000000000000000000000..8f04c546fbe55d90507ab92ee2e3e599144d7d29 --- /dev/null +++ b/src/assets/avatars/avatars.json @@ -0,0 +1,67 @@ +{ + "avatars": [ + { + "id": "botdefault", + "models": { + "low": "BotDefault_Avatar_Unlit.glb", + "high": "BotDefault_Avatar.glb" + } + }, + { + "id": "botbobo", + "models": { + "low": "BotBobo_Avatar_Unlit.glb", + "high": "BotBobo_Avatar.glb" + } + }, + { + "id": "botdom", + "models": { + "low": "BotDom_Avatar_Unlit.glb", + "high": "BotDom_Avatar.glb" + } + }, + { + "id": "botgreg", + "models": { + "low": "BotGreg_Avatar_Unlit.glb", + "high": "BotGreg_Avatar.glb" + } + }, + { + "id": "botguest", + "models": { + "low": "BotGuest_Avatar_Unlit.glb", + "high": "BotGuest_Avatar.glb" + } + }, + { + "id": "botjim", + "models": { + "low": "BotJim_Avatar_Unlit.glb", + "high": "BotJim_Avatar.glb" + } + }, + { + "id": "botpinky", + "models": { + "low": "BotPinky_Avatar_Unlit.glb", + "high": "BotPinky_Avatar.glb" + } + }, + { + "id": "botrobert", + "models": { + "low": "BotRobert_Avatar_Unlit.glb", + "high": "BotRobert_Avatar.glb" + } + }, + { + "id": "botwoody", + "models": { + "low": "BotWoody_Avatar_Unlit.glb", + "high": "BotWoody_Avatar.glb" + } + } + ] +} diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss new file mode 100644 index 0000000000000000000000000000000000000000..1663ec5b6ba92ef81c1b7fc9955f98dd0788272b --- /dev/null +++ b/src/assets/stylesheets/avatar-selector.scss @@ -0,0 +1,45 @@ +@import 'fonts'; +@import 'shared'; + +#selector-root { + height: 100%; +} +.avatar-selector { + overflow: hidden; + height: 100%; + display: flex; + justify-content: center; + + &__previous-button:focus, &__next-button:focus { + outline: none; + } + &__previous-button:active, &__next-button:active { + color: grey; + } + &__previous-button, &__next-button { + position: absolute; + top: 50%; + margin-top: -0.5em; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + background: transparent; + color: white; + border: none; + font-size: 64pt; + } + &__previous-button { + left: 0.2em; + } + &__next-button { + right: 0.2em; + } + &__loading { + @extend %default-font; + display: block; + position: absolute; + top: 50%; + text-align: center; + color: white; + } +} diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index 05b2e5d801f42cee41a9e245eb57daed200798cc..9b45afd389dd5006d1596ee11d94f41a1ee59650 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -8,17 +8,27 @@ align-items: center; display: flex; pointer-events: auto; + + &__avatar-selector { + border: none; + width: 95%; + height: 100%; + margin: 1em 0; + } } .profile-entry__box { - height: 150px; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between; align-items: center; - padding: 25px; + padding: 15px; flex: 1 1 100%; + width: 60vw; + min-width: 300px; + max-width: 700px; + height: 500px } .profile-entry__box--darkened { @@ -42,6 +52,7 @@ line-height: 2.0em; padding-left: 1.25em; padding-right: 1.25em; + margin: 0.5em 0; } .profile-entry__form-submit { diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 04b25e90c782c04b60860524f98be0600627fcfe..4c9af592fa7b15592af0961f4df84aaf3b1fe2d5 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -16,6 +16,7 @@ "profile.save": "SAVE", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Your identity", + "profile.avatar-selector.loading": "Loading Avatars...", "audio.title": "Test your audio", "audio.subtitle-desktop": "Confirm HMD speaker output", "audio.subtitle-mobile": "Earphones are recommended", diff --git a/src/avatar-selector.html b/src/avatar-selector.html new file mode 100644 index 0000000000000000000000000000000000000000..8496ef94d7f95ec90d29ae5c7e3c030c15659238 --- /dev/null +++ b/src/avatar-selector.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> + <% if(NODE_ENV === "production") { %> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script> + <% } else { %> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script> + <% } %> +</head> + +<body> + <div id="selector-root"></div> +</body> + +</html> diff --git a/src/avatar-selector.js b/src/avatar-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..34731ff37823ba04f3899dd02d34bdc6a8453e4c --- /dev/null +++ b/src/avatar-selector.js @@ -0,0 +1,53 @@ +import ReactDOM from "react-dom"; +import React from "react"; +import queryString from "query-string"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import en from "react-intl/locale-data/en"; + +import "./assets/stylesheets/avatar-selector.scss"; +import "./vendor/GLTFLoader"; + +import "./components/animation-mixer"; +import "./components/audio-feedback"; +import "./components/loop-animation"; +import "./elements/a-progressive-asset"; +import "./gltf-component-mappings"; +import { avatars } from "./assets/avatars/avatars.json"; +import { avatarIds } from "./utils/identity"; + +import { App } from "./App"; +import AvatarSelector from "./react-components/avatar-selector"; +import localeData from "./assets/translations.data.json"; + +window.APP = new App(); +const hash = queryString.parse(location.hash); +const isMobile = AFRAME.utils.device.isMobile(); +if (hash.quality) { + window.APP.quality = hash.quality; +} else { + window.APP.quality = isMobile ? "low" : "high"; +} + +const lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage) + .toLowerCase() + .split(/[_-]+/)[0]; +addLocaleData([...en]); +const messages = localeData[lang] || localeData.en; + +function postAvatarIdToParent(newAvatarId) { + window.parent.postMessage({ avatarId: newAvatarId }, location.origin); +} + +function mountUI() { + const hash = queryString.parse(location.hash); + const avatarId = hash.avatar_id; + ReactDOM.render( + <IntlProvider locale={lang} messages={messages}> + <AvatarSelector {...{ avatars, avatarId, onChange: postAvatarIdToParent }} /> + </IntlProvider>, + document.getElementById("selector-root") + ); +} + +window.addEventListener("hashchange", mountUI); +document.addEventListener("DOMContentLoaded", mountUI); diff --git a/src/components/player-info.js b/src/components/player-info.js index 8cb265a67b9c67ac26510d6b8a03efb59703c931..ac2fc6b55fcdaf0215e26327368c07dd67e88fcc 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -1,7 +1,7 @@ AFRAME.registerComponent("player-info", { schema: { displayName: { type: "string" }, - avatar: { type: "string" } + avatarSrc: { type: "string" } }, init() { this.applyProperties = this.applyProperties.bind(this); @@ -24,8 +24,8 @@ AFRAME.registerComponent("player-info", { } const modelEl = this.el.querySelector(".model"); - if (this.data.avatar && modelEl) { - modelEl.setAttribute("src", this.data.avatar); + if (this.data.avatarSrc && modelEl) { + modelEl.setAttribute("src", this.data.avatarSrc); } } }); diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js index e88f7a76bf6ee4218f37997d9497e576b3bed55a..33489e2bc1792aa2b9dd4bddca360caaa39c509b 100644 --- a/src/elements/a-gltf-entity.js +++ b/src/elements/a-gltf-entity.js @@ -2,6 +2,9 @@ const GLTFCache = {}; AFRAME.AGLTFEntity = { defaultInflator(el, componentName, componentData) { + if (!AFRAME.components[componentName]) { + throw new Error(`Inflator failed. "${componentName}" component does not exist.`); + } if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) { for (let i = 0; i < componentData.length; i++) { el.setAttribute(componentName + "__" + i, componentData[i]); @@ -220,7 +223,10 @@ AFRAME.registerElement("a-gltf-entity", { this.querySelectorAll(":scope > template").forEach(templateEl => this.templates.push({ selector: templateEl.getAttribute("data-selector"), - templateRoot: document.importNode(templateEl.content.firstElementChild, true) + templateRoot: document.importNode( + templateEl.firstElementChild || templateEl.content.firstElementChild, + true + ) }) ); } @@ -232,6 +238,10 @@ AFRAME.registerElement("a-gltf-entity", { // If the src attribute is a selector, get the url from the asset item. if (src && src.charAt(0) === "#") { const assetEl = document.getElementById(src.substring(1)); + if (!assetEl) { + console.warn(`Attempted to use non-existent asset ${src} as src for`, this); + return; + } const fallbackSrc = assetEl.getAttribute("src"); const highSrc = assetEl.getAttribute("high-src"); @@ -282,8 +292,7 @@ AFRAME.registerElement("a-gltf-entity", { this.emit("model-loaded", { format: "gltf", model: this.model }); } catch (e) { - const message = (e && e.message) || "Failed to load glTF model"; - console.error(message); + console.error("Failed to load glTF model", e.message, this); this.emit("model-error", { format: "gltf", src }); } } @@ -303,7 +312,6 @@ AFRAME.registerElement("a-gltf-entity", { if (attr === "src") { this.applySrc(newVal); } - AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal); } }, diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js index a367e374aece30163bc73802ab8cf11950f28c1a..8bf922aa17b10ce6eec09baf413e93dcf6d871f1 100644 --- a/src/elements/a-progressive-asset.js +++ b/src/elements/a-progressive-asset.js @@ -9,17 +9,17 @@ AFRAME.registerElement("a-progressive-asset", { value() { this.data = null; this.isAssetItem = true; + } + }, + attachedCallback: { + value() { if (!this.parentNode.fileLoader) { throw new Error("a-progressive-asset must be the child of an a-assets element."); } this.fileLoader = this.parentNode.fileLoader; - } - }, - attachedCallback: { - value() { const self = this; const fallbackSrc = this.getAttribute("src"); const highSrc = this.getAttribute("high-src"); diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..c534c8ac2ea7997d96ea510649e0735a4f734e57 --- /dev/null +++ b/src/react-components/avatar-selector.js @@ -0,0 +1,126 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import faAngleLeft from '@fortawesome/fontawesome-free-solid/faAngleLeft'; +import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight'; + +class AvatarSelector extends Component { + static propTypes = { + avatars: PropTypes.array, + avatarId: PropTypes.string, + 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; + } + nextAvatarIndex = () => this.getAvatarIndex(1) + previousAvatarIndex = () => this.getAvatarIndex(-1) + + emitChangeToNext = () => { + const nextAvatarId = this.props.avatars[this.nextAvatarIndex()].id; + this.props.onChange(nextAvatarId); + } + + emitChangeToPrevious = () => { + const previousAvatarId = this.props.avatars[this.previousAvatarIndex()].id; + this.props.onChange(previousAvatarId); + } + + 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 + // so we need to force it here. + const currRot = this.animation.parentNode.getAttribute('rotation'); + this.animation.setAttribute('from', `${currRot.x} ${currRot.y} ${currRot.z}`); + this.animation.stop(); + this.animation.handleMixinUpdate(); + this.animation.start(); + } + } + + render () { + const avatarAssets = this.props.avatars.map(avatar => ( + <a-progressive-asset + id={avatar.id} + key={avatar.id} + response-type="arraybuffer" + high-src={`./src/assets/avatars/${avatar.models.high}`} + low-src={`./src/assets/avatars/${avatar.models.low}`} + ></a-progressive-asset> + )); + + 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-gltf-entity position="0 0 5" rotation="0 0 0" src={'#' + avatar.id} inflate="true"> + <template data-selector=".RootScene"> + <a-entity animation-mixer></a-entity> + </template> + <a-animation + attribute="rotation" + dur="2000" + to={`0 ${this.getAvatarIndex() === i ? 360 : 0} 0`} + repeat="indefinite"> + </a-animation> + </a-gltf-entity> + </a-entity> + )); + + return ( + <div className="avatar-selector"> + <span className="avatar-selector__loading"> + <FormattedMessage id="profile.avatar-selector.loading"/> + </span> + <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="./src/assets/environments/MeetingSpace1_mesh.glb" + ></a-asset-item> + </a-assets> + + <a-entity data-avatar={this.props.avatarId}> + <a-animation + ref={anm => this.animation = anm} + attribute="rotation" + dur="1000" + easing="ease-out" + to={`0 ${360 * this.getAvatarIndex() / this.props.avatars.length + 180} 0`}> + </a-animation> + {avatarEntities} + </a-entity> + + <a-entity position="0 1.5 -5.6" rotation="-10 180 0" camera></a-entity> + + <a-entity + hide-when-quality="low" + light="type: directional; color: #F9FFCE; intensity: 0.6" + position="0 5 -15" + ></a-entity> + <a-entity + hide-when-quality="low" + light="type: ambient; color: #FFF" + ></a-entity> + <a-gltf-entity + id="meeting-space" + src="#meeting-space1-mesh" + position="0 0 0" + ></a-gltf-entity> + </a-scene> + <button className="avatar-selector__previous-button" onClick={this.emitChangeToPrevious}> + <FontAwesomeIcon icon={faAngleLeft} /> + </button> + <button className="avatar-selector__next-button" onClick={this.emitChangeToNext}> + <FontAwesomeIcon icon={faAngleRight} /> + </button> + </div> + ); + } +} + +export default injectIntl(AvatarSelector); diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 28c8cd7d9ce8c8bf9902f6a654c835bbd7fe95ea..cc20df8a4679475744f7c40f997f87b71d2b4daf 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -13,29 +13,50 @@ class ProfileEntryPanel extends Component { constructor(props) { super(props); window.store = this.props.store; - this.state = {name: this.props.store.state.profile.display_name}; + this.state = { + display_name: this.props.store.state.profile.display_name, + avatar_id: this.props.store.state.profile.avatar_id, + }; this.props.store.addEventListener("statechanged", this.storeUpdated); } storeUpdated = () => { - this.setState({name: this.props.store.state.profile.display_name}); + const { avatar_id, display_name } = this.props.store.state.profile; + this.setState({ avatar_id, display_name }); } - saveName = (e) => { + saveStateAndFinish = (e) => { e.preventDefault(); - this.props.store.update({ profile: { display_name: this.nameInput.value } }); + this.props.store.update({profile: { + display_name: this.state.display_name, + avatar_id: this.state.avatar_id + }}); this.props.finished(); } + stopPropagation = (e) => { + e.stopPropagation(); + } + + setAvatarStateFromIframeMessage = (e) => { + if (e.source !== this.avatarSelector.contentWindow) { return; } + this.setState({avatar_id: e.data.avatarId}); + } + componentDidMount() { // stop propagation so that avatar doesn't move when wasd'ing during text input. - this.nameInput.addEventListener('keydown', e => e.stopPropagation()); - this.nameInput.addEventListener('keypress', e => e.stopPropagation()); - this.nameInput.addEventListener('keyup', e => e.stopPropagation()); + this.nameInput.addEventListener('keydown', this.stopPropagation); + this.nameInput.addEventListener('keypress', this.stopPropagation); + this.nameInput.addEventListener('keyup', this.stopPropagation); + window.addEventListener('message', this.setAvatarStateFromIframeMessage); } - + componentWillUnmount() { this.props.store.removeEventListener('statechanged', this.storeUpdated); + this.nameInput.removeEventListener('keydown', this.stopPropagation); + this.nameInput.removeEventListener('keypress', this.stopPropagation); + this.nameInput.removeEventListener('keyup', this.stopPropagation); + window.removeEventListener('message', this.setAvatarStateFromIframeMessage); } render () { @@ -43,19 +64,23 @@ class ProfileEntryPanel extends Component { return ( <div className="profile-entry"> - <form onSubmit={this.saveName}> + <form onSubmit={this.saveStateAndFinish}> <div className="profile-entry__box profile-entry__box--darkened"> <div className="profile-entry__subtitle"> <FormattedMessage id="profile.header"/> </div> <input className="profile-entry__form-field-text" - value={this.state.name} onChange={(e) => this.setState({name: e.target.value})} + value={this.state.display_name} onChange={(e) => this.setState({display_name: e.target.value})} required pattern={SCHEMA.definitions.profile.properties.display_name.pattern} title={formatMessage({ id: "profile.display_name.validation_warning" })} ref={inp => this.nameInput = inp}/> + <iframe + className="profile-entry__avatar-selector" + src={`avatar-selector.html#avatar_id=${this.state.avatar_id}`} + ref={ifr => this.avatarSelector = ifr}></iframe> <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" }) }/> - </div> + </div> </form> </div> ); diff --git a/src/room.html b/src/room.html index 132e6af23c42d61553fc1c78c43ff0afc0cf188b..5f63358aa5e17651ee03d9978f098035e533b164 100644 --- a/src/room.html +++ b/src/room.html @@ -29,13 +29,69 @@ <img id="avatar" src="./assets/hud/avatar.jpg" > <a-progressive-asset - id="bot-skinned-mesh" + id="botdefault" response-type="arraybuffer" src="./assets/avatars/BotDefault_Avatar_Unlit.glb" high-src="./assets/avatars/BotDefault_Avatar.glb" low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb" ></a-progressive-asset> - <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item> + <a-progressive-asset + id="botbobo" + response-type="arraybuffer" + src="./assets/avatars/BotBobo_Avatar_Unlit.glb" + high-src="./assets/avatars/BotBobo_Avatar.glb" + low-src="./assets/avatars/BotBobo_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botdom" + response-type="arraybuffer" + src="./assets/avatars/BotDom_Avatar_Unlit.glb" + high-src="./assets/avatars/BotDom_Avatar.glb" + low-src="./assets/avatars/BotDom_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botgreg" + response-type="arraybuffer" + src="./assets/avatars/BotGreg_Avatar_Unlit.glb" + high-src="./assets/avatars/BotGreg_Avatar.glb" + low-src="./assets/avatars/BotGreg_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botguest" + response-type="arraybuffer" + src="./assets/avatars/BotGuest_Avatar_Unlit.glb" + high-src="./assets/avatars/BotGuest_Avatar.glb" + low-src="./assets/avatars/BotGuest_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botjim" + response-type="arraybuffer" + src="./assets/avatars/BotJim_Avatar_Unlit.glb" + high-src="./assets/avatars/BotJim_Avatar.glb" + low-src="./assets/avatars/BotJim_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botpinky" + response-type="arraybuffer" + src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + high-src="./assets/avatars/BotRobert_Avatar.glb" + low-src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botrobert" + response-type="arraybuffer" + src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + high-src="./assets/avatars/BotRobert_Avatar.glb" + low-src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botwoody" + response-type="arraybuffer" + src="./assets/avatars/BotWoody_Avatar_Unlit.glb" + high-src="./assets/avatars/BotWoody_Avatar.glb" + low-src="./assets/avatars/BotWoody_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item> @@ -124,7 +180,7 @@ <!-- Interactables --> <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity> - <a-entity + <a-entity gltf-model="#interactable-duck" scale="2 2 2" class="interactable" @@ -215,7 +271,7 @@ <a-gltf-entity class="model" inflate="true"> <template data-selector=".RootScene"> <a-entity - ik-controller + ik-controller animated-robot-hands animation-mixer ></a-entity> @@ -318,12 +374,12 @@ xr="ar: false" ></a-entity> - <a-cylinder - position="0 0.45 0" - material="visible: false" - height="1" radius="3.1" - segments-radial="12" - static-body + <a-cylinder + position="0 0.45 0" + material="visible: false" + height="1" radius="3.1" + segments-radial="12" + static-body class="collidable" ></a-cylinder> diff --git a/src/room.js b/src/room.js index 909e8977f7bd4ce6d30e6ce4ddfc183a7e685c3b..232bc18accb2d4150f3213e09f1cbaac5d0c4d47 100644 --- a/src/room.js +++ b/src/room.js @@ -113,13 +113,11 @@ async function exitScene() { document.body.removeChild(scene); } -function updatePlayerInfoFromStore() { +function applyProfileFromStore(playerRig) { const displayName = store.state.profile.display_name; - const qs = queryString.parse(location.search); - const playerRig = document.querySelector("#player-rig"); playerRig.setAttribute("player-info", { displayName, - avatar: qs.avatar || "#bot-skinned-mesh" + avatarSrc: '#' + (store.state.profile.avatar_id || "botdefault") }); document.querySelector("a-scene").emit("username-changed", { username: displayName }); } @@ -127,8 +125,6 @@ function updatePlayerInfoFromStore() { async function enterScene(mediaStream, enterInVR) { const scene = document.querySelector("a-scene"); const playerRig = document.querySelector("#player-rig"); - const qs = queryString.parse(location.search); - document.querySelector("a-scene canvas").classList.remove("blurred"); registerNetworkSchemas(); @@ -155,8 +151,9 @@ async function enterScene(mediaStream, enterInVR) { playerRig.setAttribute("virtual-gamepad-controls", {}); } - updatePlayerInfoFromStore(); - store.addEventListener("statechanged", updatePlayerInfoFromStore); + const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); + applyProfileOnPlayerRig(); + store.addEventListener("statechanged", applyProfileOnPlayerRig); const avatarScale = parseInt(qs.avatar_scale, 10); diff --git a/src/storage/store.js b/src/storage/store.js index 5b00b7ac8f5570c28651587997ae4c0546a3b722..037e27bf1d9603a210423baa5ee6543c2ee77fdb 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -17,6 +17,7 @@ export const SCHEMA = { additionalProperties: false, properties: { 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 72cabdf008bc2574b4408d62c8f7f17f30fc9a59..727be75580bae2701de1c8ae2c81315d60a635b5 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -1,3 +1,5 @@ +import { avatars } from "../assets/avatars/avatars.json"; + const names = [ "albattani", "allen", @@ -161,7 +163,16 @@ const names = [ "yonath" ]; +function selectRandom(arr) { + return arr[Math.floor(Math.random() * arr.length)] +} + +export const avatarIds = avatars.map(av => av.id); + export function generateDefaultProfile() { - const name = names[Math.floor(Math.random() * names.length)]; - return { display_name: name.replace(/^./, name[0].toUpperCase()) }; + const name = selectRandom(names); + return { + display_name: name.replace(/^./, name[0].toUpperCase()) , + avatar_id: selectRandom(avatarIds) + }; } diff --git a/webpack.config.js b/webpack.config.js index 4996b8925731f3e5912b954dafa5c6b1dc2a66c2..d404babbb6d5af438271621b4e3f2fbac235a266 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -77,6 +77,7 @@ const config = { entry: { lobby: path.join(__dirname, "src", "lobby.js"), room: path.join(__dirname, "src", "room.js"), + 'avatar-selector': path.join(__dirname, "src", "avatar-selector.js"), onboarding: path.join(__dirname, "src", "onboarding.js") }, output: { @@ -196,6 +197,12 @@ const config = { chunks: ["room"], inject: "head" }), + new HTMLWebpackPlugin({ + filename: "avatar-selector.html", + template: path.join(__dirname, "src", "avatar-selector.html"), + chunks: ["avatar-selector"], + inject: "head" + }), new HTMLWebpackPlugin({ filename: "onboarding.html", template: path.join(__dirname, "src", "onboarding.html"), diff --git a/yarn.lock b/yarn.lock index b2fc3a25d7605205ddad6244206d2076f41149ac..71b37b2f51e39ff013774a7a96b8e2e23c8441b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,28 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" +"@fortawesome/fontawesome-common-types@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.3.tgz#8475e0f2d1ad1f858c4ec2e76ed9a2456a09ad83" + +"@fortawesome/fontawesome-free-solid@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.9.tgz#456155a1cd82a0342ffe6a869d5a54fdadd78548" + dependencies: + "@fortawesome/fontawesome-common-types" "^0.1.3" + +"@fortawesome/fontawesome@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome/-/fontawesome-1.1.5.tgz#c7cfafdd3364245626293cc670357f9fb8487170" + dependencies: + "@fortawesome/fontawesome-common-types" "^0.1.3" + +"@fortawesome/react-fontawesome@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.0.18.tgz#4e0eb1cf9797715a67bb7705ae084fa0a410f185" + dependencies: + humps "^2.0.1" + JSONStream@^1.0.3: version "1.3.2" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" @@ -3911,6 +3933,10 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +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: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"