Custom UI for Viewer using API - tutorial

Orbitvu VIEWER (SUN, Free and Infinity 360 versions) supports API that makes it possible to customize Viewer UI. In this tutorial we will reimplement Viewer buttons to have them look like below.

Please note that this tutorial is intended for persons with at least basic knowledge about web development (HTML, JavaScript, CSS). 

Page layout

We will start by defining the layout for our page and our custom buttons. We will work with the demo presentation from the SUN server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>Orbitvu VIEWER custom UI</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Load Font Awesome -->
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz" crossorigin="anonymous">
</head>

<body>
<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">
        <!-- Embed code from Orbitvu SUN starts here -->
        <script type="text/javascript"
                async
                src="//orbitvu.co/001/WZarBpyJ7hHX77zaF49Rb9/ov3601/3/script?width=auto&height=auto&content2=yes&partial_load=yes"></script>
        <div class="orbitvu-viewer" style="width: 100%;height: 100%;">
            <div id="ovContent-WZarBpyJ7hHX77zaF49Rb9"></div>
        </div>
        <!-- Embed code from Orbitvu SUN ends here -->

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui">
            <div class="custom-ui-buttons">
                <a href="" id="ui-autorotate-WZarBpyJ7hHX77zaF49Rb9"><i class="fas fa-play-circle"></i></a>
                <a href="" id="ui-zoom-in-WZarBpyJ7hHX77zaF49Rb9"><i class="fas fa-search-plus"></i></a>
                <a href="" id="ui-zoom-out-WZarBpyJ7hHX77zaF49Rb9"><i class="fas fa-search-minus"></i></a>
                <a href="" id="ui-drag-rotate-WZarBpyJ7hHX77zaF49Rb9"><i class="fas fa-arrows-alt"></i></a>
                <a href="" id="ui-fullscreen-WZarBpyJ7hHX77zaF49Rb9"><i class="fas fa-expand-arrows-alt"></i></a>
            </div>
        </div>
    </div>
</div>
</body>
</html>

There are few interesting things to note in the above code:

Line 10: We're loading Font Awesome library in order to get nice icons for our buttons

Lines 16-23: Embed code copied from Orbitvu SUN

Lines 15, 25, 27-231: note that id of these elements is set to be unique using uid of the presentation from Orbitvu SUN (WZarBpyJ7hHX77zaF49Rb9 in this case). It will be useful if we have multiple presentations on the same page.

You might be wondering why there are two <div> elements at the top level:

1
2
3
4
5
<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">
        ...
    </div>
</div>

This is required for the Fullscreen button to work properly on some older browsers. You'll find more details about this later, when we will be adding Fullscreen button support.

If you're going to use presentation hosted on your own server (not with Orbitvu SUN), then you need to embed your presentation as described in this documentation.

We will now add some styles in order to have the desired layout of the page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<style type="text/css">
    body {
      margin: 0;
    }

    @media only screen and (min-width: 500px) {
      /* make viewer window smaller on big screens */
      .viewer-wrapper {
        width: 50%;
        margin: 0 auto;
      }
    }

    .viewer-container {
      /* width and height settings are required for fullscreen to work on this element in older Chrome */
      height: 100%;
      width: 100%;
      min-height: 200px;

      position: relative; /* required to properly position custom-ui inside this element */
      display: flex;
      align-items: center;
    }

    div.custom-ui {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;

      pointer-events: none; /* remove pointer events so that we can interact with viewer through it */
      z-index: 99;  /* bring buttons over the Viewer container */
    }

    div.custom-ui-buttons {
      font-size: 2em;
      text-align: center;
      background-color: rgba(200,200,200,0.4);
      padding: 3px;
      display: flex;
      flex-direction: column;
      border-radius: 0 5px 5px 0;
      box-shadow: 2px 0 5px #222222;
    }

    div.custom-ui-buttons a{
      pointer-events: all;
      color: #00A4B6;
    }

    div.custom-ui-buttons a:active{
      color: orange;
    }

    div.custom-ui-buttons a.active {
      color: orange;
    }
</style>

Our page now looks like below. You can see it in separate window and check the source by clicking here.

Accessing the Viewer API

We have our page layout defined so now its time to connect to the Viewer API. In order to do so, first we have to define a callback that will receive API object as soon as the Viewer API is initialized. This can be done with viewer_api_init parameter. Let's start with writing our callback function:

1
2
3
4
5
6
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    window['api_init'] = (api_obj) => {
        console.log('api initialized');
    };
</script>

Initially it will just output 'api initialized' text.

Now, let's connect our callback to the VIEWER. As we're using Orbitvu SUN embed code we will just add viewer_api_init to the querystring, like: &viewer_api_init=api_init (see highlighted part of the code below)

1
2
3
4
5
6
7
8
<!-- Embed code from Orbitvu SUN starts here -->
<script type="text/javascript"
        async
        src="//orbitvu.co/001/WZarBpyJ7hHX77zaF49Rb9/ov3601/3/script?width=auto&height=auto&content2=yes&partial_load=yes&viewer_api_init=api_init"></script>
<div class="orbitvu-viewer" style="width: 100%;height: 100%;">
    <div id="ovContent-WZarBpyJ7hHX77zaF49Rb9"></div>
</div>
<!-- Embed code from Orbitvu SUN ends here -->

Now you should see the 'api initialized' text outputted to the console. If it doesn't work check if there are any errors in the console and, in case you're not using SUN, if your VIEWER does support the API.

Making the code reusable

Callback function we've defined has one small problem. It is defined as a global function called api_init, so if we want to add another 360 degree presentation to the page, we will have to make sure the callback name is unique (so that we can connect to the proper viewer instance). 

We will use presentation uid: WZarBpyJ7hHX77zaF49Rb9 as a part of the callback name. We will also make our code embedded into the class so that it can be easily reused. 

Our callback identifier will now be 'api_init_WZarBpyJ7hHX77zaF49Rb9'. And the VIEWER embed code will be:

1
2
3
4
5
6
7
8
<!-- Embed code from Orbitvu SUN starts here -->
<script type="text/javascript"
        async
        src="//orbitvu.co/001/WZarBpyJ7hHX77zaF49Rb9/ov3601/3/script?width=auto&height=auto&content2=yes&partial_load=yes&viewer_api_init=api_init_WZarBpyJ7hHX77zaF49Rb9"></script>
<div class="orbitvu-viewer" style="width: 100%;height: 100%;">
    <div id="ovContent-WZarBpyJ7hHX77zaF49Rb9"></div>
</div>
<!-- Embed code from Orbitvu SUN ends here -->

Our reusable JavaScript class will be defined in the following way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
      constructor(uid) {
        this.uid = uid;
        this.viewer_api_obj = null;
      }

      setup_callbacks() {
        /* we're using presentation uid to easily differentiate between presentations
         * useful if there are more than 1 presentation on the same page as callbacks are global
         * */
        window['api_init_' + this.uid] = (api_obj) => {
          console.log('api initialized');
        };
      }
    }

    // pass unique UID to the CustomViewerUI constructor
    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
</script>

We're using ES6 classes and arrow functions here. If you need to support older browsers make sure to use some compilers like Babel to make it backward compatible.

Using VIEWER API

Now we can do something useful with our API object. You might have noticed (especially if you're on a slower network or have network speed limited in Developer Tools) that when our page is loaded, custom buttons are shown immediately, and the VIEWER is initialized a bit later.

This is not good as we cannot use the buttons before VIEWER is properly intialized. What we would like to do is to hide our buttons initially and show these after VIEWER initialization. This can be easily done by using the partially_initialized event that is triggered by VIEWER as soon as it has loaded at least 4 frames (if partial_load is enabled).

There is also viewer_initialized callback that is called when all frames are loaded.

First we will add display: none to our buttons container:

1
<div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">

Then we will add event handler that will show the buttons on VIEWER initialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
      constructor(uid) {
        this.uid = uid;
        this.viewer_api_obj = null;
      }

      setup_callbacks() {
        /* we're using presentation uid to easily differentiate between presentations
         * useful if there are more than 1 presentation on the same page as callbacks are global
         * */
        window['api_init_' + this.uid] = (api_obj) => {
          api_obj.addCallback(
            'viewer_partially_initialized_' + this.uid,
            'partially_initialized');
        };

        window['viewer_partially_initialized_' + this.uid] = (api_obj) => {
          this.viewer_api_obj = api_obj;

          // display custom button panels
          let custom_ui = document.getElementById('custom-ui-' + this.uid);
          custom_ui.style.display = 'flex';
        };
      }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
</script>

Line 14: binding a callback to partially_initialized event.

Line 19: definition of the callback

Line 20: store the API object in the variable

Line 24: show buttons panel by changing its display style to flex

The resulting page now looks like below. You can open it in new window and check the source code by clicking here.

Reimplementing control panel

Time to do something more useful. Let's start reimplementing the VIEWER buttons.

Autorotate button

Some requirements for Autorotate button implementation:

  • start the autorotation (if not running)
  • stop the autorotation (if running)
  • change the button color when autorotation is running
  • change the button color when autorotation is stopped

Events and API calls we will use:

Our code will define callback for autorotate_start and autorotate_stop events in order to "know" the current autorotation state - state will be stored in the variable, and to change the color of the button. Click handler for the button will trigger autorotate call to start/stop autorotation, depending on its current state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
      constructor(uid) {
        this.uid = uid;
        this.viewer_api_obj = null;
        this.state = {
          autorotate: 'no',
        };
      }

      setup_callbacks() {
        /* we're using presentation uid to easily differentiate between presentations
         * useful if there are more than 1 presentation on the same page as callbacks are global
         * */
        window['api_init_' + this.uid] = (api_obj) => {
          api_obj.addCallback(
            'viewer_partially_initialized_' + this.uid,
            'partially_initialized');
        };

        window['viewer_partially_initialized_' + this.uid] = (api_obj) => {
          this.viewer_api_obj = api_obj;
          api_obj.addCallback(
            'autorotate_start_' + this.uid,
            'autorotate_start');
          api_obj.addCallback(
            'autorotate_stop_' + this.uid,
            'autorotate_stop');

          // display custom button panels
          let custom_ui = document.getElementById('custom-ui-' + this.uid);
          custom_ui.style.display = 'flex';
        };

        window['autorotate_start_' + this.uid] = () => {
          this.state.autorotate = 'yes';
          this.refresh_autorotate_btn();
        };
        window['autorotate_stop_' + this.uid] = () => {
          this.state.autorotate = 'no';
          this.refresh_autorotate_btn();
        };
      }

      setup_button_handlers(){
        // button event handlers
        let btn_autorotate = document.getElementById('ui-autorotate-' + this.uid);

        // AUTOROTATE BUTTON
        btn_autorotate.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();

          if (this.viewer_api_obj) {
            this.viewer_api_obj.setScene({autorotate: this.state.autorotate === 'yes' ? 'no' : 'yes'})
          }
        });
      }

      refresh_autorotate_btn(){
        let btn_autorotate = document.getElementById('ui-autorotate-' + this.uid);
        if (this.state.autorotate === 'yes') {
          btn_autorotate.classList.add('active');
        }
        else {
          btn_autorotate.classList.remove('active');
        }
      }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
    vui.setup_button_handlers()
</script>
  • Line 7: store current autorotation state
  • Lines 24-29: bind callbacks to VIEWER
  • Lines 36-43:  autorotate_start and autorotate_stop callbacks - update current state and redraw the button
  • Lines 51-58: click handler for autorotate button - calls VIEWER api method: autorotate
  • Lines 61-69: change color of the button depending on current autorotation state (uses active CSS class)

Have a llook how it works now. Fullscreen example can be found here.

Zoom in and Zoom out

Zoom in and Zoom out buttons will use the following API:

First, trivial implementation can be as follows (for clarity, the code below doesn't include autorotate button implementation):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {

      // (...)

      setup_button_handlers(){
        // button event handlers

        // (...)

        let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
        let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);

        // ZOOM IN BUTTON
        btn_zoom_in.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();
          if (this.viewer_api_obj) {
            this.viewer_api_obj.setScene({scaleUp: 1});
          }
        });

        // ZOOM OUT BUTTON
        btn_zoom_out.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();

          if (this.viewer_api_obj) {
            this.viewer_api_obj.setScene({scaleDown: 1});
          }
        });
      }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
    vui.setup_button_handlers()
</script>

See below how it works (full page example can be found here):

Zoom in and zoom out buttons do work but you have to click these repeatedly each time you want to zoom in/out more. Would be better (and consistent with original implementation) if we can click the button and it will zoom in/out until we release the button. 

In order to do it we will use Hammer.js library to handle click and touch events in a standardized way, then we will use setInterval on touch/click start to continuously zoom until click/touch is ended (clearInterval).

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<!-- Load hammer.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>

<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
        constructor(uid) {
            // (...)
            this.btn_zoom_interval_id = null;
        }

        // (...)

        setup_button_handlers() {
            // button event handlers
            let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
            let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
            // (...)

            let clear_zoom_interval = () => {
                if (this.btn_zoom_interval_id) {
                    clearInterval(this.btn_zoom_interval_id);
                    this.btn_zoom_interval_id = null;
                }
            };

            // ZOOM IN BUTTON
            btn_zoom_in.addEventListener('click', (e) => {
                e.preventDefault();
            });

            // stop interval when cursor leaves the button (eg. on mouse right click and context menu popup)
            if (window.PointerEvent) {
                btn_zoom_in.addEventListener('pointerout', (e) => {
                    clear_zoom_interval();
                });
            }
            else if (window.MSPointerEvent){
                btn_zoom_in.addEventListener('MSPointerOut', (e) => {
                    clear_zoom_interval();
                });
            }
            else{
                btn_zoom_in.addEventListener('mouseout', (e) => {
                    clear_zoom_interval();
                });
            }

            let zoom_in_hammer_mgr = new Hammer.Manager(
                btn_zoom_in,
                {
                    recognizers: [
                        [Hammer.Tap, {timeout: 80}],
                        [Hammer.Press, {time: 100}]
                    ]
                }
            );
            zoom_in_hammer_mgr.on('tap', (e) => {
                clear_zoom_interval();
                if (this.viewer_api_obj) {
                    this.viewer_api_obj.setScene({scaleUp: 1});
                }
            });
            zoom_in_hammer_mgr.on('press', (e) => {
                clear_zoom_interval();
                if (this.viewer_api_obj) {
                    this.viewer_api_obj.setScene({scaleUp: 1});

                    // start setInterval for continuous zooming while button is pressed
                    this.btn_zoom_interval_id = setInterval(() => {
                        this.viewer_api_obj.setScene({scaleUp: 1});
                    }, 50);
                }
            });
            zoom_in_hammer_mgr.on('pressup', (e) => {
                clear_zoom_interval();
            });

            // ZOOM OUT BUTTON
            btn_zoom_out.addEventListener('click', (e) => {
                e.preventDefault();
            });
            // stop interval when cursor leaves the button (eg. on mouse right click and context menu popup)
            if (window.PointerEvent) {
                btn_zoom_out.addEventListener('pointerout', (e) => {
                    clear_zoom_interval();
                });
            }
            else if (window.MSPointerEvent){
                btn_zoom_out.addEventListener('MSPointerOut', (e) => {
                    clear_zoom_interval();
                });
            }
            else{
                btn_zoom_out.addEventListener('mouseout', (e) => {
                    clear_zoom_interval();
                });
            }
            let zoom_out_hammer_mgr = new Hammer.Manager(
                btn_zoom_out,
                {
                    recognizers: [
                        [Hammer.Tap, {timeout: 80}],
                        [Hammer.Press, {time: 100}]
                    ]
                }
            );
            zoom_out_hammer_mgr.on('tap', (e) => {
                clear_zoom_interval();
                if (this.viewer_api_obj) {
                    this.viewer_api_obj.setScene({scaleDown: 1});
                }
            });
            zoom_out_hammer_mgr.on('press', (e) => {
                clear_zoom_interval();
                if (this.viewer_api_obj) {
                    this.viewer_api_obj.setScene({scaleDown: 1});

                    // start setInterval for continuous zooming while button is pressed
                    this.btn_zoom_interval_id = setInterval(() => {
                        this.viewer_api_obj.setScene({scaleDown: 1});
                    }, 50);
                }
            });
            zoom_out_hammer_mgr.on('pressup', (e) => {
                clear_zoom_interval();
            });
        }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
    vui.setup_button_handlers()
</script>

We will not go into details of Hammer.js library here, it is enough to say that it defines three events that we're using: tap, press and pressup. On tap (that is just a quick press and release) we simply trigger single zoom action. For the press event which identifies longer button press, we start setInterval callback that continuously zooms the presentation (until pressup).

  • Lines 20-25: helper function to clear interval
  • Lines 28-30: even though we have callbacks set by Hammer we still have to prevent default onClick behaviour.
  • Lines 33-47: additional handlers for mouseout/pointerout events. It might happen that user clicks right mouse button while zooming and in such situation pressup event will not be triggered. Because of this we listen to these extra events to break zooming immediately.
  • Lines 61, 67, 71: call VIEWER API to zoom in
  • Lines 111, 117, 121: call VIEWER API to zoom out

Resulting page can be seen here:

Drag / Rotate button

Drag / Rotate button changes the way dragging the mouse over presentation works. It can either cause presentation rotation or movement of the current frame. By default VIEWER will be in rotate mode but when zoomed in it will automatically switch to drag mode (if only auto_drag_switch is enabled (default)).

What we have to do to reimplement this button is to change its look on mode changes (mode_changed event) and switch the mode on click.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
        constructor(uid) {
            this.uid = uid;
            this.viewer_api_obj = null;
            this.state = {
                autorotate: 'no',
                mode: 'rotate'
            };
            this.btn_zoom_interval_id = null;
        }

        setup_callbacks() {
            /* we're using presentation uid to easily differentiate between presentations
             * useful if there are more than 1 presentation on the same page as callbacks are global
             * */
            window['api_init_' + this.uid] = (api_obj) => {
                api_obj.addCallback(
                    'viewer_partially_initialized_' + this.uid,
                    'partially_initialized');
            };

            window['viewer_partially_initialized_' + this.uid] = (api_obj) => {
                // (...)
                api_obj.addCallback(
                    'mode_changed_' + this.uid,
                    'mode_changed');
            };

            // (...)
            window['mode_changed_' + this.uid] = (mode) => {
                this.state.mode = mode;
                this.refresh_drag_rotate_btn();
            };
        }

        setup_button_handlers() {
            // button event handlers
            let btn_drag_rotate = document.getElementById('ui-drag-rotate-' + this.uid);

            // (...)

            // DRAG / ROTATE button
            btn_drag_rotate.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                if (this.viewer_api_obj) {
                    if (this.state.mode === 'rotate') {
                        this.viewer_api_obj.setScene({mode: 'drag'});
                    }
                    else {
                        this.viewer_api_obj.setScene({mode: 'rotate'});
                    }
                }
            });
        }

        refresh_drag_rotate_btn() {
            let btn_drag_rotate = document.getElementById('ui-drag-rotate-' + this.uid);
            if (this.state.mode === 'rotate') {
                btn_drag_rotate.innerHTML = '<i class="fas fa-arrows-alt"></i>';
            }
            else if (this.state.mode === 'drag') {
                btn_drag_rotate.innerHTML = '<i class="fas fa-sync-alt"></i>';
            }
        }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
    vui.setup_button_handlers()
</script>

Line 9: we're storing current mode value

Lines 26-28: callback for mode_changed event is being registered

Lines 32-35: callback updates the current state and rerenders button

Lines 45-56:  click handler for the button, calls VIEWER API method

This is how it works:

Fullscreen button

The last button to implement is the Fullscreen button. Before we got into details we have to think about some important issues. First of all, it has to be noted that Fullscreen API is not supported by every browser. This means that on some browsers we will not use fullscreen at all or we will implement some workaround ourselves (eg. resize some elements on a page). 

In this tutorial, we will only stick to the browsers supporting Fullscreen API and in order to facilitate using it, we will use Screenfull library.

Second thing we have to wonder about is the element that will be switched into Fullscreen mode. Fullscreen API works by changing the styles (CSS) of the selected element and bringing it into Fullscreen. This means that we can switch Orbitvu VIEWER into the fullscreen mode but our buttons, that are defined outside the VIEWER, will be left out of fullscreen and thus invisible. What we can do is to create our own element, containing both VIEWER and our custom buttons, and use Fullscreen API on it. This is what we're going to do in this tutorial. Element that will be switched into fullscreen will be viewer-container-WZarBpyJ7hHX77zaF49Rb9.

There is one more problem with such approach. If we switch external (to the VIEWER) element into Fullscreen, then VIEWER itself doesn't "know" about this. It is important for the VIEWER to "know" about this due to the fact it is supposed to change its own dimensions even though these might have been forced, eg. by setting explicit widht and height (in other words, you might want to have small VIEWER window on page (eg. 200px x 350px) but when going to fullscreen you want it to scale up to the available space). Because of that we will have to inform VIEWER about the Fullscreen switch and it will be done using fullscreen VIEWER API call.

Have a look at the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!-- Load Screenfull library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>

<!-- Put this at the end of the page, just before closing </body> tag -->
<script type="text/javascript">
    class CustomViewerUI {
        constructor(uid) {
            this.uid = uid;
            this.viewer_api_obj = null;
            this.state = {
                autorotate: 'no',
                mode: 'rotate',
                fullscreen: false
            };
            this.btn_zoom_interval_id = null;
        }

        // (...)

        setup_button_handlers() {
            // button event handlers
            let btn_fullscreen = document.getElementById('ui-fullscreen-' + this.uid);

            // (...)

            // FULLSCREEN button
            // add evt listener to screenful;
            if (screenfull.enabled) {
                screenfull.on('change', (e) => {
                    if (e.target.id === 'viewer-container-' + this.uid) {
                        if (screenfull.isFullscreen) {
                            this.viewer_api_obj.setScene({fullscreen: 'enter'});
                        }
                        else {
                            this.viewer_api_obj.setScene({fullscreen: 'cancel'});
                        }
                    }
                });
            }

            btn_fullscreen.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                if (screenfull.enabled) {
                    this.viewer_api_obj.setScene({fullscreen: 'pre'});
                    screenfull.toggle(viewer_container);
                }
            });
        }
    }

    vui = new CustomViewerUI('WZarBpyJ7hHX77zaF49Rb9');
    vui.setup_callbacks();
    vui.setup_button_handlers()
</script>
</body>
</html>

Line 13: remember current fullscreen state

Line 32: on fullscreen change, if fullscreen is enabled, call 'fullscreen: enter' method of the VIEWER API

Line 35: on fullscreen change, if fullscreen is disabled, call 'fullscreen: cancel' method of the VIEWER API

Line 46: on fullscreen button click call 'fullscreen:pre' method of the VIEWER API.

Line 47: toggle fullscreen

As you can se we've used three API methods here, for the fullscreen call:

  • pre - stores current dimensions of the viewer elements (so that we will be able to bring these back)
  • enter - removes currently forced dimensions, eg. width (should be called after pre)
  • cancel - brings back dimensions that were stored by pre - should be called on fullscreen exit.

The last few things to do are:

  • hide default VIEWER buttons
  • disable default doubletap/doubleclick behaviour of the VIEWER that causes entering Fullscreen (as we do not want to bring just the VIEWER into the fullscreen mode)

Both these things can be easily done by adding parameters to our presentation embed code. Parameter to remove buttons is style and parameter to change doubletap behaviour is doubletap_mode.

Resulting code is:

1
2
3
4
5
6
7
8
<!-- Embed code from Orbitvu SUN starts here -->
<script type="text/javascript"
        async
        src="//orbitvu.co/001/WZarBpyJ7hHX77zaF49Rb9/ov3601/3/script?width=auto&height=auto&content2=yes&partial_load=yes&viewer_api_init=api_init&style=4&doubletap_mode=zoom"></script>
<div class="orbitvu-viewer" style="width: 100%;height: 100%;">
    <div id="ovContent-WZarBpyJ7hHX77zaF49Rb9"></div>
</div>
<!-- Embed code from Orbitvu SUN ends here -->

Complete example can be found here: