Creating simple tourism VR application using React-360

Creating simple tourism VR application using React-360

by Hlib Teteryatnikov

We are going to create a simple tourism VR application using React-360 with dynamic transitions, tooltips, and easy location managing. React 360 is a library for the creation of 3D and VR UI that can be easily used to create small tourism applications, such as application in this example.

React-360 Tourism App Logo

About the application

In this example we will create the example of Chernihiv city, Ukraine, you can change it to your city, you will only need to add photos of your city, attractions and edit locations.js file.

Our app will consist of:

  1. On click smooth transitions between locations
  2. On hover tooltips for better city attractions introduction
  3. Comfortable structure to add new locations, attractions, and transitions

Project structure

We set up the structure of our project as follows:

- components 
  - Tooltip
  - Transition
  - Wrapper
- modules
- consts
- services
- static_assets
  - icons
  - img
     - 360
     - attractions
- index.js
- client.js
- index.html
  • folder "components" includes components that used repeatedly
  • folder "modules" includes Custom Native Modules
  • folder "consts" includes data that could be stored in data base if we provided it
  • folder "services" includes services that work with entities
  • folder "static_assets" includes different icons, 360 photos and photos of attractions
  • file "index.js" is an entypoint
  • file "client.js" is the code that connects browser to the "Runtime"
  • file "index.html" we are not going to edit it, it only provides a point to mount js code

Data structure

In "consts" folder, we create the "locations.js" file in which we store data  —  including the image sources for locations, attractions, transitions, icons size on tooltips and description of attractions. If you are going to improve your tourism app in the future, it will be better to create backend to store this data in the database.

export default (locations = {
     ...
       PopudrenkoPark: {
    name: 'PopudrenkoPark',
    img: 'popudrenko_park.jpg',
    transitions: [
      {
        width: 50,
        height: 50,
        yaw: 11.2,
        pitch: 0,
        goesTo: 'CityCenter',
      },
    ],
    tooltips: [
      {
        width: 35,
        height: 35,
        yaw: 14.32,
        pitch: 0.1,
        text: 'Chernihiv National Bank',
        img: 'national_bank.jpg',
      },
    ],
  },
  ...
});

In this short example, we can see one of the locations, "Popudrenko Park" with one tooltip of 'Chernihiv National Bank' with a given image and transition to the "Сity center". Yaw and pitch are params needed to set the angle for the surface to show icon in the correct place, yaw rotates the surface location left and right, pitch rotates up and down. Width and height are params needed to set the size of the icon, we do not want to set a big icon if the attraction is far away from us, because it can be hidden behind the icon.

Services

For comfortable work with locations, we create location service with getTooltips and getTranisitions functions, which will check are there any transitions or tooltips for a given location and return them.

State management

To share state we create WrapperComponent and wrap all components, that need to know current location, wrap into this component.

import React from 'react';
import {asset, Environment} from 'react-360';

import locations from '../../consts/locations';

const locationName = locations.CityCenter.name;
const wrappers = [];

export function changeLocation(location) {
  locationName = location;
  Environment.setBackgroundImage(asset(`./img/360/${locations[`${location}`].img}`));

  wrappers.forEach(update => {
    update();
  });
}

export function wrap(Component) {
  return class Wrapper extends React.Component {
    state = {
      name: locationName,
    };

    componentDidMount() {
      wrappers.push(() => {
        this.setState({
          name: locationName,
        });
      });
    }

    render() {
      return <Component {...this.props} name={this.state.name} />;
    }
  };
}

In WrapperComponent we have the wrap function in which we wrap all components. In changeLocation function we change the state of all wrapped components and change the background image. This function is called from our TransitionComponent.

Shared Components

Here we create TooltipComponent and TransitionComponent that will be reusable many times.

TooltipComponent has two functions onMouseOn and onMouseOut which call resizeTooltip function of tooltipModule.

...
  onMouseOn () {
    tooltipModule.resizeTooltip (this.props.index, 300, 300);
    this.setState ({
      source: `img/attractions/${this.props.infoImg}`,
      width: 300,
      height: 200,
      isMouseOver: true,
    });
  }

  onMouseOut () {
    tooltipModule.resizeTooltip (
      this.props.index,
      this.props.width,
      this.props.height
    );
    this.setState ({
      source: this.props.iconImg,
      width: this.props.width,
      height: this.props.height,
      isMouseOver: false,
    });
  }
...
render () {
    return (
      <View
        hitSlop={160}
        style={styleSheet.viewPanel}
        onEnter={() => this.onMouseOn ()}
        onExit={() => this.onMouseOut ()}
      >...</View>)
}
...

TooltipComponent listens to on Enter and on Exit events and triggers appropriate function. So we will show detailed information (image of the attraction and name) on Enter and show tooltip icon on Exit event. "hitSlop" param defines how far a touch event can start away from the view. We need it to avoid blinking caused by fast component resizing. React-360 Tourism App tooltip animation

TranisitionComponent has VR-Button and on сlick it calls changeLocation function which changes the location and sets tooltips for the new location.

...
  changeLocation (location) {
    tooltipModule.setTooltips (this.props.name);
    transitionModule.setTooltips (this.props.name);
    changeLocation (location);
  }

  render () {
...
    return (
      <View style={styleSheet.viewPanel}>
        <VrButton onClick={() => this.changeLocation (this.props.goesTo)}>
        ...
        </VrButton>
      </View>
    );
  }
...

React-360 Tourism App transition animation

Native Modules

Native Modules provide the ability for React code to call back into your runtime, and provide functionality that's only available in the main browser environment. Examples include storing values between loads, requesting information about connected controllers, or manipulating the rendered environment.

We have Tooltip and Tranistion Custom Native Modules

TooltipModule:

...
  setTooltips (location) {
    this.detachAll ();

    const tooltips = LocationService.getListTooltips (location);

    tooltips.map ((item, index) => {
      this.surfaces.push (
        new Surface (item.width, item.height, Surface.SurfaceShape.Flat)
      );
      this.surfaces[index].setAngle (item.yaw, item.pitch);
      this.roots.push (
        r360.renderToSurface (
          r360.createRoot ('TooltipComponent', {
            width: item.width,
            height: item.height,
            iconImg: 'icons/question.png',
            index: index,
            text: item.text,
            infoImg: item.img,
          }),
          this.surfaces[index]
        )
      );
    });
  }

  resizeTooltip (index, width, height) {
    this.surfaces[index].resize (width, height);
  }

    detachAll () {
    for (let i = 0; i < this.roots.length; i++) {
      r360.detachRoot (this.roots[i]);
    }
  }
...

Here we can see resizeTooltip function, which resizes surface by a given index, height and width. detachAll function detachs roots (removes root from previous location). function setTooltips gets list of available tooltips from service, creates surface, sets angle(position) and renders it using Tooltip Component. TransitionModule works on the same principle.

Entry points

Each React-360 app has a default index.js and client.js files.

index.js:

...
export default class MainComponent extends React.Component {
  render () {
    NativeModules.TooltipModule.setTooltips (this.props.name);
    NativeModules.TransitionModule.setTooltips (this.props.name);
    return <View />;
  }
}

AppRegistry.registerComponent ('TransitionComponent', () =>
  wrap (TransitionComponent)
);
AppRegistry.registerComponent ('MainComponent', () => wrap (MainComponent));
AppRegistry.registerComponent ('TooltipComponent', () =>
  wrap (TooltipComponent)
);

In index.js we have the MainComponent with empty view and we register components using AppRegistry and our wrapper. By registering multiple components with the AppRegistry, you can attach them to a different surface.

client.js:

...

function init (bundle, parent, options = {}) {
  r360 = new ReactInstance (bundle, parent, {
    fullScreen: true,
    nativeModules: [new TooltipModule (), new TransitionModule ()],
    ...options,
  });

  r360.renderToSurface (
    r360.createRoot ('MainComponent'),
    r360.getDefaultSurface ()
  );
  r360.compositor.setBackground (
    r360.getAssetURL ('./img/360/city_center.jpg')
  );
}

window.React360 = {init};
...

Here we have default init function, where we set set custom native modules to the ReactInstance, render MainComponent and set default background as a city center, because our tourism app will start from this location.

Component styling

StyleSheet from React Native are used in React 360 and it allows to style Components, so you can use React Native StyleSheet documentation.

Links

© 2016 - 2019 AgileVision sp. z o.o.