The project I'll guide you through is a virtual reality app -- built using React 360 -- which allows the user to choose his or her immersive meditation environment, each of which comes with its own mantra inspired by the very excellent show "The Good Place." In case you need it for reference, my final source code is here.
This content and more like it -- including an introduction to Recompose, which I used for state management here -- are on my blog at lilydbarrett.com.
- Revamped and rebranded version of React VR
- Open source, built by Facebook
- Incorporates Three.js, a 3-D JavaScript library; React Native mobile elements; and Web VR, responsible for allowing us to view VR experiences across different browsers, including web
- Enables Flexbox for ease with fitting content to different browsers/screens
React 360 components include (among others):
View
- given to us by React Native, it"s used in place of thediv
elements React typically expects and maps to the view of whatever platform is running the codeText
- given to us by React Native, it renders 3-D textImage
- used for displaying 2-D imagesVrButton
- detects click events across web, mobile, and headset, from a computer mouse to an Oculus Go controllerEntity
- used for rendering 3-D objects in a scene
You do not need any VR devices to get started with creating React 360 apps.
- Install the React 360 CLI tool:
$ npm i -g react-360-cli
- Use it to create a new project:
$ react-360 init FindYourZen
$ cd FindYourZen
$ npm start
Take a look at the file structure.
index.js
= entry point for your appclient.js
= sets up the "runtime," which turns our React components into 3D elements in our VR landscapeindex.html
= as in the typical React application, provides a place for you to mount your React codestatic_assets
= stores images, audio files, and other external resources
Delete the 360_world
image from static_assets
and replace it with some new images, including your "home" environment image, the one that appears when the app loads.
A tip (Thank you, Coding Artist!): Search for "equirectangular" photos on Google. Flickr is a particularly good source of free, Creative Commons-licensed, high-quality panoramic photos.
Create an images
folder in your static_assets
and move your images in there. If your client.js
, update the following line to use the name of your new "home" environment image:
r360.compositor.setBackground(r360.getAssetURL('360_world.jpg'));
This line of code, which immediately sets the background image when the app is first mounted, uses the asset
utility from react-360
to automatically look inside our static_assets
folder for the correct image.
That's all well and good, but keep in mind you'll eventually want to change the image based on which environment the user selects. You can handle dynamic images from within a React event by using react-360
's Environment
module. Example:
Environment.setBackgroundImage(asset(someImage));
In index.js
, change the text from "Welcome to React 360" to "Find your zen" and the color to a nice, calming blue. (I liked #29ECCE
).
Note that any text in the application needs to be explicitly wrapped inside a Text
component.
Now, you"ll need to add logic for updating the user's environment based on which option they choose by clicking on a VrButton
component. I'll leave this open-ended as it's still just React: You can use local state, Redux, Mobx, whatever. I chose to use Recompose.
FYI, I wound up putting my data for each environment in a consts/zens.js
file:
const zens = [
{ id: 1,
mantra: "Find your inner motherforking peace",
image: "images/hawaii_beach.jpg",
audio: "sounds/waves.mp3",
text: "I'm feeling beachy keen",
},
{ id: 2,
mantra: "Breathe in peace, breathe out bullshirt",
image: "images/horseshoe_bend.jpg",
audio: "sounds/birds.mp3",
text: "Ain't no mountain high enough",
},
{ id: 3,
mantra: "Benches will be benches",
image: "images/sunrise_paris_2.jpg",
audio: "sounds/chimes.mp3",
text: "I want a baguette",
},
{ id: 4,
image: "images/homebase.png",
text: "Home"
}
]
export default zens;
You don"t really feel like you"re at the beach unless you hear the sound of waves, right?
A good source of free and Creative Commons-licensed audio is Freesound. You"ll have to make an account, but it"s quick and easy. They"ll ask you to complete a survey along the way, but you can just skip it.
After downloading the sounds -- many of which have large .wav
files -- you"ll want to compress the files. I used All2MP3, which was easy to install and worked like a dream to turn my .wav
files into more manageable .mp3
files, which I then added to a sounds
folder in my static_assets
.
For playing audio, we'll use the AudioModule
Native Module. Its playEnvironmental
method allows us to provide a path (to the audio our assets folder) and a volume at which to play said audio at a looping pace; Once the audio file stops playing, it'll start again.
One thing to keep in mind is that you'll need to tell your application when to stop playing a particular audio file when you switch scenes. (Otherwise it'll keep playing in an environment where it's not welcome! You can't listen to Parisian church bells on a Hawaiian beach!) You can do this via the AudioModule
's stopEnvironmental
method.
// index.js
import React from "react";
import {
AppRegistry,
View,
} from "react-360";
import { AppContent } from "./components";
import { withAppContext } from "./providers";
const MeditationApp = withAppContext(() => (
<View style={{
transform: [{ translate: [0, 0, -2] }]
}}>
<AppContent />
</View>
));
AppRegistry.registerComponent("AppContent", () => AppContent);
AppRegistry.registerComponent("MeditationApp", () => MeditationApp);
To break this down a bit: I've registered components in different places. My MeditationApp
is mounted to react-360
's default location -- giving my components access to the runtime -- while the content I want to display (stored in AppContent
) is mounted to react-360
's default cylindrical surface.
My client.js
deals with mounting my component to locations and surfaces:
// client.js
import { ReactInstance, Surface } from "react-360-web";
function init(bundle, parent, options = {}) {
const r360 = new ReactInstance(bundle, parent, {
fullScreen: true,
// Add custom options here
...options,
});
r360.renderToSurface(
r360.createRoot("AppContent", { /* initial props */ }),
r360.getDefaultSurface()
);
r360.renderToLocation(
r360.createRoot("MeditationApp", { /* initial props */ }),
r360.getDefaultLocation(),
);
r360.compositor.setBackground(r360.getAssetURL("images/homebase.png"));
}
window.React360 = {init};
And, for context, here's my AppContent
component:
import React from "react";
import { View } from "react-360";
import { HomeEnvironment, ZenEnvironment } from "../../scenes";
import { withAppContext } from "../../providers";
const AppContent = withAppContext(() => (
<View>
<HomeEnvironment />
<ZenEnvironment />
</View>
));
export default AppContent;
I set up my folder structure as follows:
- components // shared components
- base-button
- content
- consts
- providers
// Recompose providers live here
- scenes
- home-environment
- components
- menu
- title
- zen-button
- zens
- zen-environment
- components
- home-button
- mantra
- static_assets
- images
- sounds
Shared components live in the top-level components
folder; my HomeEnvironment
and ZenEnvironment
scenes each have their own sets of relevant components stored in scenes
. My state management is essentially handled by the providers
and composed into each component that needs knowledge of/access to state.
We get StyleSheet
from react-native
, basically, which allows us to use JavaScript to pass styling attributes to our React components. As an example:
// scenes/home-environment/components/zen-button/style.js
import { StyleSheet } from "react-360";
export default StyleSheet.create({
text: {
backgroundColor: "#29ECCE",
textAlign: "center",
color: "white",
marginTop: 30
}
})
Here, we create and export a StyleSheet
object that allows us to reference styles in a terse, DRY manner in our component itself:
// scenes/home-environment/components/zen-button/index.js
import React from "react";
import { BaseButton } from "../../../../components";
import style from "./style";
const ZenButton = ({ text, buttonClick, selectedZen }) => {
return (
<BaseButton
text={text}
selectedZen={selectedZen}
buttonClick={buttonClick}
textStyle={style.text}
/>
)
}
export default ZenButton;
When you Inspect Element
, you"ll see that React 360 bundles all its files into one giant blob that isn"t super easy to grok. Fortunately, because it supports sourcemaps, we can still access the original files, use debugger
, etc.
$ git clone https://github.com/lilybarrett/find-your-zen.git
$ cd find-your-zen
$ npm i
$ npm start
Navigate to http://localhost:8081/index.html.