diff --git a/src/assets/images/hub-preview-white.png b/src/assets/images/hub-preview-white.png new file mode 100755 index 0000000000000000000000000000000000000000..dd83924b037ec54eafc7ca63a14557ed4b44c473 Binary files /dev/null and b/src/assets/images/hub-preview-white.png differ diff --git a/src/assets/stylesheets/scene-ui.scss b/src/assets/stylesheets/scene-ui.scss new file mode 100644 index 0000000000000000000000000000000000000000..be2c9626bb5ae2fdd99e3919e04ba9bd01cdfb8a --- /dev/null +++ b/src/assets/stylesheets/scene-ui.scss @@ -0,0 +1,106 @@ +@import 'shared'; + +:local(.ui) { + @extend %default-font; + + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + pointer-events: none; + color: white; +} + +:local(.whiteOverlay) { + background-color: rgba(255, 255, 255, 0.3); + top: 0; + left: 0; + width: 100%; + height: 100%; + position: absolute; +} + +:local(.grid) { + display: grid; + grid-template-columns: 1fr 20px minmax(200px, 400px) 20px 1fr; + grid-template-rows: 1fr 3fr 1fr; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.2); +} + +:local(.mainPanel) { + grid-column: 3; + grid-row: 2; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + pointer-events: auto; + + button { + @extend %action-button; + border: 0; + } +} + +:local(.logoTagline) { + color: black; + text-shadow: 0px 0px 20px #aaa; + font-weight: bold; + text-align: center; + font-size: 1.2em; + margin-bottom: 32px; +} + +:local(.logo) { + width: 100%; + display: block; + + img { + width: 100%; + } +} + +:local(.info) { + position: absolute; + color: black; + text-shadow: 0px 0px 20px #aaa; + bottom: 12px; + left: 12px; + display: flex; + flex-direction: column; +} + +:local(.name) { + font-weight: bold; + font-size: 1.6em; +} + +:local(.attribution) { + font-size: 1.0em; + white-space: wrap; +} + +:local(.screenshot) { + position: absolute; + width: 115%; + height: 115%; + top: -40px; + left: -40px; + + img { + width: 100%; + height: 100%; + } + + background-color: black; + filter: blur(10px); +} + +:local(.screenshotHidden) { + visibility: hidden; + opacity: 0; + transition: visibility 0s 0.5s, opacity 0.5s linear; +} diff --git a/src/assets/stylesheets/scene.scss b/src/assets/stylesheets/scene.scss new file mode 100644 index 0000000000000000000000000000000000000000..44e6591aacfa994bb79a4a0354b129ae171c4741 --- /dev/null +++ b/src/assets/stylesheets/scene.scss @@ -0,0 +1,2 @@ +@import 'shared'; +@import 'loader'; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index e3fe060ac208cb8ea4159235758664033228f299..d29f96029b44573b4c01929289087abd1f92bcb7 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -78,6 +78,8 @@ "link.dont_have_a_code": "Don't have a code?", "link.create_a_room": "Create a Room", "link.try_again": "We couldn't find that code. Please try again.", - "help.report_issue": "Report an Issue" + "help.report_issue": "Report an Issue", + "scene.logo_tagline": "A new way to get together", + "scene.create_button": "create a room with this scene" } } diff --git a/src/components/scene-components.js b/src/components/scene-components.js new file mode 100644 index 0000000000000000000000000000000000000000..f7e562787e60612b1c5b1a57147416746898f1a2 --- /dev/null +++ b/src/components/scene-components.js @@ -0,0 +1,23 @@ +import "./ambient-light"; +import "./animation-mixer"; +import "./audio-feedback"; +import "./css-class"; +import "./directional-light"; +import "./duck"; +import "./gltf-model-plus"; +import "./heightfield"; +import "./hemisphere-light"; +import "./hide-when-quality"; +import "./layers"; +import "./loop-animation"; +import "./media-loader"; +import "./point-light"; +import "./quack"; +import "./scene-shadow"; +import "./scene-preview-camera"; +import "./skybox"; +import "./spawn-controller"; +import "./spot-light"; +import "./sticky-object"; +import "./super-spawner"; +import "./water"; diff --git a/src/components/scene-preview-camera.js b/src/components/scene-preview-camera.js new file mode 100644 index 0000000000000000000000000000000000000000..0b7beaa29679518f63fceda44052f9efe37c2a1e --- /dev/null +++ b/src/components/scene-preview-camera.js @@ -0,0 +1,50 @@ +/** + * Nicely pans the camera for previewing a scene. There's some weirdness with this right now + * since it ends up panning in a direction dependent upon the start camera orientation, + * but it's good enough for now. + */ +function lerp(start, end, t) { + return (1 - t) * start + t * end; +} + +const DURATION = 90.0; + +AFRAME.registerComponent("scene-preview-camera", { + init: function() { + this.startPoint = this.el.object3D.position.clone(); + this.startRotation = new THREE.Quaternion(); + this.startRotation.setFromEuler(this.el.object3D.rotation); + + this.targetPoint = new THREE.Vector3(1, 0.5, -0.5); + this.targetPoint.applyMatrix4(this.el.object3D.matrix); + this.targetPoint.add(new THREE.Vector3(0, 0, -2)); + + const targetRotDelta = new THREE.Euler(-0.15, 0.0, 0.15); + this.targetRotation = new THREE.Quaternion(); + this.targetRotation.setFromEuler(targetRotDelta); + this.targetRotation.premultiply(this.startRotation); + + this.startTime = new Date().getTime(); + this.backwards = false; + }, + + tick: function() { + const t = (new Date().getTime() - this.startTime) / (1000.0 * DURATION); + + const from = this.backwards ? this.targetPoint : this.startPoint; + const to = this.backwards ? this.startPoint : this.targetPoint; + const fromRot = this.backwards ? this.targetRotation : this.startRotation; + const toRot = this.backwards ? this.startRotation : this.targetRotation; + const newRot = new THREE.Quaternion(); + + THREE.Quaternion.slerp(fromRot, toRot, newRot, t); + + this.el.object3D.position.set(lerp(from.x, to.x, t), lerp(from.y, to.y, t), lerp(from.z, to.z, t)); + this.el.object3D.rotation.setFromQuaternion(newRot); + + if (t >= 0.99) { + this.backwards = !this.backwards; + this.startTime = new Date().getTime(); + } + } +}); diff --git a/src/hub.html b/src/hub.html index deb09091418edc0b49e8e11758bfa4f94d2bf203..4d297a996c477ad06f124674a1c3554959779b50 100644 --- a/src/hub.html +++ b/src/hub.html @@ -2,7 +2,7 @@ <html> <head> - <!-- DO NOT REMOVE/EDIT THIS COMMENT - HUB_META_TAGS --> + <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> diff --git a/src/hub.js b/src/hub.js index ce0e5dca5e8486bdc172e72562fda1367d3f36ac..590b9d5dae21e89b6d90fbed14209fe4a2a637c3 100644 --- a/src/hub.js +++ b/src/hub.js @@ -31,9 +31,9 @@ import { ObjectContentOrigins } from "./object-types"; import "./activators/shortpress"; +import "./components/scene-components"; import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future import "./components/mute-mic"; -import "./components/audio-feedback"; import "./components/bone-mute-state-indicator"; import "./components/bone-visibility"; import "./components/in-world-hud"; @@ -44,17 +44,9 @@ import "./components/character-controller"; import "./components/haptic-feedback"; import "./components/networked-video-player"; import "./components/offset-relative-to"; -import "./components/water"; -import "./components/skybox"; -import "./components/layers"; -import "./components/spawn-controller"; -import "./components/hide-when-quality"; import "./components/player-info"; import "./components/debug"; -import "./components/animation-mixer"; -import "./components/loop-animation"; import "./components/hand-poses"; -import "./components/gltf-model-plus"; import "./components/gltf-bundle"; import "./components/hud-controller"; import "./components/freeze-controller"; @@ -64,26 +56,17 @@ 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 "./components/avatar-replay"; import "./components/media-views"; import "./components/pinch-to-move"; import "./components/look-on-mobile"; import "./components/pitch-yaw-rotator"; import "./components/input-configurator"; -import "./components/sticky-object"; import "./components/auto-scale-cannon-physics-body"; import "./components/position-at-box-shape-border"; import "./components/remove-networked-object-button"; import "./components/destroy-at-extreme-distances"; -import "./components/media-loader"; import "./components/gamma-factor"; -import "./components/ambient-light"; -import "./components/directional-light"; -import "./components/hemisphere-light"; -import "./components/point-light"; -import "./components/spot-light"; import "./components/visible-to-owner"; import "./components/camera-tool"; @@ -123,18 +106,13 @@ import "aframe-physics-extras"; import "super-hands"; 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/grabbable-toggle"; import "./components/cardboard-controls"; import "./components/cursor-controller"; -import "./components/heightfield"; import "./components/nav-mesh-helper"; import "./systems/tunnel-effect"; diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js new file mode 100644 index 0000000000000000000000000000000000000000..ec04e095e135bac8bb85beff8c27c7cf2b6b3ae9 --- /dev/null +++ b/src/react-components/scene-ui.js @@ -0,0 +1,104 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import en from "react-intl/locale-data/en"; +import styles from "../assets/stylesheets/scene-ui.scss"; +import hubLogo from "../assets/images/hub-preview-white.png"; +import { getReticulumFetchUrl } from "../utils/phoenix-utils"; +import { generateHubName } from "../utils/name-generation"; + +import { lang, messages } from "../utils/i18n"; + +addLocaleData([...en]); + +class SceneUI extends Component { + static propTypes = { + scene: PropTypes.object, + sceneLoaded: PropTypes.bool, + sceneId: PropTypes.string, + sceneName: PropTypes.string, + sceneDescription: PropTypes.string, + sceneAttribution: PropTypes.string, + sceneScreenshotURL: PropTypes.string + }; + + state = { + showScreenshot: false + }; + + constructor(props) { + super(props); + + // Show screenshot if scene isn't loaded in 5 seconds + setTimeout(() => { + if (!this.props.sceneLoaded) { + this.setState({ showScreenshot: true }); + } + }, 5000); + } + + componentDidMount() { + this.props.scene.addEventListener("loaded", this.onSceneLoaded); + } + + componentWillUnmount() { + this.props.scene.removeEventListener("loaded", this.onSceneLoaded); + } + + createRoom = async () => { + const payload = { hub: { name: generateHubName(), scene_id: this.props.sceneId } }; + const createUrl = getReticulumFetchUrl("/api/v1/hubs"); + + const res = await fetch(createUrl, { + body: JSON.stringify(payload), + headers: { "content-type": "application/json" }, + method: "POST" + }); + + const hub = await res.json(); + + if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) { + document.location = hub.url; + } else { + document.location = `/hub.html?hub_id=${hub.hub_id}`; + } + }; + + render() { + return ( + <IntlProvider locale={lang} messages={messages}> + <div className={styles.ui}> + <div + className={classNames({ + [styles.screenshot]: true, + [styles.screenshotHidden]: this.props.sceneLoaded + })} + > + {this.state.showScreenshot && <img src={this.props.sceneScreenshotURL} />} + </div> + <div className={styles.whiteOverlay} /> + <div className={styles.grid}> + <div className={styles.mainPanel}> + <a href="/" className={styles.logo}> + <img src={hubLogo} /> + </a> + <div className={styles.logoTagline}> + <FormattedMessage id="scene.logo_tagline" /> + </div> + <button onClick={this.createRoom}> + <FormattedMessage id="scene.create_button" /> + </button> + </div> + </div> + <div className={styles.info}> + <div className={styles.name}>{this.props.sceneName}</div> + <div className={styles.attribution}>{this.props.sceneAttribution}</div> + </div> + </div> + </IntlProvider> + ); + } +} + +export default SceneUI; diff --git a/src/scene.html b/src/scene.html new file mode 100644 index 0000000000000000000000000000000000000000..222fa5d3be5878784e0612cc92c3717624a40036 --- /dev/null +++ b/src/scene.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + +<head> + <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS --> + + <meta charset="utf-8"> + <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"> + <title>Scene | Hubs by Mozilla</title> + <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> +</head> + +<body> + <a-scene + renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;" + vr-mode-ui="enabled: false" + gamma-factor + > + <a-entity + id="scene-root" + static-body="shape: none;" + ></a-entity> + + <a-camera id="camera" fov="80" look-controls="enabled: false" wasd-controls="enabled: false"></a-camera> + </a-scene> + + <div id="ui-root"></div> +</body> + +</html> diff --git a/src/scene.js b/src/scene.js new file mode 100644 index 0000000000000000000000000000000000000000..6f2f0732d0da231b0bebef56b8ce526326aa5ea4 --- /dev/null +++ b/src/scene.js @@ -0,0 +1,110 @@ +console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`); + +import "./assets/stylesheets/scene.scss"; + +import "aframe"; +import "./utils/logging"; +import { patchWebGLRenderingContext } from "./utils/webgl"; +patchWebGLRenderingContext(); + +import "three/examples/js/loaders/GLTFLoader"; + +import "./components/scene-components"; +import "./components/debug"; +import "./systems/nav"; + +import { getReticulumFetchUrl } from "./utils/phoenix-utils"; + +import ReactDOM from "react-dom"; +import React from "react"; +import SceneUI from "./react-components/scene-ui"; +import { disableiOSZoom } from "./utils/disable-ios-zoom"; + +import "./gltf-component-mappings"; + +import { App } from "./App"; + +window.APP = new App(); + +const qs = new URLSearchParams(location.search); +const isMobile = AFRAME.utils.device.isMobile(); + +window.APP.quality = qs.get("quality") || isMobile ? "low" : "high"; + +import "aframe-physics-system"; +import "aframe-physics-extras"; +import "./components/event-repeater"; +import "./components/controls-shape-offset"; + +import registerTelemetry from "./telemetry"; + +registerTelemetry(); + +disableiOSZoom(); + +function mountUI(scene, props = {}) { + ReactDOM.render( + <SceneUI + {...{ + scene, + ...props + }} + />, + document.getElementById("ui-root") + ); +} + +const onReady = async () => { + const scene = document.querySelector("a-scene"); + window.APP.scene = scene; + + const sceneId = qs.get("scene_id") || document.location.pathname.substring(1).split("/")[1]; + console.log(`Scene ID: ${sceneId}`); + + let uiProps = { sceneId: sceneId }; + + mountUI(scene); + + const remountUI = props => { + uiProps = { ...uiProps, ...props }; + mountUI(scene, uiProps); + }; + + const sceneRoot = document.querySelector("#scene-root"); + const sceneModelEntity = document.createElement("a-entity"); + const gltfEl = document.createElement("a-entity"); + const camera = document.getElementById("camera"); + + sceneModelEntity.addEventListener("scene-loaded", () => { + remountUI({ sceneLoaded: true }); + const previewCamera = gltfEl.object3D.getObjectByName("scene-preview-camera"); + + if (previewCamera) { + camera.object3D.position.copy(previewCamera.position); + camera.object3D.rotation.copy(previewCamera.rotation); + camera.object3D.updateMatrix(); + } + + camera.setAttribute("scene-preview-camera", ""); + }); + + const res = await fetch(getReticulumFetchUrl(`/api/v1/scenes/${sceneId}`)).then(r => r.json()); + const sceneInfo = res.scenes[0]; + + const modelUrl = sceneInfo.model_url; + console.log(`Scene Model URL: ${modelUrl}`); + + gltfEl.setAttribute("gltf-model-plus", { src: modelUrl, useCache: false, inflate: true }); + gltfEl.addEventListener("model-loaded", () => sceneModelEntity.emit("scene-loaded")); + sceneModelEntity.appendChild(gltfEl); + sceneRoot.appendChild(sceneModelEntity); + + remountUI({ + sceneName: sceneInfo.name, + sceneDescription: sceneInfo.description, + sceneAttribution: sceneInfo.attribution, + sceneScreenshotURL: sceneInfo.screenshot_url + }); +}; + +document.addEventListener("DOMContentLoaded", onReady); diff --git a/webpack.config.js b/webpack.config.js index 9531ceb26e70881f4bd4b5af7eb4367e2141f007..4c28ef4a04acaba07f91c52bf7d4bf804de6501d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,6 +64,7 @@ module.exports = (env, argv) => ({ entry: { index: path.join(__dirname, "src", "index.js"), hub: path.join(__dirname, "src", "hub.js"), + scene: path.join(__dirname, "src", "scene.js"), link: path.join(__dirname, "src", "link.js"), "avatar-selector": path.join(__dirname, "src", "avatar-selector.js") }, @@ -196,6 +197,20 @@ module.exports = (env, argv) => ({ } ] }), + new HTMLWebpackPlugin({ + filename: "scene.html", + template: path.join(__dirname, "src", "scene.html"), + chunks: ["vendor", "engine", "scene"], + inject: "head", + meta: [ + { + "http-equiv": "origin-trial", + "data-feature": "WebVR (For Chrome M62+)", + "data-expires": process.env.ORIGIN_TRIAL_EXPIRES, + content: process.env.ORIGIN_TRIAL_TOKEN + } + ] + }), new HTMLWebpackPlugin({ filename: "link.html", template: path.join(__dirname, "src", "link.html"),