Learn React VR (Chapter 8 | Building a VR Video App)

Prerequisites

Chapter 1 | Hello Virtual World
Chapter 2 | Panoramic Road Trip
Chapter 3 | Outdoor Movie Theater
Chapter 4 | Transitions and Animations
Chapter 5 | Star Wars Modeling
Chapter 6 | Vector Graphic Exploration
Chapter 7 | UI/UX Principles for VR Design

Scope of This Chapter

There are only going to be 2 chapters remaining in this book (including this one). In this chapter, we are going to be doing a real-world project together. with a focus on UI/UX.

We have made projects together but they were only for demoing a specific feature of React VR. In this project, we are going to build a VR video app. This will essentially be a more nuanced version of our Outdoor Movie Theater that we made earlier.

Oculus Video

The project is going to be based on Oculus Video with some slight modifications and simplifications. From the main dashboard, we are going allow a user to play the top six videos from Twitch within an environment of their choice. As we do this, we will focus more on UI design/animation for a good user experience than we have so far. The point is to tie together what we have learned throughout this book.

Breaking Down the Project

Let’s quickly discuss how this project will work.

Title Scene

First, there will be a title scene where a user can click to continue:

There will be both entrance and exit animation and the background will be a panoramic photo.

Dashboard Scene

Next, there will be a dashboard scene where a user will be able to select a Twitch video and the environment:

The gray menu buttons will be options for sources of videos. We will just have Twitch but we will implement three additional menu buttons anyways.

The purple tiles will be the videos and environment options. The tiles will initially be the videos and then they will dynamically update to the environment options on the click of the purple button.

When a user clicks the button to confirm the selected environment, then the next scene will appear.

The purple circles on the right will keep track of the progress of this scene (selecting a video or selecting an environment).

Like the previous scene, there will be a panoramic photo as a background and animations.

Video Player Scene

Finally, there will be a video player scene where the user will be able to watch the 2D video on a screen in the front of the world and click a button to return to the dashboard in the back of the app:

Creating the Project

Let’s go ahead and create this project and set up the project directory in using our Scenes/Layouts/Elements component hierarchy.

First, run the following to initialize our app:

react-vr init VrVideoApp

Open the project folder in a code editor.

Update the project directory to reflect this:

Creating the Title Scene Components

Static Title Scene Components

First things first, we need to create a file called TitleScene.js within the scenes folder.

This will simply nest our layout component. For now, we can just add the shell for this file:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Scene
class TitleScene extends React.Component {
render() {
return (
//insert layout component
)
}
}
module.exports = TitleScene;

Let’s move on to the layout component file for our title scene which we can name TitleLayout.js (make sure it is within the layouts folder).

Then, we can start with the shell of our code:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Layout
class TitleLayout extends React.Component {
render() {
return (
//insert elements
)
}
}
module.exports = TitleLayout;

Before we advance, we will also add a Pano component in the scene component. Reason being, each scene will have a different panoramic photo. Therefore, we want the scene components to control the panoramic photo being rendered.

Download this image at the high resolution and save it as title-background.jpg in the static_assets folder.

For now, let’s just add a Pano component with the title-background image (we also need to import asset and Pano):

import React from 'react';
import {
Text,
View,
VrButton,
asset,
Pano
} from 'react-vr';
//Scene
class TitleScene extends React.Component {
render() {
return (
<Pano source={asset('title-background.jpg')}/>
//insert layout component
)
}
}

You can also update our app component class in index.vr.js to the following:

export default class VrVideoApp extends React.Component {
render() {
return (
<View>

</View>
);
}
};

Now, we need to add the Flexbox container using Flexbox for our title scene:

The container will have a column direction and be centered (both vertically and horizontally). Therefore, we add the following to our render:

<View 
style={{
width: 2,
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}
>
//insert elements
</View>

We can now move on to creating our elements.

Our elements will be the 2 Flexbox items added to our column:

The two elements will be a title and button.

The title component will not be reused.

The button component will be reused so we will have to pass it props in order to control the text.

Let’s start with our title component.

Create a new file called Title.js within the Elements folder.

Let’s start with the shell of the code:

import React from 'react';
import {
Text,
View
} from 'react-vr';
//Element
class Title extends React.Component {
render() {
return (
<View style={{ margin: 0.1, height: 1}}>
      </View>
)
}
}
module.exports = Title;

The View component will contain the styling needed to be added as a Flexbox item. This component is already being used in the render function.

Let’s add the Text component:

<View style={{ margin: 0.1}}>
<Text style={{fontSize: 0.25, textAlign: 'center', color: "#FFFFFF"}}>
VR VIDEO APP
</Text>
</View>

Next, let’s create a file called Button.js for the button element within the Elements folder.

First, we can add the shell of the code which included the View component that styles this button as a Flexbox item:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Element
class Button extends React.Component {
render() {
return (
<View style={{ margin: 0.1, height: 0.3, backgroundColor: '#A482DF'}}>
      </View>
)
}
}
module.exports = Button;

Then, we can add the VrButton component so we can add event handling to this button later on as well as the button text (which we will be passing down as a prop called text):

<View style={{ margin: 0.1, height: 0.3, backgroundColor: '#A482DF', borderRadius: 0.1}}>
<VrButton>
<Text style={{fontSize: 0.2, textAlign: 'center', color: "#FFFFFF"}}>
{this.props.text}
</Text>
</VrButton>
</View>

Nesting Our Components

All the components for our Title scene have been completed individually. Now, we need to connect them.

Let’s nest the TitleLayout component in TitleScene.js.

To do this, we begin by importing the TitleLayout component:

import TitleLayout from './layouts/TitleLayout.js';

Then, we can use the component and wrap a View component:

<View>
<Pano source={asset('title-background.jpg')}/>
<TitleLayout/>
</View>

In addition, a text prop (which we will later define in index.vr.js) that control the button’s text will be passed to the TitleLayout component:

<TitleLayout text={this.props.text}/>

Let’s move onto the TitleLayout component found in TitleLayout.js.

First, we can import the Title and Button components:

import Title from './elements/Title.js';
import Button from './elements/Button.js';

Then, we can nest them and pass down the text prop to the button:

<View 
style={{
width: 2,
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}
>
<Title/>
<Button text={this.props.text}/>
</View>

Finally, let’s import and nest the entire TitleScene component into the app component found in index.vr.js.

//other import
import TitleScene from './components/scenes/TitleScene.js';
export default class VrVideoApp extends React.Component {
render() {
return (
<View>
<TitleScene text={"Watch a Video"}/>
</View>
);
}
};
//AppRegistry

Note that we also defined the text prop which was being passed down to the button.

Testing Our Scene

Let’s test our scene out.

cd into the project root, run npm start , and check the local host:

Woohoo! We have completed the static title scene. Now, let’s add some animation.

Animating Our Components

For our entrance animation, we want the title to slide in from the left and fade in. The button will slide in from the right and fade in.

Let’s start with our title component found in Title.js.

First, we import Animated and Easing:

import {
Text,
View,
Animated
} from 'react-vr';
import { Easing } from 'react-native';

Then, we update the Text tags to Animated.Text :

<Animated.Text style={{fontSize: 0.25, textAlign: 'center', color: "#FFFFFF"}}>
VR VIDEO APP
</Animated.Text>

Next, we can set up the shell of the local state:

class Title extends React.Component {
constructor() {
super();
this.state = { slideLeft: "", fadeIn: ""};
}
//render
}

We need to update the local state with initial values for our animated values. Let’s take a second to consider this.

The horizontal sliding (slideLeft and slideRight) will be controlled by a translateX transformation. If a translateX value is negative, it places an element to the left. If it is positive, it places an element to the right. Since we want the slides to start from these translated positions and return to their normal position, we can update the local state like so:

this.state = { slideLeft: new Animated.Value(-1), fadeIn: ""};

When we change this value to 0 in our animation, it will have the title slide from the left.

Our fadeIn will be used to take the elements from transparent to full opacity. Therefore, we can update it like so:

this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0)};

Now, we can create the shell of a lifecycle hook (for when the component mounts) and an animated sequence:

componentDidMount() {
Animated.sequence([
  ]).start();
}

Next, we want to have all their values in our local state to be changing all at once. Therefore, we can use Animated.parallel and place timing animations for all of our values within it:

componentDidMount() {
Animated.sequence([
Animated.parallel([
Animated.timing(
this.state.slideLeft,
{
toValue: 0,
duration: 2000,
easing: Easing.ease
}
),
Animated.timing(
this.state.fadeIn,
{
toValue: 1,
duration: 2000,
easing: Easing.ease
}
)
])
]).start();
}

The final step for our title is to bind the opacity and translateX in the inline styling to the local state:

<Animated.Text
style={{
fontSize: 0.25,
textAlign: 'center',
color: "#FFFFFF",
opacity: this.state.fadeIn,
transform: [
{translateX: this.state.slideLeft}
]
}}>

Let’s save and see if this is working:

Cool!

Let’s do the same thing for our button in Button.js.

First, we import Animated and Easing:

import {
Text,
View,
VrButton,
Animated
} from 'react-vr';
import { Easing } from 'react-native';

You can copy and paste the constructor and lifecycle hook from the title and update the this.state.slideLeft to this.state.slideRight .

We also want to update the local state for the slideRight value:

this.state = { slideRight: new Animated.Value(1), fadeIn: new Animated.Value(0)};

Then, we can bind the inline styling of the button to the local state and change the tag to Animated.View like so:

<Animated.View
style={{
margin: 0.1,
height: 0.3,
backgroundColor: '#A482DF',
borderRadius: 0.1,
opacity: this.state.fadeIn,
transform: [
{translateX: this.state.slideRight}
]
}}
>
//other stuff here
</Animated.View>

Let’s make sure this is working:

Perfect!

Let’s finish the next two scenes and then we will add the logic for transitions between each scene.

Creating the Dashboard Components

This scene is going to be the most challenging part of our app. One, it will be a dynamic scene. Meaning, we are going to change what is rendered in this scene before transitioning to another scene. We also need to incorporate the Twitch API to retrieve 6 videos.

In this section, we will start by creating the static scene. Then, we will add entrance animation and dynamic styling based on user input.

To get everything working, we will need to implement the Twitch API. However, this part will be done in a separate section.

Let’s get to it!

Static Dashboard Scene Components

First off, we need to create a file for our scene called Dashboard.js within the scenes folder.

Download this photo and save it in the static_assets folder as dashboard-background.jpg.

For now, we will just have the following code:

import React from 'react';
import {
Text,
View,
asset,
Pano
} from 'react-vr';
//Scene
class Dashboard extends React.Component {
render() {
return (
<View>
<Pano source={asset('dashboard-background.jpg')}/>
</View>
)
}
}
module.exports = Dashboard;

Cool! Let’s take a look at the mockup to understand how to do the layout components:

How do we express this in Flexbox?

In terms of rows, there will be 2 rows:

Our first row will have 5 Flexbox items that will each contain their own columns with Flexbox items:

The first column in this row (from the left) will be our MenuButtons component.

The next three columns will be our TileButtons component.

The far right column will be our ProgressCircles component.

The second row of this scene will be our button which is already defined in Button.js.

All that to say, we can create a layout component that will have the code to create 2 outermost rows (and nest the Flexbox items within it) in a file called DashboardLayout.js within out layouts folder.

Here is the code:

import React from 'react';
import {
View
} from 'react-vr';
//Layout
class DashboardLayout extends React.Component {
render() {
return (
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
//row 1 elements
</View>
      <View style={{
width: ,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
//row 2 elements
</View>
)
}
}
module.exports = DashboardLayout;

Note that there are 2 View components for each row within an outermost View component. Each has a Flexbox layout that specifies that it is a row and that all items within it will be centered horizontally and vertically.

Sweet! Now, let’s move on to our first element within the first row of DashboardLayout:

Create a file called MenuButtons.js within the elements folder.

Let’s create the shell of this component with the outermost View component that will be a Flexbox item within the row we created in the previous file and a container for a column:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Element
class MenuButtons extends React.Component {
render() {
return (
<View
style={{
margin: 0.1,
width: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
      </View>
)
}
}
module.exports = MenuButtons;

Next, we can do 4 View component for the buttons within this column:

<View
style={{
margin: 0.1,
width: 1,
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'center'
}}
>
<View
style={{
margin: 0.1,
height: 0.3,
backgroundColor: "#898794"
}}
>
</View>

<View
style={{
margin: 0.1,
height: 0.3,
backgroundColor: "#898794"
}}
>
</View>
  <View
style={{
margin: 0.1,
height: 0.3,
backgroundColor: "#898794"
}}
>
</View>
  <View
style={{
margin: 0.1,
height: 0.3,
backgroundColor: "#898794"
}}
>
</View>
</View>

Finally, we can insert the VrButton and Text component for each button (only the first button will have actual text):

See GitHub Gist //click to see full code since it's a bit lengthy

That completes our MenuButtons component!

After that, we can create the TileButtons.js file within our elements folder which will render the following 3 columns:

First, we can add the shell of the component and outermost Flexbox row container:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Element
class TileButtons extends React.Component {
render() {
return (
<View style={{flexDirection: 'row', alignItems: 'center', justifyContent: 'center'}}>
      </View>
)
}
}
module.exports = TileButtons;

This might seem confusing since we already defined a row container in DashboardLayout where this component will be nested. Think of this as being a row container just for our tile button within our outermost row (as defined in DashboardLayout):

Then, we add 3 View components that will be Flexbox items in the row and column containers for our tile buttons:

import React from 'react';
import {
Text,
View,
VrButton
} from 'react-vr';
//Element
class TileButtons extends React.Component {
render() {
return (
<View>
<View
style={{
margin: 0.1,
width: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
        </View>
        <View
style={{
margin: 0.1,
width: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
        </View>
        <View
style={{
margin: 0.1,
width: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
        </View>

</View>
)
}
}
module.exports = TileButtons;

Here is a visualization of columns we just defined:

Then, we add 2 items to each column which are View components with a VrButton containing empty text:

See GitHub Gist //click to see full code since a bit lengthy

Again, this is all-together rendering the following 3 columns:

This completes the TileButtons for now.

Our last element in our current row will be ProgressCircle.js:

Create another file called ProgressCircles.js within the elements folder.

Let’s add the shell of the component as well as the View component that will be an element in the current row and the column container for our two circles:

import React from 'react';
import {
View
} from 'react-vr';
//Element
class ProgressCircles extends React.Component {
render() {
return (
//Outermost View
<View>
//Column
<View
style={{
margin: 0.1,
width: 0.2,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
        </View>
</View>
)
}
}
module.exports = ProgressCircles;

Next, we add the two progress circles which are just View components that will render as circles due to the inline styling:

//Outermost View
<View>
//Column
<View
style={{
margin: 0.1,
width: 0.2,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
//Circle 1
<View
style={{
margin: 0.1,
width: 0.1,
borderRadius: 0.5,
height: 0.3,
backgroundColor: "#DBDAF1"
}}
>
    </View>
    //Circle 2
<View
style={{
margin: 0.1,
width: 0.1,
borderRadius: 0.5,
height: 0.3,
backgroundColor: "#DBDAF1"
}}
>
</View>
  </View>
</View>

Nesting Our Components

We start in DashboardLayout.js and import the 3 elements we just created:

import MenuButtons from './elements/MenuButtons.js';
import TileButtons from './elements/TileButtons.js';
import ProgressCircles from './elements/ProgressCircles.js';

Then, we nest them into the first column:

<View>
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
<MenuButtons/>
<TileButtons/>
<ProgressCircles/>
</View>
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
//row 2 elements
</View>
</View>

This will render the following:

Next, we can import our button in the second-row container:

import Button from './elements/Button.js';

Then, we nest the button within the second-row container:

<View>
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
<MenuButtons/>
<TileButtons/>
<ProgressCircles/>
</View>
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}]
}}>
<Button text={this.props.text}/>
</View>
</View>

Recall, the text of the button component will be passed down as a prop.

After that, we can go to Dashboard.js and start by importing the DashboardLayout component:

import DashboardLayout from './layouts/DashboardLayout.js';

Then, we can nest it underneath the Pano tag:

<View>
<Pano source={asset('dashboard-background.jpg')}/>
<DashboardLayout text={this.props.text} />
</View>

Note that we passing the text prop down to our button as well.

Next, we can go to index.vr.js and import the Dashboard component:

import Dashboard from './components/scenes/Dashboard.js';

Finally, remove the TitleScene component and nest the Dashboard component:

<View>
<Dashboard text={"Select Environment"}/>
</View>

I left the TitleScene component in a comment right before the return:

export default class VrVideoApp extends React.Component {
render() {
//<TitleScene text={"Watch a Video"}/>
return (...)
}
}

Testing Our Component

First off, make sure to remove any comments that I had included in the element components (TileButtons, MenuButtons, and ProgressCircles).

Save all your files and let’s refresh the local host:

Note: Your tile (light purple) buttons will be larger as I took this screenshot before updating the inline styling as you have copied it from the GitHub gist.

Looks like the row container with our button is a bit too low so let’s update the inline styling of both rows found in DashboardLayout.js:

<View>
<View style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}],
marginTop: -0.3
}}>
<MenuButtons/>
<TileButtons/>
<ProgressCircles/>
</View>
<View style={{
width: 5,
height: 0.5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -3]}],
marginTop: -0.7
}}>
<Button text={this.props.text}/>
</View>
</View>

Note that I just added some negative marginTop values.

I’m also going to add some left and right padding to our button component found in Button.js:

//in return
<Animated.View
style={{
margin: 0.1,
paddingLeft: 0.2,
paddingRight: 0.2,
height: 0.3,
backgroundColor: '#A482DF',
borderRadius: 0.1,
opacity: this.state.fadeIn,
transform: [
{translateX: this.state.slideRight}
]
}}
>
<VrButton>
<Text
style={{
fontSize: 0.2,
textAlign: 'center',
color: "#FFFFFF"
}}>
{this.props.text}
</Text>
</VrButton>
</Animated.View>

Let’s go to the local host and refresh:

Lookin’ good!

Entrance Animations

Since we are reusing our button component, it’s animation is already set to slide in from the right and fade in.

To make things easy, we will apply the same animation as we did to our title to the entire first row of DashboardLayout.

Open DashboardLayout.js and let’s start by importing Animated and Easing:

import {
View,
Animated
} from 'react-vr';
import { Easing } from 'react-native';

Then, we add the local state that has slideLeft and fadeIn animation values:

//class starts here
constructor() {
super();
this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0)};
}

Next, we can add the lifecycle hook with the animation sequences to change these values:

componentDidMount() {
Animated.sequence([
Animated.parallel([
Animated.timing(
this.state.slideLeft,
{
toValue: 0,
duration: 2000,
easing: Easing.ease
}
),
Animated.timing(
this.state.fadeIn,
{
toValue: 1,
duration: 2000,
easing: Easing.ease
}
)
])
]).start();
}

Finally, we can update the tag of the View component for the first row container to an Animated.View as well as bind the animated styles:

<Animated.View 
style={{
width: 5,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start',
layoutOrigin: [0.5, 0.5],
opacity: this.state.fadeIn,
transform: [
{translateX: this.state.slideLeft},
{translateZ: -3}
],
marginTop: -0.3
}}
>
<MenuButtons/>
<TileButtons/>
<ProgressCircles/>
</Animated.View>

Let’s test this out:

Cool beans!

Updating the Scene on User Input

There are a couple things that need to be updated based on the user’s input within this scene.

First, the user should only be able to select one video (contained in our title buttons) and the selected video should have purple border placed around it:

We also want the button to not render until a tile button is selected:

When the user clicks the button display “Select Environment”, then the tile buttons will no longer contain video options but rather an environment (panoramic photo in video player) options. We won’t be able to handle this part until we do the Twitch API code, however, the button text should change to “Watch Video”.

In addition to all this, the first progress circle should be the same color as the button to start:

When the button is clicked to proceed to the environment options, the progress circle should update like so:

That way, the progress circle is keeping track of the stage of this scene.

Let’s begin with applying the conditional rendering of our button.

Since our button component is used in multiple scenes, we need to have the boolean flag that controls whether or no it is rendered to be a passed down prop.

Open up index.vr.js and let’s update the code:

export default class VrVideoApp extends React.Component {
render() {
//<TitleScene showButton={true} text={"Watch a Video"}/>
return (
<View>
<Dashboard showButton={false} text={"Select Environment"}/>
</View>
);
}
};

In the code above, we pass in a prop of showButton to the button from both TitleScene (commented out) and Dashboard. We give it a value of true in TitleScene because we want it to render by default. We give it a value of false in Dashboard because we don’t want it to render by default.

Now, we have to pass this prop down to the button component following the path down through TitleScene and Dashboard.

Following the TitleScene path, open TitleLayout.js and let’s pass down showButton to the button component:

<Button showButton={this.props.showButton} text={this.props.text}/>

Following the Dashboard path, open DashboardLayout.js and let’s pass down showButton to the button component:

<Button showButton={this.props.showButton} text={this.props.text}/>

Finally, we can add the conditional rendering in Button.js to only display the button to start if showButton is true:

render() {
const showButton = this.props.showButton;
return (
<View>
{showButton ? (
<Animated.View
style={{
margin: 0.1,
paddingLeft: 0.2,
paddingRight: 0.2,
height: 0.3,
backgroundColor: '#A482DF',
borderRadius: 0.1,
opacity: this.state.fadeIn,
transform: [
{translateX: this.state.slideRight}
]
}}
>
<VrButton>
<Text
style={{
fontSize: 0.2,
textAlign: 'center',
color: "#FFFFFF"
}}>
{this.props.text}
</Text>
</VrButton>
</Animated.View>
) :(
<View></View>
)}
</View>
)
}

If we check our local host, we can see that the button is no longer rendering by default as expected:

Next, let’s set up the progress circles to have dynamic background colors controlled by a local state.

Open ProgressCircles.js.

First, we can add a local state that will have the color of the first circle be the darker purple (same color as button) and the second circle be the same color as we currently see:

constructor() {
super();
this.state = { circle1Color: "#A482DF" , circle2Color: "#DBDAF1"};
}

Then, we simply bind this to our inline styling:

backgroundColor: this.state.circle1Color
backgroundColor: this.state.circle2Color

Refresh the local host to now see the following:

For our next step, let’s have the button appear on the click of any tile button.

Open DashboardLayout.js.

We want to toggle on our button on the click of tile button. Both of these components are in different files but nested under DashboardLayout. DashboardLayout currently passes down the showButton prop which controls whether the button renders. Therefore, we need to have the event handling function (that will be called from the buttons in TileButtons.js) in this file so that it can change the value of the showButton prop that is passed down.

First, let’s add a property called showButton to the local state:

this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0), showButton: this.prop.showButton};
//showButton false by default

Then, we want the showButton prop that we pass down to the button element to be bound to this local state propety:

<Button showButton={this.state.showButton} text={this.props.text}/>

Next, we write the event handling function that will be passed down to the TileButtons component as a prop. This function will update showButton to true:

//component lifecycle here
updateShowButton() {
this.setState({showButton: true});
}
//render function here

Now, let’s pass this down to the TileButtons component:

<TileButtons updateShowButton={this.updateShowButton.bind(this)}/>

To conclude, we call this function on the click of any of our VrButtons in TileButton.js:

<VrButton onClick={this.props.updateShowButton}>

If we refresh the local host, we can see this in action:

Awesome! Only three things left!

The next task is to have the second progress circle to take on the darker purple color and the first progress circle to take on the white color.

We can handle this much like we did the rendering of our button in this scene. We will have a property in the local state of DashboardLayout and an event handler that can update that property. Finally, this event handler will be triggered by the click of the button component and will ultimately control the coloring of the progress circles.

We can start this task by first adding to the local state found in DashboardLayout.js:

this.state = { 
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1"
};

In the code above, we define two new properties: color1 and color2. These properties will be passed down to ProgressCircles and control the colors of the circles. Next, let’s add an event handler that will reverse the color properties to reflect the current stage of the scene:

updateScene() {
this.setState({color1: "#DBDAF1", color2: "#A482DF"});
}

Next, we pass this event handler down to the Button component:

<Button updateScene={this.updateScene.bind(this)} showButton={this.state.showButton} text={this.props.text}/>

After that, we want the VrButton in Buttos.js to call this passed down event handler on click:

<VrButton onClick={this.props.updateScene}>

Back in DashboardLayout.js, we can bind the color1 and colors2 properties in the local state to props passed down to the ProgressCircles component:

<ProgressCircles color1={this.state.color1} color2={this.state.color2}/>

The final piece to the puzzle for this task is to remove the local state in ProgressCircles.js and have the passed down props bound to the inline styling of the circle:

//remove the constructor with local state somewhere up here
//update the following in the render function
<View
style={{
margin: 0.1,
width: 0.1,
borderRadius: 0.5,
height: 0.1,
backgroundColor: this.props.color1
}}
>
</View>
<View
style={{
margin: 0.1,
width: 0.1,
borderRadius: 0.5,
height: 0.1,
backgroundColor: this.props.color2
}}
>
</View>

Let’s check the local host to make sure this task is completed:

Right on! Two more tasks then we can then we can take a breather.

The next task is pretty easy. We want to update the button component’s text to “Watch Video” at the same time that we update the progress circles.

We need to have the text prop being passed down the button component to be dynamically updated. Therefore, let’s start by updating our constructor:

constructor(props) {
super(props);
this.state = {
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1",
text: this.props.text
};
}

In the code above, we are now passing in the inherited props to the constructor so we can set the initial text property of the local state equal to this.props.text .

Then, we can update this in our updateScene event handler:

updateScene() {
this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video"});
}

Finally, we bind the text prop of Button to the local state instead of just passing down the inherited prop:

<Button updateScene={this.updateScene.bind(this)} showButton={this.state.showButton} text={this.state.text}/>

Let’s give it a whirl:

Woohoo! Just one more task!

Our final task for updating this scene on a user input is to add the dynamic styling of our tile buttons so that a darker purple border arises when a tile is clicked (and only one can have a border at a time):

The first thing to do for this is to update the inline styling of the View components right above the VrButtons within TileButtons.js:

<View
style={{
margin: 0.1,
height: 0.6,
backgroundColor: "#CAB9E5",
borderWidth: "0",
borderColor: "#A482DF",
borderStyle: "solid"
}}
>

Because the borderWidth is set to 0, no border will be displayed by default. If we adjust this, however, the border will display.

We want to have the borderWidth update on the click of each tile button so only one tile has a border at a time.

How do we do this?

Like we have been doing in the previous tasks, we will control this with passed in props from the DashboardLayout component.

Open DashboardLayout.js and let’s update the local state:

this.state = {
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1",
text: this.props.text,
borderWidths: [0, 0, 0, 0, 0, 0]
};

In the code above, we have added an array which can be used to control the styling of the borderWidth values of the tile buttons. Each number in borderWidths refers to one tile button.

We need an event handler attached to the tile buttons that says: “Hey! Tile ___ here. I was clicked so change my border’s width so you can see it.”

Because we already have an onClick event (updateShowButton) attached to the VrButtons in TileButtons.js, we need to contain one event handler called updateStage that will show the button on the first click of a tile and update the tile’s border width of every click:

//previously updateShowButton
updateStage(input) {
if(this.state.showButton === false) {
this.setState({showButton: true});
}
switch (input) {
case 1:
this.setState({borderWidths: [0.05, 0, 0, 0, 0, 0]});
break;
case 2:
this.setState({borderWidths: [0, 0.05, 0, 0, 0, 0]});
break;
case 3:
this.setState({borderWidths: [0, 0, 0.05, 0, 0, 0]});
break;
case 4:
this.setState({borderWidths: [0, 0, 0, 0.05, 0, 0]});
break;
case 5:
this.setState({borderWidths: [0, 0, 0, 0, 0.05, 0]});
break;
case 6:
this.setState({borderWidths: [0, 0, 0, 0, 0, 0.05]});
break;
}
}

In the code above, we update showButton to true if it is currently false. We also use a switch statement to update the width of the border that called this event handler.

Now, let’s pass down this event handler and the borderWidths property as props to TileButtons:

<TileButtons updateStage={this.updateStage.bind(this)} borderWidths={this.state.borderWidths}/>

Next, we can update the VrButtons in TileButtons.js to call this event handler prop that was passed down and pass in an input (key/index):

<VrButton onClick={ () => this.props.updateStage(1) }>
//update input value for each VrButton

Finally, we can bind the borderWidth values for each tile button to the values in the borderWidths prop:

<View
style={{
margin: 0.1,
height: 0.6,
backgroundColor: "#CAB9E5",
borderWidth: this.props.borderWidths[1],
borderColor: "#A482DF",
borderStyle: "solid"
}}
>

Here’s the final TileButtons.js and DashboardLayout.js code at this point.

Update the code and let’s see if this is working:

Woohoo! We have finally finished the updating of our Dashboard scene based on a user’s input.

We still need to do the Twitch API stuff, but you deserve a pat on the back and a break!

Final Code

The code for the entire project at this point is available on GitHub.

Study Break

This is definitely the lengthiest chapter that we will have in this book.

Feel free to take a deep breath, grab some coffee, stretch, whatever.

If you want to continue this later, you can use something like Pocket.

Whenever you are ready, we can continue to finish our video player component, implement the Twitch API, and then tie together transitions to complete our video app.

Creating the Video Player Components

Static Video Player Components

First, let’s create our scene component called VideoPlayer.js within the scenes folder.

Open this file and let’s add the code:

import React from 'react';
import {
Text,
View,
asset,
Pano
} from 'react-vr';
//Scene
class VideoPlayer extends React.Component {
render() {
return (
<View>
<Pano source={asset('title-background.jpg')}/>
      </View>
)
}
}
module.exports = VideoPlayer;

Remember, the Pano source will be controlled by the user’s selection in the Dashboard scene. For now, we can just leave it as title-background.jpg.

Next, we can create the VideoPlayerLayout.js file within the layouts folder.

In this file, we want a simple container for the video player element that will display in the front of the virtual scene and a simple container to display the button in the back of the virtual scene:

import React from 'react';
import {
View
} from 'react-vr';
//Layout
class VideoPlayerLayout extends React.Component {
render() {
return (
<View>
<View style={{
flex: 1,
width: 8,
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: '#333333',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, -5]}]
}}>
//insert Video element
</View>
      <View style={{
flex: 1,
width: 2.5,
flexDirection: 'column',
alignItems: 'stretch',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, 5]}]
}}>
//insert button component
</View>
</View>
)
}
}
module.exports = VideoPlayerLayout;

Note that we are using transform: [{translate: [0, 0, 5]}] to have the button component render in the back of the virtual world.

Since we are reusing the button component, we just need to create the video element in VideoElement.js (within the elements folder). Note: I included “Element” in the file naming convention since Video is a predefined component.

Open this file and replace it with the following:

import React from 'react';
import {
Video,
View,
asset
} from 'react-vr';
//Element
class VideoElement extends React.Component {
render() {
return (
<View style={{ margin: 0.1, height: 4}}>
<Video style={{height: 4}} source={asset('fireplace.mp4')} />
</View>
)
}
}
module.exports = VideoElement;

In the code above, we are adding a Flexbox item to our container and using a Video component to display a video.

Let’s use the fireplace.mp4 file that we used in Chapter 3 just so we can test this out. You can download this video here.

Cool! We can move on to nesting our components for this scene.

Nesting Our Components

First, we can nest import and nest the VideoPlayer scene in our app component found in index.vr.js:

//other imports here
import VideoPlayer from './components/scenes/VideoPlayer.js';
export default class VrVideoApp extends React.Component {
render() {
//<TitleScene showButton={true} text={"Watch a Video"}/>
//<Dashboard showButton={false} text={"Select Environment"}/>
return (
<View>
<VideoPlayer showButton={true} text={"Back to Dashboard"}/>
</View>
);
}
};
AppRegistry.registerComponent('VrVideoApp', () => VrVideoApp);

Note that I have commented out the Dashboard that was being nested and passed props into VideoPlayer so the button will render with the right text by default.

Going another level down, we import and nest VideoPlayerLayout in VideoPlayer.js:

import VideoPlayerLayout from './layouts/VideoPlayerLayout.js'
//Scene
class VideoPlayer extends React.Component {
render() {
return (
<View>
<Pano source={asset('title-background.jpg')}/>
<VideoPlayerLayout showButton={this.props.showButton} text={this.props.text}/>
</View>
)
}
}

Note that we are continuing to pass down the props to our button component.

Let’s go down to VideoPlayerLayout.js and add our imports for the elements:

import VideoElement from './elements/VideoElement.js';
import Button from './elements/Button.js';

Then, let’s nest the elements like so:

<View>
<View style={{...}}>
<VideoElement/>
</View>
  <View style={{...}}>
<Button showButton={this.props.showButton} text={this.props.text}/>
</View>
</View>

Testing Our Scene

Refresh the local host and let’s check to see if our Video Player scene is rendering:

Lookin’ good except for that dang button that is too low and needs to be flipped.

We can flip the button’s container (found in VideoPlayerLayout.js) by adding rotateY: -180 :

<View style={{
flex: 1,
width: 2.5,
flexDirection: 'column',
alignItems: 'stretch',
layoutOrigin: [0.5, 0.5],
transform: [{translate: [0, 0, 5]}, {rotateY: -180}]
}}>

If you check the local host, we will see:

We can update the translate value on the Y axis to raise the button container:

transform: [{translate: [0, 3.5, 5]}, {rotateY: -180}]

We can now see the following from the local host:

Adding Entrance Animation

The final step in this scene at the time being is to add entrance animation. Our button element already has entrance animation.

We can add the following animation to have the VideoElement component fade in (update VideoElement.js):

See GitHub Gist

In this code, we import Animated and Easing, add a local state with an animation value for controlling the opacity, bind the inline styling to this animated value, and play the animation when the component mounts.

If we refresh the local host, we now see this in effect:

Implementing the Twitch API

Creating an API Key

First off, create a Twitch account if you have not already.

Then, we can create a Twitch API key by visiting the connections page.

Under Other Connections, you can click to register an app and retrieve the API key.

When registering an app, make sure to specify the redirect URI as http://localhost:

Hit Register, copy the Client ID that is generated, and paste it into an empty file in your code editor.

We will be able to use this client ID in order to retrieve the data we need.

Installing Axios

To make API requests, we can use Axios (specifically, we will use the React Native version).

We can install it by running the following in command line:

npm install react-native-axios --save

Before we move along, let’s import this into index.vr.js where we will be making the API call:

import axios from 'react-native-axios';

Retrieving Information of Streams

We want to retrieve streaming videos from Twitch that can be played from within our video player. For our Dashboard, we want to display the video cover image (previews) of 6 streams and have the user select which one they want to watch.

We will retrieve the data of the streams from our app component and pass down the cover image addresses to our Dashboard as a prop.

We want to retrieve these streaming videos before our app component mounts so we add the following in index.vr.js:

export default class VrVideoApp extends React.Component {
componentWillMount() {

}
//render
}

Then, let’s add the shell of the Axios code that will request streaming videos and their information:

componentWillMount() {
axios.get('')
.then(response => {

})
.catch(e => {
console.log(error);
});
}

Within axios.get('') , we can put the URL that will return featured Twitch streams. The URL can be found on the official Twitch API documentation:

Knowing this, let’s insert the following URL:

componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured')
.then(response => {

})
.catch(e => {
console.log(error);
});
}

Next, we can attach some URL parameters so there is only data returned for 6 featured stream (1 for each tile in our Dashboard scene):

componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6')
.then(response => {

})
.catch(e => {
console.log(error);
});
}

Now, we need to add another URL parameter that authenticates us to retrieve data. We add the client_id which we generated previously:

//update with your client_ID
componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
.then(response => {

})
.catch(e => {
console.log(error);
});
}

So far, our code is saying: “Hey, Twitch! We have an address for getting 6 features streams and some verification that you’re cool with me doing this. Once you give us what we need, then we’ll have to do something with what you gave us. If you catch an error, just log it for us. ”

Let’s see if this is working by logging the response:

componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
.then(response => {
console.log(response);
})
.catch(e => {
console.log(error);
});
}

Refresh the local host and check the console. We should see the following data is returned:

In the retrieved data (Object), there is data (Object) for 6 featured streams within an array called featured.

Within one of the featured stream objects, there is an object called stream containing the information we need (cover/preview image URL and an ID):

Let’s create two functions that will gather the preview URLs and stream IDs into arrays: gatherPreviews and gatherIDs.

Starting with the former, let’s call this once we get a response from the API call:

componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
.then(response => {
console.log(response);
this.gatherPreviews(response);
})
.catch(e => {
console.log(error);
});
}

Note that we are passing gatherPreviews the response object.

Now, let’s add the shell of gatherPreviews:

gatherPreviews(response) {

}

Then, we want to map (go through) the stream previews from the featured arrays to a new array called previews:

gatherPreviews(response) {
const previews = response.data.featured.map(function(feat) {
return feat.stream.preview.large;
});
  console.log(previews);
}

Note that response.data.features.map and feat.stream.preview.large was known by looking at the data which we logged to the console.

Let’s refresh the local host and we should see that the previews array (which we logged) is in fact storing 6 preview image URLs:

Let’s finish the data retrieval by writing up the gatherStreamIDs function.

First, update the local state like so:

this.state = { previews: "", IDs: ""}

Next, we want to call this function when get a response from the API call:

componentWillMount() {
axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
.then(response => {
console.log(response);
this.gatherPreviews(response);
this.gatherStreamIDs(response);
})
.catch(e => {
console.log(error);
});
}

Finally, we add the function which will gather all the stream IDs, store them in a new array, and update the local state property IDs to the value of this new array (also called IDs):

gatherStreamIDs(response) {
const IDs = response.data.featured.map(function(feat) {
return feat.stream._id;
});
  console.log(IDs);
}

As you can see above, I logged IDs which is correctly storing the stream IDs:

Displaying the Preview Images

The next major step is to pass down the preview image URLs from the local state in index.vr.js to the TileButtons component so the tiles will display the preview images.

Let’s start by adding a local state and add the previews array to the local state as a property also called previews in index.vr.js:

constructor() {
super();
this.state = { previews: ""}
}
//lifecycle component here
gatherPreviews(response) {
const previews = response.data.featured.map(function(feat) {
return feat.stream.preview.large;
});
  this.setState({previews: previews});
}

Let’s pass this property down to the Dashboard scene as a prop also called previews:

render() {
//<TitleScene showButton={true} text={"Watch a Video"}/>
//<VideoPlayer showButton={true} text={"Back to Dashboard"}/>
return (
<View>
<Dashboard previews={this.state.previews} showButton={false} text={"Select Environment"}/>
</View>
);
}

Now, we will have to pass this prop down all the way down to the TileButtons component.

Let’s begin this process in Dashboard.js:

<DashboardLayout previews={this.props.previews} text={this.props.text} />Putting Together Transitions & Animations

We can then go down another level to DashboardLayout.js:

<TileButtons previews={this.props.previews} updateStage={this.updateStage.bind(this)} borderWidths={this.state.borderWidths}/>

In TileButtons.js, we are currently rendering empty Text components between the VrButtons:

<Text
style={{
fontSize: 0.2,
textAlign: 'center',
color: "#FFFFFF",
}}>
</Text>

This was necessary so the tile containers (styled with a light purple color) would display. We need to replace this with an Image component that has the source bound to one of the preview URLs.

Before we add the Image component, let’s import it:

import {
Text,
View,
VrButton,
Image
} from 'react-vr';

Then, we can add the Image component and bind the source to the preview URLs (these Image components will replace the Text components):

<VrButton onClick={ () => this.props.updateStage(1) }>
<Image source={{uri: this.props.previews[0]}} style={{width: 1, height: 0.6}}/>
</VrButton>
//See GitHub Gist for all updates

Note that we have to specify the width and height of the Image. We set it to the same height/width ratio as the containers.

Let’s see if this worked:

So cool!

The final piece in this section is to fix the borders:

This requires moving the borderWidth and borderColor inline styling properties from the View components to the Image components:

<Image
source={{uri: this.props.previews[0]}}
style=
{{
width: 1,
height: 0.6,
borderWidth: this.props.borderWidths[0],
borderColor: "#A482DF"
}}
/>
//See GitHub Gist for all updates

Refresh local host and you will see that this is working!

Display the Environment Images

When the user clicks on the button when display “Select Environment”, we want to display the images of 6 possible environments to choose from. Which image is clicked will ultimately control what panoramic photo is rendered in our Video Player scene.

For now, we just want to focus on display the environment photos.

First, we need 4 more panoramic photos. Add the Arizona, Hawaii, New Hampshire, and Texas panoramic photos from Chapter 2 into our static_assets folder.

Next, we can add an array in the local state called environments in DashboardLayout.js which will contain the file names of all the panoramic photos:

constructor(props) {
super(props);
this.state = {
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1",
text: this.props.text,
borderWidths: [0, 0, 0, 0, 0, 0],
environments: ["title-background.jpg", "dashboard-background.jpg", "Arizona.jpg", "Hawaii.jpg", "New Hampshire.jpg", "Texas.jpg"]
};
}

We also will add a property that will tell us the stage of our scene (whether a user is selecting from streams or environments):

this.state = {
//everything else
stage: 1
};

Currently, we are just updating the progress circles’ colors and button text when a user clicks the button from stage 1 (when stream images are displaying) within the updateScene function.

We also want to update the stage from within this function:

updateScene() {
this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video", stage: 2});
}

Then, we want to pass the environments and stage as props down to the TileButtons component like so:

<TileButtons
stage={this.state.stage}
environments={this.state.environments}
previews={this.props.previews}
updateStage={this.updateStage.bind(this)}
borderWidths={this.state.borderWidths}
/>

In TileButtons.js, we will use conditional rendering to either display the stream preview photos or environment photos depending on the stage of the scene.

To do this, let’s first import asset:

import {
Text,
View,
VrButton,
Image,
asset
} from 'react-vr';

Now, we can store the stage prop into a variable:

render() {
const stage = this.props.stage;
//...
}

Finally, we use this stage variable for conditional rendering where we either use the previews array prop or environments array prop for the Image source:

{stage === 1 ? (
<Image
source={{uri: this.props.previews[0]}}
style=
{{
width: 1,
height: 0.6,
borderWidth: this.props.borderWidths[0],
borderColor: "#A482DF"
}}
/>
): (
<Image
source={asset(this.props.environments[0])}
style=
{{
width: 1,
height: 0.6,
borderWidth: this.props.borderWidths[0],
borderColor: "#A482DF"
}}
/>
)}
//See GitHub Gist for all updates

The code above can be read as: “If we are in the first stage where a user is selecting the stream video, then show them all the stream preview images. Else, show them the environments they can select to display as the panoramic of the video player.”

Let’s refresh the local host and test this out:

Sweet! As you can see, the rendering is really slow given the high resolution of the panoramic photos. However, it’s not worth our time to fix this.

Setup to Record User Input From Dashboard

There is one last thing that we want to accomplish before we move on to the next main section of this chapter. That is, we want to do the setup to record the stream ID and environment panoramic that the user selects from the Dashboard scene and pass it to the VideoPlayer component.

First, let’s add the IDs array from the gatherStreamIDs function to the local state found in index.vr.js:

this.state = { previews: "", IDs: ""};
gatherStreamIDs(response) {
const IDs = response.data.featured.map(function(feat) {
return feat.stream._id;
});
  this.setState({IDs: IDs});
}

Next, let’s two properties (selectedStreamID and selectedEnv) which will ultimately store the user’s selection in the Dashboard scene:

this.state = { previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Note that we are storing these properties in the app component so we can pass down their values as props to the VideoPlayer scene component.

We can also add an event handler that will take in two parameters, the stage and the value. This will be triggered from the Dashboard scene and will allow us to capture the user inputs at each stage. Based on those parameters, we can update the local state as needed:

captureSelection(stage, value) {
switch (stage) {
case 1:
this.setState({selectedStreamID: value});
break;
case 2:
this.setState({selectedEnv: value});
break;
}
}

We want this event handler to be called from the updateScene function in DashboardLayout.js. Therefore, we will pass down this event handler to the Dashboard component in index.vr.js:

<Dashboard
captureSelection={this.captureSelection.bind(this)}
previews={this.state.previews}
showButton={false}
text={"Select Environment"}
/>

In Dashboard.js, let’s pass this prop down another level to the DashboardLayout component:

<DashboardLayout captureSelection={this.props.captureSelection} previews={this.props.previews} text={this.props.text} />

This is as far as we can go at the moment with captureSelection since the rest of the logic depends on how we transition between each scene.

There is one final thing that we can do. Back in index.vr.js, let’s pass down the selected stream and selected environment photo to the VideoPlayer component:

render() {
//<TitleScene showButton={true} text={"Watch a Video"}/>
//<VideoPlayer streamID={this.state.selectedStreamID} env={this.state.selectedEnv} showButton={true} text={"Back to Dashboard"}/>
    //...
}

Scene Transitions

In this final coding section, we are going to add the code so we can transition through each scene. We will also finish up the capturing of the users inputs from the Dashboard scene to have this all working smoothly.

Scene 1 to Scene 2 Transition

Let’s start by adding the conditional rendering from our app component to control which scene is currently being rendered.

First, let’s add a property to the local state called scene in index.vr.js:

this.state = { scene: 1, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Then, let’s update the return with conditonal rendering depending on the value of scene:

render() {
const scene = this.state.scene;
return (
<View>
{scene === 1 ? (
<TitleScene showButton={true} text={"Watch a Video"}/>
) : (
scene === 2 ? (
<Dashboard
captureSelection={this.captureSelection.bind(this)}
previews={this.state.previews}
showButton={false}
text={"Select Environment"}
/>
) : (
<VideoPlayer streamID={selectedStreamID} env={selectedEnv} showButton={true} text={"Back to Dashboard"}/>
)
)}
</View>
);
}

The code above can be read as: “If we are in scene 1, show the title scene. Else, show the dashboard scene for scene 2 or the video player scene for scene 3.”

Next, let’s add an event handler that will update the scene property so the requested scene renders next:

changeScenes(nextScene) {
switch (nextScene) {
case 1:
this.setState({scene: 1});
break;
case 2:
this.setState({scene: 2});
break;
case 3:
this.setState({scene: 3});
break;
}
}

This event handler will always be called by the Button component. In order to know what scene to request next, the button must know what scene it is currently in.

This means we will do more passing down of props.

Let’s start with passing the event handler we just created and the scene property as props to the TitleScene component:

<TitleScene
showButton={true}
text={"Watch a Video"}
changeScenes={this.changeScenes.bind(this)}
scene={this.state.scene}
/>

In TitleScene.js, we can pass the props down to the TitleLayout component:

<TitleLayout
showButton={this.props.showButton}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

In the next level down, TitleLayout.js, we can pass these props down to the button:

<Button
showButton={this.props.showButton}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

In Button.js, we want to render a button that calls changeScenes with the requested new scene passed in.

To do this, we have a variable called nextScene that has a different value depending on the current scene:

render() {
const showButton = this.props.showButton;
const currentScene = this.props.scene;
let nextScene;
switch (currentScene) {
case 1:
nextScene = 2;
break;
case 2:
nextScene = 3;
break;
case 3:
nextScene = 1;
break;
}
//return
}

Then, we update the onClick of the VrButton to be:

<VrButton onClick={() => this.props.changeScenes(nextScene)}>

Let’s refresh the local host and see if this is working:

Cool beans!

Scene 3 to Scene 1 Transition

Scene 2 to Scene 3 will be more work so let’s skip it for now.

This transition shouldn’t be too difficult.

First, let’s update the local state in index.vr.js so scene starts at 3 (this is to test as it will eventually be automatically controlled):

this.state = { scene: 3, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Next, we just pass down changeScenes and scene as props through the VideoPlayer component and down to the button:

index.vr.js

<VideoPlayer
streamID={this.state.selectedStreamID}
env={this.state.selectedEnv}
showButton={true}
text={"Back to Dashboard"}
changeScenes={this.changeScenes.bind(this)}
scene={this.state.scene}
/>

VideoPlayer.js

<VideoPlayerLayout
showButton={this.props.showButton}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

VideoPlayerLayout.js

<Button
showButton={this.props.showButton}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

If you check the local host, this will be working correctly.

Scene 2 to Scene 3 Transition

This section will be the most work of all the transitions because it’s the most complicated scene as it contains two stages.

Take a deep breath and let’s do it!

First, let’s update the scene property in the local state back to 1:

this.state = { scene: 3, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Next, we just pass down changeScenes and scene as props through the Dashboard component and down to the button:

index.vr.js

<Dashboard
captureSelection={this.captureSelection.bind(this)}
previews={this.state.previews}
showButton={false}
text={"Select Environment"}
changeScenes={this.changeScenes.bind(this)}
scene={this.state.scene}
/>

Dashboard.js

<DashboardLayout
captureSelection={this.props.captureSelection}
previews={this.props.previews}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

DashboardLayout.js

<Button
updateScene={this.updateScene.bind(this)}
showButton={this.state.showButton}
text={this.state.text}
changeScenes={this.props.changeScenes}
stage={this.state.stage}
scene={this.props.scene}
/>

Note that we also pass the stage property found in the local state of the DashboardLayout component. This will allow us to have two separate onClick events depending on what stage we are at within this Dashboard scene.

In Button.js, let’s add conditional rendering so we can have a different VrButton component for scene 2 as it will have two possible onClick events:

{currentScene === 2 ? (
<VrButton
onClick={
() => {
      }
}
>
<Text
style={{
fontSize: 0.2,
textAlign: 'center',
color: "#FFFFFF"
}}>
{this.props.text}
</Text>
</VrButton>
) : (
<VrButton onClick={() => this.props.changeScenes(nextScene)}>
<Text
style={{
fontSize: 0.2,
textAlign: 'center',
color: "#FFFFFF"
}}>
{this.props.text}
</Text>
</VrButton>
)}

Notice that we have an empty onClick event for the scene 2 VrButton.

Next, let’s store the stage prop into a variable like so:

//switch statement up here
const stage = this.props.stage;
return (
//...

Let’s then add a switch statement where we will either go to stage 2 of this scene and display our environment photos or go to scene 3 depending on the value of the stage variable we just created:

{currentScene === 2 ? (
<VrButton
onClick={
() => {
switch (stage) {
case 1:
this.props.updateScene();
break;
case 2:
this.props.changeScenes(nextScene);
break;
}
}
}
>
  //...

Our code can now be read as: “If we are in scene 2, then we want a VrButton that will either go to stage 2 of the scene or go to scene 3.”

Refresh the local host and let’s test this out:

Hoot hoot! We are almost done!

Playing the Selected Video in the Selected Environment

Currently, we have already done some setup for an event handler called captureSelection. This event handler has been passed all the way down into the DashboardLayout component from the app component.

captureSelection(stage, value) {
switch (stage) {
case 1:
this.setState({selectedStreamID: value});
break;
case 2:
this.setState({selectedEnv: value});
break;
}
}

The event handler needs to be triggered on the click of the button component when it is being used in the Dashboard scene. It will be triggered in the first and second stages of the scene with a value being passed in to update the selected stream ID and environment managed in the local state of the app component.

If we go to DashboardLayout.js, there is an event handler called updateStage which essentially keeps track of which tile button is being clicked and updates the border accordingly:

In order to know which border to highlight, a number between 1 and 6 is based in. Therefore, we can keep track of the user’s selection within this event handler.

First, let’s add a local state property called selectionIndex:

this.state = {
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1",
text: this.props.text,
borderWidths: [0, 0, 0, 0, 0, 0],
selectionIndex: ""
};

Then, let’s keep update this from within the updateStage event handler:

updateStage(input) {
if(this.state.showButton === false) {
this.setState({showButton: true});
}
switch (input) {
case 1:
this.setState({borderWidths: [0.05, 0, 0, 0, 0, 0], selectionIndex: 1});
break;
case 2:
this.setState({borderWidths: [0, 0.05, 0, 0, 0, 0], selectionIndex: 2});
break;
case 3:
this.setState({borderWidths: [0, 0, 0.05, 0, 0, 0], selectionIndex: 3});
break;
case 4:
this.setState({borderWidths: [0, 0, 0, 0.05, 0, 0], selectionIndex: 4});
break;
case 5:
this.setState({borderWidths: [0, 0, 0, 0, 0.05, 0], selectionIndex: 5});
break;
case 6:
this.setState({borderWidths: [0, 0, 0, 0, 0, 0.05], selectionIndex: 6});
break;
}
}

The next step is to write the logic so the selectionIndex value can be used to as the value passed into the captureSelection event handler when the user selects the final stream (first stage) and selects the final environment (second stage).

So, where can we call the captureSelection event handler with the selectionIndex value for the end of the first stage?

Well, when does the first stage change to second stage? It changes when an event handler called updateScene is called. Let’s call captureSelection within this event handler right before we update the stage:

updateScene() {
this.props.captureSelection(this.state.stage, this.state.selectionIndex);
this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video", stage: 2});
}

Notice how we passed in the current stage and the current selectionIndex to captureSelection who reads it as: “Hey, Update Scene! I see you have captured the user’s input for the first stage. Let me use this to update the selectedStreamID property contained here in the local state that the video player needs.”

Unfortunately, we need to do some tweaking in index.vr.js and DashboardLayout.js.

First, remove the environments array from the local state of DashboardLayout and move it to the app component’s local state in index.vr.js:

environments: ["title-background.jpg", "dashboard-background.jpg", "Arizona.jpg", "Hawaii.jpg", "New Hampshire.jpg", "Texas.jpg"]

This will make sense shortly. For now, let’s pass this array down as a prop down to the DashboardLayout.

index.vr.js

<Dashboard
captureSelection={this.captureSelection.bind(this)}
previews={this.state.previews}
environments={this.state.environments}
showButton={false}
text={"Select Environment"}
changeScenes={this.changeScenes.bind(this)}
scene={this.state.scene}
/>

Dashboard.js

<DashboardLayout
environments={this.props.environments}
captureSelection={this.props.captureSelection}
previews={this.props.previews}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

Next, let’s change the passing down of environments within DashboardLayout.js to be from props and not the local state:

<TileButtons
stage={this.state.stage}
environments={this.props.environments}
previews={this.props.previews}
updateStage={this.updateStage.bind(this)}
borderWidths={this.state.borderWidths}
/>

Ok, why did we make this change of passing down environments from the app component?

Let’s see as we update captureSelection in index.vr.js.

We want captureSelection to use the inputted value passed in to select from the arrays containing the stream IDs and environment files:

captureSelection(stage, value) {
switch (stage) {
case 1:
this.setState({selectedStreamID: this.state.IDs[value-1]});
break;
case 2:
this.setState({selectedEnv: this.state.environments[value-1]});
break;
}
}

Now, the Video Player scene will be passed in the correct stream ID and environment file that matches the user’s input. Note that we do value-1 since the passed in value is a range of 1–6 while the arrays are 0–5.

If environments was still in the DashboardLayout component then this wouldn’t work.

A lot of changes! I know. Let’s recap where we are at.

If our button were to call updateScene (as defined in the DashboardLayout component), then we will have captured the correct stream ID that is being passed to our video player.

Now, we need to write the logic so our button calls updateScene at the end of the first stage and goes to the next scene at the end of the second stage.

To do this, let’s add a stage property to the local state found in DashboardLayout.js so the button can handle things accordingly:

this.state = {
slideLeft: new Animated.Value(-1),
fadeIn: new Animated.Value(0),
showButton: false,
color1: "#A482DF",
color2: "#DBDAF1",
text: this.props.text,
borderWidths: [0, 0, 0, 0, 0, 0],
selectionIndex: "",
stage: 1
};

Now, let’s pass this down to our button:

<Button
updateScene={this.updateScene.bind(this)}
showButton={this.state.showButton}
text={this.state.text}
changeScenes={this.props.changeScenes}
stage={this.state.stage}
scene={this.props.scene}
/>

In Button.js, store the inherited stage prop into a variable right above the return:

const stage = this.props.stage;
//return here

Then, let’s call updateScene on the click of the button within the first stage and changeScenes on the click within the second stage:

{currentScene === 2 ? (
<VrButton
onClick={
() => {
switch (stage) {
case 1:
this.props.updateScene();
break;
case 2:
this.props.changeScenes(nextScene);
}
}
}
>

Let’s see if this is working by logging the captured selection of stage 1 in the captureSelection function in index.vr.js:

captureSelection(stage, value) {
switch (stage) {
case 1:
alert(this.state.IDs[value-1]);
this.setState({selectedStreamID: this.state.IDs[value-1]});
break;
case 2:
this.setState({selectedEnv: this.state.environments[value-1]});
break;
}
}

Really quickly, let’s also correct one former mistake. We want to retrieve the channel hosting the stream and not the stream ID in order to play our video when we get there. So, let’s quickly update our Twitch API call:

gatherStreamIDs(response) {
const IDs = response.data.featured.map(function(feat) {
return feat.stream.channel.name;
});
  this.setState({IDs: IDs});
}

Finally! We have completed the first capture. Let’s test it by refreshing the local host:

Super cool! If now go to http://player.twitch.tv/?channel=beyondthesummit then we can see the streaming video:

You can now see how all this effort to capture the user input in stage 1 is necessary to embed streams in our virtual world.

Before we can write the code to make this work in our Video Player scene’s components, we still need to capture the user’s input in stage 2 of the Dashboard scene which will control what panoramic photo is rendered in the video player.

At the end of stage 2, our button calls the event handler to change scenes.

Therefore, let’s add another parameter to the changeScenes function in index.vr.js for the selectionIndex (user’s input/selected tile button):

changeScenes(nextScene, selectionIndex) {
//...
}

Let’s use this new parameter to call captureSelection:

  changeScenes(nextScene, selectionIndex) {
switch (nextScene) {
case 1:
this.setState({scene: 1});
break;
case 2:
this.setState({scene: 2});
break;
case 3:
this.captureSelection(2, selectionIndex);
this.setState({scene: 3});
break;
}
}

Now, the end of stage 2 of the Dashboard scene (right before transition to scene 3) will capture the user’s input and retrieve the correct environment file from the array.

The final step is to have our Button component pass in this extra parameter.

To do this, we need start by passing down the selectionIndex prop as a prop to our Button component in DashboardLayout.js:

<Button
updateScene={this.updateScene.bind(this)}
showButton={this.state.showButton}
text={this.state.text}
changeScenes={this.props.changeScenes}
stage={this.state.stage}
scene={this.props.scene}
selectionIndex={this.state.selectionIndex}
/>

Store this prop into a variable within the render function in Button.js:

//more stuff up here
const stage = this.props.stage;
const selectionIndex = this.props.selectionIndex;
//return here

Then, our button passes this variable as the second parameter of changeScenes:

{currentScene === 2 ? (
<VrButton
onClick={
() => {
switch (stage) {
case 1:
this.props.updateScene();
break;
case 2:
this.props.changeScenes(nextScene, selectionIndex);
}
}
}
>
//...

Open index.vr.js and let’s log this captured selection to see if the correct environment value is being retrieved:

captureSelection(stage, value) {
switch (stage) {
case 1:
console.log(this.state.IDs[value-1]);
this.setState({selectedStreamID: this.state.IDs[value-1]});
break;
case 2:
console.log(this.state.environments[value-1]);
this.setState({selectedEnv: this.state.environments[value-1]});
break;
}
}

Refresh your local host and check the console:

Flippin’ cool beans! We are finally done with the Dashboard scene!

Playing the Selected Stream in the Selected Environment

To complete our app, we need to take the streamID and env props being passed down to the VideoPlayer component to render the live streaming video of a Twitch channel and the panoramic photo of the environment in the Video Player scene.

Rendering the proper panoramic photo is really easy, we can just update the following line in VideoPlayer.js:

<Pano source={asset(this.props.env)}/>

Let’s refresh the local host and see if this worked:

Very neat!

Next, we want to pass down the stream channel URL to the VideoElement component so we can embed a live stream video in place of our fireplace.

In VideoPlayer.js, let’s first create a local state that will control the streamURL:

constructor() {
super();
this.state = { streamURL: "" }
}

Before this component mounts, let’s update the streamURL:

componentWillMount() {
this.setState({ streamURL: 'http://player.twitch.tv/?channel=' + this.props.streamID })
//example: http://player.twitch.tv/?channel=beyondthesummit
}

Then, we pass this down all the way to VideoElement.js:

VideoPlayer.js

<VideoPlayerLayout
streamURL={this.state.streamURL}
showButton={this.props.showButton}
text={this.props.text}
changeScenes={this.props.changeScenes}
scene={this.props.scene}
/>

VideoPlayerLayout.js

<VideoElement streamURL={this.props.streamURL}/>

The final step of our entire application is to play the live streaming video found at the passed in URL.

Psh! That’s easy, Mike. We can just do this:

<Video style={{height: 4}} source={{uri: this.props.streamURL}} />

C’mon! Did you think it would be that easy?

The Video component in React VR embeds the video source into a simple <video> HTML element. However, the Twitch API documentation says that it needs to be embedded into an <iframe> component:

How do we do this?

Good question. I literally paused as I was writing this and spent hours and hours trying to resolve this.

7 hours later. I have had no luck which brings this chapter to an anticlimactic end.

Nevertheless, I still want to publish this chapter. Reason being, a reader will likely able to figure this last piece out.

Final Code

Final code available via GitHub.

Extra Practice/Improvements

Admittedly, there’s a lot of improvements that can be made to this app:

  1. There’s a bug where the first tile button only works when dev tools is open.
  2. The environment (panoramic) images could be compressed for faster loading.
  3. Overall, there could be better organization of this app and a more modular approach. I seemed to put way too much code in DashboardLayout.js.

There’s also some opportunities to practice your React VR skills even further:

  1. Use 3D modeling for the panoramic environments in place of static images.
  2. Incorporate API calls to load videos from sources other than Twitch. Check out GraphQL.
  3. Add animations between scenes and to provide better feedback to user inputs.
  4. Implement panoramic ambient sounds for the environment.
  5. Add loading animation.

Chapter 9

Chapter 9 is now available.

Support the Author

If you would like to support me as I write this book, you can purchase the book here.

The book is published through a platform called LeanPub which allows me to update my book as it progresses. Each time a chapter is added, you will be notified via email. The book will be free to read on Medium, however, purchasing it through LeanPub allows you to download the ebook as a PDF, EPUB, or MOBI file and helps support me financially.

Additionally, I have created a special package that will provide you with a secret link to a Discord server where you will be able to help influence how I write this book.

So go ahead and purchase this book on LeanPub if you would be so kind.


Cheers,
Mike Mangialardi
Founder of Coding Artist