diff --git a/.travis.yml b/.travis.yml index 890fcfd3ac3ca6f4d4ac9507586110c817dc3edf..57b5ad30beddf0a3e3c7f856fb89cb7fc3b11f99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js -node_js: node +node_js: "9" cache: yarn before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.5.1 diff --git a/package.json b/package.json index b70f19a6db7b579488a153b78efc7c57ee6cc7fb..bf3cae400bf95c01a23c6200b49007c4a2b72344 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@fortawesome/react-fontawesome": "^0.0.18", "aframe-billboard-component": "^1.0.0", "aframe-extras": "https://github.com/MozillaReality/aframe-extras#feature/precompute-nav-mesh", - "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array", + "aframe-input-mapping-component": "https://github.com/mozillareality/aframe-input-mapping-component#hubs/master", "aframe-physics-extras": "https://github.com/infinitelee/aframe-physics-extras#fix/physics-collider-crash", "aframe-physics-system": "https://github.com/infinitelee/aframe-physics-system#feature/shape-component", "aframe-rounded": "^1.0.3", diff --git a/scripts/build_local_reticulum.sh b/scripts/build_local_reticulum.sh index 118a801e24fba4fdbed51c5b23f793db46cc498f..f883958f8f18e028a32916f50c6af38002938893 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 ; 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=http://localhost:4000/ yarn build -- --output-path ../reticulum/priv/static diff --git a/scripts/default.env b/scripts/default.env index b5dbe2c7c9ec3b22eba8d36f07c2fbd700bf0cb0..3d6556c0fa826b118d94dc8bc88a9f2555c5a520 100644 --- a/scripts/default.env +++ b/scripts/default.env @@ -1,6 +1,7 @@ # This origin trial token is used to enable WebVR and Gamepad Extensions on Chrome 62+ # You can find more information about getting your own origin trial token here: https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/developer-guide.md -ORIGIN_TRIAL_TOKEN="ArEZ0vY0uMo3pj+oY8Up4u4Hy8QolJwKxG4/2WRhSPnTZRrviiGhzP6/y72nBdsIhdEyoundxqg//KLbs2vGnQoAAABkeyJvcmlnaW4iOiJodHRwczovL3JldGljdWx1bS5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViVlIxLjFNNjIiLCJleHBpcnkiOjE1MjYzNDg2MjEsImlzU3ViZG9tYWluIjp0cnVlfQ==" +ORIGIN_TRIAL_TOKEN="AgN/JtqSF6qpD3OZk8KgM5/UYqUUrwc166cOQSRCqvU+TIpHWdiwBUWH5V1K/jJkdtBrO4Q5I0XSGm16uB/Y4QQAAABVeyJvcmlnaW4iOiJodHRwczovL2h1YnMubW96aWxsYS5jb206NDQzIiwiZmVhdHVyZSI6IldlYlZSMS4xTTYyIiwiZXhwaXJ5IjoxNTI4MjQ1ODI1fQ==" ORIGIN_TRIAL_EXPIRES="2018-05-15" JANUS_SERVER="wss://prod-janus.reticulum.io" DEV_RETICULUM_SERVER="dev.reticulum.io" +ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io" diff --git a/src/assets/environments/environments.js b/src/assets/environments/environments.js new file mode 100644 index 0000000000000000000000000000000000000000..7c20ce61da763de912493c4e8d35385d09347b5a --- /dev/null +++ b/src/assets/environments/environments.js @@ -0,0 +1,7 @@ +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" +]; + +export const DEFAULT_ENVIRONMENT_URL = ENVIRONMENT_URLS[0]; diff --git a/src/assets/images/hub-preview.png b/src/assets/images/hub-preview.png new file mode 100755 index 0000000000000000000000000000000000000000..5a976607e2539031d67dc17e727ecff02740c3ad Binary files /dev/null and b/src/assets/images/hub-preview.png differ diff --git a/src/assets/sfx/quack.mp3 b/src/assets/sfx/quack.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5c9d5c87de7cc144af2afe5175151cf0c89ecff1 Binary files /dev/null and b/src/assets/sfx/quack.mp3 differ diff --git a/src/assets/sfx/specialquack.mp3 b/src/assets/sfx/specialquack.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ac461e6c916fc2cb86230d66661c8316bd415fbb Binary files /dev/null and b/src/assets/sfx/specialquack.mp3 differ diff --git a/src/assets/stylesheets/footer.scss b/src/assets/stylesheets/footer.scss index f0a48bc6d15b8c1efab6c902f7e7024985f12f5f..5f782fd03c56c8be7f7115b1a99519c22ad26804 100644 --- a/src/assets/stylesheets/footer.scss +++ b/src/assets/stylesheets/footer.scss @@ -10,7 +10,7 @@ // Position above virtual gamepad controls on mobile z-index: 1; - @media (min-width: 769px) and (min-height: 401px) { + @media (min-width: 769px) and (min-height: 421px) { pointer-events: auto; } } @@ -35,25 +35,25 @@ background-color: transparent; border-bottom: 1px solid rgba(32, 32, 32, 0.65); - @media (min-width: 769px) , (max-height: 401px) { + @media (min-width: 769px) , (max-height: 421px) { display: none; } } :local(.header) { background-color: rgba(0, 0, 0, 0.65); - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { background-color: transparent; } :local(.hub-info) { - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { display: none; } } :local(.hub-stats) { - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { display: none; } } @@ -64,7 +64,7 @@ margin: 16px 24px; display: flex; align-items: center; - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { margin: 16px 8px; margin-left: 24px; font-size: 0.9em; @@ -76,10 +76,10 @@ display: flex; align-items: center; justify-content: flex-end; - @media (min-width: 769px) and (min-height: 401px) { + @media (min-width: 769px) and (min-height: 421px) { flex: 1; } - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { margin: 16px 8px; } :local(.hub-participant-count) { @@ -109,13 +109,13 @@ } :local(.menu-button__narrow-close-icon) { - @media (max-width: 768px) , (max-height: 400px) { + @media (max-width: 768px) , (max-height: 420px) { display: none; } } :local(.menu-button__wide-close-icon) { - @media (min-width: 769px) and (min-height: 401px) { + @media (min-width: 769px) and (min-height: 421px) { display: none; } } diff --git a/src/components/controls-shape-offset.js b/src/components/controls-shape-offset.js new file mode 100644 index 0000000000000000000000000000000000000000..7ce8764332f0e8b7b6116add6a8e8178a84121f6 --- /dev/null +++ b/src/components/controls-shape-offset.js @@ -0,0 +1,45 @@ +import { CONTROLLER_OFFSETS } from "./hand-controls2.js"; + +AFRAME.registerComponent("controls-shape-offset", { + schema: { + additionalOffset: { default: { x: 0, y: -0.03, z: -0.04 } } + }, + init: function() { + this.controller = null; + this.shapeAdded = false; + + this._handleControllerConnected = this._handleControllerConnected.bind(this); + this.el.addEventListener("controllerconnected", this._handleControllerConnected); + }, + + remove: function() { + this.el.removeEventListener("controllerconnected", this._handleControllerConnected); + }, + + tick: function() { + if (!this.shapeAdded && this.controller) { + this.shapeAdded = true; + const hasOffset = CONTROLLER_OFFSETS.hasOwnProperty(this.controller); + const offset = hasOffset ? CONTROLLER_OFFSETS[this.controller] : CONTROLLER_OFFSETS.default; + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + offset.decompose(position, quaternion, scale); + position.add(this.data.additionalOffset); + quaternion.conjugate(); + + const shape = { + shape: "sphere", + radius: "0.02", + orientation: quaternion, + offset: position + }; + + this.el.setAttribute("shape", shape); + } + }, + + _handleControllerConnected: function(e) { + this.controller = e.detail.name; + } +}); diff --git a/src/components/css-class.js b/src/components/css-class.js new file mode 100644 index 0000000000000000000000000000000000000000..1528ed4d450aea61f63b7426524229ed63609044 --- /dev/null +++ b/src/components/css-class.js @@ -0,0 +1,17 @@ +AFRAME.registerComponent("css-class", { + schema: { + type: "string" + }, + init() { + this.el.classList.add(this.data); + }, + update(oldData) { + if (this.data !== oldData) { + this.el.classList.remove(oldData); + this.el.classList.add(this.data); + } + }, + remove() { + this.el.classList.remove(this.data); + } +}); diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index 572b17d91f606912b612fb3e1b8e440f33be46a0..2acb029db487a392802bd14f6457d2a4d5be7758 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -78,16 +78,13 @@ AFRAME.registerComponent("cursor-controller", { }, play: function() { - if (!this.inVR && this.isMobile && !this.hasPointingDevice) { - document.addEventListener("touchstart", this._handleTouchStart); - document.addEventListener("touchmove", this._handleTouchMove); - document.addEventListener("touchend", this._handleTouchEnd); - } else { - document.addEventListener("mousedown", this._handleMouseDown); - document.addEventListener("mousemove", this._handleMouseMove); - document.addEventListener("mouseup", this._handleMouseUp); - document.addEventListener("wheel", this._handleWheel); - } + document.addEventListener("touchstart", this._handleTouchStart); + document.addEventListener("touchmove", this._handleTouchMove); + document.addEventListener("touchend", this._handleTouchEnd); + document.addEventListener("mousedown", this._handleMouseDown); + document.addEventListener("mousemove", this._handleMouseMove); + document.addEventListener("mouseup", this._handleMouseUp); + document.addEventListener("wheel", this._handleWheel); window.addEventListener("enter-vr", this._handleEnterVR); window.addEventListener("exit-vr", this._handleExitVR); @@ -258,6 +255,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchStart: function(e) { + if (!this.isMobile || this.hasPointingDevice) 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); @@ -303,6 +302,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchMove: function(e) { + if (!this.isMobile || this.hasPointingDevice) return; + for (let i = 0; i < e.touches.length; i++) { const touch = e.touches[i]; if (touch.clientY / window.innerHeight >= 0.8) return true; @@ -312,6 +313,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleTouchEnd: function(e) { + if (!this.isMobile || this.hasPointingDevice) return; + for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; const thisTouchDidNotDriveMousePos = @@ -326,6 +329,8 @@ AFRAME.registerComponent("cursor-controller", { }, _handleMouseDown: function() { + if (this.isMobile && !this.inVR && !this.hasPointingDevice) return; + if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) { this._setLookControlsEnabled(false); this.data.cursor.emit("cursor-grab", {}); @@ -335,10 +340,14 @@ AFRAME.registerComponent("cursor-controller", { }, _handleMouseMove: function(e) { + if (this.isMobile && !this.inVR && !this.hasPointingDevice) return; + this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); }, _handleMouseUp: function() { + if (this.isMobile && !this.inVR && !this.hasPointingDevice) return; + this._setLookControlsEnabled(true); this.data.cursor.emit("cursor-release", {}); this._endTeleport(); diff --git a/src/components/duck.js b/src/components/duck.js new file mode 100644 index 0000000000000000000000000000000000000000..59172942074173c393c7d86e9fd9e121737ae128 --- /dev/null +++ b/src/components/duck.js @@ -0,0 +1,41 @@ +/* global CANNON */ +AFRAME.registerComponent("duck", { + schema: { + initialForce: { default: 0 }, + maxForce: { default: 6.5 }, + maxScale: { default: 5 } + }, + + init: function() { + this.physicsSystem = this.el.sceneEl.systems.physics; + this.hasBody = false; + this.position = new CANNON.Vec3(); + this.force = new CANNON.Vec3(0, this.data.initialForce, 0); + this.initialScale = this.el.object3D.scale.x; + this.maxScale = this.data.maxScale * this.initialScale; + }, + + play: function() { + this.physicsSystem.addComponent(this); + }, + + pause: function() { + this.physicsSystem.removeComponent(this); + }, + + beforeStep: function() { + if (this.el.body && NAF.utils.isMine(this.el)) { + const currentScale = this.el.object3D.scale.x; + const ratio = Math.min(1, (currentScale - this.initialScale) / (this.maxScale - this.initialScale)); + const force = ratio * this.data.maxForce; + if (force > 0) { + const angle = Math.random() * Math.PI * 2; + const x = Math.cos(angle); + const z = Math.sin(angle); + this.force.set(x, force, z); + this.position.set(x * 0.01, 0, z * 0.01); + this.el.body.applyForce(this.force, this.position); + } + } + } +}); diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js index af86cd5da272ea5aee5c60e75578da84a13aaef5..a5f212453d00a4dd6d7fdeb001dd36dc545c5057 100644 --- a/src/components/hand-controls2.js +++ b/src/components/hand-controls2.js @@ -10,7 +10,7 @@ const POSES = { mrpDown: "mrpDown" }; -const CONTROLLER_OFFSETS = { +export const CONTROLLER_OFFSETS = { default: new THREE.Matrix4(), "oculus-touch-controls": new THREE.Matrix4().makeTranslation(0, -0.015, 0.04), "vive-controls": new THREE.Matrix4().compose( @@ -18,6 +18,11 @@ const CONTROLLER_OFFSETS = { new THREE.Quaternion().setFromEuler(new THREE.Euler(-40 * THREE.Math.DEG2RAD, 0, 0)), new THREE.Vector3(1, 1, 1) ), + "windows-motion-controls": new THREE.Matrix4().compose( + new THREE.Vector3(0, -0.017, 0.13), + new THREE.Quaternion().setFromEuler(new THREE.Euler(-40 * THREE.Math.DEG2RAD, 0, 0)), + new THREE.Vector3(1, 1, 1) + ), "daydream-controls": new THREE.Matrix4().makeTranslation(0, 0, -0.04), "gearvr-controls": new THREE.Matrix4() }; diff --git a/src/components/haptic-feedback.js b/src/components/haptic-feedback.js index 35a79cd9555e2864e5c496f671fcb9ddd8831345..9a0a2337de1ef895bdbd3bb699259f3063187cf2 100644 --- a/src/components/haptic-feedback.js +++ b/src/components/haptic-feedback.js @@ -10,7 +10,7 @@ AFRAME.registerComponent("haptic-feedback", { }, init: function() { - this.pulse = this.pulse.bind(this); + this.handlePulse = this.handlePulse.bind(this); this.getActuator = this.getActuator.bind(this); this.getActuator().then(actuator => { this.actuator = actuator; @@ -37,21 +37,27 @@ AFRAME.registerComponent("haptic-feedback", { }, play: function() { - this.el.addEventListener(this.data.hapticEventName, this.pulse); + this.el.addEventListener(this.data.hapticEventName, this.handlePulse); }, pause: function() { - this.el.removeEventListener(this.data.hapticEventName, this.pulse); + this.el.removeEventListener(this.data.hapticEventName, this.handlePulse); }, - pulse: function(event) { + handlePulse: function(event) { const { intensity } = event.detail; - if (!strengthForIntensity[intensity]) { + + if (strengthForIntensity[intensity]) { + this.pulse(strengthForIntensity[intensity]); + } else if (Number(intensity) === intensity) { + this.pulse(intensity); + } else { console.warn(`Invalid intensity : ${intensity}`); - return; } + }, + pulse: function(intensity) { if (this.actuator) { - this.actuator.pulse(strengthForIntensity[intensity], 15); + this.actuator.pulse(intensity, 15); } } }); diff --git a/src/components/icon-button.js b/src/components/icon-button.js index eab80803f4d85ad425a6d5a0c71fc8f6f9ddc333..88f5e7303822ff103d7e67479ab99b3d7bddba4f 100644 --- a/src/components/icon-button.js +++ b/src/components/icon-button.js @@ -5,7 +5,10 @@ AFRAME.registerComponent("icon-button", { activeImage: { type: "string" }, activeHoverImage: { type: "string" }, active: { type: "boolean" }, - haptic: { type: "selector" } + haptic: { type: "selector" }, + tooltip: { type: "selector" }, + tooltipText: { type: "string" }, + activeTooltipText: { type: "string" } }, init() { @@ -53,5 +56,12 @@ AFRAME.registerComponent("icon-button", { const image = active ? (hovering ? "activeHoverImage" : "activeImage") : hovering ? "hoverImage" : "image"; this.el.setAttribute("src", this.data[image]); + + if (this.data.tooltip) { + this.data.tooltip.setAttribute("visible", this.hovering); + this.data.tooltip + .querySelector("[text]") + .setAttribute("text", "value", this.data.active ? this.data.activeTooltipText : this.data.tooltipText); + } } }); diff --git a/src/components/quack.js b/src/components/quack.js new file mode 100644 index 0000000000000000000000000000000000000000..e97b6f8f1bf5cfccd2efab3dcc4f486d09c2330d --- /dev/null +++ b/src/components/quack.js @@ -0,0 +1,27 @@ +AFRAME.registerComponent("quack", { + schema: { + quackPercentage: { default: 1 }, + specialQuackPercentage: { default: 0.01 } + }, + + init: function() { + this._handleGrabStart = this._handleGrabStart.bind(this); + }, + + play: function() { + this.el.addEventListener("grab-start", this._handleGrabStart); + }, + + pause: function() { + this.el.removeEventListener("grab-start", this._handleGrabStart); + }, + + _handleGrabStart: function() { + const rand = Math.random(); + if (rand < this.data.specialQuackPercentage) { + this.el.emit("specialquack"); + } else if (rand < this.data.quackPercentage) { + this.el.emit("quack"); + } + } +}); diff --git a/src/components/scene-shadow.js b/src/components/scene-shadow.js new file mode 100644 index 0000000000000000000000000000000000000000..72d77cf36b36c80279b0683531f4682f1f5b7a47 --- /dev/null +++ b/src/components/scene-shadow.js @@ -0,0 +1,30 @@ +// For use in environment gltf bundles to set scene shadow properties. +AFRAME.registerComponent("scene-shadow", { + schema: { + autoUpdate: { + type: "boolean", + default: true + }, + type: { + type: "string", + default: "pcf" + }, + renderReverseSided: { + type: "boolean", + default: true + }, + renderSingleSided: { + type: "boolean", + default: true + } + }, + init() { + this.originalShadowProperties = this.el.sceneEl.getAttribute("shadow"); + }, + update() { + this.el.sceneEl.setAttribute("shadow", this.data); + }, + remove() { + this.el.sceneEl.setAttribute("shadow", this.originalShadowProperties); + } +}); diff --git a/src/components/spawn-controller.js b/src/components/spawn-controller.js index eab8314c8664965cbdd2b19acf99fa74609052c5..1daf01b756e4308f1f9d50ceffd362aeb4b8e399 100644 --- a/src/components/spawn-controller.js +++ b/src/components/spawn-controller.js @@ -1,31 +1,26 @@ AFRAME.registerComponent("spawn-controller", { schema: { - radius: { type: "number", default: 1 } + target: { type: "selector" }, + loadedEvent: { type: "string" } }, - init() { - const el = this.el; - const center = el.getAttribute("position"); + this.onLoad = this.onLoad.bind(this); + this.data.target.addEventListener(this.data.loadedEvent, this.onLoad); + }, + onLoad() { + const spawnPoints = document.querySelectorAll("[spawn-point]"); - const angleRad = Math.random() * 2 * Math.PI; - const circlePoint = this.getPointOnCircle(this.data.radius, angleRad); - const worldPoint = { - x: circlePoint.x + center.x, - y: center.y, - z: circlePoint.z + center.z - }; - el.setAttribute("position", worldPoint); + if (spawnPoints.length === 0) { + // Keep default position + return; + } - const angleDeg = angleRad * THREE.Math.RAD2DEG; - const angleToCenter = -1 * angleDeg + 90; - el.setAttribute("rotation", { x: 0, y: angleToCenter, z: 0 }); + const spawnPointIndex = Math.round((spawnPoints.length - 1) * Math.random()); + const spawnPoint = spawnPoints[spawnPointIndex]; - el.object3D.updateMatrix(); - }, - - getPointOnCircle(radius, angleRad) { - const x = Math.cos(angleRad) * radius; - const z = Math.sin(angleRad) * radius; - return { x: x, z: z }; + spawnPoint.object3D.getWorldPosition(this.el.object3D.position); + this.el.object3D.rotation.copy(spawnPoint.object3D.rotation); } }); + +AFRAME.registerComponent("spawn-point", {}); diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index a5baed53dd2f3f5404a81a26150904353434e9e9..0d9fc26dea6304e7ea66f165f76a62bd7044b2d3 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -1,10 +1,12 @@ AFRAME.registerComponent("super-networked-interactable", { schema: { mass: { default: 1 }, + hapticsMassVelocityFactor: { default: 0.1 }, counter: { type: "selector" } }, init: function() { + this.system = this.el.sceneEl.systems.physics; this.counter = this.data.counter.components["networked-counter"]; this.hand = null; @@ -21,12 +23,23 @@ AFRAME.registerComponent("super-networked-interactable", { this.ownershipLostListener = this._onOwnershipLost.bind(this); this.el.addEventListener("grab-start", this.grabStartListener); this.el.addEventListener("ownership-lost", this.ownershipLostListener); + this.system.addComponent(this); }, remove: function() { this.counter.deregister(this.el); this.el.removeEventListener("grab-start", this.grabStartListener); this.el.removeEventListener("ownership-lost", this.ownershipLostListener); + this.system.removeComponent(this); + }, + + afterStep: function() { + if (this.el.is("grabbed") && this.hand && this.hand.components.hasOwnProperty("haptic-feedback")) { + const hapticFeedback = this.hand.components["haptic-feedback"]; + let velocity = this.el.body.velocity.lengthSquared() * this.el.body.mass * this.data.hapticsMassVelocityFactor; + velocity = Math.min(1, velocity); + hapticFeedback.pulse(velocity); + } }, _onGrabStart: function(e) { diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 9d36c6583f01909e0db08f93450c78cd78c3c3aa..41e2d7aeabbe77f2dc0ba9b3821170fab9724db5 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -3,11 +3,14 @@ AFRAME.registerComponent("super-spawner", { template: { default: "" }, useCustomSpawnPosition: { default: false }, spawnPosition: { type: "vec3" }, - events: { default: ["cursor-grab", "action_grab"] } + events: { default: ["cursor-grab", "action_grab"] }, + spawnCooldown: { default: 1 } }, init: function() { this.entities = new Map(); + this.timeout = null; + this.defaultScale = this.el.getAttribute("scale").clone(); }, play: function() { @@ -17,19 +20,27 @@ AFRAME.registerComponent("super-spawner", { pause: function() { this.el.removeEventListener("grab-start", this.handleGrabStart); + + clearTimeout(this.timeout); + this.timeout = null; + this.el.setAttribute("visible", true); + this.el.setAttribute("scale", this.defaultScale); }, remove: function() { for (const entity of this.entities.keys()) { const data = this.entities.get(entity); entity.removeEventListener("componentinitialized", data.componentinInitializedListener); - entity.removeEventListener("bodyloaded", data.bodyLoadedListener); + entity.removeEventListener("body-loaded", data.bodyLoadedListener); } this.entities.clear(); }, _handleGrabStart: function(e) { + if (this.timeout) { + return; + } const hand = e.detail.hand; const entity = document.createElement("a-entity"); @@ -51,6 +62,16 @@ AFRAME.registerComponent("super-spawner", { const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position"); entity.setAttribute("position", pos); this.el.sceneEl.appendChild(entity); + + if (this.data.spawnCooldown > 0) { + this.el.setAttribute("visible", false); + this.el.setAttribute("scale", { x: 0.0001, y: 0.0001, z: 0.0001 }); + this.timeout = setTimeout(() => { + this.el.setAttribute("visible", true); + this.el.setAttribute("scale", this.defaultScale); + this.timeout = null; + }, this.data.spawnCooldown * 1000); + } }, _handleComponentInitialzed: function(entity, e) { diff --git a/src/components/water.js b/src/components/water.js index b7f176131ea97f6f47fbbb31b27c59e80350b78b..cfa33e4b166eea99f5af30d5c5fa36d0075c7378 100644 --- a/src/components/water.js +++ b/src/components/water.js @@ -148,10 +148,14 @@ AFRAME.registerComponent("water", { distance: { type: "number", default: 1 }, speed: { type: "number", default: 0.1 }, forceMobile: { type: "boolean", default: false }, - normalMap: { type: "asset" } + normalMap: { type: "asset", default: "#water-normal-map" } }, init() { - const waterGeometry = new THREE.PlaneBufferGeometry(800, 800); + const waterMesh = this.el.getObject3D("mesh"); + const waterGeometry = waterMesh.geometry; + + // Render THREE.Water shader instead of THREE.Mesh + waterMesh.visible = false; const waterNormals = new THREE.Texture(this.data.normalMap); waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping; @@ -223,5 +227,7 @@ AFRAME.registerComponent("water", { remove() { this.el.removeObject3D("water"); + const waterMesh = this.el.getObject3D("mesh"); + waterMesh.visible = true; } }); diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index e2177fb25a1025ad56e9b135626b3abca64b1d97..99b9b0bf6c2dae246cfd33bc654bd21260d98eef 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -1,10 +1,27 @@ import "./components/gltf-model-plus"; import { resolveURL } from "./utils/resolveURL"; +AFRAME.GLTFModelPlus.registerComponent("quack", "quack"); +AFRAME.GLTFModelPlus.registerComponent("sound", "sound"); +AFRAME.GLTFModelPlus.registerComponent("collision-filter", "collision-filter"); +AFRAME.GLTFModelPlus.registerComponent("css-class", "css-class"); +AFRAME.GLTFModelPlus.registerComponent("scene-shadow", "scene-shadow"); +AFRAME.GLTFModelPlus.registerComponent("super-spawner", "super-spawner"); +AFRAME.GLTFModelPlus.registerComponent("gltf-model-plus", "gltf-model-plus"); +AFRAME.GLTFModelPlus.registerComponent("body", "body"); +AFRAME.GLTFModelPlus.registerComponent("hide-when-quality", "hide-when-quality"); +AFRAME.GLTFModelPlus.registerComponent("light", "light"); +AFRAME.GLTFModelPlus.registerComponent("skybox", "skybox"); +AFRAME.GLTFModelPlus.registerComponent("layers", "layers"); +AFRAME.GLTFModelPlus.registerComponent("shadow", "shadow"); +AFRAME.GLTFModelPlus.registerComponent("xr", "xr"); +AFRAME.GLTFModelPlus.registerComponent("water", "water"); AFRAME.GLTFModelPlus.registerComponent("scale-audio-feedback", "scale-audio-feedback"); +AFRAME.GLTFModelPlus.registerComponent("animation-mixer", "animation-mixer"); AFRAME.GLTFModelPlus.registerComponent("loop-animation", "loop-animation"); AFRAME.GLTFModelPlus.registerComponent("shape", "shape"); AFRAME.GLTFModelPlus.registerComponent("visible", "visible"); +AFRAME.GLTFModelPlus.registerComponent("spawn-point", "spawn-point"); AFRAME.GLTFModelPlus.registerComponent("nav-mesh", "nav-mesh", (el, componentName, componentData, gltfPath) => { if (componentData.src) { componentData.src = resolveURL(componentData.src, gltfPath); diff --git a/src/hub.html b/src/hub.html index 581bc3ffe008dc53cb4be3c414c119935af629e6..af1bb3ef2bf5ff1ccd6839793ac70b7395af89f0 100644 --- a/src/hub.html +++ b/src/hub.html @@ -2,6 +2,8 @@ <html> <head> + <!-- 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 %>"> @@ -28,7 +30,7 @@ <a-scene networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" - physics + physics="gravity: -6;" mute-mic="eventSrc: a-scene; toggleEvents: action_mute" freeze-controller="toggleEvent: action_freeze" personal-space-bubble="debug: false;" @@ -63,6 +65,9 @@ <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> <a-asset-item id="interactable-duck" response-type="arraybuffer" src="./assets/interactables/duck/DuckyMesh.glb"></a-asset-item> + <a-asset-item id="quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="specialquack" src="./assets/sfx/specialquack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> + <img id="water-normal-map" src="./assets/waternormals.jpg"> <!-- Templates --> @@ -136,6 +141,13 @@ personal-space-invader="radius: 0.15; useMaterial: true;" bone-visibility > + <a-cylinder + static-body + radius="0.13" + height="0.2" + position="0 0.07 0.05" + visible="false" + ></a-cylinder> </a-entity> </template> @@ -155,10 +167,11 @@ gltf-model-plus="src: #interactable-duck; inflate: true;" scale="2 2 2" class="interactable" - super-networked-interactable="counter: #counter; mass: 5;" - body="type: dynamic; shape: none; mass: 5;" + super-networked-interactable="counter: #counter; mass: 1;" + body="type: dynamic; shape: none; mass: 1;" grabbable stretchable="useWorldPosition: true;" + duck ></a-entity> </template> @@ -177,15 +190,6 @@ <!-- Interactables --> <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity> - <a-entity - gltf-model-plus="src: #interactable-duck" - scale="2 2 2" - class="interactable" - super-spawner="template: #interactable-template;" - position="2.9 1.2 0" - body="mass: 0; type: static; shape: box;" - ></a-entity> - <a-entity id="cursor-controller" cursor-controller=" @@ -219,7 +223,7 @@ <a-entity id="player-rig" networked="template: #remote-avatar-template; attachTemplateToLocal: false;" - spawn-controller="radius: 4;" + spawn-controller="loadedEvent: bundleloaded; target: #environment-root" wasd-to-analog2d character-controller="pivot: #player-camera" ik-root @@ -228,16 +232,18 @@ > <a-entity id="player-hud" - class="ui" hud-controller="head: #player-camera;" vr-mode-toggle-visibility vr-mode-toggle-playing__hud-controller > <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> - <a-image icon-button="image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image> - <a-image icon-button="image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.001" class="hud freeze"></a-image> - <a-image icon-button="image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="hud bubble" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="ui hud mic" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pause; activeTooltipText: Resume; image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.005" class="ui hud freeze"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Enable Bubble; activeTooltipText: Disable Bubble; image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud bubble" material="alphaTest:0.1;"></a-image> + <a-rounded visible="false" id="hud-tooltip" height="0.08" width="0.3" color="#000000" position="-0.15 -0.2 0" rotation="-20 0 0" radius="0.025" opacity="0.35" class="hud bg"> + <a-entity text="value: Mute Mic; align:center;" position="0.15 0.04 0.001" ></a-entity> + </a-rounded> </a-entity> </a-entity> @@ -278,7 +284,11 @@ hitOpacity: 0.3; missOpacity: 0.2;" haptic-feedback - ></a-entity> + body="type: static; shape: none;" + mixin="super-hands" + controls-shape-offset + > + </a-entity> <a-entity id="player-right-controller" @@ -295,6 +305,9 @@ hitOpacity: 0.3; missOpacity: 0.2;" haptic-feedback + body="type: static; shape: none;" + mixin="super-hands" + controls-shape-offset ></a-entity> <a-entity gltf-model-plus="inflate: true;" @@ -321,67 +334,22 @@ </template> <template data-selector=".LeftHand"> - <a-entity bone-visibility> - <a-entity - id="left-super-hand" - event-repeater=" - events: action_grab, action_release, action_primary_down, action_primary_up; - eventSource: #player-left-controller" - static-body="shape: sphere; sphereRadius: 0.02" - mixin="super-hands" - position="0 0.05 0" - ></a-entity> - </a-entity> + <a-entity bone-visibility></a-entity> </template> <template data-selector=".RightHand"> - <a-entity bone-visibility> - <a-entity - id="right-super-hand" - event-repeater=" - events: action_grab, action_release, action_primary_down, action_primary_up; - eventSource: #player-right-controller" - static-body="shape: sphere; sphereRadius: 0.02" - mixin="super-hands" - position="0 -0.05 0" - ></a-entity> - </a-entity> + <a-entity bone-visibility></a-entity> </template> </a-entity> </a-entity> - <!-- Lights --> - <a-entity - hide-when-quality="low" - light="type: directional; color: #F9FFCE; intensity: 0.6" - position="0.002 5.231 -15.3" - ></a-entity> - <!-- Environment --> <a-entity id="environment-root" nav-mesh-helper static-body="shape: none;" ></a-entity> - - <a-entity - id="skybox" - scale="8000 8000 8000" - skybox="azimuth:0.280; inclination:0.440" - light="type: ambient; color: #FFF" - layers="reflection:true" - xr="ar: false" - ></a-entity> - - <a-entity - id="water" - water="forceMobile: true; normalMap:#water-normal-map" - rotation="-90 0 0" - position="0 -88.358 -332.424" - xr="ar: false" - ></a-entity> - </a-scene> <div id="ui-root"></div> diff --git a/src/hub.js b/src/hub.js index 26b9e1f368908a0b94a4612193cd722d1b11f38b..554d1f3a7a449f3fd22571b7a251c6690ee74b77 100644 --- a/src/hub.js +++ b/src/hub.js @@ -57,6 +57,8 @@ import "./components/block-button"; import "./components/visible-while-frozen"; import "./components/stats-plus"; import "./components/networked-avatar"; +import "./components/css-class"; +import "./components/scene-shadow"; import ReactDOM from "react-dom"; import React from "react"; @@ -68,6 +70,7 @@ import "./systems/app-mode"; import "./systems/exit-on-blur"; import "./gltf-component-mappings"; +import { DEFAULT_ENVIRONMENT_URL } from "./assets/environments/environments"; import { App } from "./App"; @@ -91,6 +94,9 @@ import "./components/super-networked-interactable"; import "./components/networked-counter"; import "./components/super-spawner"; import "./components/event-repeater"; +import "./components/controls-shape-offset"; +import "./components/duck"; +import "./components/quack"; import "./components/cursor-controller"; @@ -308,8 +314,24 @@ const onReady = async () => { } }; + const getPlatformUnsupportedReason = () => { + if (typeof RTCDataChannelEvent === "undefined") { + return "no_data_channels"; + } + + return null; + }; + remountUI({ enterScene, exitScene }); + const platformUnsupportedReason = getPlatformUnsupportedReason(); + + if (platformUnsupportedReason) { + remountUI({ platformUnsupportedReason: platformUnsupportedReason }); + exitScene(); + return; + } + getAvailableVREntryTypes().then(availableVREntryTypes => { remountUI({ availableVREntryTypes }); }); @@ -328,10 +350,7 @@ const onReady = async () => { // If ?room is set, this is `yarn start`, so just use a default environment and query string room. remountUI({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 }); initialEnvironmentEl.setAttribute("gltf-bundle", { - src: "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json" - // src: "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json" - // src: "https://asset-bundles-prod.reticulum.io/rooms/atrium/AtriumMeshes.bundle.json" - // src: "https://asset-bundles-prod.reticulum.io/rooms/courtyard/CourtyardMeshes.bundle.json" + src: DEFAULT_ENVIRONMENT_URL }); return; } diff --git a/src/input-mappings.js b/src/input-mappings.js index a5c399937f0af8d129e2a177a5175bfeeb27efbd..f9985498157ac70bec292eacfe241dfbb5927355 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -26,6 +26,9 @@ const config = { "vive-controls": { trackpad: "trackpad_dpad4" }, + "windows-motion-controls": { + joystick: "joystick_dpad4" + }, "daydream-controls": { trackpad: "trackpad_dpad4" }, @@ -80,6 +83,28 @@ const config = { abuttondown: "action_primary_down", abuttonup: "action_primary_up" }, + "windows-motion-controls": { + joystick_dpad4_west: { + right: "snap_rotate_left" + }, + joystick_dpad4_east: { + right: "snap_rotate_right" + }, + "trackpad.pressedmove": { left: "move" }, + joystick_dpad4_pressed_west_down: { right: "snap_rotate_left" }, + joystick_dpad4_pressed_east_down: { right: "snap_rotate_right" }, + trackpaddown: { right: "action_primary_down" }, + trackpadup: { right: "action_primary_up" }, + menudown: "thumb_down", + menuup: "thumb_up", + gripdown: ["action_grab", "middle_ring_pinky_down"], + gripup: ["action_release", "middle_ring_pinky_up"], + trackpadtouchstart: "thumb_down", + trackpadtouchend: "thumb_up", + triggerdown: ["action_grab", "index_down"], + triggerup: ["action_release", "index_up"], + "axismove.reverseY": { left: "move" } + }, "daydream-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", trackpad_dpad4_pressed_east_down: "snap_rotate_right", diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index eff928870432936ca41843202bbdfc073adaf032..141606a83d121f0a280ac5ac6b6d997856f7fc1e 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -7,15 +7,21 @@ import styles from "../assets/stylesheets/2d-hud.scss"; const TwoDHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onToggleSpaceBubble }) => ( <div className={styles.container}> <div className={cx("ui-interactive", styles.panel, styles.left)}> - <div className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} onClick={onToggleMute} /> + <div + className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} + title={muted ? "Unmute Mic" : "Mute Mic"} + onClick={onToggleMute} + /> </div> <div className={cx("ui-interactive", styles.iconButton, styles.large, styles.freeze, { [styles.active]: frozen })} + title={frozen ? "Resume" : "Pause"} onClick={onToggleFreeze} /> <div className={cx("ui-interactive", styles.panel, styles.right)}> <div className={cx(styles.iconButton, styles.bubble, { [styles.active]: spacebubble })} + title={spacebubble ? "Disable Bubble" : "Enable Bubble"} onClick={onToggleSpaceBubble} /> </div> diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 6e780474b5ecf98164f6e34cc47e6dcc9a0551ac..401270832f397dc03039f67e84487173a461dd80 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -4,6 +4,7 @@ import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; import homeVideo from "../assets/video/home.webm"; import classNames from "classnames"; +import { ENVIRONMENT_URLS } from "../assets/environments/environments"; import HubCreatePanel from "./hub-create-panel.js"; import InfoDialog from "./info-dialog.js"; @@ -17,14 +18,6 @@ addLocaleData([...en]); const messages = localeData[lang] || localeData.en; -const ENVIRONMENT_URLS = [ - "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/theater/Theater.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/atrium/Atrium.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/courtyard/Courtyard.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json" -]; - class HomeRoot extends Component { static propTypes = { intl: PropTypes.object, diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 0b74d55b2698d51f6688e23e02e871177ecea436..f925d6ae55d82e7019f68658d3f7c03a986a038e 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,6 +6,7 @@ import classNames from "classnames"; import faAngleLeft from "@fortawesome/fontawesome-free-solid/faAngleLeft"; import faAngleRight from "@fortawesome/fontawesome-free-solid/faAngleRight"; import FontAwesomeIcon from "@fortawesome/react-fontawesome"; +import { resolveURL, extractUrlBase } from "../utils/resolveURL"; import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png"; @@ -42,11 +43,24 @@ class HubCreatePanel extends Component { _getEnvironmentThumbnail = environmentIndex => { const environment = this.props.environments[environmentIndex]; const meta = environment.meta || {}; - return ( - (meta.images || []).find(i => i.type === "preview-thumbnail") || { - srcset: default_scene_preview_thumbnail + + let environmentThumbnail = { + srcset: default_scene_preview_thumbnail + }; + + if (meta.images) { + const thumbnailImage = meta.images.find(i => i.type === "preview-thumbnail"); + + if (thumbnailImage) { + const baseURL = new URL(extractUrlBase(environment.bundle_url), window.location.href); + + environmentThumbnail = { + srcset: resolveURL(thumbnailImage.srcset, baseURL) + }; } - ); + } + + return environmentThumbnail; }; createHub = async e => { diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 43d39ba0a1e442f55dc095b29e63b64096be68ae..ab1d0229b0b529a6e3d9131df82e683f6097e11a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -69,6 +69,7 @@ class UIRoot extends Component { initialEnvironmentLoaded: PropTypes.bool, janusRoomId: PropTypes.number, roomUnavailableReason: PropTypes.string, + platformUnsupportedReason: PropTypes.string, hubName: PropTypes.string, occupantCount: PropTypes.number }; @@ -512,18 +513,9 @@ class UIRoot extends Component { }; render() { - if (this.state.exited || this.props.roomUnavailableReason) { + if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { let subtitle = null; - if (this.props.roomUnavailableReason !== "closed") { - const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : this.props.roomUnavailableReason}`; - subtitle = ( - <div> - <FormattedMessage id={exitSubtitleId} /> - <p /> - You can also <a href="/">create a new room</a>. - </div> - ); - } else { + if (this.props.roomUnavailableReason === "closed") { // TODO i18n, due to links and markup subtitle = ( <div> @@ -537,7 +529,34 @@ class UIRoot extends Component { If you have questions, contact us at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. <p /> If you'd like to run your own server, hubs's source code is available on{" "} - <a href="https://github.com/mozilla/hubs">Github</a>. + <a href="https://github.com/mozilla/hubs">GitHub</a>. + </div> + ); + } else if (this.props.platformUnsupportedReason === "no_data_channels") { + // TODO i18n, due to links and markup + subtitle = ( + <div> + Your browser does not support{" "} + <a + href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#Browser_compatibility" + rel="noreferrer noopener" + > + WebRTC Data Channels + </a>, which is required to use Hubs. + </div> + ); + } else { + const reason = this.props.roomUnavailableReason || this.props.platformUnsupportedReason; + const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : reason}`; + subtitle = ( + <div> + <FormattedMessage id={exitSubtitleId} /> + <p /> + {this.props.roomUnavailableReason && ( + <div> + You can also <a href="/">create a new room</a>. + </div> + )} </div> ); } diff --git a/src/utils/resolveURL.js b/src/utils/resolveURL.js index ddc6c86803b9adc7ea028b2b127ae0783270f521..35ccc3150278558e4026240164116785620ecfc3 100644 --- a/src/utils/resolveURL.js +++ b/src/utils/resolveURL.js @@ -15,3 +15,11 @@ export function resolveURL(url, path) { // Relative URL return path + url; } + +export function extractUrlBase(url) { + const index = url.lastIndexOf("/"); + + if (index === -1) return "./"; + + return url.substr(0, index + 1); +} diff --git a/webpack.config.js b/webpack.config.js index c0f8c88b3af3a6c2e43f7111f517841c4fcfec3c..4fc219ef830791bd6dbc6d201143ba82af6b961c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -206,6 +206,12 @@ const config = { to: "favicon.ico" } ]), + new CopyWebpackPlugin([ + { + from: "src/assets/images/hub-preview.png", + to: "hub-preview.png" + } + ]), // Extract required css and add a content hash. new ExtractTextPlugin({ filename: "assets/stylesheets/[name]-[contenthash].css", @@ -228,7 +234,8 @@ const config = { "process.env": JSON.stringify({ NODE_ENV: process.env.NODE_ENV, JANUS_SERVER: process.env.JANUS_SERVER, - DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER + DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER, + ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER }) }) ] diff --git a/yarn.lock b/yarn.lock index 321a63370a4e9eb13191ae2835ed429b7eb6c242..a9e1e2e95bf3783fdc9550bdcce76c190a646d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -166,9 +166,9 @@ aframe-billboard-component@^1.0.0: dependencies: three-pathfinding "^0.5.5" -"aframe-input-mapping-component@https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array": +"aframe-input-mapping-component@https://github.com/mozillareality/aframe-input-mapping-component#hubs/master": version "0.1.2" - resolved "https://github.com/johnshaughnessy/aframe-input-mapping-component#33d7ad4c82a5e2b74defca39c7fa5ef15aab493e" + resolved "https://github.com/mozillareality/aframe-input-mapping-component#03932457c5318db243e811d2767fe0c5a8c7e9e0" "aframe-physics-extras@https://github.com/infinitelee/aframe-physics-extras#fix/physics-collider-crash": version "0.1.2"